If C++ programmers had a bill of rights, one of those rights would be stated “The Right to own and bear pointers.” As much as modern languages tout garbage collection and dispell themselves of the concept of a pointer, the fact remains that you will achieve better performance if you manage your own memory, and pointers are a critical part of that.

This collection of tips come from the top of my head — think of it as the sort of advice I would pass on to my daughter if she ever found herself writing a memory intensive C++ application. For the record, she is six years old, so please, no complaining if this advice seems obvious or academic to you.

Oh, and disclaimer #2. This article is for C++ programmers only.

1. Pointers need well defined owners

Whenever you do an allocation it is important to identify an owner for that pointer — someone, something that is responsible to delete it when it is done with it. The owner also defines the pointer’s life span. Sometimes the ownership of an allocation can be by committee, for example, when you are using reference counting. Managing the lifetimes of allocations is the most important rule of memory management. If you consistently make ownership and lifetime absolutely clear and obvious in your code, you will avoid lots of headaches and bugs. This is the core of our second admendment rights, the right to own and bear pointers. The grantation of rights comes with a responsibility to bear. Treat pointers with the respect you would treat a firearm — and you may not find yourself shot in your own foot.

2. Use const

If you pass an object to a function by pointer and do not intend to modify the object, mark the object (not the pointer) as const.

void doSomething(const T* p) {
// do not modify *p
}

Some programmer’s eschew const because they believe that they know better than to modify an object at an inappropriate time. That’s good for them, but if you have any real world experience, you know that you will not be the one maintaining your code for the rest of it’s life. Putting these guardrails in place will prevent a rooky from putting in a bad side-effect then asking to you to figure out why everything is broken.The disadvantage of using const is that it frequently will double the number of getters you need. A very typical C++ idiom is the const and non-const getter:

class someClass {
public:
// ...
const T* getSome() const;
T* getSome();
private:
T* m_t;
};

This class clearly owns a pointer to an object of type T, and it will only surrender that pointer on equal terms. You have to have a non-const reference to someClass to get a non-const pointer to T.A final warning about const. Avoid casting away const-ness. Sometimes, it will be unavoidable. Nevertheless, your mindset should not be “oh, I just need to cast, ok, done.” You should always say to yourself “self, what have I done wrong? Maybe I am not using this code the way the author intended.” If someone took to the trouble to make a variable const, they probably had a reason.

3. Use the new, safer C++ style casts

C++ is quite willing to let you cast any pointer type to any pointer type using conventional C casts. Using the new C++ style casts will help you insure that your casts are safe and sensible.

4. Avoid pointer arithmetic

There are two ways to walk over an array represented by a pointer. Some coder’s prefer this:

void iterateArray(T* p, int n) {
for (int i=0; i // do something with *p
}
// ...
}

This is fast, and effecient, but not easy to trace, especially when multiple pointers are involved. And, by the end of the for loop, you have lost the original value for p. Yes, you can recalculate it, or save and restore it, but that just adds to the complexity. I prefer this approach instead:

void iteratorArray(T p[], int n) {
for (int i=0; i // do something with p[i]
}
// ...
}

This avoids the pointer arithmetic and is easier to read. Let the compiler identify the loop invariant and do the optimization for you. Some poor compiler engineer spent a lot of time working out that optimization. Think of that poor guy, or the next person who's going to have to read your code. It's also important to remember that the performance gains you get from using C++ and doing your own memory management will eclipse any peephole optimizations you can achieve using pointer arithmetic. Paraphrased: your code is already fast -- now make it easy to read.

5. Use new and delete

Using new and delete ensures that constructors are executed, which ensures that your member variables are initialized. For C code, use malloc and free. There are situations where you will want to write your own allocation routines when you have to squeeze out an extra ounce of performance, but do so with discretion. Why is this important? Because if you do your own memory management, you are bound to make mistakes. There are tools that will help you, such as Rational's Purify or Numega's BoundsChecker, but often using your own memory management masks the precise origin and granularity of allocations from these tools.

6. Beware of new[] and delete[]

Many memory leaks and other crashes originate from inconsistent usage of the array allocation (scalar allocation) primitives. The most common mistake is using new[] to allocate and delete (non-scalar) to deallocate. The effect is that you construct an array of objects, then only destruct the first one. Depending on the nature of scalar new and delete, this may also segment memory, cause a crash, or throw a hard exception. The opposite error, using new to allocate and delete[] to deallocate will sometimes go undetected. But, again, it depends on the implement of scalar new and delete. I advise you to find or write an array or vector template class that you are comfortable with, and use that instead any time you are tempted to use new[] and delete[]. This is an area where C++ should have been more strongly typed. Currently the type returned by new T[n] is simply a pointer, T*; it should be an array T[]. Currently C++ makes little distinction between these two types. Distinguishing the types would have allowed the compiler to detect an improper use of delete. It would have also forced programmers to use the array type when they are treating memory as an array and a pointer when it is a pointer to a single item. Taking things one step further, operator[] would not even be allowed for pointer types.

7. Bound arrays

If you do not make use of an array or vector template class, you need to bound your arrays. I prefer to keep track of the number of items allocated. Avoid null terminated arrays -- history should have taught us that lesson by now.

8. Initialize pointers to NULL

If you are not immediately initializing a pointer to a valid value, always initialize it to NULL. There is just no excuse other than laziness to write this:

T* p;

instead of:

T* p = NULL;

With optimizing compilers, it is very, very seldom that this will introduce even an extra cycle to the performance of your code, and will protect you from leaving a pointer un-initialized and later doing this:

if (p) delete p;

You should also assign pointers back to NULL after deleting them, like this:

delete p;
p = NULL;

Again, this traces back to pointer lifetimes. Pointer variables should be NULL whenever they are not pointing to valid memory. To take this to an extreme, I even like to do this in destructors. Think of it as analogous to wiping your fingerprints at the scene of a bank robbery. Leave the place as clean as it was before you showed up. Once memory is deleted, leave no trace that it ever existed.

someClass::~someClass() {
delete m_t;
m_t = NULL;
}

9. Always test pointers before using them

Do:

void someFunction(T *p) {
if ( !p ) {
// throw an exception or return an error
}
p->doSomething();
}

Avoid:

void someFunction(T *p) {
p->doSomething();
}

This is another peephole optimization that some lazy programmers defend. I admit, there is some merit to the optimization, it avoid a branch test, and we all know that branches clog CPU pipelines. However, unless the function's contract (documentation) specifies that it can not handle a NULL pointer, you need to be prepared to handle the case in an appropriate manner. A good rule of thumb is to only use pointers when the object is optional to the function.

10. Use pass by reference for non-NULL objects

Do:

void someFunction(T &p) {
p.doSomething();
}

Avoid:

void someFunction(T *p) {
p->doSomething();
}

Using a reference type makes it clear that someFunction expects a non-NULL object by contract. This also avoids the pointer check mentioned in the previous section.

11. Avoid acquiring pointers to non-heap memory

Avoid:

void someFunction() {
int a=0;
int *p = &a;
// do a bunch of stuff
}

Otherwise, you may forget that p is pointing to an object whose life is over when the function returns, which will lead to all kinds of dangerous things.

Summary

There is a great deal of over-inflated talk about the dangers of using pointers and the difficulty of doing your own memory management. Experienced Java programmers will even tell you that if you don't know how to manage memory correctly and establish documented lifetimes for objects, you will have all kinds of memory problems including leaks and painful garbage collection delays. That advice is coming from people who have garbage collection. I submit that learning how to manage memory well is a skill that is actually easier to learn by programming in C++ and using pointers in a sensible manner. These skills are not arcane remnants of a programming language that is past it's prime. They will make you a better programmer in whatever language you need to use in the future.