Fundamentals 39 min read

How to Detect and Fix C++ Memory Leaks on Linux: Tools, Tips, and Code

This article explains what memory leaks are in C++ on Linux, why they matter, how they occur, and provides practical guidance on detecting them with tools like Valgrind, AddressSanitizer, GDB, and mtrace, followed by concrete solutions such as proper new/delete usage, smart pointers, RAII, custom allocators, and memory pools.

Deepin Linux
Deepin Linux
Deepin Linux
How to Detect and Fix C++ Memory Leaks on Linux: Tools, Tips, and Code

1. What Is a Memory Leak?

In Linux, a memory leak silently consumes program resources and can eventually crash the system. Understanding the principle of how leaks arise during manual memory management provides the foundation for prevention and resolution.

In simple terms, a memory leak occurs when allocated memory is not released after it is no longer needed, causing that memory to remain occupied and unavailable for other programs. This is analogous to borrowing a book from a library and never returning it, gradually reducing the library’s available collection.

1.1 Why Does Memory Usage Grow Excessively?

Memory leak: allocating memory without proper deallocation keeps the memory unusable for other code.

Frequent dynamic allocation and deallocation can cause fragmentation, making it hard for the system to manage physical memory efficiently.

Poor choice of data structures or algorithms with high space complexity leads to excessive memory consumption.

Uncleared caches: retaining cached data without size management consumes large memory blocks.

High‑concurrency resource contention: multiple threads allocating and freeing memory without proper synchronization can cause runaway memory usage.

Third‑party libraries or frameworks with internal leaks add to overall memory pressure.

1.2 Difference Between a Leak and High Memory Usage

A leak means dynamically allocated memory is never freed, permanently reducing available physical memory. High memory usage, however, can also stem from frequent allocations, inefficient data structures, or cache mismanagement, not just leaks.

When memory is correctly managed, temporary allocations do not accumulate; only leaks cause a steady increase in overall usage.

1.3 Root Causes

Dynamic storage variables require explicit release. Forgetting to free memory allocated with new, malloc, or similar functions is the primary source of leaks, especially when functions are called frequently.

2. Mechanism of Memory Leaks

2.1 C++ Memory Management Model

C++ distinguishes stack memory (automatically managed) and heap memory (manually managed). Stack memory holds local variables and function parameters and is released automatically when the function returns.

void stackMemoryExample() {
    int a = 10; // a resides on the stack
    // a is automatically released when the function ends
}

Heap memory is allocated with new (or malloc in C) and must be released with delete (or free). The heap size is limited only by available system memory.

void heapMemoryExample() {
    int* p = new int(20); // allocate on heap
    // use p
    delete p; // free heap memory
}

In C, malloc and free perform similar roles.

void mallocFreeExample() {
    int* p = (int*)malloc(sizeof(int));
    if (p != nullptr) {
        *p = 30;
        // use p
        free(p);
    }
}
new/delete

invoke constructors and destructors, while malloc/free only allocate raw memory.

2.2 How Leaks Form

A leak occurs when allocated memory is not released. Common scenarios include:

Simple forgetfulness: allocating memory and never calling delete.

void simpleLeak() {
    int* ptr = new int; // allocated
    // no delete -> leak
}

Pointer reassignment without freeing the original pointer.

void pointerReassignmentLeak() {
    int* ptr = new int(10);
    ptr = new int(20); // original memory lost
    delete ptr; // only frees second allocation
}

Using new[] but freeing with delete instead of delete[].

void arrayLeak() {
    int* arr = new int[10];
    // ...
    delete arr; // wrong, causes leak
}

Exceptions that bypass cleanup code.

void exceptionLeak() {
    int* ptr = new int;
    try {
        if (someCondition) throw std::exception();
    } catch (...) {
        // ptr not deleted -> leak
        throw;
    }
}

3. Tools for Detecting Leaks

3.1 Valgrind

Valgrind is a powerful open‑source memory checker. Install it with: sudo apt-get install valgrind Run a program:

valgrind --leak-check=full --show-leak-kinds=all ./test

The report highlights “definitely lost” blocks with file and line numbers.

3.2 AddressSanitizer (ASan)

ASan is built into GCC/Clang. Compile with:

g++ -fsanitize=address -g -O1 your_code.cpp -o your_program

When a leak occurs, ASan prints the error type, address, and stack trace.

3.3 GDB

Use GDB to set breakpoints and inspect memory allocation hooks:

gdb ./your_program
(gdb) break main
(gdb) run
(gdb) p *pointer_variable

Custom hooks can be installed to log allocations and frees.

3.4 mtrace

mtrace is a lightweight GNU libc component. Include <mcheck.h>, call mtrace() and muntrace(), and set MALLOC_TRACE to a log file.

#include <mcheck.h>
int main() {
    mtrace();
    // ... allocate memory ...
    muntrace();
    return 0;
}

Analyze the log with mtrace ./program trace.log.

4. How to Fix Leaks

4.1 Proper Use of Allocation Operators

Always match new with delete and new[] with delete[]:

