A Short Guide to C++ Pointers

From Wikiid
Jump to: navigation, search

Pointers are one of the most confusing things for C++ newbies. This document is an effort to unravel the confusion.

Contents

Objects.

In this context, an "object" could be anything from a 'bool' to a massive 'class' containing a gigabyte or more of data. It's generally helpful to think about pointers in the general case of a large data structure. So for our purposes, let's take as our example a class that takes over a megabyte of space:

 class Thing
 {
 public:
    int x ;
    char y [ 1000000 ] ;
 } ;

...so this is a LARGE data structure. The rules are the same for smaller objects - but this helps us to get our heads around why pointers are important.

Creating objects

There are two basic ways to create an object:

  1. By simply declaring one as a variable with a name: Thing myThing ;
  2. By having 'new' create an anonymous one for you: new Thing ;

In both cases, you get a meg of RAM eaten up.

What's different is the 'lifetime' of the object.

Lifetimes

If you declare something as Thing myThing ; then the memory is grabbed when the declaration is first executed - and it lives until your code hits the nearest surrounding block (probably the nearest surrounding close-curly-brace). If you declare it outside of any functions or classes or if you declare it as static Thing myThing ; - then it lives until the program exits.

If you create it with 'new' then it lives until you 'delete' the object - or until the program exits. It's generally considered "bad form" to leave the object lying around when the program exits.

Sometimes you only want the object for a short span of code - and it's handy to declare it as Thing myThing ; - and have the compiler magically free it up for you at the end of the block. But there are other times when you want to create an object in one place in your code - and use it all over the place. In that case, you need to 'new' it - and remember to 'delete' it when you're done with it.

There is another crucial difference here. In the case where you said Thing myThing ;, you have the 'name' of the object - so you can always get at its contents by saying myThing.x. However, when you create something using 'new', you don't have a name - and you can't refer to it - which is pretty much useless. However, 'new' handily returns a pointer to your newly created 'Thing'.

...which brings us handily to...

Pointers.

So a common way to handle the situation where you create something with 'new' is to say:

  Thing *myThingPointer ;
  myThingPointer = new Thing ;

...or (identically - but more conveniently)...

  Thing *myThingPointer = new Thing ;

...the '*' in the declaration says that 'myThingPointer' isn't an actual 'Thing' - but just a pointer to one. It is important to maintain this distinction in your head. 'myThingPointer' is a tiny little four or eight-byte item. It isn't (by itself) a gigantic data structure. It just POINTS to that megabyte structure so you know where it is in memory.

More importantly, the lifetime of the new'ed Thing is NOT the lifetime of myThingPointer. Even if myThingPointer goes out of scope (like we exit the block in which the pointer was declared) - the gigantic 'Thing' that you created with 'new' is still there.

The syntax of pointers can be confusing:

  • If you have an actual 'Thing' (as Thing myThing ;) then you can get the location of it using the '&' operator. What you get back from '&' is a pointer to the 'Thing'. Hence, you can say:
 myThingPointer = & myThing ;
  • If you have a pointer to a 'Thing' (as Thing *myThingPointer ;) then you can get the Thing it's pointing at using the '*' operator. Hence, you COULD say:
 myThingPointer = & myThing ;
 (*myThingPointer).x = 6 ;

...however, C++ gives you a shorthand for that second line:

 myThingPointer -> x = 6 ;

...but what's going on under the hood is the same. The '*' operator is finding the 'Thing' that the pointer is pointing to - and the '.' is getting the 'x' field from the actual Thing. Using '->' is much more common than using (*myThingPointer).x - and you should try to use -> whenever possible.

The '*' and '&' operators are the inverse of each other - '*' takes a pointer to a thing and returns the thing, '&' takes the thing and returns a pointer to it.

Passing things into subroutines.

If you declare a function like this:

 void myFunc ( Thing anotherThing )
 {
   anotherThing . x = 6 ;
   ...
 }

...and call it like this...

 myFunc ( myThing ) ;

