Memory leak has always been one of the hardest problems, even for senior programmers. Memory leak still proceeds when they get lapsed for some time. Besides the basic leak phenomenon of allocated memory, there are many different types of memory leak, such as exception branch leaking etc. This project guides you to implement a memory leak detector.
Things to Learn
To overwrite operator new
Predefined macros __FILE__ and __LINE__
Static variables in headfile
Smart pointer std::shared_ptr
Code Test
Memory leaks are usually relevant to the inadvertently allocated memory that remains unreleased. In modern operating systems, such unreleased memory will still be reclaimed by the operating system after the regular memory having been used by one application which is terminated, so a memory leak caused by a transient application will not cause serious consequences.
However, if we are writing an application such as a server, it will always be up and running. If there is some logic leading to a memory leak, there is a high possibility that it will keep on increasing the leak of memory. Eventually, the performance of the system will be reduced and it may even cause operation failure.
A running program that continuously keeps on allocating memory is quite simple:
int main() {
// allocates memory forever, never release
while(1) int *a = new int;
return 0;
}
To detect memory leaks in a running program, we usually set checkpoints up in the application to analyze whether the memory becomes different with respect to other checkpoints.
Essentially, it is similar to a short-term program memory leaking inspection. So, the memory leak detector in this project will be implemented for a short-term memory leak.
We hope the following test code may detect a memory leak. In this code, we don't release the allocated memory and create a memory leak caused by exception branch:
//
// main.cpp
// LeakDetector
//
#include <iostream>
// Implements memory leak inspection here
// #include "LeakDetector.hpp"
// testing exception branch leaking behavior
class Err {
public:
Err(int n) {
if(n == 0) throw 1000;
data = new int[n];
}
~Err() {
delete[] data;
}
private:
int *data;
};
int main() {
// Memory Leak: forget release pointer b
int *a = new int;
int *b = new int[12];
delete a;
// Memory Leak: 0 as constructor parameter causes exception leaking
try {
Err* e = new Err(0);
delete e;
} catch (int &ex) {
std::cout << "Exception: " << ex << std::endl;
};
return 0;
}
If we comment out the #include "LeakDetector.hpp" code snippet, then this code can be run normally as shown below. However, of course we know that it actually leads to memory leaks:
The Result:
Design Memory Leak Detector
To achieve the memory leak detection, let's consider the following points:
Memory leaks are generated if delete has not been performed after the new operation.
The destructor of the first created object always executes at the end.
Then, we can perform the following actions corresponding to the two points above:
Overload new operator.
Create a static object and call the destructor at the end of the original program.
The benefit of two such steps is that memory leak detection can be done without modifying the original code. Thus, we can write LeakDetector.hpp:
//
// /home/labex/Code/LeakDetector.hpp
//
#ifndef __LEAK_DETECTOR__
#define __LEAK_DETECTOR__
void* operator new(size_t _size, char *_file, unsigned int _line);
void* operator new[](size_t _size, char *_file, unsigned int _line);
// This macro will be described in LeakDetector.cpp
#ifndef __NEW_OVERLOAD_IMPLEMENTATION__
#define new new(__FILE__, __LINE__)
#endif
class _leak_detector
{
public:
static unsigned int callCount;
_leak_detector() noexcept {
++callCount;
}
~_leak_detector() noexcept {
if (--callCount == 0)
LeakDetector();
}
private:
static unsigned int LeakDetector() noexcept;
};
static _leak_detector _exit_counter;
#endif
Why to design a callCount? The callCount ensures that our LeakDetector can only be invoked once. Consider the following simplified code snippet:
// main.cpp
#include <iostream>
#include "test.h"
int main () {
return 0;
}
// test.hpp
#include <iostream>
class Test {
public:
static unsigned int count;
Test () {
++count;
std::cout << count << ",";
}
};
static Test test;
// test.cpp
#include "test.hpp"
unsigned int Test::count = 0;
The final output is 1, 2,.
This is because the static variables in the header file are defined multiple times, and they will be defined every time they are included in a .cpp file. (In case, it will be included in main.cpp and test.cpp.) Nevertheless, essentially they are the same object.
Now, the remaining question is: How to implement a memory leak detector?
To Implement a Memory Leak Detector
Let's implement our memory leak detector.
Since we have already overloaded the operator new, it is easy to think about managing memory allocation manually and releasing memory naturally. If our delete doesn't release all the allocated memory, then memory leak will happen. Now, the question is: What structure should we use to manage memory manually?
Two-way linked list is a good option. For actual code, we don't know when we need to allocate memory, thus one-way linear list is not good enough, but a dynamic structure (e.g., a linked list) will be much more convenient.
Moreover, one-way linear list is also not convenient when deleting the object in memory leak detector.
Thus, two-way linked list is our best option at the moment:
#include <iostream>
#include <cstring>
// Defining __NEW_OVERLOAD_IMPLEMENTATION__ macros,
// it prevents overwrite operator `new`
#define __NEW_OVERLOAD_IMPLEMENTATION__
#include "LeakDetector.hpp"
typedef struct _MemoryList {
struct _MemoryList *next, *prev;
size_t size; // size of allocated memory
bool isArray; // either allocated array or not
char *file; // locating leaking file
unsigned int line; // locating leaking line
} _MemoryList;
static unsigned long _memory_allocated = 0; // saving unreleased memory size
static _MemoryList _root = {
&_root, &_root, // pointers of first element all pointing to itself
0, false, // 0 allocated memory size, not array
NULL, 0 // no file, line 0
};
unsigned int _leak_detector::callCount = 0;
Now, let's manage the allocated memory manually:
// allocating memory starts from the head of _MemoryList
void* AllocateMemory(size_t _size, bool _array, char *_file, unsigned _line) {
// calculate new size
size_t newSize = sizeof(_MemoryList) + _size;
// we can only use malloc for allocation since new has already been overwited.
_MemoryList *newElem = (_MemoryList*)malloc(newSize);
newElem->next = _root.next;
newElem->prev = &_root;
newElem->size = _size;
newElem->isArray = _array;
newElem->file = NULL;
// save file info if exitsting
if (_file) {
newElem->file = (char *)malloc(strlen(_file)+1);
strcpy(newElem->file, _file);
}
// save line number
newElem->line = _line;
// update list
_root.next->prev = newElem;
_root.next = newElem;
// save to unreleased memory size
_memory_allocated += _size;
// return allocated memory
return (char*)newElem + sizeof(_MemoryList);
}
and operator delete as well:
void DeleteMemory(void* _ptr, bool _array) {
// back to the begining of MemoryList
_MemoryList *currentElem = (_MemoryList *)((char *)_ptr - sizeof(_MemoryList));
if (currentElem->isArray != _array) return;
// update list
currentElem->prev->next = currentElem->next;
currentElem->next->prev = currentElem->prev;
_memory_allocated -= currentElem->size;
// release allocated memory for file information
if (currentElem->file) free(currentElem->file);
free(currentElem);
}
Considering we have two different ways to use new and delete:
Finally, we need to implement _leak_detector::LeakDetector and it will be called in the destructor of static _leak_detector _exit_counter. Then all the allocated objects will be released. The memory leak appears in the case static _MemoryList _root is not empty, and we only need to iterate _root to find where the leak of memory is:
unsigned int _leak_detector::LeakDetector(void) noexcept {
unsigned int count = 0;
// iterates the whole list, if there is a memory leak,
// then _LeakRoot.next alwasy doesn't pointing to itself
_MemoryList *ptr = _root.next;
while (ptr && ptr != &_root)
{
// output all leaking information
if(ptr->isArray)
std::cout << "Leaking[] ";
else
std::cout << "Leaking ";
std::cout << ptr << ", size of " << ptr->size;
if (ptr->file)
std::cout << " (located in " << ptr->file << ", line " << ptr->line << ")";
else
std::cout << " (no file info)";
std::cout << std::endl;
++count;
ptr = ptr->next;
}
if (count)
std::cout << "Existing " << count << " memory leaking behavior, "<< _memory_allocated << " byte in total." << std::endl;
return count;
}
In main.cpp, uncomment the include "LeakDetector.hpp". Here is the expected output:
Summary
In this lab, we overwrite the operator new in C++ and implement a memory leak detector.
In fact, the code can also be used for smart pointer circular reference leaking. We only need to change main.cpp as following:
#include <memory> // use smart pointer
// no changes at the begining as previous in main.cpp
// add two testing class
class A;
class B;
class A {
public:
std::shared_ptr<B> p;
};
class B {
public:
std::shared_ptr<A> p;
};
int main() {
// changes here as previous in main.cpp
// additional cases, use smart pointer
auto smartA = std::make_shared<A>();
auto smartB = std::make_shared<B>();
smartA->p = smartB;
smartB->p = smartA;
return 0;
}
Our final outputs are:
For some of you, maybe the result is:
Your result may be different. Each of the two above is right, because your environment may have difference in machine and compiler.
We use cookies for a number of reasons, such as keeping the website reliable and secure, to improve your experience on our website and to see how you interact with it. By accepting, you agree to our use of such cookies. Privacy Policy