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.

Two forms of delete

You must correctly match new and delete forms:

For example (assume the classes DataObject and Hold have been defined appropriately):

JavaC++
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:
  1. The memory allocated for array, myObj, and buffers in the heap will eventually be made available for reuse by Java's garbage collection-based memory management system.
Notes:
  1. delete is an operator that is applied to exactly one dynamically allocated object. While it is (unfortunately) syntactically valid to write, for example:
    delete a, b, c;
    this only deletes a! You must execute individual delete statements as illustrated in doSomeWorkWithData above.
  2. This example is a little over-simplified in that data dynamically allocated in one method is very frequently, if not usually, used in other methods and must therefore be deleted elsewhere when it is no longer needed. We are about to learn about destructors, a very common tool to help with memory management of this sort.

The Copy Constructor and the Destructor

The copy constructor is a special constructor in C++ whose prototype can be either:

Widget(Widget& w);

or:

Widget(const Widget& w);

The prototype for a destructor is either:

~Widget();

or (a version that we will ignore for now, returning to it later in the course):

virtual ~Widget();

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.

Memory Leaks

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.

Instant memory leak

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
    …
}

Catastrophic Pointer and Memory Management Related Programming Errors

  1. Using uninitialized pointers
    Example 1:
    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."

  2. Doing a delete on a pointer more than once. This can happen in non-obvious ways. For example:
    SomeType* x = new SomeType;
    SomeType* y = x;
    …
    delete x;
    delete y; // The heap is now corrupted
    
  3. Doing a delete on a pointer whose value is nullptr (or NULL).
  4. Doing a delete on a pointer whose value has never been initialized.
  5. Doing a delete on a pointer that is pointing to storage not allocated on the heap using new.

    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.
    }
  6. Using "dangling pointers"