Search the Catalog
Practical C Programming, 3rd Edition

Practical C Programming, 3rd Edition

By Steve Oualline
3rd Edition August 1997
1-56592-306-5, Order Number: 3065
454 pages, $32.95

Sample Chapter 13: Simple Pointers


In this chapter:
Pointers as Function Arguments
const Pointers
Pointers and Arrays
How Not to Use Pointers
Using Pointers to Split a String
Pointers and Structures
Command-Line Arguments
Programming Exercises
Answers

The choice of a point of view is
the initial act of culture.

José Ortega y Gasset

There are things and pointers to things. Knowing the difference between the two is very important. This concept is illustrated in Figure 13-1.

Figure 13-1. A thing and a pointer to a thing

 

In this book, we use a box to represent a thing. The name of the variable is written on the bottom of the box. In this case, our variable is named thing. The value of the variable is 6.

The address of thing is 0x1000. Addresses are automatically assigned by the C compiler to every variable. Normally, you don't have to worry about the addresses of variables, but you should understand that they're there.

Our pointer (thing_ptr) points to the variable thing. Pointers are also called address variables because they contain the addresses of other variables. In this case, our pointer contains the address 0x1000. Because this is the address of thing, we say that thing_ptr points to thing.

Variables and pointers are much like street addresses and houses. For example, your address might be "214 Green Hill Lane." Houses come in many different shapes and sizes. Addresses are approximately the same size (street, city, state, and zip). So, while "1600 Pennsylvania Ave." might point to a very big white house and "8347 Undersea Street" might be a one-room shack, both addresses are the same size.

The same is true in C. While things may be big and small, pointers come in one size (relatively small).[1]

Many novice programmers get pointers and their contents confused. In order to limit this problem, all pointer variables in this book end with the extension _ptr. You might want to follow this convention in your own programs. Although this notation is not as common as it should be, it is extremely useful.

Many different address variables can point to the same thing. This concept is true for street addresses as well. Table 13-1 lists the location of important services in a small town.

Table 13-1: Directory of Ed's Town USA

Service
(variable name)
Address
(address value)
Building
(thing)
Fire Department 1 Main Street City Hall
Police Station 1 Main Street City Hall
Planning office 1 Main Street City Hall
Gas Station 2 Main Street Ed's Gas Station

In this case, we have a government building that serves many functions. Although it has one address, three different pointers point to it.

As we will see in this chapter, pointers can be used as a quick and simple way to access arrays. In later chapters, we will discover how pointers can be used to create new variables and complex data structures such as linked lists and trees. As you go through the rest of the book, you will be able to understand these data structures as well as create your own.

A pointer is declared by putting an asterisk (*) in front of the variable name in the declaration statement:

int thing;        /* define a thing */ 
int *thing_ptr;  /* define a pointer to a thing */ 

Table 13-2 lists the operators used in conjunction with pointers.

 

Table 13-2: Pointer Operators

Operator Meaning
* Dereference (given a pointer, get the thing referenced)
& Address of (given a thing, point to it)

The operator ampersand (&) returns the address of a thing which is a pointer. The operator asterisk (*) returns the object to which a pointer points. These operators can easily cause confusion. Table 13-3 shows the syntax for the various pointer operators.

 

Table 13-3: Pointer Operator Syntax

C Code Description
thing Simple thing (variable)
&thing Pointer to variable thing
thing_ptr Pointer to an integer (may or may not be specific integer thing)
*thing_ptr Integer

Let's look at some typical uses of the various pointer operators:

int thing; /* Declare an integer (a thing) */
thing = 4;

The variable thing is a thing. The declaration int thing does not contain an *, so thing is not a pointer:

int *thing_ptr;      /* Declare a pointer to a thing */

The variable thing_ptr is a pointer. The * in the declaration indicates this is a pointer. Also, we have put the extension _ptr onto the name:

thing_ptr = &thing;  /* Point to the thing */ 

The expression &thing is a pointer to a thing. The variable thing is an object. The & (address of operator) gets the address of an object (a pointer), so &thing is a pointer. We then assign this to thing_ptr, also of type pointer:

*thing_ptr = 5;      /* Set "thing" to 5 */ 
                      /* We may or may not be pointing */ 
                      /* to the specific integer "thing" */ 

The expression *thing_ptr indicates a thing. The variable thing_ptr is a pointer. The * (dereference operator) tells C to look at the data pointed to, not the pointer itself. Note that this points to any integer. It may or may not point to the specific variable thing.

Introduction

These pointer operations are summarized in Figure 13-2.

Figure 13-2. Pointer operations

 

The following examples show how to misuse the pointer operations:

*thing

is illegal. It asks C to get the object pointed to by the variable thing. Because thing is not a pointer, this operation is invalid.

&thing_ptr

is legal, but strange. thing_ptr is a pointer. The & (address of operator) gets a pointer to the object (in this case thing_ptr). The result is a pointer to a pointer.

Example 13-1 illustrates a simple use of pointers. It declares one object, one thing, and a pointer, thing_ptr. thing is set explicitly by the line:

thing = 2; 

The line:

thing_ptr = &thing; 

causes C to set thing_ptr to the address of thing. From this point on, thing and *thing_ptr are the same.

Example 13-1: thing/thing.c


#include <stdio.h>
int main()
{
    int   thing_var;  /* define a variable for thing */
    int  *thing_ptr;  /* define a pointer to thing */
 
    thing_var = 2;      /* assigning a value to thing */
    printf("Thing %d\n", thing_var);
 
    thing_ptr = &thing_var; /* make the pointer point to thing */
    *thing_ptr = 3;     /* thing_ptr points to thing_var so */
                        /* thing_var changes to 3 */
    printf("Thing %d\n", thing_var);
 
    /* another way of doing the printf */
    printf("Thing %d\n", *thing_ptr);
    return (0);
}

Several pointers can point to the same thing:

1:      int something;       
2: 
3:      int      *first_ptr;     /* one pointer */ 
4:      int      *second_ptr;    /* another pointer */ 
5: 
6:      something = 1;          /* give the thing a value */ 
7: 
8:      first_ptr = &something; 
9:      second_ptr = first_ptr; 

In line 8, we use the & operator to change something, a thing, into a pointer that can be assigned to first_ptr. Because first_ptr and second_ptr are both pointers, we can do a direct assignment in line 9.

After executing this program fragment, we have the situation shown in Figure 13-3.

Figure 13-3. Two pointers and a thing

 

You should note that while we have three variables, there is only one integer (something). The following are all equivalent:

something = 1; 
*first_ptr = 1; 
*second_ptr = 1; 

Pointers as Function Arguments

C passes parameters using "call by value." That is, the parameters go only one way into the function. The only result of a function is a single return value. This concept is illustrated in Figure 13-4.

Figure 13-4. Function call

 

However, pointers can be used to get around this restriction.

Imagine that there are two people, Sam and Joe, and whenever they meet, Sam can only talk and Joe can only listen. How is Sam ever going to get any information from Joe? Simple: all Sam has to do is tell Joe, "I want you to leave the answer in the mailbox at 335 West 5th Street."

C uses a similar trick to pass information from a function to its caller. In Example 13-2, main wants the function inc_count to increment the variable count.

Passing it directly would not work, so a pointer is passed instead ("Here's the address of the variable I want you to increment"). Note that the prototype for inc_count contains an int *. This format indicates that the single parameter given to this function is a pointer to an integer, not the integer itself.

Example 13-2: call/call.c


#include <stdio.h>
void inc_count(int *count_ptr)
{
    (*count_ptr)++;
}
 
int main()
{
    int  count = 0;     /* number of times through */
 
    while (count < 10)
        inc_count(&count);
 
    return (0);
}

This code is represented graphically in Figure 13-5. Note that the parameter is not changed, but what it points to is changed.

Figure 13-5. Call of inc_count

 

Finally, there is a special pointer called NULL. It points to nothing. (The actual numeric value is 0.) The standard include file, locale.h, defines the constant NULL. (This file is usually not directly included, but is usually brought in by the include files stdio.h or stdlib.h.) The NULL pointer is represented graphically in Figure 13-6.

Figure 13-6. NULL

 

const Pointers

Declaring constant pointers is a little tricky. For example, the declaration:

const int result = 5;

tells C that result is a constant so that:

result = 10;      /* Illegal */

is illegal. The declaration:

const char *answer_ptr = "Forty-Two";

does not tell C that the variable answer_ptr is a constant. Instead, it tells C that the data pointed to by answer_ptr is a constant. The data cannot be changed, but the pointer can. Again we need to make sure we know the difference between "things" and "pointers to things."

What's answer_ptr? A pointer. Can it be changed? Yes, it's just a pointer. What does it point to? A const char array. Can the data pointed to by answer_ptr be changed? No, it's constant.

In C this is:

answer_ptr = "Fifty-One";    /* Legal (answer_ptr is a variable) */
*answer_ptr = 'X';           /* Illegal (*answer_ptr is a constant) */

If we put the const after the * we tell C that the pointer is constant.

For example:

char *const name_ptr = "Test";

What's name_ptr? It is a constant pointer. Can it be changed? No. What does it point to? A character. Can the data we pointed to by name_ptr be changed? Yes.

name_ptr = "New";           /* Illegal (name_ptr is constant) */
*name_ptr = 'B';            /* Legal (*name_ptr is a char) */

Finally, we can put const in both places, creating a pointer that cannot be changed to a data item that cannot be changed:

const char *const title_ptr = "Title";

Pointers and Arrays

C allows pointer arithmetic (addition and subtraction). Suppose we have:

char array[5]; 
char *array_ptr = &array[0]; 

In this example, *array_ptr is the same as array[0], *(array_ptr+1) is the same as array[1], *(array_ptr+2) is the same as array[2], and so on. Note the use of parentheses. Pointer arithmetic is represented graphically in Figure 13-7.

Figure 13-7. Pointers into an array

 

However, (*array_ptr)+1 is not the same as array[1]. The +1 is outside the parentheses, so it is added after the dereference. So (*array_ptr)+1 is the same as array[0]+1.

At first glance, this method may seem like a complex way of representing simple array indices. We are starting with simple pointer arithmetic. In later chapters we will use more complex pointers to handle more difficult functions efficiently.

The elements of an array are assigned to consecutive addresses. For example, array[0] may be placed at address 0xff000024. Then array[1] would be placed at address 0xff000025, and so on. This structure means that a pointer can be used to find each element of the array. Example 13-3 prints out the elements and addresses of a simple character array.

Example 13-3: array-p/array-p.c


#include <stdio.h>
 
#define ARRAY_SIZE 10   /* Number of characters in array */
/* Array to print */
char array[ARRAY_SIZE] = "0123456789";   
 
int main()
{
    int index;  /* Index into the array */
 
    for (index = 0; index < ARRAY_SIZE; ++index) {
        printf("&array[index]=0x%p (array+index)=0x%p array[index]=0x%x\n",
            &array[index], (array+index), array[index]);
    }
    return (0);
}
NOTE: When printing pointers, the special conversion %p should be used.

When run, this program prints:

&array[index] (array+index) array[index]
0x40b0         0x40b0        0x30
0x40b1         0x40b1        0x31
0x40b2         0x40b2        0x32
0x40b3         0x40b3        0x33
0x40b4         0x40b4        0x34
0x40b5         0x40b5        0x35
0x40b6         0x40b6        0x36
0x40b7         0x40b7        0x37
0x40b8         0x40b8        0x38
0x40b9         0x40b9        0x39

Characters use one byte, so the elements in a character array will be assigned consecutive addresses. A short int font uses two bytes, so in an array of short int, the addresses increase by two. Does this mean that array+1 will not work for anything other than characters? No. C automatically scales pointer arithmetic so that it works correctly. In this case, array+1 will point to element number 1.

C provides a shorthand for dealing with arrays. Rather than writing:

array_ptr = &array[0]; 

we can write:

array_ptr = array; 

C blurs the distinction between pointers and arrays by treating them in the same manner in many cases. Here we use the variable array as a pointer, and C automatically does the necessary conversion.

Example 13-4 counts the number of elements that are nonzero and stops when a zero is found. No limit check is provided, so there must be at least one zero in the array.

Example 13-4: ptr2/ptr2.c


#include <stdio.h>
 
int array[] = {4, 5, 8, 9, 8, 1, 0, 1, 9, 3};
int index;
 
int main()
{
    index = 0;
    while (array[index] != 0)
        ++index;
 
    printf("Number of elements before zero %d\n",
                  index);
    return (0);
}

Example 13-5 is a version of Example 13-4 that uses pointers.

Example 13-5: ptr3/ptr3.c


#include <stdio.h>
 
int array[] = {4, 5, 8, 9, 8, 1, 0, 1, 9, 3};
int *array_ptr;
 
int main()
{
    array_ptr = array;
 
    while ((*array_ptr) != 0)
        ++array_ptr;
 
    printf("Number of elements before zero %d\n",
                  array_ptr - array);
    return (0);
}

Notice that when we wish to examine the data in the array, we use the dereference operator (*). This operator is used in the statement:

while ((*array_ptr) != 0)

When we wish to change the pointer itself, no other operator is used. For example, the line:

++array_ptr;

increments the pointer, not the data.

Example 13-4 uses the expression (array[index] != 0). This expression requires the compiler to generate an index operation, which takes longer than a simple pointer dereference, ((*array_ptr) != 0).

The expression at the end of this program, array_ptr - array, computes how far array_ptr is into the array.

When passing an array to a procedure, C will automatically change the array into a pointer. In fact, if you put & before the array, C will issue a warning. Example 13-6 illustrates the various ways in which an array can be passed to a subroutine.

Example 13-6: init-a/init-a.c (continued)


#define MAX 10  /* Size of the array */
/********************************************************
 * init_array_1 -- Zeroes out an array.                 *
 *                                                      *
 * Parameters                                           *
 *      data -- The array to zero out.                  *
 ********************************************************/
void init_array_1(int data[])
{
    int  index;
 
    for (index = 0; index < MAX; ++index)
        data[index] = 0;
}
 
/********************************************************
 * init_array_2 -- Zeroes out an array.                 *
 *                                                      *
 * Parameters                                           *
 *      data_ptr -- Pointer to array to zero.           *
 ********************************************************/
void init_array_2(int *data_ptr)
{
    int index;
 
    for (index = 0; index < MAX; ++index)
        *(data_ptr + index) = 0;
}
int main()
{
    int  array[MAX];
 
    void init_array_1();
    void init_array_2();
 
    /* one way of initializing the array */
    init_array_1(array);
 
    /* another way of initializing the array */
    init_array_1(&array[0]);
 
    /* works, but the compiler generates a warning */
    init_array_1(&array);
 
    /* Similar to the first method but  */
    /*    function is different */
    init_array_2(array);
 
    return (0);
}

How Not to Use Pointers

The major goal of this book is to teach you how to create clear, readable, maintainable code. Unfortunately, not everyone has read this book and some people still believe that you should make your code as compact as possible. This belief can result in programmers using the ++ and -- operators inside other statements.

Example 13-7 shows several examples in which pointers and the increment operator are used together.

Example 13-7: Bad Pointer Usage


/* This program shows programming practices that should **NOT** be used */
/* Unfortunately, too many programmers use them */
int array[10];    /* An array for our data */
int main()
{
    int *data_ptr;   /* Pointer to the data */
    int value;       /* A data value */
 
    data_ptr = &array[0];/* Point to the first element */
    value = *data_ptr++;  /* Get element #0, data_ptr points to element #1 */
    value = *++data_ptr;  /* Get element #2, data_ptr points to element #2 */
    value = ++*data_ptr; /* Increment element #2, return its value */
                         /* Leave data_ptr alone */

To understand each of these statements, you must carefully dissect each expression to discover its hidden meaning. When I do maintenance programming, I don't want to have to worry about hidden meanings, so please don't code like this, and shoot anyone who does.

These statements are dissected in Figure 13-8.

Figure 13-8. Pointer operations dissected

 

This example is a little extreme, but it illustrates how side effects can easily become confusing.

Example 13-8 is an example of the code you're more likely to run into. The program copies a string from the source (p) to the destination (q).

Example 13-8: Cryptic Use of Pointers


void copy_string(char *p, char *q)
{
    while (*p++ = *q++);
}

Given time, a good programmer will decode this. However, understanding the program is much easier when we are a bit more verbose, as in Example 13-9.

Example 13-9: Readable Use of Pointers


/********************************************************
 * copy_string -- Copies one string to another.         *
 *                                                      *
 * Parameters                                           *
 *      dest -- Where to put the string                 *
 *      source -- Where to get it                       *
 ********************************************************/
void copy_string(char *dest, char *source)
{
    while (1) {
        *dest = *source;
 
        /* Exit if we copied the end of string */
        if (*dest == '\0')
            return;
 
        ++dest;
        ++source;
    }
}

Using Pointers to Split a String

Suppose we are given a string of the form "Last/First." We want to split this into two strings, one containing the first name and one containing the last name.

We need a function to find the slash in the name. The standard function strchr performs this job for us. In this program, we have chosen to duplicate this function to show you how it works.

This function takes a pointer to a string (string_ptr) and a character to find (find) as its arguments. It starts with a while loop that will continue until we find the character we are looking for (or we are stopped by some other code below).

while (*string_ptr != find) {

Next we test to see if we've run out of string. In this case, our pointer (string_ptr) points to the end-of-string character. If we have reached the end of string before finding the character, we return NULL:

if (*string_ptr == '\0')
    return (NULL);

If we get this far, we have not found what we are looking for, and are not at the end of the string. So we move the pointer to the next character, and return to the top of the loop to try again:

++string_ptr;
}

Our main program reads in a single line, stripping the newline character from it. The function my_strchr is called to find the location of the slash (/).

At this point, last_ptr points to the first character of the last name and first_ptr points to slash. We then split the string by replacing the slash (/) with an end of string (NUL or \0). Now last_ptr points to just the last name and first_ptr points to a null string. Moving first_ptr to the next character makes it point to the beginning of the first name.

The sequence of steps in splitting the string is illustrated in Figure 13-9.

Figure 13-9. Splitting a string

 

Example 13-10 contains the full program, which demonstrates how pointers and character arrays can be used for simple string processing.

Example 13-10: split/split.c (continued)


#include <stdio.h>
#include <string.h>
#include <stdlib.h>
 
/********************************************************
 * my_strchr -- Finds a character in a string.          *
 *      Duplicate of a standard library function,       *
 *      put here for illustrative purposes              *
 *                                                      *
 * Parameters                                           *
 *      string_ptr -- String to look through.           *
 *      find -- Character to find.                      *
 *                                                      *
 * Returns                                              *
 *      pointer to 1st occurrence of character          *
 *      in string or NULL for error.                    *
 ********************************************************/
char *my_strchr(char * string_ptr, char find)
{
    while (*string_ptr != find) {
 
       /* Check for end */
 
       if (*string_ptr == '\0')
           return (NULL);       /* not found */
 
        ++string_ptr;
    }
    return (string_ptr);        /* Found */
}
 
int main()
{
    char line[80];      /* The input line */
    char *first_ptr;    /* pointer to the first name */
    char *last_ptr;     /* pointer to the last name */
 
    fgets(line, sizeof(line), stdin);
 
    /* Get rid of trailing newline */
    line[strlen(line)-1] = '\0';        
 
    last_ptr = line;    /* last name is at beginning of line */
 
    first_ptr = my_strchr(line, '/');      /* Find slash */
 
    /* Check for an error */
    if (first_ptr == NULL) {
        fprintf(stderr,
            "Error: Unable to find slash in %s\n", line);
        exit (8);
    }
 
    *first_ptr = '\0';  /* Zero out the slash */
 
    ++first_ptr;        /* Move to first character of name */
 
    printf("First:%s Last:%s\n", first_ptr, last_ptr);
    return (0);
}

Question 13-2: Example 13-11 is supposed to print out:

Name: tmp1 

but instead, we get:

Name: !_@$#ds80 

(Your results may vary.) Why?

Example 13-11: tmp-name/tmp-name.c


#include <stdio.h>
#include <string.h>
 
/********************************************************
 * tmp_name -- Return a temporary filename.             *
 *                                                      *
 * Each time this function is called, a new name will   *
 * be returned.                                         *
 *                                                      *
 * Returns                                              *
 *      Pointer to the new filename.                    *
 ********************************************************/
char *tmp_name(void)
{
    char name[30];      /* The name we are generating */
    static int sequence = 0;    /* Sequence number for last digit */
 
    ++sequence; /* Move to the next filename */
 
    strcpy(name, "tmp");
 
    /* But in the sequence digit */
    name[3] = sequence + '0';
 
    /* End the string */
    name[4] = '\0';
 
    return(name);
}
 
int main()
{
    char *tmp_name(void);       /* Get name of temporary file */
 
    printf("Name: %s\n", tmp_name());
    return(0);
}

Pointers and Structures

In Chapter 12, Advanced Types, we defined a structure for a mailing list:

struct mailing { 
    char name[60];    /* last name, first name */ 
    char address1[60];/* two lines of street address */ 
    char address2[60]; 
    char city[40];      
    char state[2];    /* two-character abbreviation */ 
    long int zip;     /* numeric zip code */ 
} list[MAX_ENTRIES]; 

Mailing lists must frequently be sorted by name and zip code. We could sort the entries themselves, but each entry is 226 bytes long. That's a lot of data to move around. One way around this problem is to declare an array of pointers, and then sort the pointers:

/* Pointer to the data */ 
struct mailing *list_ptrs[MAX_ENTRIES];  
int current;    /* current mailing list entry */ 
 
    for (current = 0; current = number_of_entries; ++current) 
        list_ptrs[current] = &list[current]; 
    /* Sort list_ptrs by zip code */ 

Now, instead of having to move a 226-byte structure around, we are moving 4-byte pointers. Our sorting is much faster. Imagine that you had a warehouse full of big heavy boxes and you needed to locate any box quickly. You could put them in alphabetical order, but that would require a lot of moving. Instead, you assign each location a number, write down the name and number on index cards, and sort the cards by name.

Command-Line Arguments

The procedure main actually takes two arguments. They are called argc and argv[2]:

main(int argc, char *argv[]) 
{ 

(If you realize that the arguments are in alphabetical order, you can easily remember which one comes first.)

The parameter argc is the number of arguments on the command line (including the program name). The array argv contains the actual arguments. For example, if the program args were run with the command line:

args this is a test 

then:

   argc =     5 
argv[0] =    "args" 
argv[1] =    "this" 
argv[2] =    "is" 
argv[3] =    "a" 
argv[4] =    "test" 
argv[5] =    NULL 

NOTE: The UNIX shell expands wildcard characters like *, ?, and [ ] before sending the command line to the program. See your sh or csh manual for details.

Turbo C++ and Borland C++ expand wildcard characters if the file WILDARG.OBJ is linked with your program. See the manual for details.

Almost all UNIX commands use a standard command-line format. This standard has carried over into other environments. A standard UNIX command has the form:

command options file1 file1 file3 ... 

Options are preceded by a dash (-) and are usually a single letter. For example, the option -v might turn on verbose mode for a particular command. If the option takes a parameter, it follows the letter. For example, the option -m1024 sets the maximum number of symbols to 1024 and -ooutfile sets the output filename to outfile.

Let's look at writing a program that can read the command-line arguments and act accordingly. This program formats and prints files. Part of the documentation for the program is given here:

print_file [-v] [-llength] [-oname] [file1] [file2] ... 

where:

-v

specifies verbose options; turns on a lot of progress information messages

-llength

sets the page size to length lines (default = 66)

-oname

sets the output file to name (default = print.out)

file1, file2, ...

is a list of files to print. If no files are specified, the file print.in is printed.

We can use a while loop to cycle through the command-line options. The actual loop is:

while ((argc > 1) && (argv[1][0] == '-')) { 

One argument always exists: the program name. The expression (argc > 1) checks for additional arguments. The first one is numbered 1. The first character of the first argument is argv[1][0]. If this is a dash, we have an option.

At the end of the loop is the code:

    --argc; 
    ++argv; 
} 

This consumes an argument. The number of arguments is decremented to indicate one less option, and the pointer to the first option is incremented, shifting the list to the left one place. (Note: after the first increment, argv[0] no longer points to the program name.)

The switch statement is used to decode the options. Character 0 of the argument is the dash (-). Character 1 is the option character, so we use the expression:

switch (argv[1][1]) { 

to decode the option.

The option -v has no arguments; it just causes a flag to be set.

The option -o takes a filename. Rather than copy the whole string, we set the character pointer out_file to point to the name part of the string. By this time we know the following:

argv[1][0]   ='-' 
argv[1][1]   ='o' 
argv[1][2]   = first character of the filename 

We set out_file to point to the string with the statement:

out_file = &argv[1][2]; 

The address of operator (&) is used to get the address of the first character in the output filename. This process is appropriate because we are assigning the address to a character pointer named out_file.

The -l option takes an integer argument. The library function atoi is used to convert the string into an integer. From the previous example, we know that argv[1][2] is the first character of the string containing the number. This string is passed to atoi.

Finally, all the options are parsed and we fall through to the processing loop. This merely executes the function do_file for each file argument. Example 13-12 contains the print program.

This is one way of parsing the argument list. The use of the while loop and switch statement is simple and easy to understand. This method does have a limitation. The argument must immediately follow the options. For example, -odata.out will work, but "-o data.out" will not. An improved parser would make the program more friendly, but the techniques described here work for simple programs.

Example 13-12: print/print.c (continued)


[File: print/print.c]
/********************************************************
 * Program: Print                                       *
 *                                                      *
 * Purpose:                                             *
 *      Formats files for printing.                     *
 *                                                      *
 * Usage:                                               *
 *      print [options] file(s)                         *
 *                                                      *
 * Options:                                             *
 *      -v              Produces verbose messages.      *
 *      -o<file>        Sends output to a file          *
 *                      (default=print.out).            *
 *      -l<lines>       Sets the number of lines/page   *
 *                      (default=66).                   *
 ********************************************************/
#include <stdio.h>
#include <stdlib.h>      
 
int verbose = 0;         /* verbose mode (default = false) */
char *out_file = "print.out";   /* output filename */
char *program_name;      /* name of the program (for errors) */
int line_max = 66;       /* number of lines per page */
 
/********************************************************
 * do_file -- Dummy routine to handle a file.           *
 *                                                      *
 * Parameter                                            *
 *      name -- Name of the file to print.              *
 ********************************************************/
void do_file(char *name)
{
    printf("Verbose %d Lines %d Input %s Output %s\n",
        verbose, line_max, name, out_file);
}
/*********************************************************
 * usage -- Tells the user how to use this program and   *
 *              exit.                                    *
 ********************************************************/
void usage(void)
{
    fprintf(stderr,"Usage is %s [options] [file-list]\n", 
                                program_name);
    fprintf(stderr,"Options\n");
    fprintf(stderr,"  -v          verbose\n");
    fprintf(stderr,"  -l<number>  Number of lines\n");
    fprintf(stderr,"  -o<name>    Set output filename\n");
    exit (8);
}
int main(int argc, char *argv[])
{
    /* save the program name for future use */
    program_name = argv[0];
 
    /* 
     * loop for each option  
     *   Stop if we run out of arguments
     *   or we get an argument without a dash
     */
    while ((argc > 1) && (argv[1][0] == '-')) {
        /*
         * argv[1][1] is the actual option character
         */
        switch (argv[1][1]) {
            /*
             * -v verbose 
             */
            case 'v':
                verbose = 1; 
                break;
            /*
             * -o<name>  output file
             *    [0] is the dash
             *    [1] is the "o"
             *    [2] starts the name
             */
            case 'o':
                out_file = &argv[1][2];
                break;
            /*
             * -l<number> set max number of lines
             */
            case 'l':
                line_max = atoi(&argv[1][2]);
                break;
            default:
                fprintf(stderr,"Bad option %s\n", argv[1]);
                usage();
        }
        /*
         * move the argument list up one
         * move the count down one
         */
        ++argv;
        --argc;
    }
 
    /*
     * At this point, all the options have been processed.
     * Check to see if we have no files in the list.
     * If no files exist, we need to process just standard input stream.
     */
    if (argc == 1) {
        do_file("print.in");
    } else {
        while (argc > 1) {
          do_file(argv[1]);
          ++argv;
          --argc;
        }
    }
    return (0);
}

Programming Exercises

Exercise 13-1: Write a program that uses pointers to set each element of an array to zero.

Exercise 13-2: Write a function that takes a single string as its argument and returns a pointer to the first nonwhite character in the string.

Answers

Answer 13-1: The problem is that the variable name is a temporary variable. The compiler allocates space for the name when the function is entered and reclaims the space when the function exits. The function assigns name the correct value and returns a pointer to it. However, the function is over, so name disappears and we have a pointer with an illegal value.

The solution is to declare name static. In this manner, name is a permanent variable and will not disappear at the end of the function.

Question 13-2: After fixing the function, we try using it for two filenames. Example 13-13 should print out:

Name: tmp1 
Name: tmp2 

but it doesn't. What does it print and why?

Example 13-13: tmp2/tmp2.c


#include <stdio.h>
#include <string.h>
 
/********************************************************
 * tmp_name -- Returns a temporary filename.            *
 *                                                      *
 * Each time this function is called, a new name will   *
 * be returned.                                         *
 *                                                      *
 * Warning: There should be a warning here, but if we   *
 *      put it in, we would answer the question.        *
 *                                                      *
 * Returns                                              *
 *      Pointer to the new filename.                    *
 ********************************************************/
char *tmp_name(void)
{
    static char name[30];       /* The name we are generating */
    static int sequence = 0;    /* Sequence number for last digit */
 
    ++sequence; /* Move to the next filename */
 
    strcpy(name, "tmp");
 
    /* But in the squence digit */
    name[3] = sequence + '0';
 
    /* End the string */
    name[4] = '\0';
 
    return(name);
}
 
int main()
{
    char *tmp_name(void);       /* get name of temporary file */
    char *name1;                /* name of a temporary file */
    char *name2;                /* name of a temporary file */
 
    name1 = tmp_name();
    name2 = tmp_name();
 
    printf("Name1: %s\n", name1);
    printf("Name2: %s\n", name2);
    return(0);
}

Answer 13-2: The first call to tmp_name returns a pointer to name. There is only one name. The second call to tmp_name changes name and returns a pointer to it. So we have two pointers, and they point to the same thing, name.

Several library functions return pointers to static strings. A second call to one of these routines will overwrite the first value. A solution to this problem is to copy the values as shown below:

char name1[100]; 
char name2[100]; 
strcpy(name1, tmp_name()); 
strcpy(name2, tmp_name()); 

This problem is a good illustration of the basic meaning of a pointer; it doesn't create any new space for data, but just refers to data that is created elsewhere.

This problem is also a good example of a poorly designed function. The problem is that the function is tricky to use. A better design would make the code less risky to use. For example, the function could take an additional parameter: the string in which the filename is to be constructed:

void tmp_name(char *name_to_return);

1. This statement is not strictly true for MS-DOS/Windows compilers. Because of the strange architecture of the 8086, these compilers are forced to use both near pointers (16 bits) and far pointers (32 bits). See your C compiler manual for details.

2. Actually, they can be named anything. However, in 99.9% of programs, they are named argc and argv. When most programmers encounter the other 0.1%, they curse loudly, and then change the names to argc and argv.


oreilly.com Home | O'Reilly Bookstores | How to Order | O'Reilly Contacts
International | About O'Reilly | Affiliated Companies | Privacy Policy

© 2001, O'Reilly & Associates, Inc.