Chapter 21

Effective Memory Management

WHAT’S IN THIS CHAPTER?

  • What the different ways are to use and manage memory
  • What the often perplexing relationship is between arrays and pointers
  • A low-level look at working with memory
  • What smart pointers are and how to use them
  • Solutions to a few memory related problems

In many ways, programming in C++ is like driving without a road. Sure, you can go anywhere you want, but there are no lines or traffic lights to keep you from injuring yourself. C++, like the C language, has a hands-off approach towards its programmers. The language assumes that you know what you’re doing. It allows you to do things that are likely to cause problems because C++ is incredibly flexible and sacrifices safety in favor of performance.

Memory allocation and management is a particularly error-prone area of C++ programming. To write high-quality C++ programs, professional C++ programmers need to understand how memory works behind the scenes. This chapter explores the ins and outs of memory management. You will learn about the pitfalls of dynamic memory and some techniques for avoiding and eliminating them.

WORKING WITH DYNAMIC MEMORY

When learning to program, dynamic memory is often the first major stumbling block that novice programmers face. Memory is a low-level component of the computer that unfortunately rears its head even in a high-level programming language like C++. Many programmers only understand 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.

There are two main advantages to using dynamic memory in your programs:

  • Dynamic memory can be shared between different objects and functions.
  • The size of dynamically-allocated memory can be determined at run time.

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 21-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;

Since i is a local variable, it is allocated on the stack because it is declared as a simple type, not dynamically using the new keyword.

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

int* ptr;
ptr = new int;

Figure 21-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 either on the stack or the heap, although this fact is easy to forget. Dynamic memory, however, is always allocated on the heap.

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

int** handle;
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 21-3 shows the two levels of pointers with one pointer residing on the stack (handle) and the other residing on the heap (*handle).

Allocation and Deallocation

You should already be familiar with the basics of dynamic memory from earlier chapters in this book. 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 the 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.

For example, the following code orphans enough memory to hold an int. Figure 21-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.

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

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 used for another purpose. To free memory on the heap, simply use the delete keyword with a pointer to the memory, as shown here:

int* ptr;
ptr = new int;
delete ptr;
cross.gif

As a rule of thumb, every line of code that allocates memory with new should correspond to another line of code that releases the same memory with delete.

What about My Good Friend malloc?

If you are a C programmer, you may be wondering what was 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 we recommend avoiding 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 will allocate the appropriate size of memory and will also properly construct the object. Chapter 18 describes these two duties of new in more detail.

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

cross.gif

You should never use malloc() and free() in C++. Only use new and delete.

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 10 explains 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 (or NULL if your compiler doesn’t support nullptr yet), similar to the behavior of malloc() in C. The syntax for using this version is shown here:

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 is the version that throws an exception. For this reason, we suggest that you use the standard version of new. If out-of-memory recovery is important to your program, the techniques covered in Chapter 10 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 would be declared on the stack as follows:

int myArray[5];

Figure 21-5 shows the state of memory after the array is declared. 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];

As Figure 21-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. The advantage of putting an array on the heap is that you can use dynamic memory to define its size at run time. For example, the following function 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()
{
    int numDocs = askUserForNumberOfDocuments();
    Document* docArray = new Document[numDocs];
    return docArray;
}
pen.gif

Some compilers allow variable-sized arrays on the stack. This is not a standard feature of C++, so we recommend cautiously backing away when you see it.

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 are data structures that do dynamically adjust in size to their data, such as the STL built-in vector class. It is recommended to use these STL containers like vector instead of standard arrays because they are much safer to use.

There is a function in C++ called realloc(), which is a holdover from the C language. Don’t 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 and moving all of the old data to the new location. This approach is extremely dangerous in C++ because user-defined objects will not respond well to bitwise copying.

cross.gif

Do not use realloc() in C++. It is not your friend.

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 constructor for each of the objects will automatically be called. In this way, allocating an array of objects using new[] will return a pointer to an array of fully formed and initialized objects.

For example, consider the following class:

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

Code snippet from ArrayDeleteArrayDelete.cpp

If you were to allocate an array of four Simple objects, the Simple constructor would be called four times.

image
Simple* mySimpleArray = new Simple[4];
 

Code snippet from ArrayDeleteArrayDelete.cpp

The output of this code is:

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

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

Deleting Arrays

When you allocate memory with the array version of new (new[]), you must release it with the array version of delete (delete[]). This version will automatically destruct the objects in the array in addition to releasing the memory associated with them. If you do not use the array version of delete, your program may behave in odd ways. In some compilers, only the destructor for the 0th 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. In others, memory corruption may occur because new and new[] can use completely different memory allocation schemes.

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

Code snippet from ArrayDeleteArrayDelete.cpp

cross.gif

Always use delete on anything allocated with new, and always use delete[] on anything allocated with new[].

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

