7
Memory Management

WORKING WITH DYNAMIC MEMORY

Memory is a low-level component of the computer that sometimes unfortunately rears its head even in a high-level programming language like C++. Many programmers understand only enough about dynamic memory to get by. They shy away from data structures that use dynamic memory, or get their programs to work by trial and error. A solid understanding of how dynamic memory really works in C++ is essential to becoming a professional C++ programmer.

How to Picture Memory

Understanding dynamic memory is much easier if you have a mental model for what objects look like in memory. In this book, a unit of memory is shown as a box with a label next to it. The label indicates a variable name that corresponds to the memory. The data inside the box displays the current value of the memory.

For example, Figure 7-1 shows the state of memory after the following line is executed. The line should be in a function, so that i is a local variable:

int i = 7;

Illustration of the state of memory after the line "int i = 7;" is executed.

FIGURE 7-1

i is a so-called automatic variable allocated on the stack. It is automatically deallocated when the program flow leaves the scope in which the variable is declared.

When you use the new keyword, memory is allocated on the heap. The following code creates a variable ptr on the stack initialized with nullptr, and then allocates memory on the heap to which ptr points:

int* ptr = nullptr;
ptr = new int;

This can also be written as a one-liner:

int* ptr = new int;

Figure 7-2 shows the state of memory after this code is executed. Notice that the variable ptr is still on the stack even though it points to memory on the heap. A pointer is just a variable and can live on either the stack or the heap, although this fact is easy to forget. Dynamic memory, however, is always allocated on the heap.

Illustration of the state of memory after the "int* ptr = new int;" code is executed.

FIGURE 7-2

The next example shows that pointers can exist both on the stack and on the heap:

int** handle = nullptr;
handle = new int*;
*handle = new int;

The preceding code first declares a pointer to a pointer to an integer as the variable handle. It then dynamically allocates enough memory to hold a pointer to an integer, storing the pointer to that new memory in handle. Next, that memory (*handle) is assigned a pointer to another section of dynamic memory that is big enough to hold the integer. Figure 7-3 shows the two levels of pointers with one pointer residing on the stack (handle) and the other residing on the heap (*handle).

Illustration of two levels of pointers with one pointer residing on the stack (handle) and the other residing on the heap (*handle).

FIGURE 7-3

Allocation and Deallocation

To create space for a variable, you use the new keyword. To release that space for use by other parts of the program, you use the delete keyword. Of course, it wouldn’t be C++ if simple concepts such as new and delete didn’t have several variations and intricacies.

Using new and delete

When you want to allocate a block of memory, you call new with the type of variable for which you need space. new returns a pointer to that memory, although it is up to you to store that pointer in a variable. If you ignore the return value of new, or if the pointer variable goes out of scope, the memory becomes orphaned because you no longer have a way to access it. This is also called a memory leak.

For example, the following code orphans enough memory to hold an int. Figure 7-4 shows the state of memory after the code is executed. When there are blocks of data on the heap with no access, direct or indirect, from the stack, the memory is orphaned or leaked.

void leaky()
{
    new int;   // BUG! Orphans/leaks memory!
    cout << "I just leaked an int!" << endl;
}

Illustration of the state direct or indirect, from the stack, the memory is orphaned or leaked.

FIGURE 7-4

Until they find a way to make computers with an infinite supply of fast memory, you will need to tell the compiler when the memory associated with an object can be released and reused for another purpose. To free memory on the heap, you use the delete keyword with a pointer to the memory, as shown here:

int* ptr = new int;
delete ptr;
ptr = nullptr;

What about My Good Friend malloc?

If you are a C programmer, you may be wondering what is wrong with the malloc() function. In C, malloc() is used to allocate a given number of bytes of memory. For the most part, using malloc() is simple and straightforward. The malloc() function still exists in C++, but you should avoid it. The main advantage of new over malloc() is that new doesn’t just allocate memory, it constructs objects!

For example, consider the following two lines of code, which use a hypothetical class called Foo:

Foo* myFoo = (Foo*)malloc(sizeof(Foo));
Foo* myOtherFoo = new Foo();

After executing these lines, both myFoo and myOtherFoo will point to areas of memory on the heap that are big enough for a Foo object. Data members and methods of Foo can be accessed using both pointers. The difference is that the Foo object pointed to by myFoo isn’t a proper object because it was never constructed. The malloc() function only sets aside a piece of memory of a certain size. It doesn’t know about or care about objects. In contrast, the call to new allocates the appropriate size of memory and also calls an appropriate constructor to construct the object.

A similar difference exists between the free() function and the delete operator. With free(), the object’s destructor is not called. With delete, the destructor is called and the object is properly cleaned up.

When Memory Allocation Fails

Many, if not most, programmers write code with the assumption that new will always be successful. The rationale is that if new fails, it means that memory is very low and life is very, very bad. It is often an unfathomable state to be in because it’s unclear what your program could possibly do in this situation.

By default, your program will terminate if new fails. In many programs, this behavior is acceptable. The program exits when new fails because new throws an exception if there is not enough memory available for the request. Chapter 14 explains possible approaches to recover gracefully from an out-of-memory situation.

There is also an alternative version of new, which will not throw an exception. Instead, it will return nullptr, similar to the behavior of malloc() in C. The syntax for using this version is as follows:

int* ptr = new(nothrow) int;

Of course, you still have the same problem as the version that throws an exception—what do you do when the result is nullptr? The compiler doesn’t require you to check the result, so the nothrow version of new is more likely to lead to other bugs than the version that throws an exception. For this reason, it’s suggested that you use the standard version of new. If out-of-memory recovery is important to your program, the techniques covered in Chapter 14 give you all the tools you need.

Arrays

Arrays package multiple variables of the same type into a single variable with indices. Working with arrays quickly becomes natural to a novice programmer because it is easy to think about values in numbered slots. The in-memory representation of an array is not far off from this mental model.

Arrays of Basic Types

When your program allocates memory for an array, it is allocating contiguous pieces of memory, where each piece is large enough to hold a single element of the array. For example, a local array of five ints can be declared on the stack as follows:

int myArray[5];

Figure 7-5 shows the state of memory after the array is created. When creating arrays on the stack, the size must be a constant value known at compile time.

Illustration of state of memory after the array is created.

FIGURE 7-5

Declaring arrays on the heap is no different, except that you use a pointer to refer to the location of the array. The following code allocates memory for an array of five ints and stores a pointer to the memory in a variable called myArrayPtr:

int* myArrayPtr = new int[5];

Illustration of heap-based array.

FIGURE 7-6

As Figure 7-6 illustrates, the heap-based array is similar to the stack-based array, but in a different location. The myArrayPtr variable points to the 0th element of the array.

Each call to new[] should be paired with a call to delete[] to clean up the memory. For example,

delete [] myArrayPtr;
myArrayPtr = nullptr;

The advantage of putting an array on the heap is that you can define its size at run time. For example, the following code snippet receives a desired number of documents from a hypothetical function named askUserForNumberOfDocuments() and uses that result to create an array of Document objects.

Document* createDocArray()
{
    size_t numDocs = askUserForNumberOfDocuments();
    Document* docArray = new Document[numDocs];
    return docArray;
}

Remember that each call to new[] should be paired with a call to delete[], so in this example, it’s important that the caller of createDocArray() uses delete[] to clean up the returned memory. Another problem is that C-style arrays don’t know their size; thus callers of createDocArray() have no idea how many elements there are in the returned array!

