Hoist-Point.com

Checklist: Nine most frequent memory allocation errors in C/C++ made by developers

Over last serveral years I have been working at a large company with substantial C/C++ codebase created over last thirty years or so. I am myself not new to C/C++, and of course went through a number of C/C++ memory errors of my own making. Working with this codebase however gave me a good insight about typical memory related issues often overlooked by developers.

C/C++ is different from many higher-level programming languages when it comes to memory. It does not have automatic memory nanagement or garbage collection, as Java or Python, for example. Programmers have direct access to memory and overall are responsible for all memory management, which can sometimes be tricky. This is the price C/C++ developers pay for efficiency this language can deliver.

With this semi-formal part over, let's jump to some examples on out checklist.

1. Forgetting to initialize newly allocated memory.

The following code should print a number of digits passed as numbers (0-9) in integer array.


#include <stdio.h>
#include <stdlib.h>

void printDigits (int *digits, int count) // buggy version
{
    // allocate memory for "count" digits plus terminating zero.

    char *buffer = (char *)malloc (count + 1);

    for (int i=0; i < count; i++)
    {
        buffer[i] = digits[i] + '0';
    }
    
    printf ("%s\n", buffer);  // buffer is not zero-terminated as it should be.
    free (buffer);
}

The problem with this code is that the final character in buffer (allocated by malloc) is never initialized and contains garbage. Because C/C++ uses zero as string terminating indicator, the string (buffer) we use in printf does not have correct ending and printf will print garabge, possibly accessing invalid memory (past the boundaries of buffer) which may end up crashing the program.

Corrected code:


void printDigits (int *digits, int count) // corrected version
{
    char *buffer = (char *)malloc (count + 1);

    for (int i=0; i < count; i++)
    {
        buffer[i] = digits[i] + '0';
    }

    buffer[count] = '\0'; // put terminating zero.
    printf ("%s\n", buffer);
    free (buffer);
}

Let's consider another example. We want to output comma-separated numbers:

#include <string.h>

void printCommaSeparatedNumbers (unsigned short *numbers, int count) // buggy version
{
    // largest unsigned short has 5 decimal digits
    // add one character for comma and terminating zero.
    
    char *buffer = (char *)malloc ((count * 6) + 1); 

    for (int i=0; i < count; i++)
    {
        if (i > 0)
            strcat (buffer, ",");

        char temp[16];
        sprintf(temp, "%u", (unsigned)numbers[i]);
        strcat (buffer, temp);
    }

    printf ("%s\n", buffer);
    free (buffer);
}

On the surface there's nothing wrong with this example! Except the initial characters in buffer contain garbage, which means that the first strcat() will concatenate not at buffer's position zero, but at the position where it finds the first null character. Which may be past the size of the buffer we allocated - leading to a crash.

The correct version is of course this:

void printCommaSeparatedNumbers (unsigned short *numbers, int count) // fixed version
{
    char *buffer = (char *)malloc ((count * 6) + 1);
    
    buffer [0] = 0; // make sure buffer starts with zero!
    
    for (int i=0; i < count; i++)
    {
        if (i > 0)
            strcat (buffer, ",");

        char temp[16];
        sprintf(temp, "%u", (unsigned)numbers[i]);
        strcat (buffer, temp);
    }

    printf ("%s\n", buffer);
    free (buffer);
}

Please note: the example above is given to illustrate the memory issue, not to show the most efficient C++ code.

The printCommaSeparatedNumbers() version example above can be improved by a lot (especially when "count" parameter is large) by replacing strcat() calls, which look for string terminating zero before appending a string - something that will take longer and longer as our string grows, with directly writing symbols to buffer and keeping track of the string length:

// much more efficient version for large "count"

void printCommaSeparatedNumbers (unsigned short *numbers, int count)
{
    // largest unsigned short number has 5 decimal digits 
    // add one character for comma and terminating zero.
    
    char *buffer = (char *)malloc ((count * 6) + 1);

    size_t pos = 0;  // length of the resulting string. 
        
    for (int i = 0; i < count; i++)
    {
        if (i > 0)
        {
            buffer[pos++] = ',';
        }

        char temp[16];
        sprintf (temp, "%u", (unsigned)numbers[i]);

        for (int j = 0; j < strlen(temp); j++)
        {
            buffer[pos++] = temp[j];
        }
    }

    buffer[pos] = '\0'; // terminate string

    printf ("%s\n", buffer);
    free (buffer);
}