image
size_t arrSize = 4;
Simple** mySimplePtrArray = new Simple*[arrSize];
// Allocate an object for each pointer.
for (size_t i = 0; i < arrSize; i++) {
    mySimplePtrArray[i] = new Simple();
}
// Use mySimplePtrArray . . .
// Delete each allocated object.
for (size_t i = 0; i < arrSize; i++) {
    delete mySimplePtrArray[i];
}
// Delete the array itself.
delete [] mySimplePtrArray;
mySimplePtrArray = nullptr;

Code snippet from ArrayDeleteArrayDelete.cpp

pen.gif

Instead of storing plain old dumb pointers in your data structures like the arrays above, it is recommended to store smart pointers in your data structures. These smart pointers will automatically deallocate memory associated with them. Smart pointers are discussed in detail later in this chapter.

Multi-Dimensional Arrays

Multi-dimensional arrays extend the notion of indexed values to use 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 and accessed with some test code:

image
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).

Code snippet from tictactoe ictactoe.cpp

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 21-8. Since 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 the size of the array and the method used to access it.

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 21-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 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 21-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 21-10.

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

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 like 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 21-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:

image
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;
}

Code snippet from CharacterBoardCharacterBoard.cpp

When you wish 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:

image
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
} 

Code snippet from CharacterBoardCharacterBoard.cpp

cross.gif

Now that you know all the details to work with arrays, it is recommended to avoid old C-style arrays as much as possible because they do not provide any memory safety. Instead, use the C++ STL containers like std::array, std::vector, std::list, and so on. For example, use vector<T> for a one dimensional dynamic array. Use vector<vector<T>> for a two dimensional dynamic array and so on.

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;

The previous line builds a pointer to the memory address 7, which is likely to be random garbage or memory that is used elsewhere in the application. If you start to use areas of memory that weren’t set aside on your behalf with new, 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 manifest 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 results in program termination by the operating system or the C++ run-time library; if you are unlucky, you will just get a wrong result.

A Mental Model for Pointers

There are two ways to think about pointers. More mathematically minded readers might view pointers simply 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 simply numbers that happen to correspond to a location in memory. Figure 21-12 illustrates a two-by-two grid in the address-based view of the world.

pen.gif

The addresses in Figure 21-12 are just for illustrative purposes. Addresses on a real system are highly dependent on your hardware and operating system.

Readers who are more comfortable with spatial representations might derive more benefit from the “arrow” view of pointers. A pointer is simply a level of indirection that says to the program “Hey! Look over there.” With this view, multiple levels of pointers simply become individual steps on the path to data. Figure 21-11 showed 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 simply 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

Since 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 will refuse to perform a static cast on pointers to different 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. Consult Chapter 8 for details on dynamic casts.

const with Pointers

The interaction between the const keyword and pointers is a bit confusing because it is unclear to what you are applying const. If you dynamically allocate an array of integers and apply const to it, is the array address protected with const, or are the individual values protected? The answer depends on the syntax.

If const occurs before the type, it means that the pointed-to value is protected. In the case of an array, the individual elements of the array are const. The following function receives a pointer to a const integer. The first line will not compile because the actual value is protected by const. The second line would compile, because the pointer itself is unprotected:

void test(const int* inProtectedInt, int* anotherPtr)
{
    *inProtectedInt = 7;  // BUG! Attempts to write to const value
    inProtectedInt = anotherPtr;  // Works fine
}

To protect the pointer itself, the const keyword immediately precedes the variable name, as shown in the following code. This time, both the pointer and the pointed-to value are protected, so neither line would compile:

void test(const int* const inProtectedInt, int* anotherPtr)
{
    *inProtectedInt = 7;  // BUG! Attempts to write to const value
    inProtectedInt =  anotherPtr;  // BUG! Attempts to write to const value
}

In practice, protecting the pointer is rarely necessary. If a function is able to change the value of a pointer that you pass it, it makes little difference. The effect will only be local to the function, and the pointer will still point to its original address as far as the caller is concerned. Marking a pointer as const is more useful in documenting its purpose than for any actual protection. Protecting the pointed-to value(s), however, is quite common to protect against overwriting shared data, and to allow the compiler to perform more powerful optimizations.

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 0th element. 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 0th element. In this way, the pointer works just like a heap-based array. The following code creates an array on the stack, but uses a pointer to access the array:

