LinuxDevCenter.com
oreilly.comSafari Books Online.Conferences.

advertisement


C++ Memory Management: From Fear to Triumph
Pages: 1, 2, 3, 4

Examples of Common C++ Memory Errors

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.




Table 1. Errors in Function (or Method) Calls and Returns


ErrorExample
Returning a Reference (or Pointer) to a Local Object
//---
SimpleString&
 generate_string() {
  SimpleString localstr("I am a local object!");
  //... maybe do some processing here ...
  return localstr;  //As soon as this function
                    //returns, "localstr" is
                    //destroyed.
}
//---

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-static) inside of a scope (as shown in the example), it no longer exists after the closing curly brace of that scope.

Result: dangling reference.

Returning a const Reference Parameter by const Reference
//---
const SimpleString&
 examine_string(const SimpleString& input) {
//... maybe do some processing here ...
  return input;  //If "input" refers to a temporary 
                 //object, that object will be 
                 //destroyed as soon as this
                 //function returns.
}
//---

The C++ compiler is allowed to create an unnamed temporary object for any const reference parameter. After all, you promise not to change the parameter when you declare it const. Unfortunately, the unnamed temporary goes out of scope as soon as control leaves the function, so if you return a const reference parameter by const reference, a dangling reference can result.

Result: dangling reference.

Passing an Object by Value
//---
//This function slices objects.
void the_slicer(Base arg) {
  arg.example();
}
//---

//---
Base base;
Derived derived;

cout << "Calling 'the_slicer' on a Base" << endl;
the_slicer(base);     //O.K.

cout << endl;

cout << "Calling 'the_slicer' on a Derived" << endl;
the_slicer(derived);  // Slices "derived";
                      // this is rarely what
                      // you want.
//---