In the preceding function, docArray is a dynamically allocated array. Do not get this confused with a dynamic array. The array itself is not dynamic because its size does not change once it is allocated. Dynamic memory lets you specify the size of an allocated block at run time, but it does not automatically adjust its size to accommodate the data.

There is a function in C++ called realloc(), which is a holdover from the C language. Do not use it! In C, realloc() is used to effectively change the size of an array by allocating a new block of memory of the new size, copying all of the old data to the new location, and deleting the original block. This approach is extremely dangerous in C++ because user-defined objects will not respond well to bitwise copying.

Arrays of Objects

Arrays of objects are no different than arrays of simple types. When you use new[N] to allocate an array of N objects, enough space is allocated for N contiguous blocks where each block is large enough for a single object. Using new[], the zero-argument (= default) constructor for each of the objects is automatically called. In this way, allocating an array of objects using new[] returns a pointer to an array of fully formed and initialized objects.

For example, consider the following class:

class Simple
{
    public:
        Simple() { cout << "Simple constructor called!" << endl; }
        ~Simple() { cout << "Simple destructor called!" << endl; }
};

If you allocate an array of four Simple objects, the Simple constructor is called four times.

Simple* mySimpleArray = new Simple[4];

The memory diagram for this array is shown in Figure 7-7. As you can see, it is no different than an array of basic types.

Illustration of a memory diagram for an array.

FIGURE 7-7

Deleting Arrays

As mentioned earlier, when you allocate memory with the array version of new (new[]), you must release it with the array version of delete (delete[]). This version automatically destructs the objects in the array in addition to releasing the memory associated with them.

Simple* mySimpleArray = new Simple[4];
// Use mySimpleArray …
delete [] mySimpleArray;
mySimpleArray = nullptr;

If you do not use the array version of delete, your program may behave in odd ways. With some compilers, only the destructor for the first element of the array will be called because the compiler only knows that you are deleting a pointer to an object, and all the other elements of the array will become orphaned objects. With other compilers, memory corruption may occur because new and new[] can use completely different memory allocation schemes.

Of course, the destructors are only called if the elements of the array are objects. If you have an array of pointers, you still need to delete each object pointed to individually just as you allocated each object individually, as shown in the following code:

const size_t size = 4;
Simple** mySimplePtrArray = new Simple*[size];

// Allocate an object for each pointer.
for (size_t i = 0; i < size; i++) { mySimplePtrArray[i] = new Simple(); }

// Use mySimplePtrArray …

// Delete each allocated object.
for (size_t i = 0; i < size; i++) { delete mySimplePtrArray[i]; }

// Delete the array itself.
delete [] mySimplePtrArray;
mySimplePtrArray = nullptr;

Multi-dimensional Arrays

Multi-dimensional arrays extend the notion of indexed values to multiple indices. For example, a Tic-Tac-Toe game might use a two-dimensional array to represent a three-by-three grid. The following example shows such an array declared on the stack, zero-initialized, and accessed with some test code:

char board[3][3] = {};
// Test code
board[0][0] = 'X';   // X puts marker in position (0,0).
board[2][1] = 'O';   // O puts marker in position (2,1).

You may be wondering whether the first subscript in a two-dimensional array is the x-coordinate or the y-coordinate. The truth is that it doesn’t really matter, as long as you are consistent. A four-by-seven grid could be declared as char board[4][7] or char board[7][4]. For most applications, it is easiest to think of the first subscript as the x-axis and the second as the y-axis.

Multi-dimensional Stack Arrays

In memory, a stack-based two-dimensional array looks like Figure 7-8. Because memory doesn’t have two axes (addresses are merely sequential), the computer represents a two-dimensional array just like a one-dimensional array. The difference is in the size of the array and the method used to access it.

Illustration of a stack-based two-dimensional array.

FIGURE 7-8

The size of a multi-dimensional array is all of its dimensions multiplied together, then multiplied by the size of a single element in the array. In Figure 7-8, the three-by-three board is 3 × 3 × 1 = 9 bytes, assuming that a character is 1 byte. For a four-by-seven board of characters, the array would be 4 × 7 × 1 = 28 bytes.

To access a value in a multi-dimensional array, the computer treats each subscript as if it were accessing another subarray within the multi-dimensional array. For example, in the three-by-three grid, the expression board[0] actually refers to the subarray highlighted in Figure 7-9. When you add a second subscript, such as board[0][2], the computer is able to access the correct element by looking up the second subscript within the subarray, as shown in Figure 7-10

Illustration of a three-by-three grid.

FIGURE 7-9

Illustration of a second subscript within the subarray.

FIGURE 7-10

These techniques are extended to N-dimensional arrays, though dimensions higher than three tend to be difficult to conceptualize and are rarely used.

Multi-dimensional Heap Arrays

If you need to determine the dimensions of a multi-dimensional array at run time, you can use a heap-based array. Just as a single-dimensional dynamically allocated array is accessed through a pointer, a multi-dimensional dynamically allocated array is also accessed through a pointer. The only difference is that in a two-dimensional array, you need to start with a pointer-to-a-pointer; and in an N-dimensional array, you need N levels of pointers. At first, it might seem as if the correct way to declare and allocate a dynamically allocated multi-dimensional array is as follows:

char** board = new char[i][j]; // BUG! Doesn't compile

This code doesn’t compile because heap-based arrays don’t work like stack-based arrays. Their memory layout isn’t contiguous, so allocating enough memory for a stack-based multi-dimensional array is incorrect. Instead, you can start by allocating a single contiguous array for the first subscript dimension of a heap-based array. Each element of that array is actually a pointer to another array that stores the elements for the second subscript dimension. This layout for a two-by-two dynamically allocated board is shown in Figure 7-11.

Illustration of a two-by-two dynamically allocated board.

FIGURE 7-11

Unfortunately, the compiler doesn’t allocate memory for the subarrays on your behalf. You can allocate the first-dimension array just like a single-dimensional heap-based array, but the individual subarrays must be explicitly allocated. The following function properly allocates memory for a two-dimensional array:

char** allocateCharacterBoard(size_t xDimension, size_t yDimension)
{
    char** myArray = new char*[xDimension]; // Allocate first dimension
    for (size_t i = 0; i < xDimension; i++) {
        myArray[i] = new char[yDimension];  // Allocate ith subarray
    }
    return myArray;
}

Similarly, when you want to release the memory associated with a multi-dimensional heap-based array, the array delete[] syntax will not clean up the subarrays on your behalf. Your code to release an array should mirror the code to allocate it, as in the following function:

void releaseCharacterBoard(char** myArray, size_t xDimension)
{
    for (size_t i = 0; i < xDimension; i++) {
        delete [] myArray[i];    // Delete ith subarray
    }
    delete [] myArray;           // Delete first dimension
}

Now that you know all the details to work with arrays, it is recommended to avoid these old C-style arrays as much as possible because they do not provide any memory safety. They are explained here because you will encounter them in legacy code. In new code, you should use the C++ Standard Library containers such as std::array, std::vector, and so on (see Chapter 17). For example, use vector<T> for a one-dimensional dynamic array, use vector<vector<T>> for a two-dimensional dynamic array, and so on. Of course, working directly with data structures such as vector<vector<T>> is still tedious, especially for constructing them. If you do need N-dimensional dynamic arrays in your application, it is recommended to write helper classes that provide an easier to use interface. For example, to work with two-dimensional data with equally long rows, you should consider writing (or reusing of course) a Matrix<T> or Table<T> class template which internally might use a vector<vector<T>> data structure. See Chapter 12 for details on writing class templates