int main()
{
    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 will need 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.

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

Code snippet from ArraysAndPointersArraysAndPointers.cpp

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 simply passed by value into the function. In the case of a stack-based array, the caller can pass the array variable, and the compiler will automatically treat 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:

image
size_t arrSize = 4;
int* heapArray = new int[arrSize];
heapArray[0] = 1;
heapArray[1] = 5;
heapArray[2] = 3;
heapArray[3] = 4;
doubleInts(heapArray, arrSize);
delete [] heapArray;
heapArray = nullptr;
 
int stackArray[] = {5, 7, 9, 11};
arrSize = sizeof(stackArray) / sizeof(stackArray[0]);
doubleInts(stackArray, arrSize);
doubleInts(&stackArray[0], arrSize);

Code snippet from ArraysAndPointersArraysAndPointers.cpp

Even if the function doesn’t explicitly have a parameter that is a pointer, the parameter-passing semantics of arrays are 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:

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

Code snippet from ArraysAndPointersArraysAndPointers.cpp

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.

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!

Since the compiler lets you pass in an array where a pointer is expected, as in the doubleInts() function shown earlier, you may be lead 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!

cross.gif

Arrays are automatically referenced as pointers, but not all pointers are arrays.

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 STL classes.

With some applications, however, you may encounter the need to work with memory at a lower level. Whether for 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, since 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 had the following wide string:

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

Suppose you also had a function that took in a string and returned a new string that contains a capitalized version of the input:

wchar_t* toCaps(const wchar_t* inString);

You could capitalize myString by passing it into this function. However, if you only wanted to capitalize part of myString, you could use pointer arithmetic to refer to only a latter part of the string. The following code calls toCaps() on the World part of the 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 generally 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 18.

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 references anymore will be cleaned up automatically at some point by the run-time library.

Garbage collection is not built into the C++ language as it is in C# and Java. Most C++ programs manage memory at the object level through new and delete. It is possible to implement 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. Subclass all objects from a mix-in 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 simple 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:

  • When the garbage collector is actively running, the program will likely be 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, etc.), which is done by the destructor, is not performed until some indeterminate time in the future.

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 analog 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 24 contains further details about using object pools for performance efficiency.

Function Pointers

You don’t normally think about the location of functions in memory, but each function actually lives at a particular address. In C++, you can use functions as data. In other words, you can take the address of a function and use it like you use a variable.

Function pointers are typed according to the parameter types and return type of compatible functions. One way to work with function pointers is to use the typedef mechanism to assign a type name to the family of functions that have the given characteristics. For example, the following line declares a type called YesNoFcn that represents a pointer to any function that has two int parameters and returns a bool:

image
typedef bool(*YesNoFcn)(int, int);
 

Code snippet from FunctionPointersFunctionPointers.cpp

C++11 introduces the type alias feature which you can use instead of a typedef and which might be more readable. Type aliases are discussed in Chapter 9. The following line is basically the same as the previous typedef line:

using YesNoFcn = bool(*)(int, int);

Now that this new type exists, you could write a function that takes a YesNoFcn as a parameter. For example, the following function accepts two int arrays and their size, as well as a YesNoFcn. It iterates through the arrays in parallel and calls the YesNoFcn on corresponding elements of both arrays, printing a message if the YesNoFcn function returns true. Notice that even though the YesNoFcn is passed in as a variable, it can be called just like a regular function:

image
void findMatches(int values1[], int values2[], size_t numValues, YesNoFcn inFunc)
{
    for (size_t i = 0; i < numValues; i++) {
        if (inFunc(values1[i], values2[i])) {
            cout << "Match found at position " << i << 
                " (" << values1[i] << ", " << values2[i] << ")" << endl;
        }
    }
}

Code snippet from FunctionPointersFunctionPointers.cpp

To call the findMatches() function, all you need is any function that adheres to the defined YesNoFcn type — that is, any type that takes in two ints and returns a bool. For example, consider the following function, which returns true if the two parameters are equal:

image
bool intEqual(int inItem1, int inItem2)
{
    return inItem1 == inItem2;
}

Code snippet from FunctionPointersFunctionPointers.cpp

Because the intEqual() function matches the YesNoFcn type, it can be passed as the final argument to findMatches(), as follows:

image
int arr1[] = {2, 5, 6, 9, 10, 1, 1};
int arr2[] = {4, 4, 2, 9, 0, 3, 4};
int arrSize = sizeof(arr1) / sizeof(arr1[0]);
cout << "Calling findMatches() using intEqual():" << endl;
findMatches(arr1, arr2, arrSize, &intEqual);

Code snippet from FunctionPointersFunctionPointers.cpp

Notice that the intEqual() function is passed into the findMatches() function by taking its address. Technically, the & character is optional — if you simply put the function name, the compiler will know that you mean to take its address. The output is as follows:

Calling findMatches() using intEqual():
Match found at position 3 (9, 9)

The benefit of function pointers lies in the fact that findMatches() is a generic function that compares parallel values in two arrays. As it is used above, it compares based on equality. However, since it takes a function pointer, it could compare based on other criteria. For example, the following function also adheres to the definition of a YesNoFcn:

image
bool bothOdd(int inItem1, int inItem2)
{
    return inItem1 % 2 == 1 && inItem2 % 2 == 1;
}

Code snippet from FunctionPointersFunctionPointers.cpp

The following code calls findMatches() using bothOdd:

image
cout << "Calling findMatches() using bothOdd():" << endl;
findMatches(arr1, arr2, arrSize, &bothOdd);

