Dangling pointers

A dangling pointer is a pointer to storage that is no longer allocated. Dangling pointers are nasty bugs because they seldom crash the program until long after they have been created, which makes them hard to find. Programs that create dangling pointers often appear to work on small inputs, but are likely to fail on large or complex inputs.

As the world's leading example of an object-oriented programming language that does not rely on garbage collection, C++ makes it easy to create dangling pointers. Here are a few examples of the most popular techniques.

Note: These examples use C-style strings because we've been using an old version of Gnu C++ whose strings do not conform to the new standard.)


    delete [] s1;
    delete [] s2;
    return f (s1, s2);      // s1 and s2 are dangling pointers

This code will probably appear to work unless f or one of the functions that are called during the activation of f happen to allocate heap storage. When the bug does show up, it will probably look like a bug in f or in one of the functions that f calls.


typedef Foo_ * Foo;

Foo newFoo (char * x) {
    Foo_ tmp(x);
    return &tmp;
}

This is the classic technique for creating a dangling pointer in C.


typedef char * Foo;

Foo newFoo (char * x) {
    Foo tmp = new char [strlen (x) +1] ;
    strcpy (tmp, x);
    delete [] x;
    return tmp;
}

Here newFoo creates a dangling pointer by deleting the client's C-style string.


typedef char * Foo;

Foo newFoo (char * s) {
    return s;
}

If newFoo is supposed to return a Foo whose lifetime is independent of the lifetime of its argument, then a dangling pointer will be created when a client deletes the C-style string that was passed to newFoo. The bug might appear to lie in the client code, but newFoo would be the real culprit.


class Foo {
  public:
    Foo (char * x) : len(strlen(x)), name(x) { }
  private:
    int len;
    char * name;
};

Foo newFoo (char * s) {
    return Foo(s);
}

Once again, a dangling pointer will be created when a client deletes the C-style string that was passed to Foo or newFoo.


class Foo {
  public:
    Foo (char * x) {
        len = strlen (x);
        name = new char[len + 1];
        strcpy (name, x);
    }
    virtual ~Foo () {
        delete [] name;
    }
  private:
    int len;
    char * name;
};

Foo newFoo (char * s) {
    Foo foo = Foo(s);
    return foo;
}

This code fixes the previous bug by introducing three new bugs. The most obvious is that the compiler inserts an implicit call to foo.~Foo() when newFoo returns. This implicit call deallocates foo.name. Hence the Foo that is returned by newFoo always contains a dangling pointer.

The other bugs are illustrated by the following client code:

    Foo f1 = newFoo ("hi there");
    Foo f2 = f1;
    Foo f3;
    f3 = f2;

Since no copy operator is defined, the compiler will implicitly define a copy constructor that makes Foo f2 = f1 roughly equivalent to

    Foo f2;
    f2.len = f1.len;
    f2.name = f1.name;
Thus f2.name becomes the same pointer as f1.name.

Similarly, no assignment operator is defined, so the compiler will implicitly define an assignment operator that makes f3 = f2 roughly equivalent to

    f3.len = f2.len;
    f3.name = f2.name;
Thus each of f1, f2, and f3 contain exactly the same pointer. When they go out of scope, that pointer will be deallocated not once, but three times.

A storage leak would be created if we were to remove the destructor or to remove the call to delete, so those are not good alternatives. What we need is a copy constructor and an overloaded assignment operator.


class Foo {
  public:
    Foo (char * x) {
        len = strlen (x);
        name = new char[len + 1];
        strcpy (name, x);
    }
    virtual ~Foo () {
        delete [] name;
    }
    Foo (const Foo & foo);                     // copy constructor
    const Foo & Foo:operator= (const Foo &);   // assignment operator
  private:
    int len;
    char * name;
};

//  copy constructor

Foo::Foo (const Foo & foo) {
    len = foo.len;
    name = new char [foo.len];
    strcpy(name, foo.name);
}

//  assignment operator

const Foo & Foo::operator= (const Foo & rhs) {
    delete [] name;
    name = new char [rhs.len + 1];
    strcpy(name, rhs.name);
    return *this;                              // so x = y = z will work
}

Foo newFoo (char * s) {
    Foo foo = Foo(s);
    return foo;
}

This code still contains a bug. Consider the client code

    Foo f1 = newFoo ("hello");
    Foo f2 = newFoo ("goodbye");
    f1 = flag ? f1 : f2;

The assignment represents an implicit call to f1.operator=(flag ? f1 : f2). Suppose flag is true, so the value of the right hand side of the assignment is a reference to f1. The code for f1.operator= begins by deleting f1.name. It then passes the dangling pointer f1.name as both arguments to strcpy. Following the assignment, f1 contains a dangling pointer. When f1 goes out of scope, and its destructor is called, the delete [] operator will be called on f1.name for the second time.

The solution for this problem is to make the assignment operator check whether this is equal to the right hand side:

const Foo & Foo::operator= (const Foo & rhs) {
    if (this == &rhs) {
        delete [] name;
        name = new char [rhs.len + 1];
        strcpy(name, rhs.name);
    }
    return *this;                              // so x = y = z will work
}

What could be simpler?


Last updated 2 March 1998.