Pointers are viewed by many as the bane of C programming, because out-of-control pointers can do a lot of damage, and can be hard to track down. But real programs tend to make heavy use of pointers. How can we keep pointers under control?
The big problem with pointers, of course, is that they can point anywhere, including to places they're not supposed to. When a pointer points to the wrong place (perhaps because it was never initialized properly, such that it essentially points to a random place), a fetch of the data it ``points'' to will result in garbage (or may cause the program to crash with a memory access violation), and a write of some new data to the location it ``points'' to will damage some other part of your program, or of some other program, or of the operating system (or may cause the program to crash). Crashes, in fact, though they're frustrating and annoying, may be preferable to the alternatives, namely performing quiet but meaningless computations or damaging other code, both of which can be even more annoying and even harder to track down.
Our goal, then, is to make sure that our pointers are always valid, or when they are not, to make sure that we can know that they are not. First, then, let's discuss what we mean by a ``valid pointer.''
A valid pointer (more precisely, a valid pointer value) is one that does in fact point to an object of the type that the pointer is declared to point to. Furthermore, if the pointer will be used to store new values, the old value must be sitting in writable memory (that is, it must not be a variable that was declared const, or a string that results from a string literal). In contrast to valid pointers, we may distinguish among several kinds of invalid pointers: null pointers, uninitialized pointers, pointers to memory that used to exist but has disappeared, pointers to memory that once came from malloc but has since been freed.
The tricky thing about valid and invalid pointers is that there's no simple way in C to ask ``is this pointer valid?'' or ``is this pointer invalid?''. The only questions we can ask about pointers are ``is this pointer equal to this other pointer?'', ``is this pointer unequal to this other pointer?'', and, for pointers into the same array, ``is this pointer greater or less than this other pointer?''.
Part one of our strategy for managing pointers, then, will be to arrange that all or most invalid pointers are null pointers. Whenever we do anything which would cause a pointer to be invalid, that is, whenever we declare one (such that it would otherwise have a garbage initial value), or whenever we do something that causes the memory which one of our pointers used to point to to disappear, we'll set the pointer to NULL. Having done so, we can test whether the pointer is currently valid by checking if it's not equal to the null pointer, or contrariwise, we can test whether it's invalid by checking if it's equal to the null pointer.
Remember that C doesn't generally do any of this automatically. It does not guarantee that all newly-allocated pointers are initialized to null pointers, and it does not insert automatic validity checks before you try to use a pointer. If you want to be sure that a pointer is initialized to a null pointer, you must generally set it to NULL. If you have a pointer which you're thinking of using but which might or might not be valid (and if it's a pointer which you believe you'd have set to NULL if it was invalid), you must precede your use of the pointer with a test of the form
if(p != NULL)
Furthermore, if you write the test if(p != NULL), it does not in the general case mean ``is p valid?''. The test if(p != NULL) can only be used to mean ``is p valid?'' if you have taken care to make sure that all non-valid pointers have been set to null.
(There is one condition under which C does guarantee that a pointer variable will be initialized to a null pointer, and that is when the pointer variable is a global variable or a member of a global structure, or more precisely, when it is part of a variable, array, or structure which has static duration.)
Remember, too, that the shorthand form
if(p)is precisely equivalent to if(p != NULL). So you may be able to read if(p) as ``if p is valid'', but again, only if you've ensured that whenever p is not valid, it is set to null.
The degree of care with which you have to implement a pointer management strategy may be different for different pointer variables you use. If a pointer variable is immediately set to a valid pointer value, and if nothing ever happens which could make it become invalid, then there's no need to check it before each time you use it. Similarly, if a pointer is set to point to different locations from time to time, but it can be shown that it will always be valid, there's again no reason to test it all the time. However, if a particular pointer is valid some of the time and invalid at other times, or in particular, if it records some optional data which might or might not be present, then you'll want to be very careful to set the pointer to NULL whenever it's not valid (or whenever the optional data is not present), and to test the pointer before using it (that is, before fetching or writing to the location that it points to).
Everything we've just said about ``pointer variables'' is equally true, and perhaps more important, for pointer fields within structures. When you define a structure, you will typically be allocating many instances of that structure, so you will have many instances of that pointer. You will typically have central pieces of code which operate on instances of that structure, meaning that each time the piece of code runs, it may be operating on a different instance of the structure, so if the pointer field is one that isn't always valid (that is, isn't valid in all instances of the structure), the code had better test it before using it. Similarly, the code had better set the pointer field to NULL if it ever invalidates it.
For example, one of the first features we added to the adventure game was a long description for objects and rooms. But the long description is optional; not all objects and rooms have one. Suppose we chose to use a char * within struct object and struct room to point at a dynamically-allocated string containing the long description. (This choice would be preferable to a fixed-size array of char because it may be the case that some long descriptions will be elaborately long, and we'd neither want to limit the potential length of descriptions by having a too-small array nor waste space for objects with short or empty descriptions by always using a too-large array.) For each instance of an object or room structure, we'd initialize the description field to contain a null pointer. For each room or object with a long description, we'd set the description field to contain a pointer to the appropriate (and appropriately-allocated) string. Finally, when it came time to print the description, we'd use code like
if(objp->desc != NULL) printf("%s\n", objp->desc); else printf("You see nothing special about the %s.\n", objp->name);
Particular care is needed when pointers point to dynamically-allocated memory, managed with the standard library functions malloc, free, and realloc. Somehow, it's easier to make mistakes here, and their consequences tend to be more damaging and harder to track down.
First of all, of course, you must always ensure that the allocation functions malloc and realloc succeed. These functions return null pointers when they are unable to allocate the requested memory, so you must always check the return value to see that it is not a null pointer, before using it. (If the return value is a null pointer, you will generally print some kind of error message and abort at least the particular function that needed the allocated memory, or perhaps abort the entire program.)
Don't get in the habit of assuming that a single, simple call to malloc will ``always'' succeed. Don't make excuses like ``this program doesn't use much memory to begin with, and I'm only allocating 10 bytes here, so how can it possibly fail?'' For one thing, there are more reasons for malloc to fail--and return a null pointer--than that there was no more memory. Typically, malloc will also return a null pointer if it is able to detect that you have misused some of the memory that you have previously allocated, perhaps by writing to more of it than you asked for. In this case, malloc is trying to tell you something, something you need to know, and although its voice is small (and although tracking down the problem that it's complaining about may be difficult), you will only have more problems, which may be more difficult to track down, if malloc returns a null pointer but you then use that pointer as if it were valid. (As an example of how it can be alarmingly easy to misuse the memory that malloc gives you, consider this hypothetical scrap of code for making a dynamically-allocated copy of a string:
char *copystring = malloc(strlen(originalstring)) /* Beware... */ if(copystring != NULL) strcpy(copystring, originalstring);Hint: what about the \0 that terminates the string?)
In a program that allocates a lot of different pieces of memory for a lot of different things, it can be a real nuisance to have to check each pointer returned from each call to malloc to make sure it's not null. One popular shortcut is to define a ``wrapper'' function around malloc, which calls malloc and checks the return value in one central place. For example, the adventure game uses the function
#include <stdio.h> #include <stdlib.h> #include "chkmalloc.h" void * chkmalloc(size_t sz) { void *ret = malloc(sz); if(ret == NULL) { fprintf(stderr, "Out of memory\n"); exit(EXIT_FAILURE); } return ret; }One way to think about chkmalloc is that it centralizes the test on malloc's return value. Another way of thinking about it is that it is a special, alternate version of malloc that never returns NULL. (The fact that it never returns NULL does not mean that it never fails, but just that if/when it does fail, it signifies this by calling exit instead of returning NULL.) Aborting the entire program when a call to malloc fails may seem draconian, and there are programs (e.g. text editors) for which it would be a completely unacceptable strategy, but it's fine for our purposes, especially if it doesn't happen very often. (In any case, aborting the program cleanly with a message like ``Out of memory'' is still vastly preferable to crashing horribly and mysteriously, which is what programs that don't check malloc's return value eventually do.)
Another area of concern is that when you're calling free and realloc, there are more ways for pointers to become invalid. For example, consider the code
/* p is known to have come from malloc() */ free(p);After calling free, is p valid or invalid? C uses pass-by-value, so p's value hasn't changed. (The free function couldn't change it if it tried.) But p is most definitely now invalid; it no longer points to memory which the program can use. However, it does still point just where it used to, so if the program accidentally uses it, there will still seem to be data there, except that the data will be sitting in memory which may now have been allocated to ``someone else''! Therefore, if the variable p persists (that is, if it's something other than a local variable that's about to disappear when its function returns, or a pointer field within a structure which is all about to disappear), it would probably be a good idea to set p to NULL:
free(p); p = NULL;(Of course, setting p to NULL only accomplishes something if later uses of p check it before using it.)
Finally, let's think about realloc. realloc, remember, attempts to enlarge a chunk of memory which we originally obtained from malloc. (It lets us change our mind about how much memory we had asked for.) But realloc is not always able to enlarge a chunk of memory in-place; sometimes it must go elsewhere in memory to find a contiguous piece of memory big enough to satisfy the enlargement request. So what about this code?
newp = realloc(oldp, newsize);Is oldp valid or invalid after this call? It depends on whether realloc returned the old pointer value or not (that is, on whether it was able to enlarge the memory block in-place or had to go elsewhere). Most of the time, you will use realloc something like this:
newp = realloc(p, newsize); if(newp != NULL) { /* success; got newsize */ p = newp; } else { /* failure; p still points to block of old size */ }With a setup like this, p remains valid, and newp is a temporary variable which we don't use further after testing it and perhaps assigning it to p.
A final issue concerns pointer aliases. If several pointers point into the same block of memory, and if that block of memory moves or disappears, all the old pointers become invalid. If you have a sequence of code which amounts to
p2 = p; ... free(p); p = NULL;then setting p to NULL may not have been sufficient, because p2 just became invalid, too, and may also need setting to NULL. The situation is particularly tricky with realloc: suppose that you have a pointer to a chunk of memory:
char *p = malloc(10);and another pointer which points within that chunk:
char *p2 = p + 5;Now, if you reallocate p, and if realloc has to go elsewhere and so returns a different pointer value which you assign to p, you've also got to fix up p2, because it just had the rug yanked out from under it, and is now invalid. To keep p2 up-to-date, you might use code like this:
int p2offset = p2 - p; newp = realloc(p, newsize); if(newp != NULL) { /* success; got newsize */ p = newp; p2 = p + p2offset; } else { /* failure; p and p2 still point to block of old size */ }Before calling realloc, we record (in the int variable p2offset) how far beyond p the secondary pointer p2 used to point, so that we can generate a corresponding new value of p2 if p moves.
Read sequentially: prev next up top
This page by Steve Summit // Copyright 1996-1999 // mail feedback