Working with Pointers

Pointers get their bad reputation from the relative ease with which you can abuse them. Because a pointer is just a memory address, you could theoretically change that address manually, even doing something as scary as the following line of code:

char* scaryPointer = (char*)7;

This line builds a pointer to the memory address 7, which is likely to be random garbage or memory used elsewhere in the application. If you start to use areas of memory that weren’t set aside on your behalf, for example with new or on the stack, eventually you will corrupt the memory associated with an object, or the memory involved with the management of the heap, and your program will malfunction. Such a malfunction can manifest itself in several ways. For example, it can reveal itself as invalid results because the data has been corrupted, or as hardware exceptions being triggered due to accessing non-existent memory, or attempting to write to protected memory. If you are lucky, you will get one of the serious errors that usually result in program termination by the operating system or the C++ runtime library; if you are unlucky, you will just get wrong results.

A Mental Model for Pointers

There are two ways to think about pointers. More mathematically minded readers might view pointers as addresses. This view makes pointer arithmetic, covered later in this chapter, a bit easier to understand. Pointers aren’t mysterious pathways through memory; they are numbers that happen to correspond to a location in memory. Figure 7-12 illustrates a two-by-two grid in the address-based view of the world.

Illustration of a two-by-two grid in the address-based view of the world.

FIGURE 7-12

Readers who are more comfortable with spatial representations might derive more benefit from the “arrow” view of pointers. A pointer is a level of indirection that says to the program, “Hey! Look over there.” With this view, multiple levels of pointers become individual steps on the path to the data. Figure 7-11 shows a graphical view of pointers in memory.

When you dereference a pointer, by using the * operator, you are telling the program to look one level deeper in memory. In the address-based view, think of a dereference as a jump in memory to the address indicated by the pointer. With the graphical view, every dereference corresponds to following an arrow from its base to its head.

When you take the address of a location, using the & operator, you are adding a level of indirection in memory. In the address-based view, the program is noting the numerical address of the location, which can be stored as a pointer. In the graphical view, the & operator creates a new arrow whose head ends at the location designated by the expression. The base of the arrow can be stored as a pointer.

Casting with Pointers

Because pointers are just memory addresses (or arrows to somewhere), they are somewhat weakly typed. A pointer to an XML document is the same size as a pointer to an integer. The compiler will let you easily cast any pointer type to any other pointer type using a C-style cast:

Document* documentPtr = getDocument();
char* myCharPtr = (char*)documentPtr;

A static cast offers a bit more safety. The compiler refuses to perform a static cast on pointers to unrelated data types:

Document* documentPtr = getDocument();
char* myCharPtr = static_cast<char*>(documentPtr);   // BUG! Won't compile

If the two pointers you are casting are actually pointing to objects that are related through inheritance, the compiler will permit a static cast. However, a dynamic cast is a safer way to accomplish a cast within an inheritance hierarchy. Chapter 10 discusses inheritance in detail, while the different C++ style casts are discussed in Chapter 11.

ARRAY-POINTER DUALITY

You have already seen some of the overlap between pointers and arrays. Heap-allocated arrays are referred to by a pointer to their first element. Stack-based arrays are referred to by using the array syntax ([]) with an otherwise normal variable declaration. As you are about to learn, however, the overlap doesn’t end there. Pointers and arrays have a complicated relationship.

Arrays Are Pointers!

A heap-based array is not the only place where you can use a pointer to refer to an array. You can also use the pointer syntax to access elements of a stack-based array. The address of an array is really the address of the first element (index 0). The compiler knows that when you refer to an array in its entirety by its variable name, you are really referring to the address of the first element. In this way, the pointer works just like a heap-based array. The following code creates a zero-initialized array on the stack, and uses a pointer to access it:

int myIntArray[10] = {};
int* myIntPtr = myIntArray;
// Access the array through the pointer.
myIntPtr[4] = 5;

The ability to refer to a stack-based array through a pointer is useful when passing arrays into functions. The following function accepts an array of integers as a pointer. Note that the caller needs to explicitly pass in the size of the array because the pointer implies nothing about size. In fact, C++ arrays of any form, pointer or not, have no built-in notion of size. That is another reason why you should use modern containers such as those provided by the Standard Library.

void doubleInts(int* theArray, size_t size)
{
    for (size_t i = 0; i < size; i++) {
        theArray[i] *= 2;
    }
}

The caller of this function can pass a stack-based or heap-based array. In the case of a heap-based array, the pointer already exists and is passed by value into the function. In the case of a stack-based array, the caller can pass the array variable, and the compiler automatically treats the array variable as a pointer to the array, or you can explicitly pass the address of the first element. All three forms are shown here:

size_t arrSize = 4;
int* heapArray = new int[arrSize]{ 1, 5, 3, 4 };
doubleInts(heapArray, arrSize);
delete [] heapArray;
heapArray = nullptr;

int stackArray[] = { 5, 7, 9, 11 };
arrSize = std::size(stackArray);    // Since C++17, requires <array>
//arrSize = sizeof(stackArray) / sizeof(stackArray[0]); // Pre-C++17, see Ch1
doubleInts(stackArray, arrSize);
doubleInts(&stackArray[0], arrSize);

The parameter-passing semantics of arrays is uncannily similar to that of pointers, because the compiler treats an array as a pointer when it is passed to a function. A function that takes an array as an argument and changes values inside the array is actually changing the original array, not a copy. Just like a pointer, passing an array effectively mimics pass-by-reference functionality because what you really pass to the function is the address of the original array, not a copy. The following implementation of doubleInts() changes the original array even though the parameter is an array, not a pointer:

void doubleInts(int theArray[], size_t size)
{
    for (size_t i = 0; i < size; i++) {
        theArray[i] *= 2;
    }
}

Any number between the square brackets after theArray in the function prototype is simply ignored. The following three versions are identical:

void doubleInts(int* theArray, size_t size);
void doubleInts(int theArray[], size_t size);
void doubleInts(int theArray[2], size_t size);

You may be wondering why things work this way. Why doesn’t the compiler just copy the array when array syntax is used in the function definition? This is done for efficiency—it takes time to copy the elements of an array, and they potentially take up a lot of memory. By always passing a pointer, the compiler doesn’t need to include the code to copy the array.

There is a way to pass known-length stack-based arrays “by reference” to a function, although the syntax is non-obvious. This does not work for heap-based arrays. For example, the following doubleIntsStack() accepts only stack-based arrays of size 4:

void doubleIntsStack(int (&theArray)[4]);

A function template, discussed in detail in Chapter 12, can be used to let the compiler deduce the size of the stack-based array automatically:

template<size_t N>
void doubleIntsStack(int (&theArray)[N])
{
    for (size_t i = 0; i < N; i++) {
        theArray[i] *= 2;
    }
}

To summarize, arrays declared using array syntax can be accessed through a pointer. When an array is passed to a function, it is always passed as a pointer.

Not All Pointers Are Arrays!

