Memory Leak Detector with C++

C++C++Beginner
Practice Now

Introduction

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

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL cpp(("`C++`")) -.-> cpp/IOandFileHandlingGroup(["`I/O and File Handling`"]) cpp(("`C++`")) -.-> cpp/SyntaxandStyleGroup(["`Syntax and Style`"]) cpp(("`C++`")) -.-> cpp/BasicsGroup(["`Basics`"]) cpp(("`C++`")) -.-> cpp/ControlFlowGroup(["`Control Flow`"]) cpp(("`C++`")) -.-> cpp/AdvancedConceptsGroup(["`Advanced Concepts`"]) cpp(("`C++`")) -.-> cpp/FunctionsGroup(["`Functions`"]) cpp(("`C++`")) -.-> cpp/OOPGroup(["`OOP`"]) cpp/IOandFileHandlingGroup -.-> cpp/output("`Output`") cpp/SyntaxandStyleGroup -.-> cpp/comments("`Comments`") cpp/BasicsGroup -.-> cpp/variables("`Variables`") cpp/BasicsGroup -.-> cpp/data_types("`Data Types`") cpp/BasicsGroup -.-> cpp/operators("`Operators`") cpp/BasicsGroup -.-> cpp/booleans("`Booleans`") cpp/ControlFlowGroup -.-> cpp/conditions("`Conditions`") cpp/ControlFlowGroup -.-> cpp/while_loop("`While Loop`") cpp/BasicsGroup -.-> cpp/arrays("`Arrays`") cpp/AdvancedConceptsGroup -.-> cpp/structures("`Structures`") cpp/AdvancedConceptsGroup -.-> cpp/pointers("`Pointers`") cpp/FunctionsGroup -.-> cpp/function_parameters("`Function Parameters`") cpp/OOPGroup -.-> cpp/classes_objects("`Classes/Objects`") cpp/OOPGroup -.-> cpp/access_specifiers("`Access Specifiers`") cpp/AdvancedConceptsGroup -.-> cpp/exceptions("`Exceptions`") subgraph Lab Skills cpp/output -.-> lab-178620{{"`Memory Leak Detector with C++`"}} cpp/comments -.-> lab-178620{{"`Memory Leak Detector with C++`"}} cpp/variables -.-> lab-178620{{"`Memory Leak Detector with C++`"}} cpp/data_types -.-> lab-178620{{"`Memory Leak Detector with C++`"}} cpp/operators -.-> lab-178620{{"`Memory Leak Detector with C++`"}} cpp/booleans -.-> lab-178620{{"`Memory Leak Detector with C++`"}} cpp/conditions -.-> lab-178620{{"`Memory Leak Detector with C++`"}} cpp/while_loop -.-> lab-178620{{"`Memory Leak Detector with C++`"}} cpp/arrays -.-> lab-178620{{"`Memory Leak Detector with C++`"}} cpp/structures -.-> lab-178620{{"`Memory Leak Detector with C++`"}} cpp/pointers -.-> lab-178620{{"`Memory Leak Detector with C++`"}} cpp/function_parameters -.-> lab-178620{{"`Memory Leak Detector with C++`"}} cpp/classes_objects -.-> lab-178620{{"`Memory Leak Detector with C++`"}} cpp/access_specifiers -.-> lab-178620{{"`Memory Leak Detector with C++`"}} cpp/exceptions -.-> lab-178620{{"`Memory Leak Detector with C++`"}} end

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:

image desc

Design Memory Leak Detector

To achieve the memory leak detection, let's consider the following points:

  1. Memory leaks are generated if delete has not been performed after the new operation.
  2. 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:

  1. Overload new operator.
  2. 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:

void* operator new(size_t _size) {
    return AllocateMemory(_size, false, NULL, 0);
}
void* operator new[](size_t _size) {
    return AllocateMemory(_size, true, NULL, 0);
}
void* operator new(size_t _size, char *_file, unsigned int _line) {
    return AllocateMemory(_size, false, _file, _line);
}
void* operator new[](size_t _size, char *_file, unsigned int _line) {
    return AllocateMemory(_size, true, _file, _line);
}
// overwrite operator delete
void operator delete(void *_ptr) noexcept {
    DeleteMemory(_ptr, false);
}
void operator delete[](void *_ptr) noexcept {
    DeleteMemory(_ptr, true);
}

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;
}

Let's compile everything:

g++ main.cpp LeakDetector.cpp -std=c++11 -Wno-write-strings

In main.cpp, uncomment the include "LeakDetector.hpp". Here is the expected output:

image desc

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:

image desc

For some of you, maybe the result is:
image desc

Your result may be different. Each of the two above is right, because your environment may have difference in machine and compiler.

Other C++ Tutorials you may like