Code snippet from FunctionPointersFunctionPointers.cpp

The output will be:

Calling findMatches() using bothOdd():
Match found at position 3 (9, 9)
Match found at position 5 (1, 3)

By using function pointers, a single function, findMatches(), was customized to different uses based on a parameter, inFunc.

pen.gif

Instead of using these old-style function pointers, you can also use the C++11 std::function which is explained in Chapter 16.

Pointers to Methods and Members

You can create and use pointers to both variables and functions. Now, consider pointers to class members and methods. It’s perfectly legitimate in C++ to take the addresses of class members and methods in order to obtain pointers to them. However, you can’t access a non-static member or call a non-static method without an object. The whole point of class members and methods is that they exist on a per-object basis. Thus, when you want to call the method or access the member via the pointer, you must dereference the pointer in the context of an object. Here is an example:

image
SpreadsheetCell myCell(123);
double (SpreadsheetCell::*methodPtr) () const = &SpreadsheetCell::getValue;
cout << (myCell.*methodPtr)() << endl;

Code snippet from PtrsToMethodsAndMembersSpreadsheetCellTest.cpp

Don’t panic at the syntax. The second line declares a variable called methodPtr of type pointer to a non-static const method that takes no arguments and returns a double. At the same time, it initializes this variable to point to the getValue() method of the SpreadsheetCell class. This syntax is quite similar to declaring a simple function pointer, except for the addition of SpreadsheetCell:: before the *methodPtr. Note also that the & is required in this case.

The third line calls the getValue() method (via the methodPtr pointer) on the myCell object. Note the use of parentheses surrounding myCell.*methodPtr. They are needed because () has higher precedence than *.

Most of the time C++ programmers simplify the second line by using a typedef:

image
SpreadsheetCell myCell(123);
typedef double (SpreadsheetCell::*PtrToGet) () const;
PtrToGet methodPtr = &SpreadsheetCell::getValue;
cout << (myCell.*methodPtr)() << endl;

Code snippet from PtrsToMethodsAndMembersSpreadsheetCellTest.cpp

Pointers to methods and members usually won’t come up in your programs. However, it’s important to keep in mind that you can’t dereference a pointer to a non-static method or member without an object. Every so often, you’ll find yourself wanting to try something like passing a pointer to a non-static method to a function such as qsort() that requires a function pointer, which simply won’t work .

pen.gif

Note that C++ permits you to dereference a pointer to a static member or method without an object.

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. Smart pointers are a notion that arose from the fact that most memory-related issues are avoidable by putting everything on the stack. The stack is much safer than the heap because stack variables are automatically destructed and cleaned up when they go out of scope. Smart pointers combine the safety of stack variables with the flexibility of heap variables. There are several kinds of smart pointers. The most simple type of smart pointer takes sole ownership of the memory and when the smart pointer goes out of scope, it will free the referenced memory, for example, unique_ptr in C++11.

However, managing pointers presents more problems than just remembering to delete 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 memory properly, the last piece of code to use the memory should call delete on the pointer. However, it is often difficult to know which piece of code uses the memory 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. When all owners are finished using the pointer, the number of references drops to 0 and the smart pointer calls delete on its underlying dumb pointer. The standard C++11 shared_ptr smart pointer discussed later includes reference counting. It is important to be familiar with this concept.

Some languages provide garbage collection so that programmers are not responsible for freeing any memory. In these languages, all pointers can be thought of as smart pointers because you don’t need to remember to free any of the memory to which they point. Although some languages, such as Java, provide garbage collection as a matter of course, it is very difficult to write a garbage collector for C++. Thus, smart pointers are simply a technique to make up for the fact that C++ exposes memory management without garbage collection.

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. Second, you can provide an interface to the smart pointer objects using operator overloading that allows code to use the smart pointer objects as if they were dumb pointers. Specifically, you can overload the * and -> operators such that the client code can dereference a smart pointer object the same way it dereferences a normal pointer.

The Old Deprecated auto_ptr

The old, pre-C++11 standard template 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 STL containers like vectors. C++11 has officially deprecated auto_ptr and replaced it with shared_ptr and unique_ptr, discussed in the next section. auto_ptr is mentioned here to make sure you know about it and to make sure you never use it.

pen.gif

Do not use the old auto_ptr smart pointer anymore. Instead use the new C++11 shared_ptr or unique_ptr!

imageThe New C++11 Smart Pointers

C++11 introduces two new smart pointer classes: shared_ptr and unique_ptr. The difference between the two is that shared_ptr is a reference counted smart pointer, while unique_ptr is not reference counted. This means that you can have several shared_ptr instances pointing to the same dynamically allocated memory and the memory will only be deallocated when the last shared_ptr goes out of scope. For this reason, shared_ptr is safer/easier to use compared to unique_ptr. Most of the remainder of this section focuses on the shared_ptr template.