Sometimes it's not easy to determine the index of the null-terminating character that must end the string. A simple solution that always works is to initilalize entire buffer with zeroes right after allocating it:

// also correct albeit somewhat slower version

void printCommaSeparatedNumbers (unsigned short *numbers, int count)
{
    size_t size = count * 6 + 1;

    char *buffer = (char *)malloc (size);
    
    memset (buffer, 0, size); // now buffer we allocated contains zeroes, 
    // so we don't need to worry where to put terminating zero, 
    // even when concatenating functions don't do it for us.

    for (int i=0; i < count; i++)
    {
        if (i > 0)
            strcat (buffer, ",");

        char temp[16];
        sprintf(temp, "%u", (unsigned)numbers[i]);
        strcat (buffer, temp);
    }

    printf ("%s\n", buffer);
    free (buffer);
}

The same issue applies not only to dynamically allocated memory with malloc() but also to memory on stack. Stack memory is uninitialized by default (this can differ between debug and release builds, leading to even more confusion) and will contain garbage. However, C++ makes it easier to initialize stack allocated arrays:

The following three parts accomplish the same:

(1)
int array[10] = { 0 }; // initializes all ten members of the array with zeroes.

(2)
int array[10];
memset (array, 0, sizeof(array));

(3)
int array[10];
memset (array, 0, 10 * sizeof(int));

Finally, C++ makes it also simple to initilize POD (Plain Old Data) types such as int,char,long,short etc. with zeroes using default constructor:

int *array = new int[10](); // this fills all 10 values in array with zeroes, default value for integer type.

With C, you can also use calloc() as opposed to malloc() to allocate memory. It has two advantages:
  1. there's no need to manually calculate the total number of bytes to allocate (by multiplying number of elements and element size, something that can integer-overflow)
  2. your newly allocated memory gets filled with zeroes.
The two constructs are identical:

(1)
int * array = (int *)malloc (10 * sizeof(int));
if (array)
    memset (array, 0, 10 * sizeof(nt));

(2)
int * array = (int *)calloc(10, sizeof(int));

2. Mixing delete and delete[].

Memory allocated with `new type[]` operation must be always released with delete[]. This bug is much worse than bug #1, since it may not immediately lead to program crashes (something that makes #1 easier to catch), but to memory leaks or memory corruption with subsequent crash in completely different section of the program. Example:

void printDigits (int *digits, int count) // wrong version
{
    char *buffer = new char [count + 1];
    for (int i=0; i < count; i++)
    {
        buffer[i] = digits[i] + '0';
    }
    buffer[count] = '\0';
    printf ("%s\n", buffer);
    delete buffer; // we are not deleting array of chars correctly.
}

Correct version:

void printDigits (int *digits, int count)
{
    char *buffer = new char [count + 1];
    for (int i=0; i < count; i++)
    {
        buffer[i] = digits[i] + '0';
    }
    buffer[count] = '\0';
    printf ("%s\n", buffer);
    delete [] buffer; // note the square brackets.
}

3. Mixing C and C++ memory management functions. Never mix malloc() and delete, new and free().

Memory allocated by malloc() and family (calloc, realloc) must be de-allocated with free(). Memory allocated with "new" must be deallocated with delete. In the following example, developer apparently did not realize that free() and delete are different things:

void printDigits (int *digits, int count) // buggy version
{
    char *buffer = (char *)malloc (count + 1);
    for (int i=0; i < count; i++)
    {
        buffer[i] = digits[i] + '0';
    }
    buffer[count] = '\0';
    printf ("%s\n", buffer);
    delete buffer;  // we are not deallocating array of chars correctly. 
                    // free() must be used here instead to match malloc().
}

This bug is also frequent when C-style allocations with malloc() in legacy code get replaced by C++ allocations with new[], and developers forget to also replace free() with delete[].

void printDigits (int *digits, int count) // also a buggy version
{
    char *buffer = new char [count + 1]; // we "upgraded" to C++ here
    for (int i=0; i < count; i++)
    {
        buffer[i] = digits[i] + '0';
    }
    buffer[count] = '\0';
    printf ("%s\n", buffer);
    free (buffer);         // but forgot to replace free with delete[].
}