...or like this:

 myFunc ( *myThingPointer ) ;

Notice that the function demands an actual 'Thing' - not a mere pointer to one. Hence you can pass it 'myThing' (which is an actual 'Thing') - or you can use the '*' operator to fetch the actual Thing from a pointer to it.

This works - and is legal - but unfortunately, there is a snag. C++ guarantees that functions will not change the value of the parameter out in the outside code. Recall how functions behave on simple integers:

 void bump ( int x ) { x = x + 1 ; }
 ...
 y = 6 ;
 bump ( y ) ;
 cout << y ;    // Prints '6', not '7'.

Of course 'y' is still '6' after the call because 'bump' can't change the value outside of the function. The compiler makes a copy of the variable 'y' - calls that copy 'x' and lets the function increment the copy. But when the function exits, that temporary variable 'x' is thrown away - and hence 'y' doesn't change value.

This same behavior applies even to gigantic class objects. So when you call 'myFunc(myThing);', the compiler must guarantee that the function won't change the value of 'myThing' outside the function. That forces the compiler to make a gigantic (one megabyte!) temporary 'Thing' object called 'anotherThing', and to copy every one of the million bytes of 'myThing' into it! Then it can run 'myFunc' and at the end of that function, delete the temporary 'anotherThing'. This is all perfectly legal - but for anything but the smallest of objects, it's horrifyingly expensive in both RAM space and CPU time.

Instead - it is usually better to pass a POINTER to the thing into the function:

 void myFunc ( Thing *anotherThingPointer )
 {
   anotherThingPointer -> x = 6 ;
   ...
 }

...and call it like this...

 myFunc ( myThingPointer ) ;

...or...

 myFunc ( & myThing ) ;

...now, the compiler makes a temporary copy of 'myThingPointer' and calls it 'anotherThingPointer' and lets the function work with that. We only copied the tiny pointer (just a handful of bytes) and not the gigantic megabyte 'Thing'...but because the anotherThingPointer is a copy of 'myThingPointer', it still points at the same 'Thing'. Now...anotherThingPointer->x = 6 ; actually changes the 'Thing' in the outside world! When the function exits, 'anotherThingPointer' is destroyed - but that doesn't change the fact that the actual Thing was changed.

This distinction between passing a POINTER to a Thing into the function versus passing the actual Thing itself is essential to understanding what's going on.

Now, for large data structures, the cost of passing the actual 'Thing' is outrageous - and is something you'd rarely do. But for much smaller data structures, the cost of making the temporary copy isn't so silly - so this concern isn't an issue. Now you need to carefully consider whether you want the function to be able to change the contents of the actual 'Thing' or not. If you want it to be able to change the Thing - then passing a pointer makes sense. If you want to be sure that it can't change the data - then by all means pass the actual 'Thing' to it...providing it's fairly small.

Common things pointers are handy for

If you have a linked list of objects - then these 'links' are pointers. They have to be - there is no way to put an entire 'Thing' inside another 'Thing' because the second Thing would have to have a third Thing inside of it - and you'd pretty soon have an infinitely large data structure! However, there is no problem with putting a tiny little pointer to a Thing inside of a Thing.

NULL pointers

The value 'NULL' (which is really just zero) is a special value that any pointer can be set to. It's commonly used to mean "I'm not pointing at anything right now" - and is a good thing to initialize pointers to - and something safe to set them to when you destroy the thing it's pointing at. The operating system is good at catching errors relating to NULL pointers - so you get clearer debug and easier to debug code.

 Thing *myThingPointer ;
 ...
 myThingPointer = new Thing ;
 ...
 delete myThingPointer ;
 ...

...is legal - and will work - but it's a bit dangerous if you accidentally try to use myThingPointer at times when it's not valid. It would be safer ("defensive programming") to write:


 Thing *myThingPointer = NULL ;
 ...
 myThingPointer = new Thing ;
 ...
 delete myThingPointer ;
 myThingPointer = NULL ;
 ...

...so myThingPointer is set to NULL whenever it's not pointing at something valid.

Functions that return pointers almost always return NULL when there is an error or something. It is also common for functions that take pointers as parameters to produce useful error messages (or at least sensible default behavior) when their pointer parameter(s) are NULL.

Arrays

Just when you think you have your head around pointers, along comes a complication.

Arrays in C++ are implemented as pointers. When you say:

 int a [ 10 ] ;

...you are really making 'a' be a pointer to a block of 10 integers. In fact, you can write:

 *a = 123 ;

...which sets the zeroth element of the array (the thing that 'a' points to) to 123.

When you add some number 'n' to the pointer itself ('a' in this case) - you are saying "make a pointer to the n'th data item after 'a'"...which means that:

 *(a+0) = 123 ;
 *(a+5) = 456 ;

...is the same as saying:

 a[0] = 123 ;
 a[5] = 456 ;

...and in fact, the '[...]' notation is just a short-hand for doing the addition and using the '*' operator to follow the pointer.

Hence, when you pass an array into a function:

 void widget ( int *aa )
 {
   aa [ 6 ] = 7 ;
 }
 widget ( a ) ;

...what you're actually doing is passing a pointer into the 'widget' function. That's why the syntax of 'widget' is just like 'myFunc(Thing *anotherThingPointer)'.

This even applies to arrays of class objects - so we have:

 void myFunc ( Thing *thingPointer ) { ... }
 Thing *myThingArray = new Thing [ 10 ] ;  // Make an array of ten new 'things' - and make myThingPointer point to the zeroth one.
 myFunc ( myThingArray + 5 ) ;  // Pass a pointer to myThingPointer[5] to the function.
 myFunc ( & (myThingArray[5]) ) ;  // Same exact thing - find the fifth element of the array - and use the '&' operator to get a pointer to it - and pass that pointer to the function.

...OR...if myFunc takes an actual 'thing' rather than a pointer:

 void myFunc ( Thing thing ) { ... }
 Thing *myThingArray = new Thing [ 10 ] ;  // Make an array of ten new 'things' - and make myThingPointer point to the zeroth one.
 myFunc ( myThingArray[5] ) ;  // Pass the actual myThingArray[5] object to the function.
 myFunc ( *(myThingPointer+5) ) ;  // Same exact thing - get a pointer to the fifth element - use '&' to get the actual fifth thing and pass that to the function.

Pointers to pointers...

You can (of course) have pointers to pointers. Just use more '*'s.

 // If you declare this...
 Thing t ;
 Thing *pt ;
 Thing **ppt ;
 // And assign pointers like this...
 pt = & t ;
 ppt = & pt ;
 // Then all of these have the exact same effect!
 t.x = 6 ;
 (*pt).x = 6 ;
 (**ppt).x = 6 ;  pt -> x = 6 ;  (*ppt)-> x = 6 ;

Common bugs

It is horrifyingly easy to screw up in monumental ways with pointers. The confusion over the lifespan of the Thing and the lifespan of the pointer-to-Thing leads to more bugs than any other cause:

 Thing *myThingPointer ;
 if ( x > y )
 {
   Thing myThing ;
   myThingPointer = & myThing ;
   ...do some work...
 }
 myThingPointer -> x = 6 ;  // CRASH!!!!

...what happened is that myThing has a lifetime only from where it is declared to the end of the surrounding block. Which means that the compiler destroys it when it hits the close curly bracket. However, nobody told 'myThingPointer' - so now it's pointing at garbage memory. Writing into it may crash the program - or it may simply cause something very weird and hard to debug to happen.

Another way for that code to fail is when the "if" part turns out false. In that case, the code inside the block is never executed - so myThingPointer is pointing at nowhere in particular - and again, a crash is likely.

However, this is OK:

 Thing *myThingPointer = NULL ;
 if ( x > y )
 {
   myThingPointer = new Thing ;
   ...do some work...
 }
 if ( myThingPointer != NULL )
   myThingPointer -> x = 6 ;
Personal tools