C and C++ Programming Resources

Custom Search

C++ Memory Management

Understanding C++ Memory Errors

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++.

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.

A Memory Error Taxonomy

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

From Memory Leak To Dangling Reference?

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.

Common Dangling Reference

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.

Constructor
Initializes the object during creation. Usually involves allocating resources.
Copy Constructor
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 );
Assignment Operator
Assigns one fully constructed object to another fully constructed object. Declared like this:

SimpleString& operator=( const SimpleString& right_hand_side );
Destructor
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.

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.

Pages: [Page - 1] [Page - 2] [Page - 3] [Page - 4] [Page - 5] [Page - 6]

Tags: ,

There are 2 Comments to this post. You can follow any responses to this entry through the RSS 2.0 feed. You can skip to the end and leave a response or TrackBack from your own site.


Leave a Reply

You must be logged in to post a comment.