void correctUsage() {
    int* p1 = new int;
    delete p1;
    int* p2 = new int[5];
    delete[] p2;
}

Incorrect pairing leads to leaks.

4.2 Smart Pointers

std::unique_ptr provides exclusive ownership and automatically deletes the object when it goes out of scope.

void uniquePtrDemo() {
    std::unique_ptr<int> ptr(new int(10));
    std::cout << *ptr << std::endl;
}

std::shared_ptr enables shared ownership with reference counting.

void sharedPtrDemo() {
    auto p1 = std::make_shared<int>(30);
    auto p2 = p1; // reference count becomes 2
}

std::weak_ptr breaks circular references by holding a non‑owning reference.

class B; // forward declaration
class A { public: std::shared_ptr<B> ptrB; };
class B { public: std::weak_ptr<A> ptrA; };
void solveCircularReference() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->ptrB = b;
    b->ptrA = a; // no cycle
}

4.3 RAII (Resource Acquisition Is Initialization)

Bind resource lifetime to object lifetime. Example for file handling:

class FileHandler {
public:
    FileHandler(const std::string& name) : file(name) {
        if (!file.is_open()) throw std::runtime_error("cannot open");
    }
    ~FileHandler() { file.close(); }
    std::ifstream& get() { return file; }
private:
    std::ifstream file;
};

When the object goes out of scope, the destructor releases the resource automatically.

4.4 Memory Pools

Pre‑allocate a large block and recycle fixed‑size chunks to avoid frequent new/delete calls.

class MemoryPool {
public:
    MemoryPool(size_t blockSize, size_t numBlocks) {
        pool = new char[blockSize * numBlocks];
        freeList = pool;
        for (size_t i = 0; i < numBlocks - 1; ++i) {
            *reinterpret_cast<char**>(freeList + i * blockSize) = freeList + (i + 1) * blockSize;
        }
        *reinterpret_cast<char**>(freeList + (numBlocks - 1) * blockSize) = nullptr;
    }
    ~MemoryPool() { delete[] pool; }
    void* allocate() {
        if (!freeList) return nullptr;
        void* block = freeList;
        freeList = *reinterpret_cast<char**>(freeList);
        return block;
    }
    void deallocate(void* ptr) {
        *reinterpret_cast<char**>(ptr) = freeList;
        freeList = reinterpret_cast<char*>(ptr);
    }
private:
    char* pool;
    char* freeList;
    size_t blockSize;
    size_t numBlocks;
};

4.5 Avoid Circular References

When two shared_ptr objects reference each other, their reference counts never reach zero. Replace one side with weak_ptr to break the cycle.

5. Practical Case Study

5.1 Simulated Leak Scenario

The following program creates a DatabaseConnection object that allocates a string but never frees it, and the caller also never deletes the object.

#include <iostream>
#include <cstring>
struct DatabaseConnection {
    char* connectionString;
    int connectionId;
    DatabaseConnection(const char* str, int id) {
        connectionString = new char[strlen(str) + 1];
        std::strcpy(connectionString, str);
        connectionId = id;
    }
    ~DatabaseConnection() {
        // leak: delete[] connectionString; // omitted on purpose
    }
};
DatabaseConnection* getDatabaseConnection() {
    static int count = 0;
    const char* connStr = "mysql://localhost:3306/mydb";
    return new DatabaseConnection(connStr, count++);
}
int main() {
    for (int i = 0; i < 10; ++i) {
        DatabaseConnection* conn = getDatabaseConnection();
        std::cout << "Using connection ID: " << conn->connectionId << std::endl;
        // leak: no delete conn;
    }
    return 0;
}

5.2 Locating the Leak

Compile with debug symbols and run Valgrind:

g++ -g -o leak_demo leak_demo.cpp
valgrind --leak-check=full --show-leak-kinds=all ./leak_demo

Valgrind reports a definite loss at the constructor line where new char[] is called.

ASan produces a similar stack trace when compiled with -fsanitize=address.

5.3 Fixing the Leak

Release the allocated string in the destructor and delete the object after use:

#include <iostream>
#include <cstring>
struct DatabaseConnection {
    char* connectionString;
    int connectionId;
    DatabaseConnection(const char* str, int id) {
        connectionString = new char[strlen(str) + 1];
        std::strcpy(connectionString, str);
        connectionId = id;
    }
    ~DatabaseConnection() {
        delete[] connectionString;
    }
};
DatabaseConnection* getDatabaseConnection() {
    static int count = 0;
    const char* connStr = "mysql://localhost:3306/mydb";
    return new DatabaseConnection(connStr, count++);
}
int main() {
    for (int i = 0; i < 10; ++i) {
        DatabaseConnection* conn = getDatabaseConnection();
        std::cout << "Using connection ID: " << conn->connectionId << std::endl;
        delete conn; // proper cleanup
    }
    return 0;
}

Re‑running Valgrind confirms that no leaks remain.

LinuxMemory LeakRAIIC++smart pointersvalgrindAddressSanitizer
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.