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;Thus
f2.len = f1.len;
f2.name = f1.name;
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;Thus each of
f3.name = f2.name;
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
}