How to avoid memory leaks in C++?

How to prevent memory leaks in C++?

Contents

The reasons for memory leaks

Developers who have just started writing in C++ often ask how to prevent memory leaks. Indeed, it’s a common issue in C++ programs. Unlike languages created for managed platforms, such as C#, Java, and Python, C++ let allocating and freeing memory only manually, directly, or indirectly. In С++ there are operators operator new, operator new[], operator delete, and operator delete[]. Also, there are C-like functions: malloc, calloc, and free. Additionally, operating systems provide APIs to allocate memory. Windows API has HeapAlloc, HeapFree, VirtualAlloc, and VirtualFree. On Linux, there are mmap and munmap.

A memory leak occurs when a program allocates memory but never frees it. Why is it a problem? Well, memory available for a process is limited. That is why at a certain point, the program just fails to get a required memory block. After that, the process crashes.

Sometimes, memory is allocated only once at startup and not freed explicitly. One may say that the operating system frees all allocated memory when the process quits. However, a good developer’s practice is to free all allocated resources once they are not required. Even a single allocation that has been left until the program exits may lead to big problems in the future. Today it is just a single allocation, but a year later, another developer will decide to change the code, and then this allocation “lives” in some object.

Smart pointers

Fortunately, in C++, it is easy to manage memory safely with the help of smart pointers. A smart pointer is a class that allocates and frees memory internally; a developer who uses it may turn his mind to the program logic because smart pointers take care of memory allocations.

Usually, one needs two kinds of smart pointers, std::unique_ptr, and std::shared_ptr. std::unique_ptr is the most straightforward smart pointer. It takes ownership of allocated memory and releases it in the destructor. Unfortunately, std::unique_ptr doesn’t allow sharing ownership, but it’s very lightweight. In cases, you need to share ownership of allocated memory, use std::shared_ptr. Several instances of std::shared_ptr can hold the same memory; when the last instance containing a particular point is destroyed, the memory is released.

Some C++ libraries, such as boost, ATL, Qt, also provide their smart pointer implementations. It is up to a developer what to use; all you need to know is that using smart pointers is a C++ way to manage memory. Smart pointers allow you to forget about manual allocation and can help avoid most memory leaks.

On managed platforms, an object gets destroyed when there are no references to it. The garbage collection builds a graph of references to free objects even if they have references to each other. C++ doesn’t have garbage collection, so you may encounter a case when object #1 stores a smart pointer that owns a memory allocated for object #2. At the same time, object #2 holds object #1 via a smart pointer as well. You may imagine a tree of nodes with a collection of child nodes, std::list<std::shared_ptr<Node>>. Also, a node stores its parent node using std::shared_ptr<Node>.

Virtual destructors

Another common cause of leaks is using non-virtual destructors. Imagine, there is a class (Base) that has a field of std::list. When its destructor is called, destructors of its fields are called as well. So std::list::~std::list is called. So far, so good. Now let’s add a derived class (Derived) with another std::list. The code looks like the following:

class Base
{
public:
    Base() = default;
    ~Base() = default;

    void AddString(const std::string& value) { _strings.push_back(value); }

private:
    std::list<std::string> _strings;
};

class Derived : public Base
{
public:
    Derived() = default;
    ~Derived() = default;

    void AddValue(int value) { _integers.push_back(value); }

private:
    std::list<int> _integers;
};

Then let’s allocate Derived on the heap and store the pointer as Base*; free the memory using the operator delete:

int main()
{
    Base* object = new Derived();
    object->AddString("1");
    delete object;
}

The problem here is that the delete object calls Base::~Base; Derived::~Derived is not called at all! So destructors of Derived‘s fields are not called, and memory allocated for std::list is never freed. Making destructors virtual solves the issue:

class Base
{
public:
    Base() = default;
    virtual ~Base() = default;

    void AddString(const std::string& value) { _strings.push_back(value); }

private:
    std::list<std::string> _strings;
};

class Derived : public Base
{
public:
    Derived() = default;
    virtual ~Derived() = default;

    void AddValue(int value) { _integers.push_back(value); }

private:
    std::list<int> _integers;
};

Wrapping up

  1. Don’t use raw pointers or allocate memory directly; use smart pointers instead.
  2. Remember about cyclic references of smart pointers.
  3. Follow the rule of three; always define a virtual destructor.
  4. Use memory profilers to fix leaks: Valgrind on Linux, Deleaker on Windows.