Everyone knows that memory management is a difficult and dangerous chore in C++. This series of three articles will show you that the conventional wisdom is not true. When approached correctly, C++'s seemingly archaic memory-management scheme actually provides an opportunity to create spectacular programs — programs that would not be possible with more modern languages that handle memory automatically.
This article, part one in a series, discusses C++ in the context of several other popular languages. It also describes the kinds of memory errors that can occur in C++ programs. The most common specific errors are then presented in a set of tables, for easy reference when developing your own code.
Subsequent articles in the series will demonstrate a different way of thinking about memory management — not as a dreaded danger to be faced with stoic resolve, but as a powerful and subtle tool for improving your work. In many situations, the C++ approach to memory resources is not cause to avoid the language, but actually the reason why it should be used!
Not so long ago, C and C++ were about the only practical choices for serious software development on microcomputers. This is no longer the case. Java, Python, and Perl are three established, mainstream alternatives that can be used for many applications today. These new languages run on multiple platforms, feature extremely rich libraries for everything from encryption to graphics, and are supported by large, enthusiastic user communities. They are freely available (although Java's source code is not open like the other two). All three are widely used in business, government, and research.
In contrast to C and C++, the newer languages (Java, Python, and Perl) have automated memory management. This ranges from Python's simple reference-counted model to Java's sophisticated garbage collector. To the programmer, however, the final result is nearly always the same: no more worrying about memory errors. Why, then, even bother considering C or C++? Why not just choose one of the memory-managed languages and live happily ever after?
|
Related Reading
C++ In a Nutshell |
In many applications, a memory-managed language is indeed the right choice. Scripts to automate a small to medium-sized web site, for example, are best written in Perl or Python. To analyze your server's logfile, Perl is probably the best choice. A single-user application with an excellent GUI can be built in Java. Complex object-oriented programs are quickly and easily written in Python.
Even older systems written in C or C++ can benefit from the addition of new code in one of the memory-managed languages; Java, Python, and Perl all provide features that make such integration possible. Do C and C++, which require low-level operations to handle memory, have any significant role left to play in modern software development? The answer is yes, as the following discussion will show.
Languages that give you precise control over system resources remain highly relevant to this day — and not only for embedded systems or kernel-level code. If your project is a user-space server or must perform lots of computation (e.g., a graphics package or a game), you will save yourself a great deal of trouble by avoiding the memory-managed languages, at least for the core of your system.
As described previously, many situations are handled by the automated, general-purpose allocation strategies of the memory-managed languages. In many other cases, however, resource management becomes critical. Making efficient use of a server's RAM, for example, is a daunting task with a memory-managed language. Your control is minimal, so precisely matching data structures to algorithms is a highly uncertain process.
It is possible to learn how to use a language such as C++ correctly. On the other hand, trying to get Java's garbage collector (just to pick one example) to take a specific action at a certain time is an impossible task. Sometimes it will work, then break with no explanation. The behavior will depend on which JVM you are using, so your code will not be portable. Even on the same platform, JVM upgrades will unleash mystifying, subtle errors. This is precisely the kind of nightmare that is typically associated with memory allocation errors in C++!
Because memory-managed languages seem so automatic at first glance, many programmers assume that their greatest worries are over once they choose such a language. Hopefully, their luck will hold up, and they can live a happy life. For the multitude that will not be so fortunate, there is a hard and painful lesson waiting here: errors in memory management almost always lead to disaster. A memory-managed language does not change that; it gives you a generic (albeit well-designed) strategy to deal with the problem. If, however, the generic strategy is wrong for your project, then you will have made a memory management error, and you will pay for this error just as surely as if you had coded it yourself.
User-space programs are where C++ really shines. While kernel development, embedded software, and hard real-time systems can also benefit from the use of C++, the simple, predictable C is often the better choice for such applications. In contrast, C++'s richness is often preferable to C's simplicity in user space.
Just like C, C++ will give you precise control over system resources. Also, for a very slight performance penalty over C (a few percentage points at most for a well-written program -- see [Mey98]) C++ offers many more tools for the capable designer. A good architecture for a complex project is much more easily expressed in C++.
C++ is highly suitable for large projects done by small, competent teams. A group of several developers willing to treat C++ with respect will get exceptional results with the language. Using C++ in larger teams can be risky, however. In such situations, it is probably best to build the core of the system in C++, and then augment it with a memory-managed language. Such an arrangement is much more forgiving of programmer error than having several hundred people code only in C++.
If you must use C++ exclusively on a project with many developers, you'll
need to make a small group responsible for all memory allocation. Everyone
else would then rely on the facilities developed by that group (all code should
actually be scanned, making sure that calls to new and
delete are restricted to the designated part of the program).
Almost all classes developed by a large team should work correctly with C++
default copy constructors and assignment operators (the technical details will
be covered in articles two
and three
of this series). You will also need a system to track the classes that require
custom implementations of these methods, because such classes can easily lead
to very dangerous and subtle memory errors.
While C++'s demands for low-level coding may look clumsy at first glance, elegant C++ programs (such as the widely used Standard Template Library) are amongst the most beautiful of all software. Some truly amazing things are indeed possible with the language; hopefully, this series of articles will help you achieve some of these things yourself.
The modern programmer has a truly breathtaking choice of programming languages. It is beyond the scope of this article — whose main focus is C++ — to discuss them all. In the language overview provided here, several excellent mainstream languages are discussed. Of course, there is a lot more out there: Smalltalk, Fortran, Forth, COBOL, Ruby, Lua, Scheme, Guile, etc. One of these other languages might be the right one for you or your project. So keep exploring, and don't forget that older languages such as C++ are not automatically obsolete even when a great new invention (e.g., Python) comes along.
Having thought about C++ and its alternatives, it is now time to take a specific look at C++ memory management, and see what elegant programs you yourself can build with its powerful, subtle tools.
|
The first step to good C++ memory management is understanding the major errors that are common in this area. An overview is given here; there are also a number of excellent books that cover memory errors in C++. A bibliography with annotations is provided towards the end of this article.
Even when presented in a condensed form, there are enough subtleties in C++ memory management to result in quite a number of warning and caveats. The key is not to get discouraged at the start. After describing the potential dangers, subsequent articles in this series will proceed to show you how to overcome them with several simple, straightforward techniques. You will also see how the power of good software design can go beyond merely coping with problems, and turn C++ memory management into a powerful tool.
As an aside, note that utilities are available to help detect memory-related errors. This series of articles, however, focuses on what you can do when you design and code your programs. Together with a good design and careful coding, the tools can certainly add value. The first strategy, however, must always be to architect memory errors out of your system. This is the only way to make programs that work great, rather than ones that just barely pass a test. In the end, the best tool is still between your ears.
Memory errors come in two basic types: the dangling reference and the memory leak. The former happens when memory is freed up, but some other code still maintains a reference or pointer to the released area as though it were still allocated. The latter happens when a design calls for allocation and deallocation of memory, but the deallocation step is left out (maybe only in some places) by mistake. The program keeps allocating memory, but does not free all of it; the amount of total available memory in the system keeps going down until something critical breaks because it cannot get the memory it needs.
Strictly speaking, a dangling reference is part of a larger set of errors sometimes known as the wild pointer [Cli95]. A wild pointer can also be generated by forgetting to initialize a local pointer variable (i.e., it then points at some random location), or by setting the pointer to an incorrect value. Fortunately, these errors are typically eliminated by very basic good programming practices. Dangling references are much more complex and subtle.
Out of the two major types of non-trivial memory errors, dangling references are by far the deadliest — they are hardest to debug, and the least susceptible to detection by automated tools. While you should not forget about the danger of memory leaks, dangling references should be your first concern.
Here is a classic case of how a memory leak is introduced into a C++ class implementation. Unfortunately, the fix causes something even worse: a dangling reference.
First, the original code, which causes the leak.
Example 1. A Common Memory Leak
//*** A TYPICAL MEMORY LEAK ***
#include <iostream> //N.B. no ".h": new-style include.
#include <cstring>
using namespace std; //Everything in 'std' is accessed directly.
//A simple string class.
class SimpleString {
public:
explicit SimpleString(char* data = ""); //Use 'explicit' keyword to disable
//automatic type conversions --
//generally a good idea.
virtual ~SimpleString(); //Virtual destructor, in case someone inherits
//from this class.
virtual const char* to_cstr() const; //Get a read-only C string.
//Many other methods are needed to create a complete string class.
//This example implements only a tiny subset of these, in order
//to keep the discussion focused.
//N.B. no 'inline' methods -- add inlining later, if needed for
//optimization.
private:
char* data_p_; //Distinguish private class members: a trailing underscore
//in the name is one common method.
};
//Constructor.
SimpleString::SimpleString(char* data_p) :
data_p_(new char[strlen(data_p)+1]) {
strcpy(data_p_,data_p);
}
//Destructor.
SimpleString::~SimpleString() {
//OOPS, forgot to delete "data_p".
}
//Returns a read-only C string representation.
const char* SimpleString::to_cstr() const {
return data_p_;
}
int main() {
//Create a local SimpleString.
SimpleString name("O'Reilly Onlamp");
//Print it out.
cout << name.to_cstr() << endl;
}
//*** END: A TYPICAL MEMORY LEAK ***
As you can see, the memory allocated in SimpleString's
constructor was not released in the destructor. This is a common mistake.
When a SimpleString object is destroyed, the the memory pointed to
by data_p_ is simply lost. It seems that a simple change
(deleting the memory in the destructor) will fix this problem. It does, but
the dreaded dangling reference is now introduced.
Example 2. A Common Dangling Reference
//*** A TYPICAL DANGLING REFRENCE ***
#include <iostream> //N.B. no ".h": new-style include.
#include <cstring>
using namespace std; //Everything in 'std' is accessed directly.
//A simple string class.
class SimpleString {
public:
explicit SimpleString(char* data = ""); //Use 'explicit' keyword to disable
//automatic type conversions --
//generally a good idea.
virtual ~SimpleString(); //Virtual destructor, in case someone inherits
//from this class.
virtual const char* to_cstr() const; //Get a read-only C string.
//Many other methods are needed to create a complete string class.
//This example implements only a tiny subset of these, in order
//to keep the discussion focused.
//N.B. no 'inline' methods -- add inlining later, if needed for
//optimization.
private:
char* data_p_; //distinguish private class members: a trailing underscore
//in the name is one common method
};
//Constructor
SimpleString::SimpleString(char* data_p) :
data_p_(new char[strlen(data_p)+1]) {
strcpy(data_p_,data_p);
}
//Destructor
SimpleString::~SimpleString() {
//N.B. Use of 'delete []' corresponds to previous use of 'new []'.
// Using just 'delete' here would be a disaster.
delete [] data_p_;
}
//Returns a read-only C string representation.
const char* SimpleString::to_cstr() const {
return data_p_;
}
int main() {
//Create a local SimpleString.
SimpleString name("O'Reilly Onlamp");
//Print it out.
cout << name.to_cstr() << endl;
//Dynamically create another SimpleString; make it a copy of the local one.
SimpleString* name_copy_p = new SimpleString(name);
//Print out the copy.
cout << name_copy_p->to_cstr() << endl;
//Print out the original again.
cout << name.to_cstr() << endl;
//Delete the copy; set the pointer to null just in case it's used again.
delete name_copy_p;
name_copy_p = 0;
//This looks fine... but the results are highly system-dependent.
cout << name.to_cstr() << endl;
}
//*** END: A TYPICAL DANGLING REFERENCE ***
This program looks innocent at first glance, but look at the output it produces on the author's system.
Example 3. One Possible Result of a Dangling Reference
O'Reilly Onlamp
O'Reilly Onlamp
O'Reilly Onlamp
3"@3"@ Onlamp
Segmentation fault
The first line of the output results from printing the data in the local
object name. Then, a copy of name is dynamically
allocated; the name_copy_p pointer stores the address of the copy.
Printing the data in the copy produces the second line of the output. So far,
everything is fine. We can even print the contents of name again
— see the third line of the output.
Next, the SimpleString object pointed to by
name_copy_p is deleted. Our modified destructor will free the
memory buffer (pointed to by the data_p_ member) being used by the
object. The original name object still exists, however, so we
should be able to continue using it.
Unfortunately, when we try to use name again (the fourth line
in the output) something goes terribly wrong. The data has clearly been
damaged; it even causes the program to crash (as shown in the last line of the
output). Somehow, deleting a copy of name has seriously broken
the original!
This code exhibits a classic dangling reference. To understand how this happened, it is helpful to review the key member functions that every C++ class is required to have. The following list describes these methods.
Initializes the object during creation. Usually involves allocating resources.
A very special constructor, used to create an object that is a copy of an existing object. It is declared like this:
SimpleString( const SimpleString& original );
Assigns one fully constructed object to another fully constructed object. Declared like this:
SimpleString& operator=( const SimpleString& right_hand_side );
Cleans up the object's internals just prior to deletion. Usually involves freeing up resources.
The most important thing to realize about the methods just listed is that the C++ compiler will generate default versions of them if you do not provide your own. The copy constructor and the assignment operator are particularly easy to forget; unfortunately, the defaults often do not do what you want.
The default copy constructor and assignment operator make a simple,
shallow copy of every data member. In the A Common Dangling Reference example, this means that the data_p_
pointer inside of the object stored at name_copy_p will now point to
the same chunk of memory as name's data_p_. No
attempt is made to allocate more memory and make a deep copy of the
data.
When delete name_copy_p; is executed, the
SimpleString destructor is called; it frees the memory pointed to
by data_p_. Unfortunately, this memory is now being shared with
the original object, name. Now, name's
data_p_ points at deallocated memory. A dangling reference is
born. Figure 1 gives a graphical representation of how
this condition arises.

Figure 1. A Dangling Reference Caused by the Default
Copy Constructor
An analogous situation occurs when the default assignment operator is
applied to SimpleString objects. Figure
2 illustrates what happens.

Figure 2. A Dangling Reference Caused by the Default
Assignment Operator
In general, three basic strategies are available to deal with the fact that compiler-generated copy constructors and assignment operators are so often dangerously wrong. These are shown in the following list. Subsequent articles in this series will discuss all three approaches in detail.
Write your own copy constructors and assignment operators that will work correctly with your classes.
Disable
copying and assignment altogether by making the copy constructor and
assignment operator private.
Modify your classes so that the default copy constructor and assignment
operator are correct (by using member objects instead of dynamic allocation, or
certain types
of smart pointers such as the shared_ptr
from Boost.org).
Having closely examined a classic sequence of memory errors, let's now look at the other common ways in which such errors can be introduced into a C++ program.
|
In addition to the classic case covered
previously,
there are several more aspects of C++ programming that tend to cause memory
errors. The following set of tables describes these situations. Note that the
SimpleString used here is the corrected
version, rather than the broken one used to illustrate dangling references. Article
three in this series will discuss the corrected version of
SimpleString in detail.
| Error | Example |
|---|---|
| Returning a Reference (or Pointer) to a Local Object | |
The local object is destroyed when the function returns. In
general, anything inside curly braces is a scope; if you define a
local object (i.e. non- Result: dangling reference. | |
Returning a const Reference Parameter by const Reference |
|
The C++ compiler is allowed to create an unnamed temporary object
for any Result: dangling reference. | |
| Passing an Object by Value |
|
| When a function parameter is an object, rather than a pointer or a reference to one, then the argument (supplied during a call to the function) for that parameter is passed by value. The compiler will make a copy of the argument (the copy constructor will be invoked) in order to generate the function call. If the argument's type is a derived class of the parameter, then the famous object slicing problem occurs. Any derived class functionality — including overridden virtual methods — is simply thrown away. The function gets what it asked for: its own object of the exact class that was specified in the declaration. While all of this makes perfect sense to the compiler, it is often counterintuitive to the programmer. When you pass an object of the derived class, you expect the overridden virtual methods to be called. After all, this is what virtual methods are for. Unfortunately, it won't work like that if a derived object is passed by value. Result: derived parts are "sliced off." Depending on your design, there may be no memory-related issues, but the slicing problem is important enough to be mentioned here. | |
| Returning a Reference to a Dynamically Allocated Object |
|
Your callers are highly unlikely to take an address of the reference and deallocate the object, especially if the return value is used inside of an expression instead of being immediately saved into a variable. Result: memory leak. |
| Error | Example |
|---|---|
| C++ Default Methods | See A Common Memory Leak and A Common Dangling Reference. |
You must make sure that the default methods that C++ generates work well with your design. The default copy constructor and assignment operator tend to cause the most trouble to programmers. This was extensively covered previously. Result: memory leak and/or dangling reference. | |
| The Non-Virtual Destructor |
|
If you intend others to inherit from your class, you must declare
its destructor Result: memory leak; possibly other problems. |
| Error | Example | |||||
|---|---|---|---|---|---|---|
| Using Arrays Polymorphically |
| |||||
The ability to access a derived object through a reference or
pointer of the base type is central to C++. It allows an important kind of
polymorphism (literally, an ability to assume different forms) in
which Unfortunately, this sort of polymorphism does not work with arrays of classes. C++ inherits its arrays from C — there is basically no difference between an array and a pointer. As you index into an array of derived objects via a base-type pointer, the compiler is happily doing the pointer arithmetic using the size of the base class. The derived class, however, is almost certainly larger than the base class, due to extra members that have been added. Thus, the returned data is rarely a single valid object, but rather pieces of two adjacent ones. Result: wild pointer (recall that dangling references are a special case of wild pointer). | ||||||
| Mistakes Using Casts |
See the | |||||
Casts have been likened to the infamous Result: wild pointer. | ||||||
| Bitwise Copying of Objects |
| |||||
Mechanisms such as the Besides Result: memory leaks and dangling references are both possible, as well as other problems. | ||||||
| Deallocation of Memory That Was Not Dynamically Allocated |
| |||||
It is important to make sure that any memory you free has, in fact,
been dynamically allocated. Actions such as freeing local objects or
deallocating memory more than once will be disastrous to your program. This
also applies to using Result: memory leaks and dangling references, as well as corruption of operating system data structures. | ||||||
| Mismatched Method of Allocation and Deallocation |
|
|||||
Here is a list of common, matched allocation and deallocation methods.
It is a serious error to allocate with one method, and then use something
other than the corresponding deallocation method to release the memory. In
addition, note that You should also never call an object's destructor directly. The only
exception to this is when you allocate the object using the placement
new syntax (e.g. Result: memory leaks and corruption of operating system data structures. |
| Error | Example |
|---|---|
| Partially Constructed Objects |
See the aggregated |
When an exception prevents a constructor from running to completion, C++ will not call the destructor of the partially constructed object. Member objects, however, will still be properly destructed. This is not to say that exceptions are to be avoided in constructors. In fact, throwing an exception is usually the best way to indicate that a constructor has failed. The alternative is leaving around a broken object, which the caller must check for validity. While you should often allow exceptions to propagate out of a constructor, it is important to remember that the destructor will never be called in such cases, so you are responsible for cleaning up the partially constructed object (e.g. by catching and then rethrowing the exception, using smart pointers, etc.). Result: memory leak. | |
| Exceptions Leaving a Destructor |
See the |
If a function throws an exception, that exception may be caught by the caller, or the caller's caller, etc. This flexibility is what makes error handling via exceptions into such a powerful tool. In order to propagate through your program, however, an exception needs to leave one scope after another — almost as if one function after another were returning from a chain of nested of calls. This process is called stack unwinding . As a propagating exception unwinds the stack, it encounters local objects.
Just like in a return from a function, these local objects must be properly
destroyed. This is not a problem unless an object's destructor throws an
exception. Because there is no general way to decide which exception to
continue processing (the currently active one or the the new one thrown by the
destructor), C++ simply calls the global Another consequence of an exception leaving a destructor is that the
destructor itself does not finish its work. This could lead to memory leaks.
Your destructor does not necessarily have to throw an exception itself for the
problem to happen. It is far more common that something else called by the
destructor throws the exception. In general, it is best if you make sure that
exceptions never propagate out of your destructors under any circumstances
(even if your compiler implements the Boolean Result: memory leak (destructor does not run to completion); local objects will not be properly destructed if another exception is active. Various resource leaks and state inconsistency are therefore possible. | |
| Improper Throwing |
|
When throwing exceptions, it is important to remember that the object being thrown is always copied. Hence, it is safe to throw local objects, but throwing dynamically allocated objects by value or by reference will cause them to be leaked. Copying is always based on the static type of the object, so if you have a base-type reference to an object of a derived class, and you decide to throw that reference, an object of the base type will be thrown. This is another variant of the object slicing problem covered earlier. A more subtle slicing error occurs when rethrowing exceptions. If you want
to rethrow the exact same object that you got in your You also need to make sure that the copy constructor of the class that you are throwing will not cause dangling references. It is generally not recommended to throw exceptions by pointer; in these situations, only the pointer itself, rather than the actual object, is copied. Thus, throwing a pointer to a local object is never safe. On the other hand, throwing a non-local object by pointer raises the question of whether it needs to be deallocated or not, and what is responsible for the deallocation. Result: object slicing, dangling references, and memory leaks are all possible. | |
| Improper Catching |
|
Improper catching of exceptions can also lead to object slicing. As
you might have guessed, catching by value will slice. The order of the
Result: object slicing; memory-related errors and other problems are
possible if a base-class |
|
This article has focused on finding the right role for C++ amongst today's other popular languages, and on understanding its most difficult aspect: memory management. The tables of common memory-related errors presented here can be used as a handy reference, to find and avoid such errors in your own code. Subsequent articles of the series will continue to discuss C++ memory management in greater detail.
The second article will be devoted to describing the nature of the C++ memory management mechanism, so that you can begin to apply it creatively in your designs. After that, the third article will present a series of specific techniques that you can use as building blocks in your programs.
C++ memory management is an enormously useful tool for creating elegant software. Having gained a clear awareness of its dangers, you are now ready to understand its benefits. Enabling you to do so is, ultimately, the purpose of this series of articles.
|
Related Reading
C++ Pocket Reference |
A number of very useful resources are available regarding C++. Notes on these resources are provided here (the Bibliography itself follows).
First, you need a book with broad coverage, which can serve as an introduction, a reference, and for review. Ira Pohl's C++ by Dissection [Poh02] is an example of such a book. It features a particularly gentle ramp-up into working with the language.
In addition to a book with broad coverage, you will need books that focus specifically on the most difficult aspects of the language and present techniques to deal with them. Three titles that you should find very valuable are Effective C++ [Mey98] and More Effective C++ [Mey96] (both by Scott Meyers), and C++ FAQs [Cli95] by Marshall P. Cline and Greg A. Lomow, which is also available in an online version.
The key to reading all three of these books is not to panic. They contain a great deal of difficult technical details, and are broken up into a large number of very specific topics. Unless you are merely reviewing material with which you are already familiar, reading any of these books from cover to cover is unlikely to be useful.
A good strategy is to allocate some time (even as little as 15 minutes) each day to work with either of Meyers' books, or with C++ FAQs. Begin your session by looking over the entire table of contents, which, in all three books, has a very detailed listing of all of the items covered. Don't ignore this important step; it will take you progressively less time as you become familiar with each particular book.
Next, try to read the items that are most relevant to the current problem that you are trying to solve, ones where you feel that you are weak, or even those that seem most interesting to you. An item that looks completely unfamiliar is also a good candidate — it is likely an important aspect of C++ of which you are not yet aware.
Finally, when you want insights into bureaucracy, tips on what to do with your icewater during NASA meetings (answer: dip booster rocket O-ring material into it), or just a good laugh when you are frustrated with C++, try Richard P. Feynman's "What Do You Care What Other People Think?" [Fey88]. The second article in this series will describe why Feynman's book is so important.
See Further Reading for notes on this bibliography.
[Cli95] Marshall P. Cline and Greg A. Lomow. C++ FAQs: Frequently Asked Questions. Addison-Wesley Publishing Co., Inc.. Copyright © 1995. 0-201-58958-3.
[Fey88] Richard Feynman and Ralph Leighton. "What Do You Care What Other People Think?": Further Adventures of a Curious Character. W.W. Norton & Company, Inc.. Copyright © 1998 Gweneth Feynman and Ralph Leighton. 0-393-02659-0.
[Mey96] Scott Meyers. More Effective C++: 35 New Ways to Improve Your Programs and Designs. Addison-Wesley Longman, Inc.. Copyright © 1996. 020163371X.
[Mey98] Scott Meyers. Effective C++: 50 Specific Ways to Improve Your Programs and Designs. Second. Addison-Wesley. Copyright © 1998. 0-201-92488-9.
[Poh02] Ira Pohl. C++ by Dissection: The Essentials of C++ Programming. Addison-Wesley. Copyright © 2002. 0-201-74396-5.
SimpleStringNotDerived ClassGeorge Belotsky is a software architect who has done extensive work on high-performance internet servers, as well as hard real-time and embedded systems.
Return to the Linux DevCenter.
Copyright © 2009 O'Reilly Media, Inc.