Understanding C Pointers
This document is part of an illustration of a method described by
Andy Oram
in his article
Rethinking Community Documentation
for evaluating the quality of computer documentation through the use of
quizzes. The end of this document links to a quiz.
The document is aimed at beginning C programmers. It aims to explain
how to use pointers simply and safely.
Although pointers are critical in the C language, small slip-ups can
lead to program failures that are hard to diagnose. Even the most
professional programmers, as you have probably seen in the trade
press, can produce the dreaded "buffer overflows" that open up
security flaws.
I assume you already know how to use pointers for strings. In addition
to that, the most common uses for pointers are to:
We'll look at each use in this article. Along the way, the article
will show you safe practices and teach you the background concepts you
need to diagnose problems.
(Function pointers, which let you choose dynamically what function to
invoke, will not be covered in this article.)
In C, an array is a pointer. For instance, if you declare:
int array[20];
the variable
array is a pointer to the beginning of the 20
integers. It is convenient to think of
array and
array[0] as pointing to the same location, which is a good
reason for arrays in C to be numbered starting at 0.
We don't ever want to change where
array points. We want to
make sure we can always find the start of the array. So let's declare
another pointer:
int *array_p;
and set it to point to the same place;
array_p=array;
The previous simple statement works because pointers are simply integers
referring to locations in memory. (The C standard warns against
treating a pointer as an integer data type, but for this conceptual
discussion we can consider the pointer to be an integer.) Think of
memory as a set of numbered slots. The array we just declared takes up
20 slots; if an integer takes up four bytes, the slots might be
numbered 100, 104, 108... and the array variable points to
100. Don't confuse these numbers with the values in the elements of
the array, which you can think of as pieces of paper inserted into the
slots.
Using this pointer now, we can step through the array and initialize
each element:
for (i=0; i<20; i++)
{
*array_p = i*i;
array_p++;
}
The two statements within the loop treat the pointer very
differently. The first statement has an asterisk, so it looks at the
value that the pointer is pointing to. This is called
dereferencing the pointer. The statement changes the value,
not the pointer itself. Thus, the first element of the array is set
to 0*0, the second element to 1*1, and so on.
The second statement changes the pointer itself, because there is no
asterisk on it. The values within the array do not change.
Before going on (because the next example can be confusing), make sure
you understand when code looks at the pointer itself (when there's no
asterisk) versus when code looks at the memory to which the pointer is
pointing (when there is an asterisk). Here's an analogy: imagine you
are walking next to a set of 20 mailboxes, which collectively
represent the array. The statement *array_p = i*i
puts a slip of paper holding a number into the mailbox. The statement
array_p++ walks to the next mailbox.
Most programmers, secure in their understanding of pointers, would
combine the two statements within the loop:
for (i=0; i<20; i++)
{
*array_p++ = i*i;
}
The construct
*array_p++ looks confusing at first. Rest
assured that C is guaranteed to dereference the pointer first
(
*array_p) and set the element of the array, then to
increment the pointer itself. The loop is exactly equivalent to the
previous one, once it is compiled.
Suppose you didn't want to increment the pointer; you wanted to
increment the element of the array. You can put parentheses around the
asterisk and pointer, so the deferencing occurs first:
(*array_p)++;
Back to our program: having set the values within the array, we can
reuse the pointer to print them. The following line of code is
extremely important, because the program has left array_p
pointing to a location past the end of the array. You must not
deference array_p now, because you'll access memory you're
not supposed to access. That's a buffer overflow, and it could
terminate the program or overwrite critical memory (which could
produce wrong results or be used by exploiters to compromise the
system).
Therefore, reset
array_p before using it:
array_p=array;
Now you can step through the array without the need for any
i
variable:
for (i=0; i<20; i++)
{
(void) printf("%d\n", *array_p++);
}
So we've used a pointer to step through an array, but this is not
much better than using
i as an index into the array. After
all, the program has gone to the trouble of defining
i, so we
could eliminate the
array_p pointer and write:
for (i=0; i<20; i++)
{
array[i] = i*i;
}
Using the pointer produces a tiny bit less overhead than using the
index, because the program doesn't have to do the mathematics of
calculating where it is within the array. But this is a trivial
saving.
The pointer is more useful when you have a value you know is in the
array (often a special value to mark the end of the array), and you
are stepping through the array till you find that value. For instance,
when programmers create arrays of structures (something not covered in
this article), they often set the last element of the array to NULL,
which is a special macro recognized by C. The loop can then check the
pointer against NULL at each iteration and stop when NULL is reached:
for (struc_p=array; struc_p != NULL; struc_p++) { ...
The practice just shown is common, but to use it you have to be
absolutely sure the array contains a value equal to NULL. Otherwise,
the loop causes a buffer overflow, proceeding until it happens to hit
some value in memory that equals NULL, or causes a fatal error by
exceeding the memory allocated to the program.
The C header files define a constant called NULL that's equal to
0. You could just as easily check for 0 where you check for NULL, but
it's conventional to check for NULL where pointers are involved. The C
standard warns against assuming that a pointer is the same as an
integer.
Because uninitialized pointers often point to 0 (depending on the
operating system), some programmers believe that a function should
always test a pointer against NULL if the pointer was passed to it
from outside, and refuse to dereference the pointer if it's NULL:
if (pointer==NULL) return;
This practice can catch a common type of error, but there are many
other ways a buggy program can pass an invalid pointer to a function,
and the function can't catch everything.
We can create a simple program that loops until the end of the array is
reached by using a string. As you have learned, a string in C ends in
a 0 byte, often called nil (not NULL, although both are essentially 0)
and represented by the character '\0'. Thus, the following string:
char title[100] = "understanding C pointers";
contains 25 characters: 24 visible characters and a nil byte at the
end that isn't shown.
Suppose we have a rule that a title starts with an uppercase letter,
and that all other letters are lowercase. To convert the string just
shown, the program can pass it to a function:
title_fix(title);
Note that no asterisk is needed. We're not passing the value to which
the pointer points; we're passing the pointer itself.
The
title_fix function is declared as follows, showing that
it does in fact receive a pointer as its argument:
void title_fix(char *string)
string is a local variable; it exists in the memory assigned
to
title_fix. But what memory does it point to? Memory in the
calling function. This means that any changes to the elements of the
array are reflected in the calling function. Thus,
string is
a new variable that points to the same memory as
title.
We won't bother checking for a NULL pointer, but we can check the
string to make sure there's at least one character in it. If an empty
string was passed, the first character would be nil. If it's not nil,
we convert the first character to uppercase. We could compare the
first character in the string to the nil byte as follows:
if (*string != '\0')
But C offers a simpler syntax:
if (*string)
Thus, we change the first character to uppercase as follows:
if (*string)
{
*string++ = toupper(*string);
}
The statement doing the conversion is very compact. C is guaranteed to
evaluate the right side of the expression before the left side. The
toupper function returns an uppercase character (or the
original character, if it wasn't a letter at all) without changing its
input. As we've seen, the left side will change the location
string points to before it increments the pointer.
At the end the pointer points to the second character of the
string. Note that the function doesn't bother creating a new pointer;
it simply uses string to step through the array. This is
because it won't need to return to the beginning of the array later.
We can convert the rest of the array to lowercase through a loop that
stops automatically when the pointer reaches the nil byte at the end.
while (*string)
{
*string++ = tolower(*string);
}
Finally, it should be mentioned that the array does not have to be
returned through a
return statement. This is because the
function has manipulated the memory in the calling function directly;
the change has been made and is available to the caller afterward:
(void) printf("Title is: %s\n", title);
This prints:
Title is: Understanding c pointers
Because a pointer is essentially an integer, you can print the value
of a pointer from your program if you want, thus seeing the location
in memory to which it points. This is useful mainly for debugging, and
is a feature debuggers offer.
Let's see the usefulness of passing pointers to structures as
arguments to functions. We'll show excerpts of a
trivial program
that doesn't use a pointer; it simply passes a whole structure to a
function.
The structure we'll use is:
struct _record {
char title[100];
char filename[255];
unsigned int example_num;
};
The program can create an instance of the structure with the name
example:
struct _record example;
and after setting some values (which is not shown here), can pass the
structure to a function:
process_record(example);
Note what is happening in memory. The entire structure--over 355
bytes--is being passed into the process_record function,
which makes a copy of it. We have used up a lot of memory. When a
function makes a copy of the arguments passed in, it's called
passing by value.
The function can access and set members of its own copy of the
structure:
(void) printf("Processing record %d: %s\n", record.example_num, record.title);
record.example_num++;
But this has no effect on the original value in the calling
program. When
process_record returns, its local memory is
freed and all changes are thrown away.
More commonly, programs pass pointers to structures, called
passing by reference. This means no copying is necessary, and
that any changes made by the function are reflected in the caller.
To understand the difference between passing by value and passing by
reference, imagine two situations where a friend is taking a trip. In
the first, you are giving a friend money to take with her. You can
just hand her the money, which is like passing by value. You don't
expect to get it back.
In the second situation, imagine the friend is bringing back something
large and heavy. You might meet her with a cart and ask her, "please
put the item in my cart."
Our new main program declares the structure with a pointer:
struct _record *example;
But now a crucial additional step is necessary before the values in
the structure are set: the program has to allocate memory for the
structure. C provides a convenient macro called
sizeof that
can figure out how much space any data structure, array, or other item
of data requires:
example = (struct _record *) malloc ( sizeof (example) );
A pointer is like a collapsable, empty box; it contains no space for
data. For instance, the example pointer might point to
location 690 in memory. The next variable could lie at location 694;
the pointer in this case occupies four bytes and there is no room for
other data. If you try to set values in the structure to which
example points, you will cause the dreaded buffer overflow,
which either terminates the program or overwrites memory used for
other things.
To create space (expanding your box, in our metaphor) and allow data
to be set, the program must allocate memory. The malloc is
usually used for this purpose.
Memory allocated by malloc is in the general area shared by the whole
program, and stays in place until you deallocate it. Most modern
operating systems free all memory allocated by the program when the
program terminates, but it's not a good idea to depend on this. First,
your program might run sometime on an operating system that does not
free memory. Second, your code might expand into a long-running
program that people keep open for months at a time.
So you should free any memory allocated by
malloc when you no
longer need it; this is easily done with the
free call:
free(example);
Freeing memory is crucial when a function allocates memory that one
its local variables points to. Otherwise, when the function returns,
the program can no longer access the memory, but the memory is still
allocated to the program. This error is probably the biggest
programming problem after buffer overflows: the memory
leak. Over time, repeated memory leaks can slow the whole system
and cause the program or system to hang.
The statement passing the structure looks the same as before, because
passing a pointer looks the same as passing anything else:
process_record(example);
The declaration of the function shows that it takes a pointer to a
structure:
void process_record(struct _record *record)
Note how it refers to members of the structure. Instead of a period,
it uses the
-> construct, which was chosen to look like an
arrow pointing to the member:
(void) printf("Processing record %d: %s\n", record->example_num, record->title);
record->example_num++;
Whenever you use a pointer to a structure, access it's members through
the -> construct.
The record->example_num++ refers to the value to which the
example_num member points; it doesn't increment the
pointer. This is because the -> is evaluated before the
++ operator, and thus creates the desired behavior.
After the function returns, the caller has the new value in its
example_num member.
The calling program can allocate a structure without using a pointer,
and still pass the structure by reference. It simply has to precede
the variable name with an ampersand (&), which creates an unnamed
pointer to the variable. The relevant parts of the program look like
this:
struct _record {
char title[100];
char filename[255];
unsigned int example_num;
};
...
process_record( &example );
Having seen pointer used with arrays, strings, and structures, you can
easily understand the final use shown here. A function in C can return
only one value. You can return a pointer to a structure that contains
many values. But for some purposes (such as setting error codes),
programmers often like to pass a variable as a parameter to the
function, and let the function set it. The simplest way to do this is
pass a variable using & to make a pointer out of it:
diff = cancel_out(int1, int2, &err);
The function declares it as a pointer:
int cancel_out( int arg_one, int arg_two, int *err)
The function then sets the memory that the pointer points to:
*err = 1;
and this value is available to the caller afterward:
if (err)
{
exit(1);
}
Try a short quiz to find out whether you understand key concepts from
this article. The quiz also helps the author determine how effective
the article is.