As a rule of thumb, always store dynamically allocated objects in stack-based instances of shared_ptr. For example, 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();
}

Using the shared_ptr template, the object is still not explicitly deleted; but when the shared_ptr instance goes out of scope (at the end of the function) it automatically deallocates the Simple object in its destructor:

void notLeaky()
{
    shared_ptr<Simple> mySimpleSmartPtr(new Simple());
    mySimpleSmartPtr->go();
}

You need to include the <memory> header file which defines these smart pointer templates. One of the greatest characteristics of smart pointers is that they provide enormous benefit without requiring the user to learn a lot of new syntax. As you can see in the preceding code, the smart pointer can still be dereferenced (using * or ->) just like a standard pointer.

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;
}

The above 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 will never be executed, causing a memory leak. So, also in this case it is recommended to use smart pointers.

cross.gif

Never assign the result of a memory allocation to a dumb pointer. Whether you use new or malloc() or any other memory allocation method, always immediately give the resulting memory pointer to a smart pointer, shared_ptr or unique_ptr.

If you use old C-style arrays, you cannot use shared_ptr to manage their memory. Instead of using the old C-style arrays, you should switch to modern memory safe containers like the std::array, std::vector and so on, or give the C-style array to a unique_ptr which is allowed.

cross.gif

Never use a shared_ptr to manage a pointer to a C-style array. Use unique_ptr to manage the C-style array, or use STL containers instead of C-style arrays.

A shared_ptr can be created in two ways. With the first way, you have to mention the type you want to allocate twice: once as template parameter for shared_ptr and once as parameter to the new operator:

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

The second syntax is as follows:

auto mySimpleSmartPtr = make_shared<Simple>();

This syntax uses the auto keyword and uses make_shared(), so you only have to specify the allocated type (Simple) once. If the Simple constructor requires parameters, you put them in between the parentheses of the make_shared() call. In fact, using make_shared() not only requires less typing, but the compiler can also generate more efficient code than using the longer syntax. For example, with Microsoft Visual C++ if you define your shared_ptr with the syntax shown in the following line, VC++ needs to allocate memory for the object and then allocate memory for a so called reference count control block that stores the reference count. The details of the reference count control block are specific to VC++ and are not important for this discussion.

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

However, by using make_shared() as follows, VC++ optimizes this memory allocation and allocates enough memory for both the object and the reference count control block with a single memory allocation call:

auto mySimpleSmartPtr = make_shared<Simple>();

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

image
int* malloc_int(int value)
{
    int* p = (int*)malloc(sizeof(int));
    *p = value;
    return p;
}
int main()
{
    shared_ptr<int> myIntSmartPtr(malloc_int(42), free);
    return 0;
}

Code snippet from shared_ptrshared_ptr_malloc_int.cpp

This will allocate the memory for the integer with malloc_int(), and the shared_ptr will deallocate the memory using the standard free() function. As mentioned earlier, in C++ you should never use malloc() but new instead. However, this feature of shared_ptr is available because it is very 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 shared_ptr goes out of scope. The following example uses a shared_ptr to store a file pointer. When the shared_ptr goes out of scope, the file pointer is automatically closed with a call to the CloseFile() function. Remember that C++ has proper object oriented classes to work with files (see Chapter 15). Those classes will already automatically close their files when they go out of scope. This example using the old C fopen() and fclose() functions is just to give a demonstration for what shared_ptrs can be used for besides pure memory:

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

Code snippet from shared_ptrshared_ptr_file.cpp

Both shared_ptr and unique_ptr support the C++11 move semantics remove discussed in Chapter 9 to make them very efficient. Because of this, it is also efficient to return a shared_ptr or a unique_ptr from a function. For example, you can write the following function func() and use it as demonstrated in the main() function:

image
// ... definition of Simple not shown for brevity
shared_ptr<Simple> func()
{
    auto ptr = make_shared<Simple>();
    return ptr;
}
int main()
{
    shared_ptr<Simple> mySmartPtr = func();
    return 0;
}

Code snippet from shared_ptrshared_ptr_return_from_function.cpp

This will be efficient because C++11 will automatically call std::move() on the return statement in the func() function, which will trigger the move semantics of the shared_ptr. The same happens if you would use unique_ptr instead of shared_ptr. The unique_ptr does not support the normal copy assignment operator and copy constructor, but it does support the move assignment operator and move constructor, and that is why you can return a unique_ptr from a function. Take the following unique_ptr assignments:

unique_ptr<int> p1(new int(42));
unique_ptr<int> p2 = p1;        // Error: does not compile
unique_ptr<int> p3 = move(p1);  // OK

The line defining p2 will not compile because it is trying to use the copy constructor, which is not available for unique_ptr. The definition of p3 is good because std::move() (defined in the <utility> header file, see Chapter 9) is used to trigger the move constructor of unique_ptr. After p3 is defined, p1 will be reset to nullptr and you will only be able to access the integer 42 through p3; ownership has been moved from p1 to p3.

There is one more class in C++11 that is related to the shared_ptr template; the weak_ptr. A weak_ptr can contain a reference to memory that is managed by a shared_ptr. The weak_ptr does not own the memory, so the shared_ptr is not prevented from deallocating the memory. A weak_ptr will not destroy the pointed to memory when it goes out of scope; however, it can be used to determine if the memory has been deallocated 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 the weak_ptr you need to convert it to a shared_ptr. There are two ways to do this: Use the lock() method on the weak_ptr instance, which will return a shared_ptr, or create a new shared_ptr instance and give the weak_ptr as argument to the shared_ptr constructor. The weak_ptr is a rather advanced feature of the C++11 smart pointers. It is rarely used and not further discussed in this book, but it is good to know its existence.

Writing Your Own Smart Pointer Class

It is highly recommended to use the standard shared_ptr or unique_ptr classes. However, sometimes you need some functionality not provided by these standard implementations, or your compiler might not yet support shared_ptr in which case you can implement your own reference-counted smart pointer. This section explains how you can implement your own reference-counted smart pointer. Chapter 18 provides an initial version of a smart pointer class called Pointer which is using operator overloading so the user can use the Pointer class just like a normal dumb pointer. This section enhances this Pointer class to include reference counting which is missing from the initial version.

The Need for Reference Counting

As a general concept, reference counting is the technique for keeping track of the number of instances of a class or particular object that are 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 the following class, Nothing, which simply prints out messages when an object is created or destroyed:

image
class Nothing
{
    public:
        Nothing() { cout << "Nothing::Nothing()" << endl; }
        ~Nothing() { cout << "Nothing::~Nothing()" << endl; }
};

Code snippet from shared_ptrshared_ptr_double_delete.cpp

If you were to create two standard shared_ptrs and have them both refer to the same Nothing object as follows, both smart pointers would attempt to delete the same object when they go out of scope:

image
void doubleDelete()
{
    Nothing* myNothing = new Nothing();
    shared_ptr<Nothing> smartPtr1(myNothing);
    shared_ptr<Nothing> smartPtr2(myNothing);
}

Code snippet from shared_ptrshared_ptr_double_delete.cpp

The output of the previous function would be:

Nothing::Nothing()
Nothing::~Nothing()
Nothing::~Nothing()

Yikes! One call to the constructor and two calls to the destructor? You will get exactly the same result with the unique_ptr template and with the old deprecated auto_ptr smart pointer. You might be surprised that even the reference-counted shared_ptr class behaves this way. However, this is correct behavior according to the C++11 standard. You should not use shared_ptr like in the previous doubleDelete() function to create two shared_ptrs pointing to the same object. Instead, you should simply use the assignment operator as follows:

image
void noDoubleDelete()
{
    Nothing* myNothing = new Nothing();
    shared_ptr<Nothing> smartPtr1(myNothing);
    shared_ptr<Nothing> smartPtr2 = smartPtr1;
}

Code snippet from shared_ptrshared_ptr_double_delete.cpp

The output of this code is:

Nothing::Nothing()
Nothing::~Nothing()

Even though there are two shared_ptrs pointing to the same Nothing object, the Nothing object is only destroyed once. Remember that unique_ptr is not reference counted. In fact, unique_ptr will not allow you to use the assignment operator as in the noDoubleDelete() function. That’s another reason to prefer shared_ptr instead of unique_ptr.

pen.gif

If your program uses smart pointers by copying them, assigning them, or passing them as arguments to functions, the shared_ptr is the perfect solution.

If you really need to write code as shown in the previous doubleDelete() function, you will need to implement your own smart pointer to prevent double deletion. You can also implement your own reference-counted smart pointer in case your compiler does not yet support shared_ptr. How to do this is shown in the next section. But again, it is recommended to use the standard shared_ptr template and simply avoid code as in the doubleDelete() function, and use assignment instead:

shared_ptr<Nothing> smartPtr2 = smartPtr1;

The SuperSmartPointer

This section explains how to write your own reference-counting smart pointer that you can use to solve the double deletion problem from the doubleDelete() function in the previous section.

The approach for the SuperSmartPointer, a reference-counting smart pointer implementation is to keep a static map for reference counts. Each key in the map is the memory address of a traditional pointer that is referred to by one or more SuperSmartPointers. The corresponding value is the number of SuperSmartPointers that refer to that object.

The implementation of SuperSmartPointer that follows is based on the smart pointer code shown in Chapter 18. You may want to review that code before continuing. The major changes occur when a new pointer is set (through the single argument constructor, the copy constructor, or operator=) and when a SuperSmartPointer is finished with an underlying pointer (upon destruction or reassignment with operator=).

