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.

Table of contents

Sections:

  1. Walking through arrays
  2. Passing structures to functions
  3. Returning multiple values from functions
  4. Quiz

Concept boxes:

Examples:

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.)

Walking through arrays

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;

Concept: Pointers and locations
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.
Concept: The pointer and what it points to
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.
Concept: NULL
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.
The code for the first integer array program and the second string program are available as complete, compilable programs.

Passing structures to functions

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.
Concept: Passing by value and passing by reference
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) );

Concept: Allocating and freeing memory
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 );

The code for the first passing by value program, the second passing by reference program, and the third passing with an ampersand program are available as complete, compilable programs.

Returning multiple values from functions

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);
    }

The code for this trivial error-handling program is available as a complete, compilable program.

Quiz

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.

Question 1:
Memory allocated by a malloc call is freed:
  1. When the program releases its memory with a free call .
  2. When the program releases its memory with a free call or the function that issued the malloc call returns.
  3. When the program releases its memory with a free call, the function that issued the malloc call returns, or the pointer that points to the allocated memory wanders outside that memory.
  4. When the program releases its memory with a free call, the function that issued the malloc call returns, or the pointer that points to the allocated memory is reset to NULL.

Question 2:
If you create the following structure and want to pass it by reference to a function, you should put before the variable when you pass it as a parameter to the function:

                    struct _record {
                      char title[100];
                      char filename[255];
                      unsigned int example_num;
                    } *record;


How easy did you find it to read the document, Understanding C Pointers?

Comment (optional):
Your name (optional):
Your email address (optional):
Your name and email address will be used only by the quiz's authors to discuss the quiz, document, and related projects with you.