Because the compiler lets you pass in an array where a pointer is expected, as in the doubleInts() function in the previous section, you may be led to believe that pointers and arrays are the same. In fact, there are subtle, but important, differences. Pointers and arrays share many properties and can sometimes be used interchangeably (as shown earlier), but they are not the same.

A pointer by itself is meaningless. It may point to random memory, a single object, or an array. You can always use array syntax with a pointer, but doing so is not always appropriate because pointers aren’t always arrays. For example, consider the following code:

int* ptr = new int;

The pointer ptr is a valid pointer, but it is not an array. You can access the pointed-to value using array syntax (ptr[0]), but doing so is stylistically questionable and provides no real benefit. In fact, using array syntax with non-array pointers is an invitation for bugs. The memory at ptr[1] could be anything!

LOW-LEVEL MEMORY OPERATIONS

One of the great advantages of C++ over C is that you don’t need to worry quite as much about memory. If you code using objects, you just need to make sure that each individual class properly manages its own memory. Through construction and destruction, the compiler helps you manage memory by telling you when to do it. Hiding the management of memory within classes makes a huge difference in usability, as demonstrated by the Standard Library classes. However, with some applications or with legacy code, you may encounter the need to work with memory at a lower level. Whether for legacy, efficiency, debugging, or curiosity, knowing some techniques for working with raw bytes can be helpful.

Pointer Arithmetic

The C++ compiler uses the declared types of pointers to allow you to perform pointer arithmetic. If you declare a pointer to an int and increase it by 1, the pointer moves ahead in memory by the size of an int, not by a single byte. This type of operation is most useful with arrays, because they contain homogeneous data that is sequential in memory. For example, assume you declare an array of ints on the heap:

int* myArray = new int[8];

You are already familiar with the following syntax for setting the value in position 2:

myArray[2] = 33;

With pointer arithmetic, you can equivalently use the following syntax, which obtains a pointer to the memory that is “2 ints ahead” of myArray and then dereferences it to set the value:

*(myArray + 2) = 33;

As an alternative syntax for accessing individual elements, pointer arithmetic doesn’t seem too appealing. Its real power lies in the fact that an expression like myArray + 2 is still a pointer to an int, and thus can represent a smaller int array. Suppose you have the following wide string. Wide strings are discussed in Chapter 19, but the details are not important at this point. For now, it is enough to know that wide strings support so-called Unicode characters to represent, for example, Japanese strings. The wchar_t type is a character type that can accommodate such Unicode characters, and it is usually bigger than a char (1 byte). To tell the compiler that a string literal is a wide-string literal, you prefix it with an L:

const wchar_t* myString = L"Hello, World";

Suppose you also have a function that takes in a wide string and returns a new string that contains a capitalized version of the input:

wchar_t* toCaps(const wchar_t* inString);

You can capitalize myString by passing it into this function. However, if you only want to capitalize part of myString, you can use pointer arithmetic to refer to only a latter part of the string. The following code calls toCaps() on the World part of the wide string by just adding 7 to the pointer, even though wchar_t is usually more than 1 byte:

toCaps(myString + 7);

Another useful application of pointer arithmetic involves subtraction. Subtracting one pointer from another of the same type gives you the number of elements of the pointed-to type between the two pointers, not the absolute number of bytes between them.

Custom Memory Management

For 99 percent of the cases you will encounter (some might say 100 percent of the cases), the built-in memory allocation facilities in C++ are adequate. Behind the scenes, new and delete do all the work of handing out memory in properly sized chunks, maintaining a list of available areas of memory, and releasing chunks of memory back to that list upon deletion.

When resource constraints are extremely tight, or under very special conditions, such as managing shared memory, implementing custom memory management may be a viable option. Don’t worry—it’s not as scary as it sounds. Basically, managing memory yourself means that classes allocate a large chunk of memory and dole out that memory in pieces as it is needed.

How is this approach any better? Managing your own memory can potentially reduce overhead. When you use new to allocate memory, the program also needs to set aside a small amount of space to record how much memory was allocated. That way, when you call delete, the proper amount of memory can be released. For most objects, the overhead is so much smaller than the memory allocated that it makes little difference. However, for small objects or programs with enormous numbers of objects, the overhead can have an impact.

When you manage memory yourself, you might know the size of each object a priori, so you might be able to avoid the overhead for each object. The difference can be enormous for large numbers of small objects. The syntax for performing custom memory management is described in Chapter 15.

Garbage Collection

At the other end of the memory hygiene spectrum lies garbage collection. With environments that support garbage collection, the programmer rarely, if ever, explicitly frees memory associated with an object. Instead, objects to which there are no longer any references will be cleaned up automatically at some point by the runtime library.

Garbage collection is not built into the C++ language as it is in C# and Java. In modern C++, you use smart pointers to manage memory, while in legacy code you will see memory management at the object level through new and delete. Smart pointers such as shared_ptr (discussed later in this chapter) provide something very similar to garbage-collected memory, that is, when the last shared_ptr instance for a certain resource is destroyed, at that point in time the resource is destroyed as well. It is possible but not easy to implement true garbage collection in C++, but freeing yourself from the task of releasing memory would probably introduce new headaches.

One approach to garbage collection is called mark and sweep. With this approach, the garbage collector periodically examines every single pointer in your program and annotates the fact that the referenced memory is still in use. At the end of the cycle, any memory that hasn’t been marked is deemed to be not in-use and is freed.

A mark-and-sweep algorithm could be implemented in C++ if you were willing to do the following:

  1. Register all pointers with the garbage collector so that it can easily walk through the list of all pointers.
  2. Derive all objects from a mixin class, perhaps GarbageCollectible, that allows the garbage collector to mark an object as in-use.
  3. Protect concurrent access to objects by making sure that no changes to pointers can occur while the garbage collector is running.

As you can see, this approach to garbage collection requires quite a bit of diligence on the part of the programmer. It may even be more error-prone than using delete! Attempts at a safe and easy mechanism for garbage collection have been made in C++, but even if a perfect implementation of garbage collection in C++ came along, it wouldn’t necessarily be appropriate to use for all applications. Among the downsides of garbage collection are the following:

  • When the garbage collector is actively running, the program might become unresponsive.
  • With garbage collectors, you have so-called non-deterministic destructors. Because an object is not destroyed until it is garbage-collected, the destructor is not executed immediately when the object leaves its scope. This means that cleaning up resources (such as closing a file, releasing a lock, and so on), which is done by the destructor, is not performed until some indeterminate time in the future.

Writing a garbage collection mechanism is very hard. You will undoubtedly do it wrong, it will be error prone, and more than likely slow. So, if you do want to use garbage-collected memory in your application, I highly recommend you to research existing specialized garbage-collection libraries that you can reuse. See Chapter 4 for a discussion on code reuse.

Object Pools

Garbage collection is like buying plates for a picnic and leaving any used plates out in the yard where the wind will conveniently blow them into the neighbor’s yard. Surely, there must be a more ecological approach to memory management.

Object pools are the equivalent of recycling. You buy a reasonable number of plates, and after using a plate, you clean it so that it can be reused later. Object pools are ideal for situations where you need to use many objects of the same type over time, and creating each one incurs overhead.

Chapter 25 contains further details on using object pools for performance efficiency.

SMART POINTERS