On initialization of a new pointer, the initPointer() method checks the static map to see if the pointer is already contained by an existing SuperSmartPointer. If it is not, the count is initialized to 1. If it is already in the map, the count is bumped up. When the pointer is reassigned or the containing SuperSmartPointer is destroyed, the finalizePointer() method is called. This method throws an exception if the pointer is not found in the map. If the pointer is found, its count is decremented by one. If this brings the count down to zero, the underlying pointer can be safely released. At that time, the key/value pair is explicitly removed from the map to keep the map size down:

image
template <typename T>
class SuperSmartPointer
{
    public:
        explicit SuperSmartPointer(T* inPtr);
        virtual ~SuperSmartPointer();
        SuperSmartPointer(const SuperSmartPointer<T>& src);
        SuperSmartPointer<T>& operator=(const SuperSmartPointer<T>& rhs);
        const T& operator*() const;
        const T* operator->() const;
        T& operator*();
        T* operator->();
        operator void*() const { return mPtr; }
    protected:
        T* mPtr;
        static std::map<T*, int> sRefCountMap;
        void finalizePointer();
        void initPointer(T* inPtr);
};
template <typename T>
std::map<T*, int> SuperSmartPointer<T>::sRefCountMap;
template <typename T>
SuperSmartPointer<T>::SuperSmartPointer(T* inPtr)
{
    initPointer(inPtr);
}
template <typename T>
SuperSmartPointer<T>::SuperSmartPointer(const SuperSmartPointer<T>& src)
{
    initPointer(src.mPtr);
}
template <typename T>
SuperSmartPointer<T>& 
    SuperSmartPointer<T>::operator=(const SuperSmartPointer<T>& rhs)
{
    if (this == &rhs) {
        return *this;
    }
    finalizePointer();
    initPointer(rhs.mPtr);
    return *this;
}
template <typename T>
SuperSmartPointer<T>::~SuperSmartPointer()
{
    finalizePointer();
}
template<typename T>
void SuperSmartPointer<T>::initPointer(T* inPtr)
{
    mPtr = inPtr;
    if (sRefCountMap.find(mPtr) == sRefCountMap.end()) {  
        sRefCountMap[mPtr] = 1;
    } else {
        sRefCountMap[mPtr]++;
    }
}
template<typename T>
void SuperSmartPointer<T>::finalizePointer()
{
    if (sRefCountMap.find(mPtr) == sRefCountMap.end()) {
        throw std::runtime_error("ERROR: Missing entry in map!");
    }
    sRefCountMap[mPtr]--;
    if (sRefCountMap[mPtr] == 0) {
        // No more references to this object--delete it and remove from map
        sRefCountMap.erase(mPtr);
        delete mPtr;
        mPtr = nullptr;
    }
}
template <typename T>
const T* SuperSmartPointer<T>::operator->() const
{
    return mPtr;
}
template <typename T>
const T& SuperSmartPointer<T>::operator*() const
{
    return *mPtr;
}
template <typename T>
T* SuperSmartPointer<T>::operator->() 
{
    return mPtr;
}
template <typename T>
T& SuperSmartPointer<T>::operator*() 
{
    return *mPtr;
}

Code snippet from SuperSmartPointerSuperSmartPointer.cpp

Unit Testing the SuperSmartPointer

The Nothing class defined earlier can be employed for a simple unit test for SuperSmartPointer. One modification is needed to determine if the test passed or failed. Two static members are added to the Nothing class, which track the number of allocations and the number of deletions. The constructor and destructor modify these values instead of printing a message. If the SuperSmartPointer works, the numbers should always be equivalent when the program terminates. Here is the adapted Nothing class:

image
class Nothing
{
    public:
        Nothing() { sNumAllocations++; }
        virtual ~Nothing() { sNumDeletions++; }
        static int sNumAllocations;
        static int sNumDeletions;
};
int Nothing::sNumAllocations = 0;
int Nothing::sNumDeletions = 0;

Code snippet from SuperSmartPointerSuperSmartPointer.cpp

Following is the actual test. Note that an extra set of curly braces is used to keep the SuperSmartPointers in a smaller scope, so the example can show automatic allocation and deallocation self-contained within one block:

image
Nothing* myNothing = new Nothing();
{
    SuperSmartPointer<Nothing> ptr1(myNothing);
    SuperSmartPointer<Nothing> ptr2(myNothing);
}
if (Nothing::sNumAllocations != Nothing::sNumDeletions) {
    std::cout << "TEST FAILED: " << Nothing::sNumAllocations <<
            " allocations and " << Nothing::sNumDeletions << 
            " deletions" << std::endl;
} else {
    std::cout << "TEST PASSED" << std::endl;
}

Code snippet from SuperSmartPointerSuperSmartPointer.cpp

A successful execution of this test program will result in the following output:

TEST PASSED

You should also write additional tests for the SuperSmartPointer class. For example, you should test the copy construction and operator= functionality. This is left as an exercise for the reader.

Enhancing This Implementation