4. Assuming malloc() always allocates memory.

In all our examples above where I used malloc() I did not care to check if it returns a valid memory address. But malloc() is designed to return NULL when it cannot allocate requested memory. This is less of a concern on modern computers with lots of memory, but still can happen when your program consumes a lot of resources, has leaks (as large C/C++ programs often do - more on this below) or you're developing on devices with limited per-application memory, such as embedded software.

A well written professional software should check for failed memory allocations and handle them properly rather than simply crash.

C and C++ use different approaches here: in C++ you can rely on exceptions when allocating memory with new operator. Assuming that at some point this exception is caught (as it should be), the program will not crash and you can show out-of-memory error (possibly prompting the user to save her work or attempt saving it automatically if needed).

C language, on the other hand, does not have exceptions and must rely on explicit checks and function return codes. Our improved C version with return code:

#include <errno.h>

int printDigits (int *digits, int count) // improved version: notice int return.
{
    char *buffer = (char *)malloc (count + 1);

    if (!buffer)     // in case we ignore NULL and proceed, the program will crash 
    {                // after trying to write to memory with address zero. 
        return ENOMEM;
    }

    for (int i=0; i < count; i++)
    {
        buffer[i] = digits[i] + '0';
    }

    buffer[count] = '\0';
    printf ("%s\n", buffer);
    free (buffer);

    return 0;
}

Correspondingly, the calling function must check for errors:

int main()
{
    int digits[] = { 10, 22, 2021 };

    int err = printDigits (digits, 3);

    if (err != 0)
    {
        printf ("%s\n", strerror (err)); // print error message
    }

    return 0;
}

In our trivial example above we could have simply printed the error message inside printDigits() itself rather than returning error code. In practice, memory-allocating functions can be deep in the call stack and the calling function often would also have to exit on error, propagating the error code up the stack until it can be properly handled.

It's important to note that C or C++ dont force the programmer to check for function return codes. So when we changed the signature of printDigits() from void to return int/errno, this does not mean all functions calling it will automatically know to check for error. In a large project a function can be called from many places - so if we want to make sure we always check for possible errors, we can add error code parameter instead:

void printDigits (int *digits, int count, int & err) // add err as parameter instead:
{
    err = 0;
    char *buffer = (char *)malloc (count + 1);

    if (!buffer)
    {
        err = ENOMEM;
        return;
    }

    for (int i=0; i < count; i++)
    {
        buffer[i] = digits[i] + '0';
    }

    buffer[count] = '\0';
    printf ("%s\n", buffer);
    free (buffer);
}

Now when you recompile your project, compiler will force you to add "err" parameter to every call of printDigits () showing you all places where a check for error must be added.

5. Wrongly assuming that failed C++ "new" will return NULL, just like malloc() does in C.

This is a typical C++ newbie programmer error:

int printDigits (int *digits, int count) // version with error.
{
    char *buffer = new char [count + 1];

    if (!buffer)   
    {
        return ENOMEM;  // the program will never get here.
    }

    for (int i=0; i < count; i++)
    {
        buffer[i] = digits[i] + '0';
    }
    buffer[count] = '\0';
    printf ("%s\n", buffer);
    delete [] buffer;

    return 0;
}

In case "normal" new operator fails to allocate memory, it throws std::bad_alloc exception rather than returning NULL. So in the above example, function printDigits() will never return ENOMEM because the exception will force immediate exit. If you intend to stick with C-style check, use (std::nothrow) with new instead:

int printDigits (int *digits, int count) // corrected version.
{
    char *buffer = new (std::nothrow) char [count + 1]; // now if new fails, 
		// buffer will be set to NULL (just like with malloc).

    if (!buffer)   
    {
        return ENOMEM;  
    }

    for (int i=0; i < count; i++)
    {
        buffer[i] = digits[i] + '0';
    }

    buffer[count] = '\0';
    printf ("%s\n", buffer);
    delete [] buffer;

    return 0;
}

6. Deleting the same memory more than once. Forgetting to set pointers to NULL after calling delete (or free).

A starightforward example looks like this:

int printDigits (int *digits, int count) // do you see error?
{
    char *buffer = new (std::nothrow) char [count + 1]; 

    if (!buffer)   
    {
        return ENOMEM;  
    }

    for (int i=0; i < count; i++)
    {
        buffer[i] = digits[i] + '0';
    }

    buffer[count] = '\0';
    printf ("%s\n", buffer);
    delete [] buffer; // now memory pointed by buffer is 
                      // deallocated and is "invalid". 

    // more code.

    delete [] buffer; // bug - we are "deleting" invalid memory.

    return 0;
}

A simple fix in this case is to always set the buffer to NULL after calling delete or free(). If you then call delete or free() again on pointer set to NULL, such call gets ignored and your program continues normally.

The more spaghetti-like code you have in your project, the more important is to set freed/dealloced memory pointers to zero to avoid accidentally using them after deallocation or calling free again.

Let's consider a less obvious example in C++:

class Holder // buggy version
{
    char *m_ptr;
    void cleanup ()
    {
        delete[] m_ptr;
    }
    public:

    Holder () : m_ptr(nullptr) {}
    ~Holder ()
    {
        cleanup ();
        delete[] m_ptr;
    }
    void doStuff ()
    {
        delete[] m_ptr;
        m_ptr = new char [256];
    }
};

There are actually two bugs in the above code. First, destructor calls cleanup() function which deallocates m_ptr. Then it calls delete[] on m_ptr again, which at this point is invalid. Let's correct this:

class Holder // better but still buggy
{
    char *m_ptr;
    void cleanup ()
    {
        delete[] m_ptr;
        m_ptr = nullptr; // it is good practice to set deallocated member 
                         // pointer to NULL if we're not in destructor.
    }
    public:

    Holder () : m_ptr(nullptr) {}
    ~Holder ()
    {
        cleanup ();
        delete[] m_ptr;
    }
    void doStuff ()
    {
        delete[] m_ptr;
        m_ptr = new char [256];
    }
};

Second, take a closer look at doStuff() function. It also deallocates memory pointed by m_ptr. Then it tries to allocate memory by calling new[]. What if this new[] call does not succeed and throws std::bad_alloc exception? The program then will at some point call class destructor, which will again try to deallocate m_ptr, which contains garbage at this point, possibly leading to a crash.

Here's corrected version:

class Holder // corrected version
{
    char *m_ptr;
    void cleanup ()
    {
        delete[] m_ptr;
        m_ptr = nullptr; 
    }
    public:

    Holder () : m_ptr(nullptr) {}
    ~Holder ()
    {
        delete[] m_ptr;
    }
    doStuff ()
    {
        cleanup();
        m_ptr = new char [256]; // now even if exception happens here, 
                                // m_ptr is already set to NULL.
    }
};

When your program uses multiple copies of pointers to dynamically allocated memory, it gets even harder to make sure you deallocate this memory only once. Your best approach in this case is to use C++ shared pointers which automate memory deallocation, and specifically designed to handle this.

7. Leaking memory with failed realloc().

Very frequently C/C++ programs using realloc have a construct like this:

class Holder 
{
    char *buffer;
    int m_size;

    public:
    Holder ()
    {
        m_size = 256;
        buffer = (char *)malloc (m_size);
    }
    ~Holder ()
    {
        free (buffer);
    }
    void someFunction (int new_size)
    {
        if (new_size > m_size)
        {
            buffer = (char *)realloc (buffer, new_size);
            m_size = new_size;
        }
    }
};

Everything works well in this example until realloc() returns NULL when it cannot allocate enough memory. When this happens, it does not free "old" memory pointed by buffer, and buffer pointer gets set to NULL, losing track of the memory we allocated before. This leads to a memory leak. The fix is to always check for realloc's return value and then assign the value of the pointer. Ideally we should also either return error on failure or throw std::bad_alloc exception:

    errno someFunction (int new_size) // C-style fix
    {
        if (new_size > m_size)
        {
            char *ptr = (char *)realloc (buffer, new_size);
            if (ptr)
            { 
                buffer = ptr;
                m_size = new_size;
            }
            else 
                return ENOMEM;
        }
        ...

        return 0;
    }


Or:

    #include <new>

    void someFunction (int new_size)    // C++ style fix using exceptions.
    {
        if (new_size > m_size)
        {
            char *ptr = (char *)realloc (buffer, new_size);

            if (!ptr) throw std::bad_alloc{};

            buffer = ptr;
            m_size = new_size;
        }
        ...
    }

Finally, if you are calling realloc on a local function pointer and exiting early on error, don't forget to free "old" memory when realloc fails:

void someFunction ()
{
    size_t size = 256;
    char *buffer = (char *)malloc (size);
    
    ...

    if (size < new_size)
    {
        char * ptr = (char *)realloc (buffer, new_size);
        if (!ptr)
        {
            free (buffer);
            
            throw std::bad_alloc{}; // function exits on throw, so
                                    // 'free' buffer before it is lost.
                                    
        }
        buffer = ptr;
        size = new_size;
    }

    free (buffer);
}

8. Failing to initialize extra memory allocated by realloc() when needed.

Realloc() is normally used to "increase" allocated memory size when already allocated memory is filled up. When realloc succeeds, it copies previously allocated bytes to new address and then frees old memory. What is important to remember is that just like "raw" malloc(), realloc does not initialize (fill with zeroes) the "extra memory". It initially will contain garbage. Let's take a look at this simple class containing a set of string pointers:

class SimpleStringArray // buggy code in addString function
{
    char **array;
    int current_size, set_size;

    public:

    SimpleStringArray (int initial_size = 100)
    {
        set_size = 0; // number of set string pointers.
        current_size = 0;

        array = (char **)malloc (initial_size * sizeof(char *));

        if (array)
        {
           memset (array, 0, initial_size * sizeof(char *));
           current_size = initial_size;
        }
    }

    ~SimpleStringArray ()
    {
        for (int i = 0; i < current_size; i++)
        {
            free (array[i]);
        }

        free (array);
    }

    void removeString (char *ptr)
    {
        for (int i = 0; i < current_size; i++)
        {
            if (ptr == array[i])
            {
                free (ptr);
                array[i] = NULL;
                set_size--;
                break;
            }
        }
    }

    char * addString (const char *s) // returning pointer to string copy
    {
        if (set_size == current_size)
        {
            char ** temp = (char **)realloc (array, 
                            (current_size + 100) * sizeof(char *));

            if (temp)
            {
                current_size += 100;
                array = temp; // the "new" part of the array is uninitialized & 
                              // contains garbage!
            }
            else 
                throw std::bad_alloc{};
        }

        for (int i=0; i < current_size; i++)
        {
            if (array[i] == NULL)   // at least one available slot should 
                                    // exist, right? Think twice.
            {
                array[i] = strdup (s);

                if (!array[i])
                    throw std::bad_alloc{};

                set_size++;
                return array[i];
            }
        }

        return NULL;
    }
};

Notice that this bug will most likely not be discovered until we call the SimpleStringArray's destructor which will try to call free() on bogus memory addresses. Of course, new strings "added" to the array with addString() method also will not get added because no available slots will be found.

Corrected and more reliable code :

    #include <assert.h>

    char * addString (const char *s) // returning pointer to string copy
    {
        if (set_size == current_size)
        {
            char ** temp = (char **)realloc (array, 
                                    (current_size + 100) * sizeof(char *));
            if (temp)
            {
                memset (temp + current_size, 0, 
                            100 * sizeof (char *)); // fill "new" part with zeroes.

                /* we can also use this instead:
                for (int i=0; i < 100; i++)
                    temp[i + current_size] = NULL;
                */

                current_size += 100;
                array = temp;
            }
            else 
                throw std::bad_alloc{};
        }

        for (int i = 0; i < current_size; i++)
        {
            if (array[i] == NULL) // at last one available slot should exist. 
            {
                array[i] = strdup (s);

                if (!array[i])
                    throw std::bad_alloc{};
                
                set_size++;
                return array[i];
            }
        }

        assert (0);  // we should never get here under normal circumstances.
    }

9. Leaks due to forgetting to dealloc local memory on early exits (because of errors or exceptions).

Even if you understand that all dynamically allocated memory must at some point be de-allocated, it is easy to overlook when using procedural C-style approach:

// let's imagine a function which allocates and returns string 
// lengths and prints one concatenated string, 
// and the callingFunction which uses it: 

