Lesson #25

Pointers to Pointers and Command Line Processing

Overview

In this lesson I will cover the following topics

  1. Pointers to pointers.

  2. The difference between pre and post incrementing/decrementing pointers and how the increment/decrement operators interact with the indirection operator.

  3. Processing a program's command line.

Body

Pointers to pointers

We are now about 80% of the way through this course. The techniques and features that we will be looking at from now on are more advanced. However, many of them are still very important.

In this lesson we will look at pointers to pointers and at one very important application of such beasts: command line processing. Let's start with the basics.

What is a pointer?

A pointer is an address of something else. But a pointer is a variable too. Consequently a pointer is stored in memory just like any other variable. Thus pointers have addresses of their own. The syntax of managing the address of a pointer is exactly as you would expect. Here is an example.

int main(void)
{
  int   number = 1;
  int  *p;           // p is a "pointer to int".
  int **pp;          // pp is a "pointer to a pointer to int".

  p  = &number;      // Put the address of Number into p.
  pp = &p;           // Put the address of p into pp.

   *p = 2;           // Assign 2 to the thing pointed at by p.
 **pp = 3;           // Assign 3 to the thing pointed at by
                     //   (the thing pointed by pp).
   ...

Everything in the above example is legal. After declaring my variables I then put the address of the integer number into the pointer to integer, p. The address of p is the address of a pointer to integer. Such an address is thus a pointer to a pointer to int. I am assigning it to pp which is of the correct type so there is no problem. When I put 2 into *p I am putting the 2 into number since p is currently pointing at number. When I put 3 into **pp I am putting the three into number as well. This is because *pp is p and so *(*pp) is the same as *p.

If you find this confusing, don't feel bad. Many people do.

The C language has no limits on how deeply you can do this. The following declaration is legal.

int **********p;

Here I'm declaring p to be a "pointer to a pointer to a pointer to a pointer to a pointer to a pointer to a pointer to a pointer to a pointer to a pointer to int". If that sounds scary, don't worry. I have never seen such a thing in a serious program. Pointers to pointers are common enough, and I've even seen pointers to pointers to pointers. But I have never encounted anything more than that.

Arrays of pointers

Actually raw pointers to pointers are not all that common. What is common are arrays of pointers. Here is how that would be declared.

char *names[10];

This example declares an array of 10 pointers to character. You will see this sort of thing all the time. Why? Because you can use such an array to hold pointers to strings. Let me initialize names and you will see what I mean.

char *names[] = {
  "Peter", "Paul", "Mary", NULL
};

Remember that string literals are really pointers. For each literal above, the compiler creates an anonymous (null terminated) array of characters and replaces the literal with the address of the first character in that array. So I'm initializing an array of pointers with a bunch of pointers. That is just fine. I put a NULL pointer at the end of the array to mark the end. I can't use the null character for that purpose because this is not an array of characters. This is an array of pointers!

Now suppose I wanted to sweep down this array and print out every name in it. I can do that like this

char **p;

for (p = names; *p; p++) {
  printf("%s\n", *p);
}

Looks pretty simple, huh? Here is what's happening. First I'm going to use a pointer to keep track of where in the array I'm located. Since this is an array of pointers, I'll need a pointer to a pointer. No problem. The name names is the name of an array without an index. The compiler treats this as the address of the first element in the array. But the array elements are pointers to character so the address of an element is a pointer to a pointer to character. I'm assigning it to p which is of the correct type so there is no problem.

When I ask if *p is true I'm asking if the thing pointed at by p is zero or not. But the thing pointed at by p is a pointer to character. The null pointer acts like zero so what I'm really asking is: does p point at the null pointer? If no (non-zero) I execute the loop body. There I print out *p as a string. This is fine since the thing pointed at by p is a pointer to a null terminated string. When I increment p, I'm just advancing it to the next pointer in the array of pointers.

Here's a picture of my array:

     +------+
p -> |      |----> P e t e r \0
     +------+
     |      |----> P a u l \0
     +------+
     |      |----> M a r y \0
     +------+
     | NULL |
     +------+

This shows each array element as pointing to its own null terminated array of characters. The pointer p, in turn, points at an array element. When I talk about *p, I'm talking about what is inside the box p is pointing at. When I say p++, I'm advancing p to point at the next box.

Now consider the problem of sorting this list of names. All you would have to do is compare the strings pointed at by the various array elements and then, as necessary, swap pointers in the array. You don't actually have to move the strings themselves in memory. Only their addresses need to be rearranged. This is a good thing. In general strings are long and moving them around is slow. But pointers are small and swapping them will always be quick even when the things they point at are huge.

Also notice how each string pointed at by the elements of names can be of different length. In my example above, the first pointer in the array points at "Peter" which is five characters long. The second pointer in the array points at "Paul" which is only four characters long. Array elements are all exactly the same size. But an array of pointers can manage things of variable size.

Some languages support a feature called "multidimensional arrays". In fact, C also has that feature. However, it is rarely used. Most C programs use arrays of pointers (and thus pointers to pointers) in situations where other languages would use a multidimensional array.

The command line

Probably one of the most common and important application of arrays of pointers is the command line. So far all our programs have taken no command line arguments. But many of the Unix system commands do accept command line arguments. For example to copy files there is the cp program.

$ cp afile.txt bfile.txt

How does cp know which file we want to copy and what we want to copy it to? We tell it those names on the command line as we execute the program, but how does that information actually find its way into cp?

It turns out that function main actually can be written to take two parameters. Although our "void" parameter has been fine for our purposes so far, a more generally useful way to write main is

int main(int argc, char **argv)
{
  ...

The first parameter to main, traditionally called argc for "argument count," is the number of command line arguments including the program's own name. The second parameter to main, traditionally called argv for "argument vector," is a pointer to the first element of an array of pointers that give us access to the arguments themselves. For example when you do

$ cp afile.txt bfile.txt

The cp program (which is written in C) is started with argc == 3 and argv as shown below.

        +------+
argv -> |      |----> c p \0
        +------+
        |      |----> a f i l e . t x t \0
        +------+
        |      |----> b f i l e . t x t \0
        +------+
        | NULL |
        +------+

Here's how the cp program might work

#include <stdio.h>

int main(int argc, char *argv)
{
  int   return_code = 0;
  int   ch;
  FILE *infile;
  FILE *outfile;

  if (argc != 3) {
    printf("mycp: Needs two filenames as arguments.\n");
    return_code = 1;
  }
  else if ((infile = fopen(argv[1], "r")) == NULL) {
    printf("mycp: Can't open the file %s for reading.\n", argv[1]);
    return_code = 1;
  }
  else if ((outfile = fopen(argv[2], "w")) == NULL) {
    printf("mycp: Can't open the file %s for writing.\n", argv[2]);
    fclose(infile);
    return_code = 1;
  }
  else {
    while ((ch = getc(infile)) != EOF) putc(ch, outfile);
    fclose(infile);
    fclose(outfile);
  }

  return return_code;
}

If you want to copy and paste this example into an editor to try it, I suggest you use the name "mycp.c" to avoid conflicting with the system's cp command.

Actually the real cp is much more complicated than this. It does a lot more than you might think. Nevertheless this example shows the basic way to access the command line from a program. Notice how in my example, I'm passing things like argv[1] and argv[2] to the fopen function. The argv parameter is declared as a pointer to a pointer to character. Yet C allows me to put an index on any pointer. This makes it easy for me to use the argv pointer to access any element of the array of pointers that I want. The value of argv[1], in turn, is an ordinary pointer to character. That is exactly what fopen is expecting to get so everything works just fine.

But it gets better. Another look at ++ and --

Let's step back a bit and talk about the ++ and -- operators. Their action seems easy enough to understand. The ++ operator increments something and the -- operator decrements it. However, these operators can be used in two different ways. For example x = i++ or x = ++i.

In the first case the value of i is assigned to x and then i is incremented afterwards. In the second case i is incremented first and then the new value is assigned to x. The first case is called "postincrementing" and the second case is called "preincrementing". This only makes a difference when we try to use the result of ++ or -- in a larger expression. In a case like

i++;
++i;

it really doesn't matter because nothing more is done besides the incrementing.

When you start doing things with pointers, life gets really interesting. Consider this example:

char *name = "Peter";

*name++ = 'x';

Here I'm using both the indirection operator and the postincrement operator. The precedence of these operators is the same and they associate from right to left. Thus the expression above is really *(name++) = 'x'. This causes the value of name to be used with the indirection operator before name is incremented. The overall effect is to put the 'x' into the location pointed at by name and then increment name. The resulting string is "xeter".

Constrast this with *++name = 'x'. This causes name to be incremented first and then used as a pointer. The resulting string is "Pxter".

Both of these examples applied an increment operator to a pointer. You can also increment the thing the pointer is pointing at. For example, ++*name causes the 'P' in "Peter" to become 'Q'. If I use a postincrement I get the same effect because just like ++i and i++ there is nothing being done with the result of the incrementation anyway. However, to get a postincrement I have to use explicit parentheses as in (*name)++. Without the parentheses the ++ operator is applied directly to name and not *name.

Putting this all together

Now to see how all of this works together let me show you a more typical way for the command line to be processed. In this example, I'm building a program that accepts several command line options as well as command line arguments. Let's assume the -h option invokes the program's help, the -v option turns on "verbose" mode, and the -i option sets some sort of time interval in seconds. The program then operates on all the files otherwise named on the command line. For example, I should be able to invoke this program like so

$ prog -v -i10 afile.txt bfile.txt cfile.txt

To turn on verbose mode, set the time interval to 10 seconds and process the three files afile.txt, bfile.txt and cfile.txt. Here's how it might be done. Study this example carefully... it brings together quite a few of the techniques we have studied in this course so far.

int main(int argc, char **argv)
{
  int verbose_mode  = OFF;
  int time_interval = DEFAULT_VALUE;

  // Step down the command line until I come to the end.
  while (*++argv) {

    // If this command line argument is an option...
    if (**argv == '-') {

      // What option is it? Handle each one.
      switch (*++*argv) {
        case 'h':
          display_help();
          break;

        case 'v':
          verbose_mode = ON;
          break;

        case 'i':
          time_interval = atoi(++*argv);
          break;

        default:
          printf("Unknown option (%c) ignored.\n", **argv);
          break;
      }
    }

    // If this command line argument is not an option, it must be a file.
    else {
      process_file(*argv, verbose_mode, time_interval);
    }
  }
  return 0;
}

Before trying to puzzle this example out, you should draw a picture of the argument vector. Here it is

        +------+
argv -> |      |----> p r o g \0
        +------+
        |      |----> - v \0
        +------+
        |      |----> - i 1 0 \0
        +------+
        |      |----> a f i l e . t x t \0
        +------+
        |      |----> b f i l e . t x t \0
        +------+
        |      |----> c f i l e . t x t \0
        +------+
        | NULL |
        +------+

Now let's look at the condition in the outer while loop: *++argv. This involves preincrementing argv and then asking: does argv point at the null pointer? The preincrementing is necessary to skip over the program's name and get right to the argument list. When *++argv refers to the null pointer, the while condition will be false and the loop will end. The program is over after it has completely handled the command line.

Because of the ++ operator being applied to argv in the while condition, the picture looks like below when the while loop is entered for the first time.

        +------+
        |      |----> p r o g \0
        +------+
argv -> |      |----> - v \0
        +------+
        |      |----> - i 1 0 \0
        +------+
        |      |----> a f i l e . t x t \0
        +------+
        |      |----> b f i l e . t x t \0
        +------+
        |      |----> c f i l e . t x t \0
        +------+
        | NULL |
        +------+

There I ask if **argv == '-'. Using two indirection operators on argv brings me all the way to the first character in the argument. In this case it is a dash so I go inside the if statement. The controlling expression on the switch is *++*argv. This says: go to the place argv is pointing (that's the pointer in the array). Increment what you find there (this causes that pointer to skip over the dash) and check the thing the resulting pointer is pointing at (the first character after the dash). There is a case in the switch for every legal option.>

Two of the options merely have to exist. The -i option, however, has an integer associated with it. To get at that integer, I use the library atoi function with ++*argv as the argument. The atoi function (declared in <stdlib.h>) takes a pointer to a null terminated array of characters and returns the integer value of those charactes. For example, it will convert "123" into the integer 123. The expression ++*argv causes the array element *argv to be incremented again so that it points just past the option name ('i') and at the first digit of the associated number. I then pass that pointer to atoi for conversion.

All of these gyrations with pointers, arrays of pointers, pointers to pointers, and incrementation may seem rather obscure. However, this sort of activity is very common in C programs. Seasoned C programmers are very used to this and do these things often. If you ever do much work with C you will see it again and again.

Summary

  1. Pointers hold the address of something else, but pointers are just variables too. As a result, there is no problem taking the address of a pointer and getting a pointer to a pointer. More typically programs use arrays of pointers (especially arrays of pointers to null terminated strings). A pointer that points into such an array must be a pointer to a pointer.

  2. An expression such as *name++ = 'x' installs the character 'x' into the location pointed at by name and then advances name by one position. Even though the ++ operator is applied to name first (due to the associativity rules), name is advanced after it is used with the indirection operator because this is an instance of postincrementing.

    An expression such as *++name = 'x' preincrements name and then installs 'x' into the location pointed at by name's new value. Many C programmers make extensive use of this distinction.

  3. Function main can be written to take two arguments.

    int main(int argc, char **argv) { ...
    

    The first is a count of the number of words on the command line (including the name of the program). The second is a pointer to an array of pointers. Each pointer in the array points at a null terminated string containing a separate command line argument. There is a NULL pointer at the end of the array to mark the end. This arrangement allows you to access your program's command line. Users can type information on the command line that your program can then use to customize what it does. This is a very common and important technique.

© Copyright 2003 by Peter C. Chapin.
Last Revised: July 7, 2003