Memory management in C++ is a perennial source of errors and bugs. Many of these bugs arise from the use of dynamic memory allocation and pointers. When you extensively use dynamic memory allocation in your program and pass many pointers between objects, it’s difficult to remember to call delete on each pointer exactly once and at the right time. The consequences of getting it wrong are severe: when you free dynamically allocated memory more than once, you can cause memory corruption or a fatal run-time error, and when you forget to free dynamically allocated memory, you cause memory leaks.

Smart pointers help you manage your dynamically allocated memory and are the recommended technique for avoiding memory leaks. Conceptually, a smart pointer can hold a dynamically allocated resource, such as memory. When a smart pointer goes out of scope or is reset, it can automatically free the resource it holds. Smart pointers can be used to manage dynamically allocated resources in the scope of a function, or as data members in classes. They can also be used to pass ownership of dynamically allocated resources through function arguments.

C++ provides several language features that make smart pointers attractive. First, you can write a type-safe smart pointer class for any pointer type using templates, see Chapter 12. Second, you can provide an interface to the smart pointer objects using operator overloading, see Chapter 15, that allows code to use the smart pointer objects as if they were dumb pointers. Specifically, you can overload the * and -> operators such that client code can dereference a smart pointer object the same way it dereferences a normal pointer.

There are several kinds of smart pointers. The simplest type of smart pointer takes sole/unique ownership of the resource, and when the smart pointer goes out of scope or is reset, it frees the referenced resource. The Standard Library provides std::unique_ptr which is a smart pointer with unique ownership semantics.

However, managing pointers presents more problems than just remembering to free them when they go out of scope. Sometimes several objects or pieces of code contain copies of the same pointer. This problem is called aliasing. In order to free all resources properly, the last piece of code to use the resource should free the resource pointed to by the pointer. However, it is sometimes difficult to know which piece of code uses the resource last. It may even be impossible to determine the order when you code because it might depend on run-time inputs. Thus, a more sophisticated type of smart pointer implements reference counting to keep track of its owners. Every time such a reference-counted smart pointer is copied, a new instance is created pointing to the same resource, and the reference count is incremented. When such a smart pointer instance goes out of scope or is reset, the reference count is decremented. When the reference count drops to zero, there are no owners of the resource anymore, so the smart pointer frees the resource. The Standard Library provides std::shared_ptr which is a smart pointer with shared ownership semantics using reference counting. The standard shared_ptr is thread-safe, but this does not mean that the pointed-to resource is thread-safe! See Chapter 23 for a discussion on multithreading.

Both standard smart pointers, unique_ptr and shared_ptr, are discussed in detail in the next sections. Both require you to include the <memory> header file.

unique_ptr

As a rule of thumb, always store dynamically allocated resources in instances of unique_ptr.

Creating unique_ptrs

Consider the following function that blatantly leaks memory by allocating a Simple object on the heap and neglecting to release it:

void leaky()
{
    Simple* mySimplePtr = new Simple();  // BUG! Memory is never released!
    mySimplePtr->go();
}

Sometimes you might think that your code is properly deallocating dynamically allocated memory. Unfortunately, it most likely is not correct in all situations. Take the following function:

void couldBeLeaky()
{
    Simple* mySimplePtr = new Simple();
    mySimplePtr->go();
    delete mySimplePtr;
}

This function dynamically allocates a Simple object, uses the object, and then properly calls delete. However, you can still have memory leaks in this example! If the go() method throws an exception, the call to delete is never executed, causing a memory leak.

In both cases you should use a unique_ptr. The object is not explicitly deleted; but when the unique_ptr instance goes out of scope (at the end of the function, or because an exception is thrown), it automatically deallocates the Simple object in its destructor:

void notLeaky()
{
    auto mySimpleSmartPtr = make_unique<Simple>();
    mySimpleSmartPtr->go();
}

This code uses make_unique() from C++14, in combination with the auto keyword, so that you only have to specify the type of the pointer, Simple in this case, once. If the Simple constructor requires parameters, you put them in between the parentheses of the make_unique() call.

If your compiler does not yet support make_unique(), you can create your unique_ptr as follows. Note that Simple must be mentioned twice:

unique_ptr<Simple> mySimpleSmartPtr(new Simple());

Before C++17, you had to use make_unique() not only because you have to specify the type only once, but also because of safety reasons! Consider the following call to a function called foo():

foo(unique_ptr<Simple>(new Simple()), unique_ptr<Bar>(new Bar(data())));

If the constructor of Simple or Bar, or the data() function, throws an exception, depending on your compiler optimizations, it was very possible that either a Simple or a Bar object would be leaked. With make_unique(), nothing would leak:

foo(make_unique<Simple>(), make_unique<Bar>(data()))

With C++17, both calls to foo() are safe, but I still recommend using make_unique() as it results in code that is easier to read.

Using unique_ptrs

One of the greatest characteristics of the standard smart pointers is that they provide enormous benefit without requiring the user to learn a lot of new syntax. Smart pointers can still be dereferenced (using * or ->) just like standard pointers. For example, in the earlier example, the -> operator is used to call the go() method:

mySimpleSmartPtr->go();

Just as with standard pointers, you can also write this as follows:

(*mySimpleSmartPtr).go();

The get() method can be used to get direct access to the underlying pointer. This can be useful to pass the pointer to a function that requires a dumb pointer. For example, suppose you have the following function:

void processData(Simple* simple) { /* Use the simple pointer… */ }

Then you can call it as follows:

auto mySimpleSmartPtr = make_unique<Simple>();
processData(mySimpleSmartPtr.get());

You can free the underlying pointer of a unique_ptr and optionally change it to another pointer using reset(). For example:

mySimpleSmartPtr.reset();             // Free resource and set to nullptr
mySimpleSmartPtr.reset(new Simple()); // Free resource and set to a new
                                   // Simple instance

You can disconnect the underlying pointer from a unique_ptr with release(). The release() method returns the underlying pointer to the resource and then sets the smart pointer to nullptr. Effectively, the smart pointer loses ownership of the resource, and as such, you become responsible for freeing the resource when you are done with it. For example:

Simple* simple = mySimpleSmartPtr.release(); // Release ownership
// Use the simple pointer…
delete simple;
simple = nullptr;

Because a unique_ptr represents unique ownership, it cannot be copied! Using the std::move() utility (discussed in Chapter 9), it is possible to move one unique_ptr to another one using move semantics. This is used to explicitly move ownership, as in this example:

class Foo
{
    public:
        Foo(unique_ptr<int> data) : mData(move(data)) { }
    private:
        unique_ptr<int> mData;
};

auto myIntSmartPtr = make_unique<int>(42);
Foo f(move(myIntSmartPtr));

unique_ptr and C-Style Arrays

A unique_ptr is suitable to store a dynamically allocated old C-style array. The following example creates a unique_ptr that holds a dynamically allocated C-style array of ten integers:

auto myVariableSizedArray = make_unique<int[]>(10);

Even though it is possible to use a unique_ptr to store a dynamically allocated C-style array, it’s recommended to use a Standard Library container instead, such as std::array or std::vector.

Custom Deleters

By default, unique_ptr uses the standard new and delete operators to allocate and deallocate memory. You can change this behavior as follows: 

int* malloc_int(int value)
{
    int* p = (int*)malloc(sizeof(int));
    *p = value;
    return p;
}

int main()
{
    unique_ptr<int, decltype(free)*> myIntSmartPtr(malloc_int(42), free);
    return 0;
}

