NOTE: Modern versions of C++ (especially C++11 and newer) have "smart pointers" which are intended to help programmers avoid many common memory management and pointer-related programming errors. Those facilities are beyond the scope of these notes. The material on this page is intended to help new C++ programmers using conventional "raw" pointers to be aware of the source of common and often catastrophic programming errors.
Unlike Java, C++ is not a garbage collected language. Storage allocated by new remains allocated on the heap until it is explicitly returned to free storage by using the delete operator on a pointer currently holding the address of the dynamically allocated data.
In other words, you (as the programmer)
are responsible for deleting dynamically allocated
objects once they are no longer needed.
When writing medium to large
scale C++ programs, you must develop a discipline of considering
from the early design stages where and when objects dynamically
allocated with new
will be returned to free storage using
delete
. It is beyond the scope
of these notes to treat this important issue in the depth it
deserves. For now I would just say that this is
a topic whose importance cannot be overstated. As we proceed through
the course, we will see several typical strategies.
The
C++ destructor (roughly comparable to the Java method finalize
defined in the Object
class) is an important tool for
this type of memory management. Understanding how to use destructors as well as understanding
how the copy constructor and destructor relate to one another
is an extremely important topic that we will introduce below.
We first consider the operator that returns dynamically allocated memory: delete. It is essentially the "inverse" of new.
You must correctly match new and delete forms:
Foo* bar = new Foo; •
•
• delete bar; // NOT: delete [ ] bar;
Foo* bar = new Foo[20]; •
•
• delete [ ] bar; // NOT: delete bar;
For example (assume the classes DataObject
and Hold
have
been defined appropriately):
Java | C++ |
void doSomeWorkWithData(int num) { double[ ] array = new double[num]; DataObject myObj = new DataObject(); Hold[ ] buffers = new Hold[2*num]; // ... // ... work with array, myObj, and buffers // ... // Then just leave! } |
void doSomeWorkWithData(int num) { double* array = new double[num]; DataObject* myObj = new DataObject(); Hold* buffers = new Hold[2*num]; // ... // ... work with array, myObj, and buffers // ... // To avoid a memory leak, we must deallocate all dynamically allocated // storage that will no longer be needed once we leave this method. // NOTE: // bracketed (i.e., array) "new" calls must be matched by bracketed "delete" calls; // ditto for non-bracketed calls. // Mismatching these can lead to runtime segmentation faults. delete [ ] array; delete myObj; delete [ ] buffers; } |
Notes:
|
Notes:
|
The copy constructor is a special constructor in C++ whose prototype can be either:
or:
The prototype for a destructor is either:
or (a version that we will ignore for now, returning to it later in the course):
The copy constructor is special in C++ because the language specification requires that it be used in the following situations (assume Widget has been defined as a class and that wOriginal is an instance of class Widget):
Similarly, the destructor will automatically be called:
If you do not define and implement a copy constructor for a class you create, the compiler will automatically create one for you. The compiler-generated one will perform a so-called "shallow copy".
Similarly, if you do not define and implement a destructor, the compiler will automatically create one for you. The compiler-generated one will have an empty implementation.
The compiler-generated copy constructor and destructor are perfectly fine, provided no dynamically allocated data is being stored in your class and provided you need do nothing else such as closing files.
The purpose of the destructor is to give you an opportunity to free up any dynamically allocated data associated with the object and/or do other operations such as closing files. Implementing a destructor is vital for objects that store pointers to dynamically allocated data in their instance variables. You must of course also provide a compatible and properly implemented copy constructor for such an object.
A memory leak refers to the situation that arises when dynamically allocated memory is not returned to the free storage pool when it is no longer used or referenced. This will generally lead to your program consuming more memory than it needs. In extreme cases, it can lead to runtime program crashes when subsequent memory allocations fail.
To avoid memory leaks, you must have a plan to guarantee that you will do a delete on any dynamically allocated piece of memory once you are finished using it. As noted above, destructors are one of the most common tools in object-oriented programs to help us accomplish this.
The basic idea is simple: apply the delete operator to a pointer that currently references the dynamically allocated memory you wish to return to free store. If you allocated the storage as an array (e.g., new DataType[someSize]), then be sure to use the bracketed form of the delete operator (i.e., delete [ ]) as illustrated in the example above.
The following is representative of a very common mistake made by novice C++ programmers:
void someClientMethod(Foo fooParam) { Foo* temp = new Foo; temp = &fooParam; // can no longer access the "Foo" we just allocated … }
void foo() { int* p1; … zero or more lines of code with no assignment to p1 … *p1 = 15; // 15 gets written to an unpredictable location in memory (or a // segmentation fault occurs) … }
Example 2:
void update(double* v) { *v = 1.1; // again, 1.1 gets written to an unpredictable location (or a // segmentation fault occurs) } void caller() { double* vbl; update(vbl); }
"I kept trying things until it compiled. But I don't understand why it crashes."
SomeType* x = new SomeType; SomeType* y = x; … delete x; delete y; // The heap is now corrupted
Such storage is typically either a stack-allocated instance or a statically created variable at class or global scope. One example:
void uhOh() { int val = 10; int* pVal = &val; … delete pVal; // Noooooooooo! Both the heap and the stack are corrupted. }
SomeType* x = new SomeType; SomeType* y = x; … delete x; … y->someMethod(…); // Noooooooooo!