|
This material concerns the concept and use of pointers. The subject seems to be difficult for neophyte programmers. Perhaps a grounding in assembly language, where one deals routinely with immediate, direct, and indirect references, was a help to many of us old salts. I hope to demystify the subject enough to prevent beginning users from falling into the most common traps. In honor of the subject, the navigational icon has been provided with a small surplus. |
Pointers
“Our memories are addressed and referenced... by significant fragments of their own content”
Frederick Turner, in a quite different (and more poetic) context...
What is a pointer?
A pointer is a
reference
A pointer is a programming language datatype whose value is used to
refer to
("points to") another value stored elsewhere in the computer
memory.
. It is not a "reference type" in the strict sense used in C++, but it
does refer to another variable or other object. The dictionary definition is sufficient here.
In that context the name of a variable, its address, and any terms of endearment your spouse or
lover use also qualify.
A pointer is like a name in your little black book. It tells you where someone lives. The someone does not live in your address book. If the address does not exist, it's a fool's errand to go there hoping for a date tonight. A pointer must be valid to be useful. It is part of a pair. It's worthless, indeed a hazard, without the mate.
To
dereference
Obtaining the value that a pointer refers
to is called dereferencing the pointer. a
pointer is to follow its directions in order to locate the thing
it refers to. If that thing doesn’t
exist, there may be trouble in River City. Dereferencing an invalid pointer is
somewhat like picking a bad address out of your little black book. On your way
to a hot date you may step into a hatchet fight and experience a
segmentation fault
Memory access violation.
Accessing memory in a manner or at a location forbidden to your program. It is
generally considered a severe enough fault to cause your program to terminate
or your machine to crash.
Other terms (with possibly additional connotations):
Core dump
Bus error
of the worst kind. In the same way, suppose I tell you to
point to the person you want to help you dig a ditchThis is the definition. Make it a great, big, long, humongous one similar to the one that talked about the ditch and the undesirability of the digging, thereof.. If you point
to where no one is standing, you're gonna be down in a hole with
a pick all by yourself.
The pointer is not the actual object you are seeking, but a way to locate that object; the address, the pointing finger. You cannot store your sweetheart in your address book; don't even try. You have to use the address specified. Likewise, as a programmer, you may not store your integers and other such objects in a pointer without courting disaster.
| Copyright © 2005 David Mills | Contact Me |
How Pointers Work
Consider the following code. When you name your variables, whether they're pointers or integers or whatever, the name is a mnemonic, an alias, for a memory address. A convict and his number are irretrievably linked, but the number is not the convict, and is cheaper to feed. Suppose you write these statements:
|
int iValue = 25; |
The first statement will cause the compiler to set aside for you a memory location large enough to hold an integer value. It will also initialize the memory with the value, 25. Nice compiler. The memory set aside for you may live in one place or another, depending upon the context, but you know where it is in a useful way because you named it and the name is an alias for a numerical location in your memory area.
The second statement will cause the compiler to set aside enough memory to hold a pointer. It is not initialized; it contains nothing useful. There are many for whom this will be a surprise. When they declare a pointer, they expect it to automagically generate an object to point to (a mate, the other half of the pair mentioned in the beginning) as well as a value for itself that points there. These people have a predictable and recurring agenda: they write code, compile it, run it, then log on to Dev Shed and post a question asking why they got a segmentation fault or core dump. Possibly there's a reboot before that final step. This code,
char *buffer;
scanf ("%s", buffer);
(or any other character-transfer mechanism you care to substitute for 'scanf') is a common, and often fatal (to the program) error. If it were fatal to the programmer, the average competency-level would be rising by the minute.
The third statement will cause a problem. iNumber has not been defined at this point. If one could actually get away with the declaration, it would constitute a likely path through a hatchet fight. Presumably your compiler will gripe and you won't actually have to walk through the fight. This statement looks okay on the surface. After all, It meets the requirements of having two things in a pointer-object relationship. The relationship is a lie. The second thing doesn't exist.
The next statement stores a value in pIntOne. The value is the address of the variable, iValue. It is not the value of iValue. It is merely a reference. The final statement sets aside additional memory for another value, but stores no meaningful value there. Assuming 32-bit integers, 32-bit pointers, and memory for our declarations beginning at hex address 0x1000, the memory will look like this:
0x1000: iValue |
contents: 25 |
0x1004: pIntOne |
contents: 0x1000 |
0x1008: pIntTwo |
contents: unknown, junk |
0x100c: iCopy |
contents: unknown, junk |
The programmer that uses pIntTwo at this point is going to be down in the ditch by himself with a pick and no date. The programmer that tries to store an int to pIntTwo is also going to be in trouble. One may force the issue by casting and place any value, say 37, there. If one then uses that as a pointer, it will be referring to the contents at address 37, which may very well not be usable memory. It may be off-limits. Possibly, it doesn’t even exist. Your program will trot off into the woods and be breakfast for a bear. You will be introduced to the segmentation fault, the core-dump, the dreaded blue screen of death.
The value of pIntOne (yes it holds a value) is 0x1000. "pIntTwo = pIntOne" will assign the value, 0x1000, to pIntTwo, not the value 25. To use the 25 pointed to by pIntOne, one must 'dereference' it. One does this with the indirection operator, '*' (and sometimes the '->' operator, which is not discussed here). pIntOne has the value, 0x1000. *pIntOne has the value, 25 (the value in memory location 0x1000). "pIntTwo = pIntOne" (notice pIntOne isn't dereferenced here) assigns the value, 0x1000, found in pIntOne, to pIntTwo. "iCopy = *pIntTwo" (notice the dereference operator) assigns the value, 25, found at the location referred to by pIntTwo, not in pIntTwo, to iCopy.
Of course, we could have written directly into the variable, so what good is a pointer, you ask? On occassion, one may need to write to a variable whose location is not known until runtime. Perhaps it has been dynamically allocated in response to changing conditions, and the only reference we have to it is a pointer. Thanks to our pointer conduit, memory now looks like this:
0x1000: iValue |
contents: 25 |
0x1004: pIntOne |
contents: 0x1000 |
0x1008: pIntTwo |
contents: 0x1000 |
0x100c: iCopy |
contents: 25 |
What is a Pointer?
Confustoids
| Copyright © 2005 David Mills | Contact Me |
Confustoids
Confustoid is an age-old technical term I just made up. It is a factoid that has some tendency to introduce confusion despite its factuality. In their zeal to make our programming life easier, our language writers sometimes introduce shortcuts. Things may be expressed in more than one way. Perhaps there’s a way highly amenable to the human way of thinking and another way more suited to the actual architecture of the machine we are attempting to manipulate. Sometimes these variations are great for the initiated but may be confustoids when encountered by the neophyte. Pointers, when used with arrays, strew the path with a number of these little beasties, the confustoids, but they're nothing an aspiring geek can't overcome and learn to appreciate.
Array notation is generally converted by the compiler to what amounts to pointer arithmetic. In its implementation, your compiler does things that may lead to misperceptions. The next sections deal with the subject in more detail, but consider this code,
int iArray [4] = {2, 4, 6, 8};
int *pArray = iArray;
Now, memory looks like this:
0x1000: iArray [4] |
contents: 2 |
0x1004: |
contents: 4 |
0x1008: |
contents: 6 |
0x100c: |
contents: 8 |
0x1010: pArray |
contents: 0x1000 |
Perhaps the second statement would have been clearer had it been written,
int *pArray = &iArray;
( '&' in this context meaning "address of").
The confustoid occurs because of what the compiler chooses to do behind the scenes with the items, iArray and pArray. It often treats them on the surface as if they are the same thing! Suppose you wish to access element five. You may use a statement like iArray[5] or pArray[5], each of which delivers the same value. Since iArray is the name of an array, the compiler will take the address of its beginning element and add five to it. pArray, however, is the name of a pointer. The compiler knows this. The compiler will take the value from the pointer (which is the same as the address of the beginning of the array) and add five. Confustoids incoming!!
C/C++ does not have an array object, per se. Nothing that behaves
atomically
In computer programming, atomic describes a unitary action or object that is essentially indivisible,
unchangeable, whole, and irreducible. A pointer or an integer is atomic, a structure or an array is not.
Because it is not a single value (though you may view it as a single object) you may not use the assignment operator ("=") to assign to it; nor may you compare it as a single value with the equality ( "==") and inequality operators (">=", et al). The individual elements must have their values assigned or compared individually.
Since an array is a distributed collection of values, how does one refer to it? What is an "array reference"? By convention, the address of the beginning of the array constitutes such a reference. Consequently, iArray (by compiler convention), &iArray (address of), and &iArray[0] (address of the first element) are equal quantities. This is a distinct confustoid.
It is particularly so in the case of the first, iArray. A name (or label, or address), otherwise unqualified, normally indicates the value stored at the address represented by that name. It is simply a convention of the language and not universally true. Since an array, as a whole, has no "single" value, the notation has been treated specially, again by convention, to indicate the address and not the value. In most cases, such a meaning would require the '&' operator. If one writes, "newVar = iVariable" (the variables being integers, say), one gets the value of iVariable transferred to newVar. If, on the other hand, iVariable is an integer array, newVar (presuming it's now an integer pointer) will receive the address of iVariable, and not the contents. Yeppers, watch your step around the confustoids.
Another confustoid rears its ugly head when we write,
char *myString = "This is the string in question";
This little beauty certainly appears to be assigning an entire array to a pointer, thereby violating a couple of the rules and getting away with it. Again, our language writers' tendencies to spoil us have come to the fore. This would be less confusing if we were required to write,
char anonymousMemory [32] = "This is the string
in question";
char *myString = anonymousMemory;
We have been delivered from this requirement at the risk of finding a confustoid in our stew. The compiler has been taught to recognize this particular construct and perform the individual tasks for us. A significant number of modern machines will write-protect the memory containing the character-storage area, also. If we attempt to modify anonymousMemory we may generate a memory-violation error.
Less damaging, perhaps, is the similar statement, but without the sizing constant.:
char myString [] = "This is another string";
Our compilers have told us again and again that we need a constant when we declare the size, and have slapped our wrists often enough. (The fact that some newer, standards-compliant compilers will accept a variable in the size declaration is entirely ignored here.) This example is a case of the compiler making appropriate inferences regarding the required size and the contents of the array.
How Pointers Work
What is not a Pointer?
| Copyright © 2005 David Mills | Contact Me |
What is not a Pointer?
An array is not a pointer.
It is very important that we ignore statements to the effect that a pointer and an array are the same thing.
They are not. In some circumstances, the notation is equivalent. Equivalent is not equal, despite the common root word. Some South Sea islanders once rolled around giant stone wheels for currency -- the equivalent of, but not the same thing as, our pocket change. We spent some time in the preceding section discussing the syntactical similarities in using the two. There are additional pitfalls. To reiterate one of the points there: given the array above, we may write,
value = iArray [2];
and assign the value, 6. Since we may also write,
value = pArray [2];
and assign the very same value, it's obvious that the array, iArray, and the pointer, pArray, are the same thing. Right? Duck!! Confustoid!!! Despite the identical expressions, they are not the same thing. The compiler can tell the difference between an array and a pointer because of the declarations. By convention it treats the two as equivalent for this method of expression.
If one examines the emitted code for each statement, one will discover that it is not the same. Again, our compiler is ‘just’ helping (and perhaps hindering the new among us). When applied to a pointer, the array notation adds a dereferencing step prior to applying the pointer arithmetic. Perhaps it should be written,
value = *(pArray+2);
inasmuch as that's the way the machine code is emitted. Note the parentheses. Without them, *pArray+2 is four, not six, because *pArray refers to the first element, 2, which added to 2 results in 4. Don't leave your parentheses at home from laziness.
Confustoids
Pointer Arithmetic
| Copyright © 2005 David Mills | Contact Me |
Pointer Arithmetic
Your system’s memory, presuming it’s typical, is a collection of storage locations which are numbered sequentially from beginning to end so that access is simplified. It amounts to one long contiguous array. (It is typically implemented physically as a 2D array of bits, but that's another subject.) Part of that address space might not be "stuffed" with actual memory, or the locations may, via hardware machinations, be locations in other physical devices. One doesn't want to try to address those parts unknowingly. Many systems will protect them with the appropriate permissions.
On its journey from your fingertips to its final use, the representation of a variable, say iValue, will change. At some points it may be construed as a value relative to some location in your program. It will, sooner or later, become a binary pattern at an absolute position within the system’s memory.
If that final position is in element 4096, it may be found in an infinite number of ways. Locations 4095+1, 1+4095, and 96+4000 will all result in the same answer. One's arrays also have multiple locations. One chooses among them by pointing, sometimes with the notation, iArray[n]. The value of the effective address of this expression may be had by summing a value represented by the token outside the braces with a value represented by the token inside the braces. One cannot tell whether iArray is the name of an array or the name of a pointer in the context of this particular paragraph. One must look back. Fortunately, compilers keep good notes, because the "value represented by the token..." is obtained in different ways for the two possibilities.
It is considered bad practice to reverse the array notation, as in, 5[iArray], but it is nonetheless valid. There is no dearth of high priests and gurus, each with a sackful of such dictums, chomping at the bit to say you nay. Use your own judgment and try to make it reasonably sensible. The relevant point is that the compiler chooses to use the value in the brackets as a quantity to add as an offset to the value before the brackets. iArray+5 results in the same answer as 5+iArray. This is simple and while it may represent something you haven’t thought about previously, it isn’t a confustoid. The confustoid occurs because of what the compiler chooses to do behind the scenes with the item, iArray. If iArray is the name of an array, the compiler will take the address of its beginning element and add the proper offset to it. If iArray is the name of a pointer, the compiler will take the VALUE from the pointer variable and add the offset. Confustoids incoming!!
Suppose you declare an array and use it,
int myArray [32];
myArray [64] = 1257;
It is obvious that the size is 32. It is obvious that the second statement exceeds the bounds, possibly with an extremely detrimental effect. Unless your compiler is better than mine, it will not warn you! If you aren't safe in this exceedingly local contextual scope, why would you expect to be safe off in the hinterlands of a remote function to whom you have passed a reference (pointer) to myArray? Observing the bounds is your responsibility and you shirk it at your peril. The mere notation provides no protection beyond that available with...
int myArray [32];
int *pArray = myArray;
pArray = pArray+64;
*pArray = 1257;
...which is none.
One thing the compiler will do for you is observe the size of your elements. If your integer is 4 bytes the compiler will see to it that an effective address calculation will move 4 bytes for each increment, whether it's implemented as an explicit addition or by one of the incrementation operators. In order to do this for you, the compiler must know that the reference item, whether direct address or pointer, refers to an integer-sized object. This means that it is your responsibility to declare the arguments to your functions properly if you want the information available inside the function.
There is this about misusing your pointers: you may destroy things without immediate and noticable effect. Quite often the result of misuse becomes apparent as soon as run-time arrives. This is not necessarily the case. Information that one inadvertently destroys may have been used for a task that has been completed. It might not be attempted again, or, if it is, it might regenerate its data. Information that one destroys may not be detrimental to the operation of the program except in rare circumstances of low probability. Mr. Murphy says those circumstances will occur at the most damaging moment.
When a program is being compiled and tested in debug mode, which tends to be the time one is looking hardest for problems, there is a lot of non-critical (to the functioning of the application) information shoe-horned into nearby memory in the form of bookkeeping information. Overwriting this information may never be noticed. If it's noticed, it may be ignored. There are plenty of times when a watched variable may indicate "bad pointer" when things are precisely as they should be.
If your misuse or molestation of a pointer is brought to your attention, and your response is, "It has to be okay procedure, the program works and doesn't crash," then you need an attitude adjustment. A whop upside the head with a 2 x 4 would not be amiss.
What is not a Pointer?
Sneaky Pointers
| Copyright © 2005 David Mills | Contact Me |
Sneaky Pointers
An example of a sneaky pointer is an array that has apparently become a pointer while you weren't looking. If one passes an array (remember, arrays are represented by the address of their beginning element) as a parameter to a function, the array address gets copied to the stack as a pointer to the array. The parameters passed to a function are, for all practical purposes, local definitions within the function. The writer declares them with the declaration or definition of the function and initializes them with the call. They are merely separated from the other locals by some microprocessor overhead and bookkeeping values. The parameters passed to a function are, for all practical purposes, local definitions within the function. The writer declares them with the declaration or definition of the function and initializes them with the call. You may never have thought of it that way.
If one declares a function like this,
void someFunction (char theArray []);
and calls it like this,
char myArray [32];
...
someFunction (myArray);
One may just as well have declared it thusly:
void someFunction (char *theArray);
The compiler performs the translation. someFunction has no inkling of the size of the array which is being referred to. If the function needs size information, add another argument. If one increments theArray it will increment by the size of one element, in this case one byte (the size of a char). If one wishes to increment it beyond the array bounds, the compiler won't sense that and chastise one severely. One may wind up at run-time trotting off into the weeds and barfing on one’s shoes. Mama compiler does many favors, but she is not a psychic miracle worker. Similarly, if one declares the function like this,
void someFunction (char theArray [][5]);
the compiler infers (correctly) that theArray is a pointer to an array of arrays of five chars. The effect is the same as if coded like this:
char myArray [10][5]; |
Note the definition of the pointer. The parentheses dictate that it is a pointer to a char array containing five chars, not an array of five char pointers. |
someFunction will behave in precisely the same way for both calls. If one increments theArray it will increment by five elements, the number of chars in each 'row'.
A definition such as this, if carried into multiple dimensions, allows one to use array notation inside the function in lieu of multiplying a bare element pointer by the size of lower dimensions. It is sufficient that it be clear to the compiler, by one declaration or another, what the various sizes are below the highest dimension. Regardless of the pointer type deduced by the compiler (element, row, layer, whatever), information concerning the total size of the array is lost! Observing the boundary limits is the responsibility of the programmer.
Pointer Arithmetic
The NULL Pointer
| Copyright © 2005 David Mills | Contact Me |
The NULL Pointer
The pointer containing a NULL is your friend. It does not deliver you from the evil clutches of the segmentation fault or core-dump monsters. If you dereference it, the chances are very, very good that you will be delivered to that particular hell.
NULL is a special value that is used to identify an invalid pointer. The standards do not dictate that it is the value, zero, though it often is. They do require that a pointer with the value, zero, behave as if it contains the value, NULL.
What in the world is a pointer good for if you can’t dereference it? It tells you unequivocally that it is no cottonpickin’ good! If you know of another value which will never be a valid memory address wherever your program might roam, or one that is as easily remembered, feel free to use it as a reminder. Bear in mind, though, that those who have gone before tend to rely on the NULL value. The writers of "malloc" will, these days, accept and ignore a NULL value should you try to free memory with it. Things are likely to backfire if you use, say, 0xcccccc, however effective that value might be as a memory-jogger.
When one has borrowed memory from the heap The heap is an area of system memory, sometimes called the "free store," from which a programmer may request additional storage. The only reference to such "borrowed" memory is via pointer. The memory should be returned when the application no longer needs it. Consequently, the pointer must remain intact (or a copy made) during the period of use. , and returned it, setting the pointer that held its address to NULL will let everyone (who bothers to check) know that it is no longer useful. It is spent and currently in retirement. If one forgets and attempts to free it again, it's no big deal. Initializing a pointer to NULL when it is declared, if one doesn’t plan to use it immediately, is a good idea for the same reason. Its value will let you (or your code-sharing cohorts) know that it isn't useful; don’t dereference it.
One isn’t required to use a pointer variable as a pointer. One may very well just store an address there and pass it around. If it doesn’t get dereferenced, you’re walking in tall cotton. Unfortunately, that approach discards much of the utility of the pointer as a tool. It is better to conquer it, use it, and move on.
Sneaky Pointers
| Copyright © 2005 David Mills | Credits | Contact Me |