int * someFunction (const char *strings [], int count, int & err)
{
    err = 0;

    unsigned int current_size = 256;

    int * element_lengths = (int *)malloc (sizeof(int) * count);

    if (!element_lengths)
    {
        err = ENOMEM;
        return NULL;
    }

    char * print_buffer = (char *)malloc (current_size);

    if (!print_buffer)
    {
        err = ENOMEM;
        return NULL; // memory leak. element_lengths has not been de-allocated.
    }

    print_buffer[0] = '\0';

    for (int i = 0; i < count; i++)
    {
        element_lengths [i] = strlen (strings[i]);

        if (strlen (print_buffer) + strlen (strings[i]) + 2 > current_size) 
        // allow extra character for comma and one for terminating zero
        {
            char *ptr = (char *)realloc (print_buffer, current_size + 256);

            if (!ptr)
            {
                free (print_buffer);
                err = ENOMEM;
                return NULL;  // leak here. we correctly deallocated print_buffer 
                              // but forgot about "element_lengths".
            }

            current_size += 256;
            print_buffer = ptr;
        }

        if (i > 0)
            strcat (print_buffer, ",");

        strcat (print_buffer, strings[i]);
    }

    printf ("%s\n", print_buffer);

    free (print_buffer);

    return element_lengths;
}

bool callingFunction ()
{
    const char * strings[] = { "January", "March", "May", "July", 
                               "August", "October", "December" };

    int err = 0;

    int count = sizeof(strings) / sizeof(strings[0]);

    int * lengths = someFunction (strings, count, err);

    if (err != 0)
    {
        return false;
    }

    if (0 != someOtherFunction (lengths, count))
    {
        free (lengths);
        return false; // possible leak here. 
                      // What if someOtherFunction() throws exception?
    }

    free (lengths);

    return true;
}

In our example, there are at least three possible issues. First, when we initially check allocated memory for print_buffer and element_lengths, we don't free element_length before exiting when print_buffer fails to allocate.

Second, in case realloc() fails in the middle of the loop, we dont call free on "element_lengths".

Finally, when someOtherFunction() is executed, an exception can happen, causing an early exit and "length" array never be de-allocated, resulting in a leak. This exampe works well - there are no leaks - under normal system load, when mallocs and reallocs return memory and no exceptions happen in someOtherFunction(). The problems start to occur when system load increases or exceptions start to happen.

Here is corrected version:

int * someFunction (const char *strings [], int count, int & err)
{
    err = 0;

    unsigned int current_size = 256;

    int * element_lengths = (int *)malloc (sizeof(int) * count);

    char * print_buffer = (char *)malloc (current_size);

    if (element_lengths == NULL || print_buffer == NULL)
    {
        free (element_lengths); // calling free() on NULL is safe.
        free (print_buffer);
        err = ENOMEM;
        return NULL; 
    }

    print_buffer[0] = '\0';

    for (int i = 0; i < count; i++)
    {
        element_lengths [i] = strlen (strings[i]);

        if (strlen (print_buffer) + strlen (strings[i]) + 2 > current_size) 
        // allow extra character for comma and for one terminating zero
        {
            char *ptr = (char *)realloc (print_buffer, current_size + 256);

            if (!ptr)
            {
                free (print_buffer);
                free (element_lengths);
                err = ENOMEM;
                return NULL; 
            }

            current_size += 256;
            print_buffer = ptr;
        }

        if (i > 0)
            strcat (print_buffer, ",");

        strcat (print_buffer, strings[i]);
    }

    printf ("%s\n", print_buffer);

    free (print_buffer);

    return element_lengths;
}

bool callingFunction ()
{
    const char * strings[] = { "January", "March", "May", "July", 
                               "August", "October", "December" };

    int err = 0;

    bool ret = true;

    int count = sizeof(strings) / sizeof(strings[0]);

    int * lengths = someFunction (strings, count, err);

    if (err != 0)
    {
        return false;
    }

    try 
    {
        if (0 != someOtherFunction (lengths, count))
        {
            free (lengths);
            return false;
        }
    }
    catch (...)
    {
        free (lengths);
        lengths = NULL;
        ret = false;
    }

    free (lengths);

    return ret;
}

As you can see from this example, making sure your C/C++ program is always leak-free can be difficult and require lots of attention when programming raw pointers. Should you put all function bodies in a try-catch blocks (as in our example with someOtherFunction)? May be it's just easier to take a care-free approach of letting program function correctly under normal load and let it crash when it cannot allocate memory?

Luckily, the modern C++ offers a better solution, using smart pointers and RAII programming idiom, which let you worry less about releasing resources. It is done automatically, and not just with memory but also with file pointers, handlers, sockets, and other resources - as they get out of scope. Still, older projects and legacy software cannot always easily be re-written to use this approach.