See the Base class | See the Derived class | See the output

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
//---
SimpleString& 
  xform_string_copy(const SimpleString& input) {

  SimpleString *xformed_p = 
    new SimpleString("I will probably be leaked!");
  
  //... maybe do some processing here ...

  return *xformed_p;  //Callers are highly unlikely 
                      //to ever free this 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.



Table 2. Errors when Defining Methods in Classes


ErrorExample
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
//---
//NVDBase has a non-virtual destructor.
NVDBase* base_p = new NVDBase;  

NVDDerived *derived_p = new NVDDerived;

//A base-type pointer to a derived object:
//an essential feature in C++.
NVDBase* btd_p = new NVDDerived;

delete base_p;    //O.K.
delete derived_p; //O.K.
delete btd_p;     //Error! Will call only NVDBase's 
                  //destructor, even though "btd_p" 
                  //points at an NVDDerived!  The 
                  //object will not be properly 
                  //destructed.
//---

See the Classes | See the output

If you intend others to inherit from your class, you must declare its destructor virtual. Otherwise, the derived class' destructor may not be called in circumstances such as the one shown in the example.

Result: memory leak; possibly other problems.



Table 3. Errors in Handling of Allocated Memory or Objects


ErrorExample
Using Arrays Polymorphically
//---
void process_array(Base* array, unsigned len) {
  for (int i = 0; i < len; ++i) {
    array[i].example();
  }
}
//---

//---
Base array_of_base[3];
Derived array_of_derived[3];

//This works as expected.
process_array(array_of_base,3); 

cout << endl;

//This is a disaster!
process_array(array_of_derived,3);
//---

See the Base class | See the Derived class | See the output

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 virtual method calls are made based on the actual (fully derived) type of the object, even though the pointer or reference is of the base type.

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
//---
Base base;
Base *base_p = &base;

//Here, the programmer mistakenly believes that 
//NotDerived is derived from Base, and that "base_p"
//points to a NotDerived object (which would be 
//perfectly legal if, in fact, NotDerived *was* 
//derived from Base).  

//Old C cast is indiscriminate. 
NotDerived* wild_p = (NotDerived*)base_p;
    
cout << "Value of 'wild_p' is now: "  
<< wild_p << endl;

//This will not be soothing at all!
wild_p->soothe_me();  

cout << endl;

//The C++ dynamic_cast is specifically for 
//"casting down" the class hierarchy.
wild_p = dynamic_cast<NotDerived*>(base_p);  

//The pointer "base_p" does *not* point at a 
//NotDerived object; dynamic_cast returns a 
//NULL pointer.
cout << "Value of 'wild_p' is now: "  
<< wild_p << endl;

//At least the program will just crash immediately, 
//instead of doing something crazy.  A NULL pointer
//is much better than a wild pointer!
wild_p->soothe_me();
//---

See the Base class | See the NotDerived class | See the output

Casts have been likened to the infamous goto [Cli95]. This is not completely fair, but nevertheless, casts are rarely required in a well-designed C++ program. A cast will, for example, allow you to convert between two pointers to completely unrelated classes. This produces a wild pointer (recall that dangling references are a type of wild pointer). If you decide to use a cast, make it a very specific, limited part of your program — preferably hidden away in the internal details of a class. Also, try to use the C++ style casts in preference to the older C style casts. The former come in several varieties, each capable only of a particular kind of conversion, generally making them far safer than the indiscriminate C casts.

Result: wild pointer.

Bitwise Copying of Objects
//---
//Get multiple strings via a variable argument
//list, and print them out.
void print_strings(unsigned num ...) {
  va_list narg_p;
  va_start (narg_p, num);
  
  for (unsigned i = 0; i < num; ++i) {
    SimpleString str = va_arg(narg_p, SimpleString);
    cout << str.to_cstr()  << " ";
  }

  cout << endl;
}
//---

//---
SimpleString first("one");
SimpleString second("two");

//Print out the SimpleStrins directly,
//just to show that everything is O.K.
cout << first.to_cstr() << " "
<< second.to_cstr() << endl;

//Causes bitwise copies of the 
//objects -- serious error.
print_strings(2,first,second);

//We may or may not get here --
//it all depends on luck!
cout << first.to_cstr() << " "
<< second.to_cstr() << endl;
//---

See the output

Mechanisms such as the memcpy function simply copy memory bit by bit. Not all objects, however, can be copied this way without damage. A C++ object is not a piece of dead data; it is a living, intelligent collection of data and functions. When an object is copied, for example, its copy constructor might need to allocate memory, notify other objects, etc. A bitwise copy circumvents these kinds of operations. This results in a seriously broken copy, in many cases.

Besides memcpy, other bit-by-bit copies can result from realloc (it makes such copies when it reallocates memory). An especially easy mistake to make is with variable argument lists. Passing a C++ object using a variable argument list results in a bitwise copy, and is therefore very dangerous (although passing pointers to objects will work, as long as you are mindful of the fact that va_arg uses casts).

Result: memory leaks and dangling references are both possible, as well as other problems.

Deallocation of Memory That Was Not Dynamically Allocated
//---
SimpleString str("I am a local object!");
SimpleString* str_p = &str;

//O.K. to use, like any other pointer.
cout << str_p->to_cstr() << endl;

//The object pointed to by "str_p" was not
//dynamically allocated; you should *never* 
//do the following. 
delete str_p;
//---

See the output

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 delete this; inside of a method of your class — you must indeed be sure that the object has been dynamically allocated before you try to delete it.

Result: memory leaks and dangling references, as well as corruption of operating system data structures.

Mismatched Method of Allocation and Deallocation
//---
//Allocate via "new []" -- the array new.
SimpleString* str_pa = new SimpleString[10];

//A common but serious error -- you must use the
//array delete (i.e. "delete [] str_pa;") here.
delete str_pa;
//---

Here is a list of common, matched allocation and deallocation methods.

newdelete
new []delete []
malloc()free()

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 malloc and free are not recommended in C++ — they are not typesafe, and do not call constructors and destructors.

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. new (ptr_to_allocated_mem) MyClass;). In that case, you should call the object's destructor when you are finished using it, especially before freeing the memory pointed to by pointer_to_allocated_mem (how you free the memory depends on how you allocated it in the first place).

Result: memory leaks and corruption of operating system data structures.



Table 4. Errors Related to Exceptions


