Detecting Memory Leaks by Overriding New in C++

Introduction

Most modern C++ compilers, such as g++, have the ability to compile in tooling to detect memory leaks. In the case of g++ and gcc, the -fsanitize=address option will compile in tooling that hooks to memory allocations and print useful debugging information when there is a memory leak, use-after-free, null pointer de-reference and other memory issues. However, some compilers, such as those used for older or proprietary systems, do not have this functionality, making detecting and fixing memory leaks using dynamic analysis harder.

This article will explain how basic C++ features can be used to add similar, if more basic, functionality. The examples used will all compile using C++ 98 with the minimum number of required headers.

Overriding New and Delete

The new operator is used by C++ to allocate memory on the heap in a similar way to malloc in C. The delete operator is used to deallocate said memory, in a similar manner to free in C. Both of these operators can be overwritten to add additional tooling:

#include <iostream>
#include <cstdlib>

void* operator new(size_t size, const char* file, int line)
{
    void * ptr = malloc(size);
    std::cout << "New called in " << file << " on line " << line << std::endl;
    return ptr;
}

void operator delete(void* ptr)
{
    std::cout << "Delete called" << std::endl;
    free(ptr);
}

#define new new(__FILE__, __LINE__)

In the case above, the tooling is simply printing when new and delete get called. In C++ 11 the noexcept keyword should be added after delete(void* ptr).

Adding Useful Tooling

Just printing when new and delete get called isn’t all that useful. Instead, we want to keep an updated list of all allocated memory and where in our code it has been allocated. Then, at the end of our program, we can print out all the locations where we have leaked memory. We create a structure with the following data:

We will implement the list as a vector in its own library. The header file will define the API for printing the currently allocated memory as well as the overwritten new and delete operators:

#include <cstddef>

#ifndef __LEAK_CHECK_HH__
#define __LEAK_CHECK_HH__

void* operator new(size_t, const char*, int);
void operator delete(void*);
void PrintMemory();

#define new new(__FILE__, __LINE__)

#endif

The source file will then contain the implementation of these:

#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>

struct MemoryAllocation {
    void* ptr;
    std::string filename;
    int line;
};

static std::vector<MemoryAllocation> mem_al_;

void* operator new(size_t sz, const char* f, int line) {
    void* output = malloc(sz);

    MemoryAllocation mem;
    mem.ptr = output;
    mem.filename = std::string(f);
    mem.line = line;

    mem_al_.push_back(mem);
    
    return output;
}

void operator delete(void *ptr) {
    for (size_t i = 0; i < mem_al_.size(); i++) {
        if (mem_al_.at(i).ptr == ptr) {
            mem_al_.erase(mem_al_.begin() + i);
            return;
        }
    }
}

void PrintMemory() {
    std::cout << "Currently allocated memory: " << std::endl;
    for (size_t i = 0; i < mem_al_.size(); i++) {
        std::cout << mem_al_.at(i).filename << " " << mem_al_.at(i).line <<
                                                                     std::endl;
    }
}

An Example

We can test this implementation to see if it works with the following test:

#include <iostream>

#include "leak_check.hh"

class Object {
 public:
    Object() {
        std::cout << "Constructor Called" << std::endl;
    }

    ~Object() {
        std::cout << "Destructor Called" << std::endl;
    }
};

int main() {
    int* i = new int;
    int* j = new int;
    int* k = new int;
    Object* obj = new Object;

    delete obj;
    delete i;
    delete j;

    PrintMemory();
}

This can be compiled and run using the following:

g++ -std=c++98 -c leak_check.cpp -o leak_check.o
g++ -std=c++98 test.cpp leak_check.o -o test
./test

This produces the following output showing that constructors and destructors are still called using the overwritten new and delete operators, as well as showing that memory allocated on line 19 is actually being leaked:

Constructor Called
Destructor Called
Currently allocated memory: 
test.cpp 19

Limitations

For ultimate compatibility the presented implementation is not thread safe. In order to make it thread-safe, some form of mutex protection for the mem_al_ vector would be required. Using the C++ 11 threading library this could be implemented as follows:

static std::vector<MemoryAllocation> mem_al_;
std::mutex mtx_;

void* operator new(size_t sz, const char* f, int line) {
    void* output = malloc(sz);

    MemoryAllocation mem;
    mem.ptr = output;
    mem.filename = std::string(f);
    mem.line = line;

    mtx.lock();
    mem_al_.push_back(mem);
    mtx.unlock();

    return output;
}

void operator delete(void *ptr) {
    mtx.lock();
    for (size_t i = 0; i < mem_al_.size(); i++) {
        if (mem_al_.at(i).ptr == ptr) {
            mem_al_.erase(mem_al_.begin() + i);
            mtx.unlock();
            return;
        }
    }
    mtx.unlock();
}