The previous SuperSmartPointer implementation is not completely free of problems. Templates exist on a per-type basis. In other words, if you have some SuperSmartPointers that store pointers to integers and others that store pointers to characters, there are actually two classes generated at compile time: SuperSmartPointer<int> and SuperSmartPointer<char>. Because the reference count map is stored statically within the class, two maps will be generated. In most cases, this won’t cause a problem, but you could cast a char* to an int* resulting in two SuperSmartPointers of two different template classes that refer to the same allocated memory. Because the table data is separate, double deletion would occur, as demonstrated by the following code:

char* ch = new char;
SuperSmartPointer<char> ptr1(ch);
SuperSmartPointer<int> ptr2((int*)ch);  // BUG! Double deletion will occur!

One solution to this problem is to make the reference map a global variable, though globals are often frowned upon. Another solution would be to wrap the map in a non-template singleton class, perhaps called MapManager, which is referenced by the SuperSmartPointer template classes. The singleton pattern is discussed in Chapter 29.

The other issue with this implementation is that it is not thread safe. As Chapter 22 explains, threads are a new feature of C++11. Threads are very common in modern programming so you should be aware of thread safety. Access to the static map should be protected by a lock so that concurrent additions and deletions do not conflict with each other. Chapter 22 gives you all the details about how to protect access to data in multithreaded programs.

cross.gif

Remember, the SuperSmartPointer class is given here for illustrative purposes only. You should use the standard C++11 shared_ptr or unique_ptr in your programs unless you have a valid reason and you know exactly what you are doing.

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 string functions will not adhere to a fixed size — they will happily write off the end of the string into uncharted memory.

The following code reads data off a network connection and puts it in a C-style string. This is done in a loop because the network connection only receives 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.

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:

1. Use C++-style strings, which will 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 will return no more characters than that; then track the amount of space left and the current position in the buffer.

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. However, when the doSomething() function is called, the pointer is changed to another Simple object without deleting the old one. The doSomething() function doesn’t delete the old object on purpose to demonstrate memory leaks. Once you lose a pointer to an object, it’s nearly impossible to delete it.

image
class Simple 
{
    public:
        Simple() { mIntPtr = new int(); }
        ~Simple() { delete mIntPtr; }
        void setIntPtr(int inInt) { *mIntPtr = inInt; }
    protected:
        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;
}

Code snippet from LeakyLeaky.cpp

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 is 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 already 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 include the following three lines at the beginning of your code:

image
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

Code snippet from LeakyLeaky MS VC++.cpp

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

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

Code snippet from LeakyLeaky MS VC++.cpp

Note that this is within a “#ifdef _DEBUG” statement so the redefinition of new will only be done 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:

image
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
 

Code snippet from LeakyLeaky MS VC++.cpp

This tells the Visual C++ CRT (C Run Time) 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(19) : {59} normal block at 0x007E3330, 4 bytes long.
 Data: <    > 00 00 00 00 
c:leakyleaky.cpp(37) : {58} normal block at 0x007E32F0, 4 bytes long.
 Data: <03~ > 30 33 7E 00 
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 brackets is a counter for the memory allocations. For example, {59} means the 59th 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, add the following line to the beginning of your main() function to let the debugger break on the 59th allocation:

_CrtSetBreakAlloc(59);

In this leaky program, there are two leaks — the first Simple object that is never deleted and the heap-based integer that it creates. 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. Some debuggers, like Visual C++, 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, amongst 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 that is 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:8)
==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:24)
==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. 
cross.gif

It is strongly recommended to use smart pointers as much as possible to avoid memory leaks.

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 hard 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 might work indefinitely because the associated memory is not 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 checking programs, such as Microsoft Visual C++ and Valgrind, will also detect 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 will prevent you from accidentally deleting the same pointer twice or to use an invalid pointer. It’s worth noting that you are allowed to call delete on a nullptr pointer; it simply will not do anything.

Accessing Out-of-Bounds Memory

Earlier in this chapter, you read that since 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 attempts to fill the string with all 'm' characters, would instead continue to fill the contents of memory following the string with 'm's:

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

If an improperly terminated string were handed to this function, it would only be 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. Such bugs have been exploited by several high-profile malware programs, for example 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 as well. Also, using higher-level constructs like C++ strings and vectors will help prevent numerous bugs associated with writing to C-style strings and arrays.

cross.gif

Avoid using old C-style strings and arrays that offer no protection whatsoever. Instead, use modern and safe constructs like C++ strings and vectors that manage all their memory for you.

SUMMARY

In this chapter, you learned the ins and outs of dynamic memory. Aside from memory checking tools and careful coding, there are two keys to avoiding dynamic memory-related problems. First, you need to understand how pointers work under the hood. In reading about two different mental models for pointers, we hope you are confident that you know how the compiler doles out memory. Second, you can avoid all sorts of dynamic memory issues by obscuring pointers with stack-based objects, like the C++ string class, vector class, smart pointers, and so on.

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

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

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