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
Error
Example
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.
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.
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.
//---
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
Error
Example
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);
//---
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();
//---
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;
//---
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;
//---
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.
new
delete
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
Error
Example
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_;
}
//---
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;
}
//---
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!
}
//---
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();
}
//---
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.