This code allocates memory for an integer with malloc_int(). The unique_ptr deallocates the memory by calling the standard free() function. As said before, in C++ you should never use malloc(), but new instead. However, this feature of unique_ptr is available because it is useful to manage other resources instead of just memory. For example, it can be used to automatically close a file or network socket or anything when the unique_ptr goes out of scope.

Unfortunately, the syntax for a custom deleter with unique_ptr is a bit clumsy. You need to specify the type of your custom deleter as a template type parameter. In this example, decltype(free) is used which returns the type of free(). The template type parameter should be the type of a pointer to a function, so an additional * is appended, as in decltype(free)*. Using a custom deleter with shared_ptr is much easier. The following section on shared_ptr demonstrates how to use a shared_ptr to automatically close a file when it goes out of scope.

shared_ptr

You use shared_ptr in a similar way to unique_ptr. To create one, you use make_shared(), which is more efficient than creating a shared_ptr directly. Here’s an example:

auto mySimpleSmartPtr = make_shared<Simple>();

Starting with C++17, a shared_ptr can also be used to store a pointer to a dynamically allocated old C-style array, just as you can do with a unique_ptr. This was not possible before C++17. However, even though it is now possible in C++17, it is still recommended to use Standard Library containers instead of C-style arrays.

A shared_ptr also supports the get() and reset() methods, just as a unique_ptr. The only difference is that when calling reset(), due to reference counting, the underlying resource is only freed when the last shared_ptr is destroyed or reset. Note that shared_ptr does not support release(). You can use use_count() to retrieve the number of shared_ptr instances that are sharing the same resource.

Just like unique_ptr, shared_ptr by default uses the standard new and delete operators to allocate and deallocate memory, or new[] and delete[] when storing a C-style array with C++17. You can change this behavior as follows:

// Implementation of malloc_int() as before.
shared_ptr<int> myIntSmartPtr(malloc_int(42), free);

As you can see, you don’t have to specify the type of the custom deleter as a template type parameter, so this makes it much easier than a custom deleter with unique_ptr.

The following example uses a shared_ptr to store a file pointer. When the shared_ptr is reset (in this case when it goes out of scope), the file pointer is automatically closed with a call to CloseFile(). Note that C++ has proper object-oriented classes to work with files (see Chapter 13). Those classes already automatically close their files. This example using the old C functions fopen() and fclose() is just to give a demonstration of what shared_ptrs can be used for besides pure memory:

void CloseFile(FILE* filePtr)
{
    if (filePtr == nullptr)
        return;
    fclose(filePtr);
    cout << "File closed." << endl;
}
int main()
{
    FILE* f = fopen("data.txt", "w");
    shared_ptr<FILE> filePtr(f, CloseFile);
    if (filePtr == nullptr) {
        cerr << "Error opening file." << endl;
    } else {
        cout << "File opened." << endl;
        // Use filePtr
    }
    return 0;
}

Casting a shared_ptr

The functions that are available to cast shared_ptrs are const_pointer_cast(), dynamic_pointer_cast(), and static_pointer_cast(). C++17 adds reinterpret_pointer_cast() to this list. These behave and work similar to the non-smart pointer casting functions const_cast(), dynamic_cast(), static_cast(), and reinterpret_cast(), which are discussed in detail in Chapter 11.

The Need for Reference Counting

As a general concept, reference counting is a technique for keeping track of the number of instances of a class or particular object in use. A reference-counting smart pointer is one that keeps track of how many smart pointers have been built to refer to a single real pointer, or single object. This way, smart pointers can avoid double deletion.

The double deletion problem is easy to provoke. Consider again the Simple class introduced earlier in this chapter, which simply prints out messages when an object is created and destroyed. If you were to create two standard shared_ptrs and have them both refer to the same Simple object, as in the following code, both smart pointers would attempt to delete the same object when they are destroyed:

void doubleDelete()
{
    Simple* mySimple = new Simple();
    shared_ptr<Simple> smartPtr1(mySimple);
    shared_ptr<Simple> smartPtr2(mySimple);
}

Depending on your compiler, this piece of code might crash! If you do get output, it could be as follows:

Simple constructor called!
Simple destructor called!
Simple destructor called!

Yikes! One call to the constructor and two calls to the destructor? You get the same problem with unique_ptr. You might be surprised that even the reference-counted shared_ptr class behaves this way. However, this is correct behavior according to the C++ standard. You should not use shared_ptr as in the previous doubleDelete() function to create two shared_ptrs pointing to the same object. Instead, you should make a copy as follows:

void noDoubleDelete()
{
    auto smartPtr1 = make_shared<Simple>();
    shared_ptr<Simple> smartPtr2(smartPtr1);
}

Here is the output of this code:

Simple constructor called!
Simple destructor called!

Even though there are two shared_ptrs pointing to the same Simple object, the Simple object is destroyed only once. Remember that unique_ptr is not reference counted. In fact, unique_ptr does not allow you to use its copy constructor as in the noDoubleDelete() function.

If you really need to be able to write code as shown in the previous doubleDelete() function example, you will need to implement your own smart pointer to prevent double deletion. But again, it is recommended to use the standard shared_ptr template for sharing a resource. Simply avoid code like that in the doubleDelete() function, and use the copy constructor instead.

Aliasing

A shared_ptr support so-called aliasing. This allows a shared_ptr to share ownership over a pointer (owned pointer) with another shared_ptr, but pointing to a different object (stored pointer). It can, for example, be used to have a shared_ptr pointing to a member of an object while owning the object itself. Here’s an example:

class Foo
{
    public:
        Foo(int value) : mData(value) { }
        int mData;
};

auto foo = make_shared<Foo>(42);
auto aliasing = shared_ptr<int>(foo, &foo->mData);

The Foo object is only destroyed when both shared_ptrs (foo and aliasing) are destroyed.

The owned pointer is used for reference counting, while the stored pointer is returned when you dereference the pointer or when you call get() on it. The stored pointer is used with most operations, such as the comparison operators. You can use the owner_before() method, or the std::owner_less class to perform comparisons based on the owned pointer instead. This can be useful in certain situations, such as storing shared_ptrs in an std::set. Chapter 17 discusses the set container in detail.

weak_ptr

There is one more class in C++ that is related to shared_ptr, called weak_ptr. A weak_ptr can contain a reference to a resource managed by a shared_ptr. The weak_ptr does not own the resource, so the shared_ptr is not prevented from deallocating the resource. A weak_ptr does not destroy the pointed-to resource when the weak_ptr is destroyed (for example when it goes out of scope); however, it can be used to determine if the resource has been freed by the associated shared_ptr or not. The constructor of a weak_ptr requires a shared_ptr or another weak_ptr as argument. To get access to the pointer stored in a weak_ptr, you need to convert it to a shared_ptr. There are two ways to do this:

  • Use the lock() method on a weak_ptr instance, which returns a shared_ptr. The returned shared_ptr is nullptr if the shared_ptr associated with the weak_ptr has been deallocated in the meantime.
  • Create a new shared_ptr instance and give a weak_ptr as argument to the shared_ptr constructor. This throws an std::bad_weak_ptr exception if the shared_ptr associated with the weak_ptr has been deallocated.

The following example demonstrates the use of weak_ptr:

void useResource(weak_ptr<Simple>& weakSimple)
{
    auto resource = weakSimple.lock();
    if (resource) {
        cout << "Resource still alive." << endl;
    } else {
        cout << "Resource has been freed!" << endl;
    }
}

int main()
{
    auto sharedSimple = make_shared<Simple>();
    weak_ptr<Simple> weakSimple(sharedSimple);

    // Try to use the weak_ptr.
    useResource(weakSimple);

    // Reset the shared_ptr.
    // Since there is only 1 shared_ptr to the Simple resource, this will
    // free the resource, even though there is still a weak_ptr alive.
    sharedSimple.reset();

    // Try to use the weak_ptr a second time.
    useResource(weakSimple);

    return 0;
}

The output of this code is as follows:

Simple constructor called!
Resource still alive.
Simple destructor called!
Resource has been freed!

Starting with C++17, weak_ptr also supports C-style arrays, just as shared_ptr supports C-style arrays since C++17.

Move Semantics

The standard smart pointers, shared_ptr, unique_ptr, and weak_ptr all support move semantics to make them efficient. Move semantics is discussed in detail in Chapter 9; however, the details are not important at this time. What is important is that this means it is very efficient to return such a smart pointer from a function. For example, you can write the following create() function and use it as demonstrated in main():

unique_ptr<Simple> create()
{
    auto ptr = make_unique<Simple>();
    // Do something with ptr…
    return ptr;
}

int main()
{
    unique_ptr<Simple> mySmartPtr1 = create();
    auto mySmartPtr2 = create();
    return 0;
}

enable_shared_from_this

The std::enable_shared_from_this mixin class allows a method on an object to safely return a shared_ptr or weak_ptr to itself. Mixin classes are discussed in Chapter 28. Basically, the enable_shared_from_this mixin class adds the following two methods to a class:

  • shared_from_this(): returns a shared_ptr that shares ownership of the object.
  • imageweak_from_this(): returns a weak_ptr that tracks ownership of the object.

This is an advanced feature not discussed in detail, but the following code briefly demonstrates its use:

class Foo : public enable_shared_from_this<Foo>
{
    public:
        shared_ptr<Foo> getPointer() {
            return shared_from_this();
        }
};

int main()
{
    auto ptr1 = make_shared<Foo>();
    auto ptr2 = ptr1->getPointer();
}

Note that you can only use shared_from_this() on an object if its pointer has already been stored in a shared_ptr. In the example, make_shared() is used in main() to create a shared_ptr called ptr1 which contains an instance of Foo. After this shared_ptr creation, it is allowed to call shared_from_this() on that Foo instance.

The following would be a completely wrong implementation of the getPointer() method:

class Foo
{
    public:
        shared_ptr<Foo> getPointer() {
            return shared_ptr<Foo>(this);
        }
};

If you use the same code for main() as shown earlier, this implementation of Foo causes a double deletion. You have two completely independent shared_ptrs (ptr1 and ptr2) pointing to the same object, which will both try to delete the object when they go out of scope.

The Old Deprecated/Removed auto_ptr

The old, pre-C++11 Standard Library included a basic implementation of a smart pointer, called auto_ptr. Unfortunately, auto_ptr has some serious shortcomings. One of these shortcomings is that it does not work correctly when used inside Standard Library containers such as vectors. C++11 and C++14 officially deprecated auto_ptr, and C++17 finally removed it entirely. It has been replaced with unique_ptr and shared_ptr. auto_ptr is mentioned here to make sure you know about it and to make sure you never use it.

COMMON MEMORY PITFALLS

It is difficult to pinpoint the exact situations that can lead to a memory-related bug. Every memory leak or bad pointer has its own nuances. There is no magic bullet for resolving memory issues, but there are several common categories of problems and some tools you can use to detect and resolve them.

Underallocating Strings

The most common problem with C-style strings is underallocation. In most cases, this arises when the programmer fails to allocate an extra character for the trailing '' sentinel. Underallocation of strings also occurs when programmers assume a certain fixed maximum size. The basic built-in C-style string functions do not adhere to a fixed size—they will happily write off the end of the string into uncharted memory.

The following code demonstrates underallocation. It reads data off a network connection and puts it in a C-style string. This is done in a loop because the network connection receives only a small amount of data at a time. On each loop, getMoreData() is called, which returns a pointer to dynamically allocated memory. When nullptr is returned from getMoreData(), all of the data has been received. strcat() is a C function that concatenates the C-style string given as a second argument to the end of the C-style string given as a first argument. It expects the destination buffer to be big enough.

char buffer[1024] = {0};   // Allocate a whole bunch of memory.
while (true) {
    char* nextChunk = getMoreData();
    if (nextChunk == nullptr) {
        break;
    } else {
        strcat(buffer, nextChunk); // BUG! No guarantees against buffer overrun!
        delete [] nextChunk;
    }
}

There are three ways to resolve the possible underallocation problem. In decreasing order of preference, they are as follows:

  1. Use C++-style strings, which handle the memory associated with concatenation on your behalf.
  2. Instead of allocating a buffer as a global variable or on the stack, allocate it on the heap. When there is insufficient space left, allocate a new buffer large enough to hold at least the current contents plus the new chunk, copy the original buffer into the new buffer, append the new contents, and delete the original buffer.
  3. Create a version of getMoreData() that takes a maximum count (including the '' character) and returns no more characters than that; then track the amount of space left and the current position in the buffer.

Accessing Out-of-Bounds Memory

Earlier in this chapter, you read that because a pointer is just a memory address, it is possible to have a pointer that points to a random location in memory. Such a condition is quite easy to fall into. For example, consider a C-style string that has somehow lost its '' termination character. The following function, which fills the string with all 'm' characters, continues to fill the contents of memory after the string with 'm's:

void fillWithM(char* inStr)
{
    int i = 0;
    while (inStr[i] != '') {
        inStr[i] = 'm';
        i++;
    }
}

If an improperly terminated string is handed to this function, it is only a matter of time before an essential part of memory is overwritten and the program crashes. Consider what might happen if the memory associated with the objects in your program is suddenly overwritten with 'm's. It’s not pretty!

Bugs that result in writing to memory past the end of an array are often called buffer overflow errors. These bugs have been exploited by several high-profile malware programs such as viruses and worms. A devious hacker can take advantage of the ability to overwrite portions of memory to inject code into a running program.

Many memory-checking tools detect buffer overflows. Also, using higher-level constructs like C++ strings and vectors helps prevent numerous bugs associated with writing to C-style strings and arrays.

Memory Leaks

Finding and fixing memory leaks can be one of the more frustrating parts of programming in C or C++. Your program finally works and appears to give the correct results. Then, you start to notice that your program gobbles up more and more memory as it runs. Your program has a memory leak. The use of smart pointers to avoid memory leaks is a good first approach to solving the problem.

Memory leaks occur when you allocate memory and neglect to release it. At first, this sounds like the result of careless programming that could easily be avoided. After all, if every new has a corresponding delete in every class you write, there should be no memory leaks, right? Actually, that’s not always true. In the following code, the Simple class is properly written to release any memory that it allocates.

When doSomething() is called, the outSimplePtr pointer is changed to another Simple object without deleting the old one to demonstrate a memory leak. Once you lose a pointer to an object, it’s nearly impossible to delete it.

class Simple
{
    public:
        Simple() { mIntPtr = new int(); }
        ~Simple() { delete mIntPtr; }
        void setValue(int value) { *mIntPtr = value; }
    private:
        int* mIntPtr;
};

void doSomething(Simple*& outSimplePtr)
{
    outSimplePtr = new Simple(); // BUG! Doesn't delete the original.
}

int main()
{
    Simple* simplePtr = new Simple(); // Allocate a Simple object.
    doSomething(simplePtr);
    delete simplePtr; // Only cleans up the second object.
    return 0;
}

In cases like the preceding example, the memory leak probably arose from poor communication between programmers or poor documentation of code. The caller of doSomething() may not have realized that the variable was passed by reference and thus had no reason to expect that the pointer would be reassigned. If they did notice that the parameter was a non-const reference to a pointer, they may have suspected that something strange was happening, but there is no comment around doSomething() that explains this behavior.

Finding and Fixing Memory Leaks in Windows with Visual C++

Memory leaks are hard to track down because you can’t easily look at memory and see what objects are not in use and where they were originally allocated. However, there are programs that can do this for you. Memory leak detection tools range from expensive professional software packages to free downloadable tools. If you work with Microsoft Visual C++*, its debug library has built-in support for memory leak detection. This memory leak detection is not enabled by default, unless you create an MFC project. To enable it in other projects, you need to start by including the following three lines at the beginning of your code:

#define _CRTDBG_MAP_ALLOC
#include <cstdlib>
#include <crtdbg.h>

These lines should be in the exact order as shown. Next, you need to redefine the new operator as follows:

#ifdef _DEBUG
    #ifndef DBG_NEW
        #define DBG_NEW new ( _NORMAL_BLOCK , __FILE__ , __LINE__ )
        #define new DBG_NEW
    #endif
#endif  // _DEBUG

Note that this is within an “#ifdef _DEBUG” statement so the redefinition of new is done only when compiling a debug version of your application. This is what you normally want. Release builds usually do not do any memory leak detection.

The last thing you need to do is to add the following line as the first line in your main() function:

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

This tells the Visual C++ CRT (C RunTime) library to write all detected memory leaks to the debug output console when the application exits. For the previous leaky program, the debug console will contain lines similar to the following:

Detected memory leaks!
Dumping objects ->
c:leakyleaky.cpp(15) : {147} normal block at 0x014FABF8, 4 bytes long.
 Data: <    > 00 00 00 00 
c:leakyleaky.cpp(33) : {146} normal block at 0x014F5048, 4 bytes long.
 Data: <Pa  > 50 61 20 01
Object dump complete. 

The output clearly shows in which file and on which line memory was allocated but never deallocated. The line number is between parentheses immediately behind the filename. The number between the curly braces is a counter for the memory allocations. For example, {147} means the 147th allocation in your program since it started. You can use the VC++ _CrtSetBreakAlloc() function to tell the VC++ debug runtime to break into the debugger when a certain allocation is performed. For example, you can add the following line to the beginning of your main() function to instruct the debugger to break on the 147th allocation:

_CrtSetBreakAlloc(147);

In this leaky program, there are two leaks: the first Simple object that is never deleted (line 33) and the heap-based integer that it creates (line 15). In the Visual C++ debugger output window, you can simply double-click on one of the memory leaks and it will automatically jump to that line in your code.

Of course, programs like Microsoft Visual C++ (discussed in this section) and Valgrind (discussed in the next section) can’t actually fix the leak for you—what fun would that be? These tools provide information that you can use to find the actual problem. Normally, that involves stepping through the code to find out where the pointer to an object was overwritten without the original object being released. Most debuggers provide “watch point” functionality that can break execution of the program when this occurs.

Finding and Fixing Memory Leaks in Linux with Valgrind

Valgrind is an example of a free open-source tool for Linux that, among other things, pinpoints the exact line in your code where a leaked object was allocated.

The following output, generated by running Valgrind on the previous leaky program, pinpoints the exact locations where memory was allocated but never released. Valgrind finds the same two memory leaks—the first Simple object never deleted and the heap-based integer that it creates:

==15606== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
==15606== malloc/free: in use at exit: 8 bytes in 2 blocks.
==15606== malloc/free: 4 allocs, 2 frees, 16 bytes allocated.
==15606== For counts of detected errors, rerun with: -v
==15606== searching for pointers to 2 not-freed blocks.
==15606== checked 4455600 bytes.
==15606==
==15606== 4 bytes in 1 blocks are still reachable in loss record 1 of 2
==15606==    at 0x4002978F: __builtin_new (vg_replace_malloc.c:172)
==15606==    by 0x400297E6: operator new(unsigned) (vg_replace_malloc.c:185)
==15606==    by 0x804875B: Simple::Simple() (leaky.cpp:4)
==15606==    by 0x8048648: main (leaky.cpp:24)
==15606==
==15606==
==15606== 4 bytes in 1 blocks are definitely lost in loss record 2 of 2
==15606==    at 0x4002978F: __builtin_new (vg_replace_malloc.c:172)
==15606==    by 0x400297E6: operator new(unsigned) (vg_replace_malloc.c:185)
==15606==    by 0x8048633: main (leaky.cpp:20)
==15606==    by 0x4031FA46: __libc_start_main (in /lib/libc-2.3.2.so)
==15606==
==15606== LEAK SUMMARY:
==15606==    definitely lost: 4 bytes in 1 blocks.
==15606==    possibly lost:   0 bytes in 0 blocks.
==15606==    still reachable: 4 bytes in 1 blocks.
==15606==         suppressed: 0 bytes in 0 blocks.

Double-Deleting and Invalid Pointers

Once you release memory associated with a pointer using delete, the memory is available for use by other parts of your program. Nothing stops you, however, from attempting to continue to use the pointer, which is now a dangling pointer. Double deletion is also a problem. If you use delete a second time on a pointer, the program could be releasing memory that has since been assigned to another object.

Double deletion and use of already released memory are both difficult problems to track down because the symptoms may not show up immediately. If two deletions occur within a relatively short amount of time, the program potentially could work indefinitely because the associated memory might not be reused that quickly. Similarly, if a deleted object is used immediately after being deleted, most likely it will still be intact.

Of course, there is no guarantee that such behavior will work or continue to work. The memory allocator is under no obligation to preserve any object once it has been deleted. Even if it does work, it is extremely poor programming style to use objects that have been deleted.

Many memory leak-detection programs, such as Microsoft Visual C++ and Valgrind, are capable of detecting double deletion and use of released objects.

If you disregard the recommendation for using smart pointers and instead still use dumb pointers, at least set your pointers to nullptr after deallocating their memory. This prevents you from accidentally deleting the same pointer twice or using an invalid pointer. It’s worth noting that you are allowed to call delete on a nullptr pointer; it simply will not do anything.

SUMMARY

In this chapter, you learned the ins and outs of dynamic memory. Aside from memory-checking tools and careful coding, there are two key takeaways to avoid dynamic memory-related problems. First, you need to understand how pointers work under the hood. After reading about two different mental models for pointers, you should now know how the compiler doles out memory. Second, you can avoid all sorts of dynamic memory issues by using objects which automatically manage such memory, like the C++ string class, the vector container, smart pointers, and so on.

If there is one takeaway from this chapter, it is that you should try to avoid using old C-style constructs and functions as much as possible, and use the safe C++ alternatives.

NOTE

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset