The Great Pointer Conspiracy

One of the great tragedies of C and C++ is that they are taught wrong — that a num­ber of per­fectly straight­for­ward fea­tures are taught and described as if they were myth­i­cal and super­nat­ural enti­ties that no mor­tal can truly under­stand. Mem­ory man­age­ment in C++ is one such fea­ture (it is actu­ally very sim­ple, once you know the trick), but the biggest of all is prob­a­bly pointers.

Every­one who learns C++ fears point­ers. Every­one who is new to the lan­guage, or who has merely heard of the lan­guage con­sider point­ers to be some kind of magic — arcane con­structs that give the pro­gram­mer access to Real Ulti­mate Power — a fea­ture that both mark C/C++ as supe­rior and more pow­er­ful than other lan­guages, but is also feared as dan­ger­ous or unsafe*.

None of this is true.

Point­ers are simple.

Point­ers are not magical.

Point­ers are safe (as long as you use them only as allowed by the language)

It is very well-defined what you may, and may not, do with a pointer. The only prob­lem is that the com­piler is unable to enforce most of this, so it relies on your own dis­ci­pline, and knowl­edge of the rules. But the rules exist. And if you stay within the rules, if your C++ pro­gram is legal, then point­ers are per­fectly safe.

This post is my lit­tle attempt to debunk The Great Pointer Con­spir­acy. It seems there is some hid­den rule that when­ever we teach oth­ers C or C++, we must describe pointers

  • as more com­pli­cated than they are, and,
  • as some­thing they are not. It some­times makes sense to lie to your pupil in order to teach them the truth a bit at a time (sim­i­lar to how most of what you learned in ele­men­tary school turns out to be wrong when you get to uni­ver­sity. They didn’t mis­lead you, they just taught you sim­pli­fied ver­sions of the truth to get you on the right track). But in the case of point­ers, the model taught is not merely wrong, it is also more com­plex and harder to understand!

So what is a pointer then?

Let’s start with a crash course in syn­tax, just to get that out of the way.

  • A pointer to type T is denoted T* (pro­nounced pointer to T)
  • A pointer is cre­ated with the & oper­a­tor. Assum­ing an int i, we can cre­ate a pointer to it: int* p = &i; (&i is typ­i­cally pro­nounced as take the address of i)
  • A pointer can be deref­er­enced with the * oper­a­tor, yield­ing the value it points to: int j = *p;

That’s easy, right? The only point of con­fu­sion is the dual role of *, as both part of the type, and as the deref­er­enc­ing oper­a­tor. There’s a bit of sym­me­try here, because & can be used in both places as well. As above, it can be used to take the address of an object, but it can also be used as part of the type, to cre­ate a ref­er­ence: int& k = i cre­ates a ref­er­ence to the pre­vi­ously defined inte­ger i. But ref­er­ences aren’t the sub­ject of this post. I only men­tion it because of the related syntax.

So, on to what point­ers are, and what they can do:

Point­ers are references

A pointer is lit­tle more than a ref­er­ence (in the con­cep­tual sense — not the spe­cific C++ ref­er­ences men­tioned in the pre­vi­ous sec­tion) to a vari­able. If we have mul­ti­ple ref­er­ences to the same vari­able, they will all see changes made by each oth­ers. Here’s an example:

void Foo(int* ptr){ // Because we're passed a pointer, we have a reference to the original variable, and can modify it so the changes are visible outside the function
  *ptr = 2; // set whatever ptr points to, to 2
}

int main(){
  // create a local variable i. This isn't a pointer, but it can be referenced by one.
  int i;
  int* p = &i; // create a pointer to i by taking the address (see below) of i, and store that as a pointer p
  i = 1;
  assert(*p == 1); // the value referenced by p is now equal to 1
  Foo(p);
  assert(i == 2 && *p == 2);
}

impor­tant note: Yes, I used the word “address” in the com­ment above. It is impor­tant to real­ize what I mean by this. I do not mean “the mem­ory address at which the data is phys­i­cally stored”, but sim­ply an abstract “what­ever we need in order to locate the value. The address of i might be any­thing, but once we have it, we can always find and mod­ify i. If you want a real-world anal­ogy, what is an address in the real world? My email-address has noth­ing to do with my house address. My phone num­ber could be con­sid­ered a third address. Even my social secu­rity num­ber, or my full name could be con­sid­ered addresses in this sense. All of these allow you to locate or con­tact me, which is all we require.

So far, so good. Point­ers are sim­ply ref­er­ences to other vari­ables, with slightly quirky syn­tax in that we have to use *p to get the value that the pointer p points to, and we have to use &i to cre­ate a pointer to i.

Of course point­ers can do a bit more than this though. They’re not as com­plex as peo­ple often try to con­vince begin­ners, but they’re not that sim­ple either.

Point­ers can be reseated

Once a pointer exists, we can change what it points to. For example:

int main() {
  int i = 1;
  int j = 2;
  int* p = &i; // make the pointer p point to i
  assert(*p == 1);
  p = &j; // and now make it point to j
  assert(*p == 2);
  *p = 3; // modify the variable p points to
  assert(j == 3);  // j is now 3
  assert(i == 1);  // but i is untouched, because p no longer points to it.
}

See, that’s not rocket sci­ence either, is it? What­ever the pointer points to, we can look at and mod­ify. And when it no longer points to that, they have no con­nec­tion any more.

Point­ers can be null

Next up, point­ers don’t have to point to some­thing. They can be null point­ers. And just like with addresses in the pre­vi­ous exam­ple, it is impor­tant to be clear on what we mean by this. A null pointer is exactly what I said: a pointer which does not point to any object.

In par­tic­u­lar, it is not a pointer to the address zero. Of course, here is where it becomes tricky, because the fol­low­ing does cre­ate a null pointer:

int* ptr = 0;

The trick here is that the C++ lan­guage stan­dard makes a spe­cial rule for this case. Assign­ing the con­stant zero to a pointer cre­ates a null pointer, and not a pointer to address zero. The “con­stant” part is impor­tant too. Here is the pre­cise word­ing in the stan­dard (Sec­tion 4.10 [conv.ptr], para­graph 1:

A null pointer con­stant is an inte­gral con­stant expres­sion (5.19) rvalue of inte­ger type that eval­u­ates to zero. A null pointer con­stant can be con­verted to a pointer type; the result is the null pointer value of that type…

A “con­stant expres­sion” is essen­tially an inte­gral value which can be eval­u­ated at compile-time. So 42, 2+2 or const int i = 99 are con­stant expressions.

int* p0 = 0; // null pointer
const int zero1 = 0; // constant expression
int* p1 = zero1; // null pointer
const int zero2 = 2 - 2; // constant expression
int* p2 = zero2; // null pointer
int zero3 = 0; // not a constant expression
int* p3 = zero3; // not a null pointer
int a = 2;
int b = 2;
int zero4 = a - b; // not a constant expression
int* p4 = zero4; // not a null pointer
const int c = 2;
const int d = 2;
int zero4 = c -d; // constant expression
int* p4 = zero4; // null pointer

Obvi­ously, the com­piler is unable to enforce all of this, but that doesn’t make it less true. Accord­ing to “the rules”, a null pointer is nei­ther a pointer point­ing to address zero, or a pointer to which the value zero has been assigned. It is a pointer to which the con­stant expres­sion zero has been assigned.

As for what you’re allowed to do with a null pointer? Basi­cally noth­ing. You may com­pare it to other point­ers, and… that’s basi­cally it.

With me so far? You might have noticed that what I have described so far is almost exactly what ref­er­ences in C# or Java (or many other lan­guages) are. A vari­able of a ref­er­ence type behaves pretty much exactly like this. We can set it to point to another valid object (but we are not allowed to ever set it to an invalid object), or we can set it to null.

Point­ers are much like ref­er­ence types in most other lan­guages. This is an impor­tant point. Like I said to begin with, point­ers are not dif­fi­cult. They are a very sim­ple con­cept, as the above shows. Where the con­fu­sion arises is in the one extra thing they can do, which I will describe next. Note that while this does make them some­what more flex­i­ble than C# ref­er­ences, it is still a far cry from the “raw mem­ory address” con­cept that peo­ple often think point­ers are.

Point­ers can tra­verse arrays

Now comes the (slightly) tricky part — the one that usu­ally gets peo­ple con­fused, or gives them the wrong idea. If we have a pointer to an ele­ment within an array, we are allowed to move the pointer around within the array

char arr[] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j' };
char* ptr = arr; // arrays are not pointers, but can *decay* into a pointer to the first element. So now we have a pointer to arr[0]
assert(*ptr == 'a');
++ptr; // move ptr to the next element
assert(*ptr == 'b');
ptr += 5;
assert(*ptr == g);
assert(*(ptr + 2) == 'i');
assert(*(--ptr) == 'f');
ptr -= 3;
assert(*ptr == 'c')

So far, so good. At this point it is prob­a­bly a good idea to men­tion that when you incre­ment a pointer, it always moves to the next ele­ment, and not to the next byte. Once again, being care­ful with the idea of “addresses” pays off. The pointer stores the address of an object. By adding one to that address, we get the address of the next object, no mat­ter how big the object is. Think of your house address. It doesn’t mat­ter how big your house is, the next address is always the neigh­bor­ing house. It is not your garage door or your kitchen window.

If, for the sake of argu­ment, point­ers had merely been mem­ory addresses, then adding one to a pointer would have pro­duced an address that was one byte higher, which means the pointer would no longer have pointed to a valid object. Good thing we don’t live in that messy kind of world, eh? In C++ land, a pointer points to an object, and incre­ment­ing it gives us a pointer to the next object.

Now comes the next lit­tle sur­prise: You are allowed to move the pointer one step past the end of the array. Assum­ing the same array as above:

char* p = arr + 9; 
assert(*p == 'j'); // no surprises here, just verifying that we're at the end of the array.
char* q = arr + 10; // this is legal
++p; // so is this

But once again, we have to be care­ful. The lan­guage has only given us per­mis­sion to go one step past the end. A pointer to arr + 11 is down­right ille­gal, even if we don’t deref­er­ence it. The mere exis­tence of the pointer is ille­gal. The com­piler prob­a­bly won’t com­plain, and your code may even appear to work, but it is no longer a legal C++ program.

We have also not been given per­mis­sion to deref­er­ence the one-past-the-end-pointer. *(arr + 10) is not legal. Again, it may seem to work, on your com­puter, with your com­piler, on this par­tic­u­lar day. But it may not work tomor­row. Or on my com­piler. Or when I run your program.

So the lan­guage allows us to cre­ate, and move point­ers around freely, from the start of the array, and up to one past the end of the array. And it allows us to deref­er­ence point­ers that point to any ele­ment in the array, but not one past the end.

And that’s basi­cally it. This is the dreaded pointer arith­metic that usu­ally have begin­ners run­ning scared. Not all that scary, is it?

Of course, For the sake of com­plete­ness, there is one other arith­metic oper­a­tion that is legal under much the same circumstances:

Two point­ers point­ing to the same array may be sub­tracted, yield­ing the dis­tance between them, expressed as a num­ber of ele­ments. And for the pur­poses of pointer arith­metics, sin­gle ele­ments are con­sid­ered arrays of size one, mean­ing that all the above is true for sin­gle vari­ables too — they’re just treated as arrays with only a sin­gle element.

And one final detail

Now let’s get self-referential. There is noth­ing new in this — it fol­lows as a log­i­cal con­clu­sion of the above, but it often comes as a sur­prise, so let’s men­tion it:

Point­ers may point to point­ers. Again, there is no magic, no spe­cial cases. A pointer is sim­ply a ref­er­ence to an object, remem­ber? And a pointer is an object too, so obvi­ously we can point to that as well!

We don’t often need to do that, but there is one case where it is used. Typ­i­cally when you call a library func­tion, and want it to give you a pointer to some resource it has cre­ated, you do this:

Resource* ptr = 0; // this is going to be our pointer to the resource. For now, make it a null pointer to avoid confusion
bool success = CreateResource(&ptr); // pass the address of our pointer to the function

Note that the func­tion wishes to return a sta­tus code to let us know if the oper­a­tion suc­ceeded, so it can’t sim­ply return the pointer we want. So it has to resort to pointer-pointer trick­ery instead.

The insides of CreateResource might look some­thing like this:

bool CreateResource(Resource** res){
  Resource* actualResource = new Resource(); // create the resource, and temporarily store a pointer to it
  // now we need to pass this pointer to the caller. If res had been a regular "single" pointer, it would simply have been a null pointer. 
  // And sure, we could have made it point to our resource instead, but the caller wouldn't know, because we only received a *copy* of the original null pointer. So even if we change what it points to, we can't change what the *original* points to.
  // Instead, we use a pointer to a pointer. We know that 'res' now points to the caller's Resource pointer. So if we manipulate the value pointed to by 'res', we're actually manipulating the caller's pointer.
  *res = actualResource; // so take our newly allocated resource pointer, and store that into the caller's pointer, which we get by dereferencing res.
}

It may help to remem­ber that func­tion argu­ments in C++ are always copied. If you pass an int to a func­tion, it receives a copy of that int. And if you pass a pointer, then the func­tion receives a copy of that pointer. A copy which points to the same address, so any­thing we do to the pointed-at address will be vis­i­ble out­side the func­tion as well. But if we change the pointer itself, no one else will see it, because the func­tion has been given its own copy.

So if we pass a pointer p0 to a pointer p1, then this is again copied. The func­tion receives a copy of p0, let’s call it p2 which points to p1. So if we change what p2 points to, the call­ing func­tion won’t see it, but if we change what p1 points to, it will be vis­i­ble to the caller, because p0 still points to p1.

Yes, this added level of indi­rec­tion may take some get­ting used to, but the impor­tant part is that there’s noth­ing fun­da­men­tally spe­cial. It is sim­ply the log­i­cal con­clu­sions of the rules I described pre­vi­ously, so even if you don’t get it now, you will when you’ve got a bit more expe­ri­ence with point­ers. It’s sim­i­lar to how, when you first learned to read “See Spot Run”, you had all the rules nec­es­sary to read longer words, like “stew­ardesses” or “pro­gram­mat­i­cally”. After that, you pretty much just needed practice.

So that’s it. That’s all point­ers are. If you hadn’t pre­vi­ously encoun­tered point­ers, you can stop read­ing here. But if you were already taught about point­ers, we prob­a­bly have to undo some of the damage.

So the fol­low­ing will dis­cuss what point­ers are not — that is, the mis­con­cep­tions that typ­i­cally exist about point­ers, and which begin­ners are almost invari­ably taught. I’ll try to explain why these lim­i­ta­tions exist as well, partly so you can take the rule seri­ously as “some­thing with real-world relevance”.

The Pointer Abuse Rehab and Cor­rec­tion Center

In the fol­low­ing, assume that i, j are inte­ger vari­ables (int), and p, q are point­ers to inte­gers (int*) and n is a null pointer:

  • A pointer is not just a num­ber. For exam­ple, i + j is legal, but p + q is not. Try it. Your com­piler will give you an error. Like­wise, i*j is valid, but i * p is not. Inte­gers may be added to or sub­tracted from point­ers, and point­ers may be sub­tracted from point­ers (as long as they both point to the same array). And on some com­put­ers, a pointer isn’t imple­mented as an inte­ger either. Some machines have seg­mented mem­ory space, so an address is a tuple con­sist­ing of a seg­ment iden­ti­fier plus an off­set. Sure, you can com­bine those two in a sin­gle num­ber, in the same way that you can com­bine the coun­try code with my phone num­ber to cre­ate a sin­gle inte­ger. But the address is still, fun­da­men­tally, a tuple of two num­bers on that machine.

  • A pointer is not a mem­ory address! I men­tioned this above, but let’s say it again. Point­ers are typ­i­cally imple­mented by the com­piler sim­ply as mem­ory addresses, yes, but they don’t have to be. A pointer may not point to just any address (and again, some com­put­ers, which have sep­a­rate address and inte­ger reg­is­ters, are actu­ally able to enforce this at run­time, gen­er­at­ing a hard­ware fault if you try to cre­ate a pointer to an address that is not allo­cated to your process.) The same goes for mov­ing past the end of an array. You’re allowed to go one ele­ment past, but point­ing two past the end is not allowed, and again, some com­put­ers are able to enforce this, at least in some cases. (imag­ine that the array is located at the very top of the address space, so mov­ing two ele­ments past the end pro­duces an over­flow. On a CPU with ded­i­cated address reg­is­ters, over­flows prob­a­bly won’t be allowed. They’ll be caught and they’ll gen­er­ate an exception).

  • All point­ers are not born equal. A pointer to T may not be con­vert­ible to a valid pointer to U. Some machines require datatypes to be aligned. Typ­i­cally, a 4-byte inte­ger will have to be aligned so it starts on an address that is divis­i­ble by 4. But a sin­gle byte datatype such as a char can be placed any­where. So that means three out of four char point­ers will not be valid inte­ger point­ers! We also can’t rely on cast­ing as much as we’d typ­i­cally expect. reinterpret_cast in par­tic­u­lar often trips peo­ple up. (For non-C++ pro­gram­mers, you can assume that we had used the “tra­di­tional” cast­ing syn­tax, as in (float*)i. The dif­fer­ence is not important.)

int* i; // assume we have a pointer i and that it points to a valid integer
float* f = reinterpret_cast<float*>(i); // #1
int* j = reinterpret_cast<int*>(f); // #2
assert(i == j);

In the above, we know noth­ing about the value of f after the cast on line #1. We know that it con­tains an “implementation-defined map­ping” of the orig­i­nal i. But we are not guar­an­teed that it points to the same address, or even that it con­tains the same bit pat­tern!

True, the stan­dard says that the map­ping is “intended to be unsur­pris­ing to those who know the address­ing struc­ture of the under­ly­ing machine”, but in gen­eral, we can’t rely on that. All we are guar­an­teed is that once we cast back to the orig­i­nal type, we’re given the orig­i­nal value. So the stan­dard guar­an­tees that i and j in the above will point to the same address. But we know noth­ing about f, other than that the com­piler is able to con­vert the value stored in it back to the orig­i­nal pointer i.

Con­clu­sion

By now, I hope it’s clear that point­ers actu­ally become a lot sim­pler when we treat them as what they are, reseat­able ref­er­ences to objects. If we start pre­tend­ing that they are mem­ory addresses, we get a whole host of com­pli­ca­tions: we start think­ing that they should be allowed to point to any mem­ory address, or even worse, that they are just num­bers, and that all the usual arith­metics work on them. (Remem­ber, adding or sub­tract­ing inte­gers is legal, but it adjusts the pointer by that num­ber of objects, not bytes, as we would have expected if point­ers were just mem­ory addresses. And pointer + pointer, pointer * pointer or pointer / pointer are sim­ply not defined at all.)

As if that wasn’t bad enough, we also require the stu­dent to under­stand the under­ly­ing hard­ware, in par­tic­u­lar the con­cept of a mem­ory space, and of phys­i­cal (or vir­tual) hard­ware addresses.

But if we treat point­ers as what they are, that is no longer nec­es­sary. A pointer points to a C++ object, not a mem­ory address, so to under­stand point­ers you merely have to under­stand C++ objects, not mem­ory addresses.

Share and Enjoy: These icons link to social book­mark­ing sites where read­ers can share and dis­cover new web pages.
  • Digg
  • del.icio.us
  • StumbleUpon
  • Reddit
  • Technorati

Tags: , ,

2 Responses to The Great Pointer Conspiracy

  1. You refer to the “dual role of *, as both part of the type, and as the deref­er­enc­ing oper­a­tor”. It actu­ally isn’t a dual role at all — it’s one and the same! In a pointer dec­la­ra­tion, that * is just another oper­a­tor that’s allowed in a type dec­la­ra­tion — along with [] and () (the func­tion call oper­a­tor). Think of the right-hand side of a dec­la­ra­tion (not just pointer dec­la­ra­tions!) as a spec­i­fi­ca­tion of what one has to do to the vari­able to yield an atom of the type spec­i­fied by the left-hand side.

  2. jalf says:

    I don’t see how you get to that con­clu­sion. They’re related, sure, but they’re not “one and the same”. An oper­a­tor applied to a value is not, and can never be, the same as a part of a type spec­i­fi­ca­tion. When used in a type dec­la­ra­tion, it is not an oper­a­tor. Nei­ther are [] and (). Syn­tac­ti­cally they’re the same, but not seman­ti­cally. (Again, there’s obvi­ously a rela­tion­ship between those two seman­tic mean­ings, in that they are both used to relate a pointer to the type it points to. But hardly “one and the same”) :)

Leave a Reply

Name and Email Address are required fields. Your email will not be published or shared with third parties.