Certainly you will want to set your pointers to NULL
when deleted (or when declared if they are not initialized) and you'll want to test your pointers to ensure that they are not null before using them.
SomeClass * pPointer = NULL; // initialize to NULL if not otherwise
// initialized
pPointer = SomeMethod(); // now assign the pointer
if ( pPointer != NULL) // test before using!
pPointer->SomeMemberFunction();
// … continue using the pointer
delete pPointer; // delete the pointer
pPointer = NULL; // reset to NULL after deleting!
Setting the pointer to NULL
(or 0
) has two advantages. First, it allows you to test your pointer before using it. If the test fails (if pPointer
is NULL
), then you can then take appropriate action (e.g. throw an exception, skip a block of code, etc.) More important, deleting a pointer (or more accurately, the object pointed to by the pointer) which has already been deleted is not legal in C++. Deleting a NULL
pointer, however, is perfectly safe. By setting the pointer to NULL
after calling delete, you protect yourself from crashing should you subsequently delete the same (already deleted) pointer.
Pointers are quite tricky, even for experienced programmers. It is quite easy to lose the pointer and to create a memory leak, or to find yourself with a stray pointer which no longer points to valid memory. Copying objects with pointers is even trickier, and it is critical to be careful about ensuring that objects are properly allocated and deleted when you are done with them.
A key element in using pointers well is to ensure that you provide the four canonical C++ methods:
Do not use the default methods provided by the compiler. You must write your own so that you can be certain to manage memory appropriately.
When you pass an object by value, a temporary copy of that object is made by the compiler. You pass by value when you pass a parameter that is not a pointer or a reference, or when you return an object from a method.
If the object you pass is of a user-defined class, the class's copy constructor is called by the compiler. All copy constructors take a single parameter: a reference to a constant object of the same type. For example:
Employee::Employee(const Employee & rhs);
The convention is to refer to the parameter as rhs
, which stands for right hand side. This convention began because of the assignment operator, discussed shortly. When you write,
myEmployee = yourEmployee;
the compiler translates this code into:
myEmployee.operator=(yourEmployee);
Thus, the parameter passed in is the variable on the right hand side of the assignment operator (=
).
In any case, if you do not create a copy constructor, the compiler will provide one for you, but that compiler-provided copy constructor will perform a member-wise (or shallow) copy of the object. This means that each member variable of the first object will be copied into the member variables of the second object.
A shallow or member-wise copy copies the exact values of one object's member variables into another object.
If these member variables are just objects, this will work fairly well, but if they are pointers, then disaster ensues. Let's look at an example to see why. Suppose you have an Employee
class that looks like this:
class Employee
{
public:
Employee();
~Employee();
// note no copy constructor
// using the compiler-supplied shallow copy constructor
// other methods here...
private:
int myAge; // how old is the employee
EmployeeLevel* pEmpLevel; // ptr to EmployeeLevel object
};
For the moment, we don't care what an EmployeeLevel
object looks like, but we note that every Employee
object has a pointer to one. If we then pass an Employee
object into a method by value, like this:
Employee fred;
SomeMethodWhichTakesAnEmployeeByValue(fred);
Void SomeMethodWhichTakesAnEmployeeByValue(Employee theEmployee)
{
// does some work with theEmployee
}
Then the compiler must copy fred
to a temporary object, named theEmployee
, inside the method SomeMethodWhichTakesAnEmployeeByValue()
. Because we did not provide a copy constructor, the compiler-supplied shallow copy constructor will take over. It will copy the value in fred.myAge
into theEmployee.myAge
and it will copy the value inside fred.pEmpLevel
into theEmployee.pEmpLevel
.
What is wrong with that? The value in fred.pEmpLevel
is a memory address — the address of the EmployeeLevel
object which was presumably created on the heap when fred
was initialized. Unfortunately, now two objects point to that same memory:
When the function returns, the temporary object (theEmployee
) will be destroyed. The Employee
destructor will be called, which ought to delete the pointer. Unfortunately, the pointer points to the same object in memory as that pointed to by fred
, leaving fred
with a pointer to what is now a destroyed EmployeeLevel
object. The result is that when you use fred
's pointer again, the system will crash.
The alternative is to write your own copy constructor and to allocate memory for the new pointer. This creates a deep copy — one which copies not only the absolute value of each member variable, but which copies those objects that are pointed to as well.
A deep copy copies the values allocated on the heap to newly allocated memory.
Employee::Employee( const Employee & rhs):
myAge(rhs.myAge),
pEmpLevel(new EmployeeLevel(rhs.pEmpLevel))
{
}
This copy constructor now initializes the member variables appropriately, allocating memory for the object pointed to by pEmpLevel
. If you are not familiar with object initialization, take a moment to look up this technique. It is far more efficient to initialize these objects than to assign them in the body of the constructor:
Employee::Employee( const Employee & rhs)
{
myAge = rhs.myAge;
pEmpLevel = new EmployeeLevel(rhs.pEmpLevel);
}
While this works, it creates unnecessary temporary copies, and thus is slower and uses more memory. In addition, if any of the member variables are constant or are references you must initialize and cannot use assignment.