The modern C++ program would use std::vector and std::string to handle all that is required in our latest example:

#include <vector>
#include <string>

std::vector<int> someFunctions (const char *strings [], int count)
{
    std::vector<int> element_lengths;

    element_lengths.reserve (count);

    std::string print_buffer;

    for (int i = 0; i < count; i++)
    {
        element_lengths [i] = strlen (strings[i]);

        if (i > 0)
            print_buffer += ",";

        print_buffer += strings[i];
    }

    printf ("%s\n", print_buffer.c_str());

    // there's no need to do anything about print_buffer here. 
    // It will be released automatically as local variable.

    return element_lengths; // the copy of this vector will 
                            // be returned to the calling function.
}

bool callingFunction ()
{
    const char * strings[] = { "January", "March", "May", "July", 
                               "August", "October", "December" };

    int count = sizeof(strings) / sizeof(strings[0]);

    auto lengths = someFunction (strings, count);
    
    ..

    if (0 != someOtherFunction (lengths)) // here we pass vector 
                                          // which knows its size.
    {
        return false;
    }

    // again, here there's no need to deallocate lengths vector. 
    // It will be deallocated as it goes out of scope.
    
    return true;
}

This C++ version is way shorter and simpler! We replaced print_buffer array with std::string which automatically handles reallocation when it becomes too long; we also replaced element_lengths array with integer std::vector. It will free all the memory it uses as soon as it goes out of scope (the same applies to print_buffer std::string).

The out-of-memory std::bad_alloc exception still can happen as we add strings or even when we call reserve() function. In this case, the current function exits and the calling function or some function in the calling stack (main, as the last resort) should catch std::bad_alloc exception by showing or logging error and doing all proper cleanup.

This is of course very simple example, but it shows that C++, after all, offers some good things compared to old trusted C.
Finally, since I mentioned the use of smart pointers, let's take a look at the version which uses them in place of vectors and strings. It is more involved : notice the syntax to access "raw" pointers. However, just like with vectors exampe above, there are no explicit deallocations in this code. All memory gets released as we stop referencing it:

#include <memory>

std::unique_ptr<int[]> someFunction (const char *strings [], int count, int & err)
{
    err = 0;

    unsigned int current_size = 256;

    std::unique_ptr<int[]> element_lengths (new int[count]);

    std::unique_ptr<char[]> print_buffer (new char[current_size]);

    if (!print_buffer || !element_lengths)
    {
        err = ENOMEM;
        return NULL; 
    }

    print_buffer[0] = '\0';

    for (int i = 0; i < count; i++)
    {
        element_lengths [i] = strlen (strings[i]);

        if (strlen (print_buffer.get()) + strlen (strings[i]) + 2 > current_size) 
        // allow extra character for comma and for one terminating zero
        {
            char * larger_buffer = new (std::nothrow) char[current_size + 256];

            if (!larger_buffer) 
            {
                err = ENOMEM;
                return NULL;
            }

            strcpy (larger_buffer, print_buffer.get());

            print_buffer.reset (larger_buffer);

            current_size += 256;
        }

        if (i > 0)
            strcat (print_buffer.get(), ",");

        strcat (print_buffer.get(), strings[i]);
    }

    printf ("%s\n", print_buffer.get());

    return element_lengths; // yes, you can safely return std::unique_ptr.
}


bool callingFunction ()
{
    const char * strings[] = { "January", "March", "May", "July", 
                               "August", "October", "December" };

    int err = 0;

    int count = sizeof(strings) / sizeof(strings[0]);

    auto lengths = someFunction (strings, count, err);

    if (err != 0)
    {
        return false;
    }

    if (0 != someOtherFunction (lengths.get(), count))
    {
        return false; // no leak here. lengths is deallocated automatically.
    }
  

    for (int i=0; i < count; i++)
    {
        printf ("%d\n", lengths[i]);
    }

    return true;
}

C++ smart pointers make it easier to deal with memory leaks and duplicate deletions but come with their own set of caveats, which are beyond the scope of this article.

 
I hope you find this checklist useful. Have I missed some imporant class of memory issues related to dynamic memory allocations in C/C++? Write me for a chance to be added as #10 on this list.
Yuri Yakimenko,