ErrorExample
Partially Constructed Objects
//---
class CtorThrow {
public:
  CtorThrow();
  ~CtorThrow();    //N.B. non-virtual, 
                   //not meant for subclassing.
private:
  Base* first_p_;
  Base* second_p_;
  Derived  member_obj_;

  //See the Training Wheels Class 
  //for an explanation of these declarations.
  CtorThrow(const CtorThrow&);
  CtorThrow& operator=(const CtorThrow&);

};

CtorThrow::CtorThrow() : first_p_(0), second_p_(0),
		       member_obj_() {
  first_p_ = new Base;
  
  //Could also call a function/method that throws;
  //in any case, "first_p_" is leaked.
  throw SimpleString("Exception!");

  second_p_ = new Base;
}

//Destructor will not be called when exception
//leaves the constructor.
CtorThrow::~CtorThrow() {
  cout << "Destroying CthorThrow" << endl;

  delete first_p_;
  delete second_p_;
}
//---

See the aggregated Base class | See the contained Derived class | See the output

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
//---
//When DtorThrow throws an exception in in its
//destructor, but no other exception is active, 
//all other objects are properly destroyed.
try {
  cout << "Only one exception ..." << endl;

  Base obj1;
  Derived obj2;
  DtorThrow obj3;
  
} catch(...) {//Catch everything.
  cout << "Caught an exception" << endl;
}

cout << endl;

//When another exception is active, and DtorThrow 
//throws an exception in its destructor, the other
//objects are *not* properly destroyed.
try {
  cout << "Exception during another exception ..."
       << endl;

  Base obj1;
  Derived obj2;
  DtorThrow obj3;

  throw SimpleString("Exception!");

} catch (...) {//Catch everything; 
               //we never get here.
  cout << "Caught an exception" << endl;
}
//---

See the DtorThrow class | See other classes used (Base, Derived) | See the output

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 terminate function. By default, terminate calls abort, which abruptly ends the program. In consequence, local objects are not properly destructed. While the operating system should reclaim the memory when your program terminates, any complex resource that requires your destructors to run (e.g., a database connection) will not be properly cleaned up.

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 uncaught_exception function, which can be used to test if an exception is already in progress).

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
//---
//Outer try block.
try {
  //Inner try block.
  try {

    throw Derived();

  } 
//Catch by reference -- won't slice.
  catch (Base& ex) {
  
    ex.example();  //O.K.

    //Rethrow ...

    throw ex; //Mistake -- slices!
              //Should just use "throw;".

  }
  //END Inner try block.
} 

//Should be fine, but ...
catch(Base& ex) {
  ex.example(); //... not what we expected!
}
//---

See the Base class | See the Derived class | See the output

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 catch clause, simply use throw; — not something like throw arg;. The latter will construct a new copy of the object, which will slice off any derived parts of the original.

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
//---
try {

  throw Derived();

} 
//This not only shadows the Derived catch
//clause -- it slices, too!
catch (Base ex) {
  cout << "caught a Base" << endl;
  ex.example();  //Sliced!
}

//We never get here!
catch (Derived ex) {
  cout << "caught a Derived" << endl;
  ex.example();
}
//---

See the Base class | See the Derived class | See the output

Improper catching of exceptions can also lead to object slicing. As you might have guessed, catching by value will slice. The order of the catch clauses matters, too; always list the catch for an exception of a derived class before the catch of its base class. The exception mechanism uses the first catch clause that works, so listing base classes up front will always shadow the derived classes.

Result: object slicing; memory-related errors and other problems are possible if a base-class catch shadows a derived-class catch, thus preventing the latter from taking actions specific to a derived-type exception.

 

Pages: 1, 2, 3, 4

Next Pagearrow




Linux Online Certification

Linux/Unix System Administration Certificate Series
Linux/Unix System Administration Certificate Series — This course series targets both beginning and intermediate Linux/Unix users who want to acquire advanced system administration skills, and to back those skills up with a Certificate from the University of Illinois Office of Continuing Education.

Enroll today!


Linux Resources
  • Linux Online
  • The Linux FAQ
  • linux.java.net
  • Linux Kernel Archives
  • Kernel Traffic
  • DistroWatch.com


  • Sponsored by: