Pointers - Programming in C (Fourth Edition) (2015)

Programming in C (Fourth Edition) (2015)

10. Pointers

In this chapter, you examine one of the most sophisticated features of the C programming language: pointers. In fact, the power and flexibility that C provides in dealing with pointers serve to set it apart from many other programming languages. Pointers enable you to effectively represent complex data structures, to change values passed as arguments to functions, to work with memory that has been allocated “dynamically” (see Chapter 16, “Miscellaneous and Advanced Features”), and to more concisely and efficiently deal with arrays.

As you become a more proficient C programmer, you will find yourself using pointers in all aspects of the development process, so this chapter covers a wide range of ways to implement and use pointers including

Image Defining simple pointers

Image Using pointers in common C expressions

Image Implementing pointers to structures, arrays, and functions

Image Using pointers to create linked lists

Image Applying the const keyword to pointers

Image Passing pointers as arguments to functions

Again, while this is some of the most challenging topics you will cover in your C programming learning process, once you gain a fundamental understanding of these topics, your programs will gain significant elegance and power.

Pointers and Indirection

To understand the way in which pointers operate, it is first necessary to understand the concept of indirection. You are familiar with this concept from your everyday life. For example, suppose you need to buy a new ink cartridge for your printer. In the company that you work for, all purchases are handled by the Purchasing department. So, you call Jim in Purchasing and ask him to order the new cartridge for you. Jim, in turn, calls the local supply store to order the cartridge. This approach to obtain your new cartridge is actually an indirect one because you are not ordering the cartridge directly from the supply store yourself.

This same notion of indirection applies to the way pointers work in C. A pointer provides an indirect means of accessing the value of a particular data item. And just as there are reasons why it makes sense to go through the Purchasing department to order new cartridges (you don’t have to know which particular store the cartridges are being ordered from, for example), so are there good reasons why, at times, it makes sense to use pointers in C.

Defining a Pointer Variable

But enough talk—it’s time to see how pointers actually work. Suppose you define a variable called count as follows:

int count = 10;

You can define another variable, called int_pointer, that can be used to enable you to indirectly access the value of count by the declaration

int *int_pointer;

The asterisk defines to the C system that the variable int_pointer is of type pointer to int. This means that int_pointer is used in the program to indirectly access the value of one or more integer values.

You have seen how the & operator was used in the scanf() calls of previous programs. This unary operator, known as the address operator, is used to make a pointer to an object in C. So, if x is a variable of a particular type, the expression &x is a pointer to that variable. The expression &xcan be assigned to any pointer variable, if desired, that has been declared to be a pointer to the same type as x.

Therefore, with the definitions of count and int_pointer as given, you can write a statement such as

int_pointer = &count;

to set up the indirect reference between int_pointer and count. The address operator has the effect of assigning to the variable int_pointer, not the value of count, but a pointer to the variable count. The link that has been made between int_pointer and count is conceptualized in Figure 10.1. The directed line illustrates the idea that int_pointer does not directly contain the value of count, but a pointer to the variable count.

Image

Figure 10.1 Pointer to an integer.

To reference the contents of count through the pointer variable int_pointer, you use the indirection operator, which is the asterisk *. So, if x is defined as type int, the statement

x = *int_pointer;

assigns the value that is indirectly referenced through int_pointer to the variable x. Because int_pointer was previously set pointing to count, this statement has the effect of assigning the value contained in the variable count—which is 10—to the variable x.

The previous statements have been incorporated into Program 10.1, which illustrates the two fundamental pointer operators: the address operator, &, and the indirection operator, *.

Program 10.1 Illustrating Pointers


#include <stdio.h>

int main (void)
{
int count = 10, x;
int *int_pointer;

int_pointer = &count;
x = *int_pointer;

printf ("count = %i, x = %i\n", count, x);

return 0;
}


Program 10.1 Output


count = 10, x = 10


The variables count and x are declared to be integer variables in the normal fashion. On the next line, the variable int_pointer is declared to be of type “pointer to int.” Note that the two lines of declarations could have been combined into the single line

int count = 10, x, *int_pointer;

Next, the address operator is applied to the variable count. This has the effect of creating a pointer to this variable, which is then assigned by the program to the variable int_pointer.

Execution of the next statement in the program,

x = *int_pointer;

proceeds as follows: The indirection operator tells the C system to treat the variable int_pointer as containing a pointer to another data item. This pointer is then used to access the desired data item, whose type is specified by the declaration of the pointer variable. Because you told the compiler that int_pointer points to integers when you declared the variable, the compiler knows that the value referenced by the expression *int_pointer is an integer. And because you set int_pointer to point to the integer variable count in the previous program statement, it is the value of count that is indirectly accessed by this expression.

You should realize that Program 10.1 is a manufactured example of the use of pointers and does not show a practical use for them in a program. Such motivation is presented shortly, after you have become familiar with the basic ways in which pointers can be defined and manipulated in a program.

Program 10.2 illustrates some interesting properties of pointer variables. Here, a pointer to a character is used.

Program 10.2 More Pointer Basics


#include <stdio.h>

int main (void)
{
char c = 'Q';
char *char_pointer = &c;

printf ("%c %c\n", c, *char_pointer);

c = '/';
printf ("%c %c\n", c, *char_pointer);

*char_pointer = '(';
printf ("%c %c\n", c, *char_pointer);

return 0;
}


Program 10.2 Output


Q Q
/ /
( (


The character variable c is defined and initialized to the character 'Q'. In the next line of the program, the variable char_pointer is defined to be of type “pointer to char,” meaning that whatever value is stored inside this variable should be treated as an indirect reference (pointer) to a character. Notice that you can assign an initial value to this variable in the normal fashion. The value that you assign to char_pointer in the program is a pointer to the variable c, which is obtained by applying the address operator to the variable c. (Note that this initialization generates a compiler error if c had been defined after this statement because a variable must always be declared before its value can be referenced in an expression.)

The declaration of the variable char_pointer and the assignment of its initial value could have been equivalently expressed in two separate statements as

char *char_pointer;
char_pointer = &c;

(and not by the statements

char *char_pointer;
*char_pointer = &c;

as might be implied from the single-line declaration).

Always remember, that the value of a pointer in C is meaningless until it is set pointing to something.

The first printf() call simply displays the contents of the variable c and the contents of the variable that is referenced by char_pointer. Because you set char_pointer to point to the variable c, the value that is displayed is the contents of c, as verified by the first line of the program’s output.

In the next line of the program, the character '/' is assigned to the character variable c. Because char_pointer still points to the variable c, displaying the value of *char_pointer in the subsequent printf() call correctly displays this new value of c at the terminal. This is an important concept. Unless the value of char_pointer is changed, the expression *char_pointer always accesses the value of c. So, as the value of c changes, so does the value of *char_pointer.

The previous discussion can help you to understand how the program statement that appears next in the program works. Unless char_pointer is changed, the expression *char_pointer always references the value of c. Therefore, in the expression

*char_pointer = '(';

you are assigning the left parenthesis character to c. More formally, the character '(' is assigned to the variable that is pointed to by char_pointer. You know that this variable is c because you placed a pointer to c in char_pointer at the beginning of the program.

The preceding concepts are the key to your understanding of the operation of pointers. Please review them at this point if they still seem a bit unclear.

Using Pointers in Expressions

In Program 10.3, two integer pointers, p1 and p2, are defined. Notice how the value referenced by a pointer can be used in an arithmetic expression. If p1 is defined to be of type “pointer to integer,” what conclusion do you think can be made about the use of *p1 in an expression?

Program 10.3 Using Pointers in Expressions


// More on pointers

#include <stdio.h>

int main (void)
{
int i1, i2;
int *p1, *p2;

i1 = 5;
p1 = &i1;
i2 = *p1 / 2 + 10;
p2 = p1;

printf ("i1 = %i, i2 = %i, *p1 = %i, *p2 = %i\n", i1, i2, *p1, *p2);

return 0;
}


Program 10.3 Output


i1 = 5, i2 = 12, *p1 = 5, *p2 = 5


After defining the integer variables i1 and i2 and the integer pointer variables p1 and p2, the program then assigns the value 5 to i1 and stores a pointer to i1 inside p1. Next, the value of i2 is calculated with the following expression:

i2 = *p1 / 2 + 10;

As implied from the discussions of Program 10.2, if a pointer px points to a variable x, and px has been defined to be a pointer to the same data type as is x, then use of *px in an expression is, in all respects, identical to the use of x in the same expression.

Because in Program 10.3 the variable p1 is defined to be an integer pointer, the preceding expression is evaluated using the rules of integer arithmetic. And because the value of *p1 is 5 (p1 points to i1), the final result of the evaluation of the preceding expression is 12, which is the value that is assigned to i2. (The pointer reference operator * has higher precedence than the arithmetic operation of division. In fact, this operator, as well as the address operator &, has higher precedence than all binary operators in C.)

In the next statement, the value of the pointer p1 is assigned to p2. This assignment is perfectly valid and has the effect of setting p2 to point to the same data item to which p1 points. Because p1 points to i1, after the assignment statement has been executed, p2 also points to i1 (and you can have as many pointers to the same item as you want in C).

The printf() call verifies that the values of i1, *p1, and *p2 are all the same (5) and that the value of i2 was set to 12 by the program.

Working with Pointers and Structures

You have seen how a pointer can be defined to point to a basic data type, such as an int or a char. But pointers can also be defined to point to structures. In Chapter 8, “Working with Structures,” you defined your date structure as follows:

struct date
{
int month;
int day;
int year;
};

Just as you defined variables to be of type struct date, as in

struct date todaysDate;

so can you define a variable to be a pointer to a struct date variable:

struct date *datePtr;

The variable datePtr, as just defined, then can be used in the expected fashion. For example, you can set it to point to todaysDate with the assignment statement

datePtr = &todaysDate;

After such an assignment has been made, you then can indirectly access any of the members of the date structure pointed to by datePtr in the following way:

(*datePtr).day = 21;

This statement has the effect of setting the day of the date structure pointed to by datePtr to 21. The parentheses are required because the structure member operator . has higher precedence than the indirection operator *.

To test the value of month stored in the date structure pointed to by datePtr, a statement such as

if ( (*datePtr).month == 12 )
...

can be used.

Pointers to structures are so often used in C that a special operator exists in the language. The structure pointer operator ->, which is the dash followed by the greater than sign, permits expressions that would otherwise be written as

(*x).y

to be more clearly expressed as

x->y

So, the previous if statement can be conveniently written as

if ( datePtr->month == 12 )
...

Program 8.1, the first program that illustrated structures, was rewritten using the concept of structure pointers, as shown in Program 10.4.

Program 10.4 Using Pointers to Structures


// Program to illustrate structure pointers

#include <stdio.h>

int main (void)
{
struct date
{
int month;
int day;
int year;
};

struct date today, *datePtr;

datePtr = &today;

datePtr->month = 9;
datePtr->day = 25;
datePtr->year = 2015;

printf ("Today's date is %i/%i/%.2i.\n",
datePtr->month, datePtr->day, datePtr->year % 100);

return 0;
}


Program 10.4 Output


Today's date is 9/25/15.


Figure 10.2 depicts how the variables today and datePtr would look after all of the assignment statements from the preceding program have been executed.

Image

Figure 10.2 Pointer to a structure.

Once again, it should be pointed out that there is no real motivation shown here as to why you should even bother using a structure pointer when it seems as though you can get along just fine without it (as you did in Program 8.1). You will discover the motivation shortly.

Structures Containing Pointers

Naturally, a pointer also can be a member of a structure. In the structure definition

struct intPtrs
{
int *p1;
int *p2;
};

a structure called intPtrs is defined to contain two integer pointers, the first one called p1 and the second one p2. You can define a variable of type struct intPtrs in the usual way:

struct intPtrs pointers;

The variable pointers can now be used in the normal fashion, remembering that pointers itself is not a pointer, but a structure variable that has two pointers as its members.

Program 10.5 shows how the intPtrs structure can be handled in a C program.

Program 10.5 Using Structures Containing Pointers


// Function to use structures containing pointers

#include <stdio.h>

int main (void)
{
struct intPtrs
{
int *p1;
int *p2;
};

struct intPtrs pointers;
int i1 = 100, i2;

pointers.p1 = &i1;
pointers.p2 = &i2;
*pointers.p2 = -97;

printf ("i1 = %i, *pointers.p1 = %i\n", i1, *pointers.p1);
printf ("i2 = %i, *pointers.p2 = %i\n", i2, *pointers.p2);
return 0;
}


Program 10.5 Output


i1 = 100, *pointers.p1 = 100
i2 = -97, *pointers.p2 = -97


After the variables have been defined, the assignment statement

pointers.p1 = &i1;

sets the p1 member of pointers pointing to the integer variable i1, whereas the next statement

pointers.p2 = &i2;

sets the p2 member pointing to i2. Next, −97 is assigned to the variable that is pointed to by pointers.p2. Because you just set this to point to i2, −97 is stored in i2. No parentheses are needed in this assignment statement because, as mentioned previously, the structure member operator . has higher precedence than the indirection operator. Therefore, the pointer is correctly referenced from the structure before the indirection operator is applied. Of course, parentheses could have been used just to play it safe, as at times it can be difficult to try to remember which of two operators has higher precedence.

The two printf() calls that follow each other in the preceding program verify that the correct assignments were made.

Figure 10.3 has been provided to help you understand the relationship between the variables i1, i2, and pointers after the assignment statements from Program 10.5 have been executed. As you can see in Figure 10.3, the p1 member points to the variable i1, which contains the value100, whereas the p2 member points to the variable i2, which contains the value −97.

Image

Figure 10.3 Structure containing pointers.

Linked Lists

The concepts of pointers to structures and structures containing pointers are very powerful ones in C, for they enable you to create sophisticated data structures, such as linked lists, doubly linked lists, and trees.

Suppose you define a structure as follows:

struct entry
{
int value;
struct entry *next;
};

This defines a structure called entry, which contains two members. The first member of the structure is a simple integer called value. The second member of the structure is a member called next, which is a pointer to an entry structure. Think about this for a moment. Contained inside an entry structure is a pointer to another entry structure. This is a perfectly valid concept in the C language. Now suppose you define two variables to be of type struct entry as follows:

struct entry n1, n2;

You set the next pointer of structure n1 to point to structure n2 by executing the following statement:

n1.next = &n2;

This statement effectively makes a link between n1 and n2, as depicted in Figure 10.4.

Image

Figure 10.4 Linked structures.

Assuming a variable n3 were also defined to be of type struct entry, you could add another link with the following statement:

n2.next = &n3;

This resulting chain of linked entries, known more formally as a linked list, is illustrated in Figure 10.5. Program 10.6 illustrates this linked list.

Image

Figure 10.5 A linked list.

Program 10.6 Using Linked Lists


// Function to use linked lists

#include <stdio.h>

int main (void)
{
struct entry
{
int value;
struct entry *next;
};

struct entry n1, n2, n3;
int i;

n1.value = 100;
n2.value = 200;
n3.value = 300;

n1.next = &n2;
n2.next = &n3;

i = n1.next->value;
printf ("%i ", i);

printf ("%i\n", n2.next->value);

return 0;
}


Program 10.6 Output


200 300


The structures n1, n2, and n3 are defined to be of type struct entry, which consists of an integer member called value and a pointer to an entry structure called next. The program then assigns the values 100, 200, and 300 to the value members of n1, n2, and n3, respectively.

The next two statements in the program

n1.next = &n2;
n2.next = &n3;

set up the linked list, with the next member of n1 pointing to n2 and the next member of n2 pointing to n3.

Execution of the statement

i = n1.next->value;

proceeds as follows: The value member of the entry structure pointed to by n1.next is accessed and assigned to the integer variable i. Because you set n1.next to point to n2, the value member of n2 is accessed by this statement. Therefore, this statement has the net result of assigning 200 to i, as verified by the printf call that follows in the program. You might want to verify that the expression n1.next->value is the correct one to use and not n1.next.value, because the n1.next field contains a pointer to a structure, and not the structure itself. This distinction is important and can quickly lead to programming errors if it is not fully understood.

The structure member operator . and the structure pointer operator -> have the same precedence in the C language. In expressions such as the preceding one, where both operators are used, the operators are evaluated from left to right. Therefore, the expression is evaluated as

i = (n1.next)->value;

which is what was intended.

The second printf() call in Program 10.6 displays the value member that is pointed to by n2.next. Because you set n2.next to point to n3, the contents of n3.value are displayed by the program.

As mentioned, the concept of a linked list is a very powerful one in programming. Linked lists greatly simplify operations such as the insertion and removal of elements from large lists of sorted items.

For example, if n1, n2, and n3 are as defined previously, you can easily remove n2 from the list simply by setting the next field of n1 to point to whatever n2 is pointing to:

n1.next = n2.next;

This statement has the effect of copying the pointer contained in n2.next into n1.next, and, because n2.next was set to point to n3, n1.next is now pointing to n3. Furthermore, because n1 no longer points to n2, you have effectively removed it from your list. Figure 10.6 depicts this situation after the preceding statement is executed. Of course, you could have set n1 pointing to n3 directly with the statement

n1.next = &n3;

Image

Figure 10.6 Removing an entry from a linked list.

but this latter statement is not as general because you must know in advance that n2 is pointing to n3.

Inserting an element into a list is just as straightforward. If you want to insert a struct entry called n2_3 after n2 in the list, you can simply set n2_3.next to point to whatever n2.next was pointing to, and then set n2.next to point to n2_3. So, the sequence of statements

n2_3.next = n2.next;
n2.next = &n2_3;

inserts n2_3 into the list, immediately after entry n2. Note that the sequence of the preceding statements is important because executing the second statement first overwrites the pointer stored in n2.next before it has a chance to be assigned to n2_3.next. The inserted element n2_3 is depicted in Figure 10.7. Notice that n2_3 is not shown between n1 and n3. This is to emphasize that n2_3 can be anywhere in memory and does not have to physically occur after n1 and before n3. This is one of the main motivations for the use of a linked list approach for storing information: Entries of the list do not have to be stored sequentially in memory, as is the case with elements in an array.

Image

Figure 10.7 Inserting an element into a linked list.

Before developing some functions to work with linked lists, two more issues must be discussed. Usually associated with a linked list is at least one pointer to the list. Often, a pointer to the start of the list is kept. So, for your original three-element list, which consisted of n1, n2, and n3, you can define a variable called list_pointer and set it to point to the beginning of the list with the statement

struct entry *list_pointer = &n1;

assuming that n1 has been previously defined. A pointer to a list is useful for sequencing through the entries in the list, as you see shortly.

The second issue to be discussed involves the idea of having some way to identify the end of the list. This is needed so that a procedure that searches through the list, for example, can tell when it has reached the final element in the list. By convention, a constant value of 0 is used for such a purpose and is known as the null pointer. You can use the null pointer to mark the end of a list by storing this value in the pointer field of the last entry of the list.1

1. A null pointer is not necessarily internally represented as the value 0. However, the compiler must recognize assignment of the constant 0 to a pointer as assigning the null pointer. This also applies to comparing a pointer against the constant 0: The compiler interprets it as a test to see if the pointer is null.

In your three-entry list, you can mark its end by storing the null pointer in n3.next:

n3.next = (struct entry *) 0;

You see in Chapter 12, “The Preprocessor,” how this assignment statement can be made a bit more readable.

The type cast operator is used to cast the constant 0 to the appropriate type (“pointer to struct entry”). It’s not required, but makes the statement more readable.

Figure 10.8 depicts the linked list from Program 10.6, with a struct entry pointer called list_pointer pointing to the start of the list and the n3.next field set to the null pointer.

Image

Figure 10.8 Linked list showing list pointer and terminating null.

Program 10.7 incorporates the concepts just described. The program uses a while loop to sequence through the list and display the value member of each entry in the list.

Program 10.7 Traversing a Linked List


// Program to traverse a linked list

#include <stdio.h>

int main (void)
{
struct entry
{
int value;
struct entry *next;
};

struct entry n1, n2, n3;
struct entry *list_pointer = &n1;

n1.value = 100;
n1.next = &n2;

n2.value = 200;
n2.next = &n3;

n3.value = 300;
n3.next = (struct entry *) 0; // Mark list end with null pointer

while ( list_pointer != (struct entry *) 0 ) {
printf ("%i\n", list_pointer->value);
list_pointer = list_pointer->next;
}

return 0;
}


Program 10.7 Output


100
200
300


The program defines the variables n1, n2, and n3 and the pointer variable list_pointer, which is initially set to point to n1, the first entry in the list. The next program statements link together the three elements of the list, with the next member of n3 set to the null pointer to mark the end of the list.

A while loop is then set up to sequence through each element of the list. This loop is executed as long as the value of list_pointer is not equal to the null pointer. The printf() call inside the while loop displays the value member of the entry currently pointed to bylist_pointer.

The statement that follows the printf() call,

list_pointer = list_pointer->next;

has the effect of taking the pointer from the next member of the structure pointed to by list_pointer and assigning it to list_pointer. So, the first time through the loop, this statement takes the pointer contained in n1.next (remember, list_pointer was initially set pointing to n1) and assigns it to list_pointer. Because this value is not null—it’s a pointer to the entry structure n2—the while loop is repeated.

The second time through, the while loop results in the display of n2.value, which is 200. The next member of n2 is then copied into list_pointer, and because you set this value to point to n3, list_pointer points to n3 by the end of the second pass through the loop.

When the while loop is executed the third time, the printf call displays the value of 300 as contained in n3.value. At that point, list_pointer->next (which is actually n3.next) is copied into list_pointer, and, because you set this member to the null pointer, the whileloop terminates after it has been executed three times.

Trace through the operation of the while loop just discussed, using a pencil and paper, if necessary, to keep track of the values of the various variables. Understanding the operation of this loop is the key to your understanding the operation of pointers in C. Incidentally, it should be noted that this same while loop can be used to sequence through the elements of a list of any size, provided the end of the list is marked by the null pointer.

When working with actual linked lists in a program, you will not normally link together list entries that have been explicitly defined like in the program examples in this section. You did that here just to illustrate the mechanics of working with a linked list. In actual practice, you will typically ask the system to give you memory for each new list entry and you will link it into the list while the program is executing. This is done with a mechanism known as dynamic memory allocation, and is covered in Chapter 16.

The Keyword const and Pointers

You have seen how a variable or an array can be declared as const to alert the compiler as well as the reader that the contents of a variable or an array will not be changed by the program. With pointers, there are two things to consider: whether the pointer will be changed, and whether the value that the pointer points to will be changed. Think about that for a second. Assume the following declarations:

char c = 'X';
char *charPtr = &c;

The pointer variable charPtr is set pointing to the variable c. If the pointer variable is always set pointing to c, it can be declared as a const pointer as follows:

char * const charPtr = &c;

(Read this as “charPtr is a constant pointer to a character.”) So, a statement like this:

charPtr = &d; // not valid

causes the GNU C compiler to give a message like this:2

2. Your compiler may give a different warning message, or no message at all.

foo.c:10: warning: assignment of read-only variable 'charPtr'

Now if, instead, the location pointed to by charPtr will not change through the pointer variable charPtr, that can be noted with a declaration as follows:

const char *charPtr = &c;

(Read this as “charPtr points to a constant character.”) Now of course, that doesn’t mean that the value cannot be changed by the variable c, which is what charPtr is set pointing to. It means, however, that it won’t be changed with a subsequent statement like this:

*charPtr = 'Y'; // not valid

which causes the GNU C compiler to issue a message like this:

foo.c:11: warning: assignment of read-only location

In the case in which both the pointer variable and the location it points to will not be changed through the pointer, the following declaration can be used:

const char * const *charPtr = &c;

The first use of const says the contents of the location the pointer references will not be changed. The second use says that the pointer itself will not be changed. Admittedly, this looks a little confusing, but it’s worth noting at this point in the text.3

3. The keyword const is not used in every program example where it can be employed; only in selected examples. Until you are familiar with reading expressions such as previously shown, it can make understanding the examples more difficult.

Pointers and Functions

Pointers and functions get along quite well together. That is, you can pass a pointer as an argument to a function in the normal fashion, and you can also have a function return a pointer as its result.

The first case cited previously, passing pointer arguments, is straightforward enough: The pointer is simply included in the list of arguments to the function in the normal fashion. So, to pass the pointer list_pointer from the previous program to a function called print_list(), you can write

print_list (list_pointer);

Inside the print_list() routine, the formal parameter must be declared to be a pointer to the appropriate type:

void print_list (struct entry *pointer)
{
...
}

The formal parameter pointer can then be used in the same way as a normal pointer variable. One thing worth remembering when dealing with pointers that are sent to functions as arguments: The value of the pointer is copied into the formal parameter when the function is called. Therefore, any change made to the formal parameter by the function does not affect the pointer that was passed to the function. But here’s the catch: Although the pointer cannot be changed by the function, the data elements that the pointer references can be changed! Program 10.8 helps clarify this point.

Program 10.8 Using Pointers and Functions


// Program to illustrate using pointers and functions

#include <stdio.h>

void test (int *int_pointer)
{
*int_pointer = 100;
}

int main (void)
{
void test (int *int_pointer);
int i = 50, *p = &i;

printf ("Before the call to test i = %i\n", i);

test (p);
printf ("After the call to test i = %i\n", i);

return 0;
}


Program 10.8 Output


Before the call to test i = 50
After the call to test i = 100


The function test() is defined to take as its argument a pointer to an integer. Inside the function, a single statement is executed to set the integer pointed to by int_pointer to the value 100.

The main() routine defines an integer variable i with an initial value of 50 and a pointer to an integer called p that is set to point to i. The program then displays the value of i and calls the test() function, passing the pointer p as the argument. As you can see from the second line of the program’s output, the test() function did, in fact, change the value of i to 100.

Now consider Program 10.9.

Program 10.9 Using Pointers to Exchange Values


// More on pointers and functions

#include <stdio.h>

void exchange (int * const pint1, int * const pint2)
{
int temp;

temp = *pint1;
*pint1 = *pint2;
*pint2 = temp;
}

int main (void)
{
void exchange (int * const pint1, int * const pint2);
int i1 = -5, i2 = 66, *p1 = &i1, *p2 = &i2;

printf ("i1 = %i, i2 = %i\n", i1, i2);

exchange (p1, p2);
printf ("i1 = %i, i2 = %i\n", i1, i2);

exchange (&i1, &i2);
printf ("i1 = %i, i2 = %i\n", i1, i2);

return 0;
}


Program 10.9 Output


i1 = -5, i2 = 66
i1 = 66, i2 = -5
i1 = -5, i2 = 66


The purpose of the exchange() function is to interchange the two integer values pointed to by its two arguments. The function header

void exchange (int * const pint1, int * const pint2)

says that the exchange() function takes two integer pointers as arguments, and that the pointers will not be changed by the function (the use of the keyword const).

The local integer variable temp is used to hold one of the integer values while the exchange is made. Its value is set equal to the integer that is pointed to by pint1. The integer pointed to by pint2 is then copied into the integer pointed to by pint1, and the value of temp is then stored in the integer pointed to by pint2, thus making the exchange complete.

The main() routine defines integers i1 and i2 with values of −5 and 66, respectively. Two integer pointers, p1 and p2, are then defined and are set to point to i1 and i2, respectively. The program then displays the values of i1 and i2 and calls the exchange() function, passing the two pointers, p1 and p2, as arguments. The exchange() function exchanges the value contained in the integer pointed to by p1 with the value contained in the integer pointed to by p2. Because p1 points to i1, and p2 to i2, the values of i1 and i2 end up getting exchanged by the function. The output from the second printf() call verifies that the exchange worked properly.

The second call to exchange() is a bit more interesting. This time, the arguments that are passed to the function are pointers to i1 and i2 that are manufactured right on the spot by applying the address operator to these two variables. Because the expression &i1 produces a pointer to the integer variable i1, this is right in line with the type of argument that your function expects for the first argument (a pointer to an integer). The same applies for the second argument as well. And as can be seen from the program’s output, the exchange() function did its job and switched the values of i1 and i2 back to their original values.

You should realize that without the use of pointers, you could not have written your exchange() function to exchange the value of two integers because you are limited to returning only a single value from a function and because a function cannot permanently change the value of its arguments. Study Program 10.9 in detail. It illustrates with a small example the key concepts to be understood when dealing with pointers in C.

Program 10.10 shows how a function can return a pointer. The program defines a function called findEntry() whose purpose is to search through a linked list to find a specified value. When the specified value is found, the program returns a pointer to the entry in the list. If the desired value is not found, the program returns the null pointer.

Program 10.10 Returning a Pointer from a Function


#include <stdio.h>

struct entry
{
int value;
struct entry *next;
};

struct entry *findEntry (struct entry *listPtr, int match)
{
while ( listPtr != (struct entry *) 0 )
if ( listPtr->value == match )
return (listPtr);
else
listPtr = listPtr->next;

return (struct entry *) 0;
}

int main (void)
{
struct entry *findEntry (struct entry *listPtr, int match);
struct entry n1, n2, n3;
struct entry *listPtr, *listStart = &n1;

int search;

n1.value = 100;
n1.next = &n2;

n2.value = 200;
n2.next = &n3;

n3.value = 300;
n3.next = 0;

printf ("Enter value to locate: ");
scanf ("%i", &search);

listPtr = findEntry (listStart, search);

if ( listPtr != (struct entry *) 0 )
printf ("Found %i.\n", listPtr->value);
else
printf ("Not found.\n");

return 0;
}


Program 10.10 Output


Enter value to locate: 200
Found 200.


Program 10.10 Output (Rerun)


Enter value to locate: 400
Not found.


Program 10.10 Output (Second Rerun)


Enter value to locate: 300
Found 300.


The function header

struct entry *findEntry (struct entry *listPtr, int match)

specifies that the function findEntry() returns a pointer to an entry structure and that it takes such a pointer as its first argument and an integer as its second. The function begins by entering a while loop to sequence through the elements of the list. This loop is executed until eithermatch is found equal to one of the value entries in the list (in which case the value of listPtr is immediately returned) or until the null pointer is reached (in which case the while loop is exited and a null pointer is returned).

After setting up the list as in previous programs, the main() routine asks the user for a value to locate in the list and then calls the findEntry() function with a pointer to the start of the list (listStart) and the value entered by the user (search) as arguments. The pointer that is returned by findEntry() is assigned to the struct entry pointer variable listPtr. If listPtr is not null, the value member pointed to by listPtr is displayed. This should be the same as the value entered by the user. If listPtr is null, then a “Not found.” message is displayed.

The program’s output verifies that the values 200 and 300 were correctly located in the list, and the value 400 was not found because it did not, in fact, exist in the list.

The pointer that is returned by the findEntry() function in the program does not seem to serve any useful purpose. However, in more practical situations, this pointer might be used to access other elements contained in the particular entry of the list. For example, you could have a linked list of your dictionary entries from Chapter 9, “Character Strings.” Then, you could call the findEntry() function (or rename it lookup() as it was called in that chapter) to search the linked list of dictionary entries for the given word. The pointer returned by the lookup() function could then be used to access the definition member of the entry.

Organizing the dictionary as a linked list has several advantages. Inserting a new word into the dictionary is easy: After determining where in the list the new entry is to be inserted, it can be done by simply adjusting some pointers, as illustrated earlier in this chapter. Removing an entry from the dictionary is also simple. Finally, as you learn in Chapter 16, this approach also provides the framework that enables you to dynamically expand the size of the dictionary.

However, the linked list approach for the organization of the dictionary does suffer from one major drawback: You cannot apply your fast binary search algorithm to such a list. This algorithm only works with an array of elements that can be directly indexed. Unfortunately, there is no faster way to search your linked list other than by a straight, sequential search because each entry in the list can only be accessed from the previous one.

One way to glean the benefits of easy insertion and removal of elements, as well as fast search time, is by using a different type of data structure known as a tree. Other approaches, such as using hash tables, are also feasible. The reader is respectfully referred elsewhere—such as to The Art of Computer Programming, Volume 3, Sorting and Searching (Donald E. Knuth, Addison-Wesley)—for discussion of these types of data structures, which can be easily implemented in C with the techniques already described.

Pointers and Arrays

One of the most common uses of pointers in C is as pointers to arrays. The main reasons for using pointers to arrays are ones of notational convenience and of program efficiency. Pointers to arrays generally result in code that uses less memory and executes faster. The reason for this will become apparent through our discussions in this section.

If you have an array of 100 integers called values, you can define a pointer called valuesPtr, which can be used to access the integers contained in this array with the statement

int *valuesPtr;

When you define a pointer that is used to point to the elements of an array, you don’t designate the pointer as type “pointer to array”; rather, you designate the pointer as pointing to the type of element that is contained in the array.

If you have an array of characters called text, you could similarly define a pointer to be used to point to elements in text with the statement

char *textPtr;

To set valuesPtr to point to the first element in the values array, you simply write

valuesPtr = values;

The address operator is not used in this case because the C compiler treats the appearance of an array name without a subscript as a pointer to the array. Therefore, simply specifying values without a subscript has the effect of producing a pointer to the first element of values (see Figure 10.9).

Image

Figure 10.9 Pointer to an array element.

An equivalent way of producing a pointer to the start of values is to apply the address operator to the first element of the array. Thus, the statement

valuesPtr = &values[0];

can be used to serve the same purpose as placing a pointer to the first element of values in the pointer variable valuesPtr.

To set textPtr to point to the first character inside the text array, either the statement

textPtr = text;

or

textPtr = &text[0];

can be used. Whichever statement you choose to use is strictly a matter of taste.

The real power of using pointers to arrays comes into play when you want to sequence through the elements of an array. If valuesPtr is as previously defined and is set pointing to the first element of values, the expression

*valuesPtr

can be used to access the first integer of the values array, that is, values[0]. To reference values[3] through the valuesPtr variable, you can add 3 to valuesPtr and then apply the indirection operator:

*(valuesPtr + 3)

In general, the expression

*(valuesPtr + i)

can be used to access the value contained in values[i].

So, to set values[10] to 27, you could obviously write the expression

values[10] = 27;

or, using valuesPtr, you could write

*(valuesPtr + 10) = 27;

To set valuesPtr to point to the second element of the values array, you can apply the address operator to values[1] and assign the result to valuesPtr:

valuesPtr = &values[1];

If valuesPtr points to values[0], you can set it to point to values[1] by simply adding 1 to the value of valuesPtr:

valuesPtr += 1;

This is a perfectly valid expression in C and can be used for pointers to any data type.

So, in general, if a is an array of elements of type x, px is of type “pointer to x,” and i and n are integer constants or variables, the statement

px = a;

sets px to point to the first element of a, and the expression

*(px + i)

subsequently references the value contained in a[i]. Furthermore, the statement

px += n;

sets px to point n elements farther in the array, no matter what type of element is contained in the array.

The increment and decrement operators ++ and -- are particularly handy when dealing with pointers. Applying the increment operator to a pointer has the same effect as adding one to the pointer, while applying the decrement operator has the same effect as subtracting one from the pointer. So, if textPtr is defined as a char pointer and is set pointing to the beginning of an array of chars called text, the statement

++textPtr;

sets textPtr pointing to the next character in text, which is text[1]. In a similar fashion, the statement

--textPtr;

sets textPtr pointing to the previous character in text, assuming, of course, that textPtr was not pointing to the beginning of text prior to the execution of this statement.

It is perfectly valid to compare two pointer variables in C. This is particularly useful when comparing two pointers in the same array. For example, you can test the pointer valuesPtr to see if it points past the end of an array containing 100 elements by comparing it to a pointer to the last element in the array. So, the expression

valuesPtr > &values[99]

is TRUE (nonzero) if valuesPtr is pointing past the last element in the values array, and is FALSE (zero) otherwise. Recall from previous discussions that you can replace the preceding expression with its equivalent

valuesPtr > values + 99

because values used without a subscript is a pointer to the beginning of the values array. (Remember, it’s the same as writing &values[0].)

Program 10.11 illustrates pointers to arrays. The arraySum function calculates the sum of the elements contained in an array of integers.

Program 10.11 Working with Pointers to Arrays


// Function to sum the elements of an integer array

#include <stdio.h>

int arraySum (int array[], const int n)
{
int sum = 0, *ptr;
int * const arrayEnd = array + n;

for ( ptr = array; ptr < arrayEnd; ++ptr )
sum += *ptr;

return sum;
}

int main (void)
{
int arraySum (int array[], const int n);
int values[10] = { 3, 7, -9, 3, 6, -1, 7, 9, 1, -5 };

printf ("The sum is %i\n", arraySum (values, 10));

return 0;
}


Program 10.11 Output


The sum is 21


Inside the arraySum() function, the constant integer pointer arrayEnd is defined and set pointing immediately after the last element of array. A for loop is then set up to sequence through the elements of array. The value of ptr is set to point to the beginning of array when the loop is entered. Each time through the loop, the element of array pointed to by ptr is added into sum. The value of ptr is then incremented by the for loop to set it pointing to the next element in array. When ptr points past the end of array, the for loop is exited, and the value ofsum is returned to the calling routine.

A Slight Digression About Program Optimization

It is pointed out that the local variable arrayEnd was not actually needed by the function because you could have explicitly compared the value of ptr to the end of the array inside the for loop:

for ( ...; pointer <= array + n; ... )

The sole motivation for using arrayEnd was one of optimization. Each time through the for loop, the looping conditions are evaluated. Because the expression array + n is never changed from within the loop, its value is constant throughout the execution of the for loop. By evaluating it once before the loop is entered, you save the time that would otherwise be spent reevaluating this expression each time through the loop. Although there is virtually no savings in time for a 10-element array, especially if the arraySum() function is called only once by the program, there could be a more substantial savings if this function were heavily used by a program for summing large-sized arrays, for example.

The other issue to be discussed about program optimization concerns the very use of pointers themselves in a program. In the arraySum() function discussed earlier, the expression *ptr is used inside the for loop to access the elements in the array. Formerly, you would have written yourarraySum() function with a for loop that used an index variable, such as i, and then would have added the value of array[i] into sum inside the loop. In general, the process of indexing an array takes more time to execute than does the process of accessing the contents of a pointer. In fact, this is one of the main reasons why pointers are used to access the elements of an array—the code that is generated is generally more efficient. Of course, if access to the array is not generally sequential, pointers accomplish nothing, as far as this issue is concerned, because the expression*(pointer + j) takes just as long to execute as does the expression array[j].

Is It an Array or Is It a Pointer?

Recall that to pass an array to a function, you simply specify the name of the array, as you did previously with the call to the arraySum() function. You should also remember that to produce a pointer to an array, you need only specify the name of the array. This implies that in the call to thearraySum() function, what was passed to the function was actually a pointer to the array values. This is precisely the case and explains why you are able to change the elements of an array from within a function.

But if it is indeed the case that a pointer to the array is passed to the function, then you might wonder why the formal parameter inside the function isn’t declared to be a pointer. In other words, in the declaration of array in the arraySum function, why isn’t the declaration

int *array;

used? Shouldn’t all references to an array from within a function be made using pointer variables?

To answer these questions, recall the previous discussion about pointers and arrays. As mentioned, if valuesPtr points to the same type of element as contained in an array called values, the expression *(valuesPtr + i) is in all ways equivalent to the expression values[i], assuming that valuesPtr has been set to point to the beginning of values. What follows from this is that you also can use the expression *(values + i) to reference the ith element of the array values, and, in general, if x is an array of any type, the expression x[i] can always be equivalently expressed in C as *(x + i).

As you can see, pointers and arrays are intimately related in C, and this is why you can declare array to be of type “array of ints” inside the arraySum function or to be of type “pointer to int.” Either declaration works just fine in the preceding program—try it and see.

If you are going to be using index numbers to reference the elements of an array that is passed to a function, declare the corresponding formal parameter to be an array. This more correctly reflects the use of the array by the function. Similarly, if you are using the argument as a pointer to the array, declare it to be of type pointer.

Realizing now that you could have declared array to be an int pointer in the preceding program example, and then could have subsequently used it as such, you can eliminate the variable ptr from the function and use array instead, as shown in Program 10.12.

Program 10.12 Summing the Elements of an Array


// Function to sum the elements of an integer array Ver. 2

#include <stdio.h>

int arraySum (int *array, const int n)
{
int sum = 0;
int * const arrayEnd = array + n;

for ( ; array < arrayEnd; ++array )
sum += *array;

return sum;
}



int main (void)
{
int arraySum (int *array, const int n);
int values[10] = { 3, 7, -9, 3, 6, -1, 7, 9, 1, -5 };

printf ("The sum is %i\n", arraySum (values, 10));

return 0;
}


Program 10.12 Output


The sum is 21


The program is fairly self-explanatory. The first expression inside the for loop was omitted because no value had to be initialized before the loop was started. One point worth repeating is that when the arraySum() function is called, a pointer to the values array is passed, where it is called array inside the function. Changes to the value of array (as opposed to the values referenced by array) do not in any way affect the contents of the values array. So, the increment operator that is applied to array is just incrementing a pointer to the array values, and not affecting its contents. (Of course, you know that you can change values in the array if you want to, simply by assigning values to the elements referenced by the pointer.)

Pointers to Character Strings

One of the most common applications of using a pointer to an array is as a pointer to a character string. The reasons are ones of notational convenience and efficiency. To show how easily pointers to character strings can be used, write a function called copyString() to copy one string into another. If you write this function using normal array indexing methods, the function might be coded as follows:

void copyString (char to[], char from[])
{
int i;

for ( i = 0; from[i] != '\0'; ++i )
to[i] = from[i];

to[i] = '\0';
}

The for loop is exited before the null character is copied into the to array, thus explaining the need for the last statement in the function.

If you write copyString() using pointers, you no longer need the index variable i. A pointer version is shown in Program 10.13.

Program 10.13 Pointer Version of copyString()


#include <stdio.h>

void copyString (char *to, char *from)
{
for ( ; *from != '\0'; ++from, ++to )
*to = *from;

*to = '\0';
}

int main (void)
{
void copyString (char *to, char *from);
char string1[] = "A string to be copied.";
char string2[50];

copyString (string2, string1);
printf ("%s\n", string2);

copyString (string2, "So is this.");
printf ("%s\n", string2);

return 0;
}


Program 10.13 Output


A string to be copied.
So is this.


The copyString() function defines the two formal parameters to and from as character pointers and not as character arrays as was done in the previous version of copyString(). This reflects how these two variables are used by the function.

A for loop is then entered (with no initial conditions) to copy the string pointed to by from into the string pointed to by to. Each time through the loop, the from and to pointers are each incremented by one. This sets the from pointer pointing to the next character that is to be copied from the source string and sets the to pointer pointing to the location in the destination string where the next character is to be stored.

When the from pointer points to the null character, the for loop is exited. The function then places the null character at the end of the destination string.

In the main() routine, the copyString() function is called twice, the first time to copy the contents of string1 into string2, and the second time to copy the contents of the constant character string "So is this." into string2.

Constant Character Strings and Pointers

The fact that the call

copyString (string2, "So is this.");

works in the previous program implies that when a constant character string is passed as an argument to a function, what is actually passed is a pointer to that character string. Not only is this true in this case, but it also can be generalized by saying that whenever a constant character string is used in C, it is a pointer to that character string that is produced. So, if textPtr is declared to be a character pointer, as in

char *textPtr;

then the statement

textPtr = "A character string.";

assigns to textPtr a pointer to the constant character string "A character string." Be careful to make the distinction here between character pointers and character arrays, as the type of assignment just shown is not valid with a character array. So, for example, if text is defined instead to be an array of chars, with a statement such as

char text[80];

then you could not write a statement such as

text = "This is not valid.";

The only time that C lets you get away with performing this type of assignment to a character array is when initializing it, as in

char text[80] = "This is okay.";

Initializing the text array in this manner does not have the effect of storing a pointer to the character string "This is okay." inside text, but rather the actual characters themselves inside corresponding elements of the text array.

If text is a character pointer, initializing text with the statement

char *text = "This is okay.";

assigns to it a pointer to the character string "This is okay."

As another example of the distinction between character strings and character string pointers, the following sets up an array called days, which contains pointers to the names of the days of the week.

char *days[] =
{ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };

The array days is defined to contain seven entries, each a pointer to a character string. So days[0] contains a pointer to the character string "Sunday", days[1] contains a pointer to the string "Monday", and so on (see Figure 10.10). You could display the name of the third weekday, for example, with the following statement:

printf ("%s\n", days[3]);

Image

Figure 10.10 Array of pointers.

The Increment and Decrement Operators Revisited

Up to this point, whenever you used the increment or decrement operator, it was the only operator that appeared in the expression. When you write the expression ++x, you know that this has the effect of adding 1 to the value of the variable x. And as you have just seen, if x is a pointer to an array, this has the effect of setting x to point to the next element of the array.

The increment and decrement operators can be used in expressions in which other operators also appear. In such cases, it becomes important to know more precisely how these operators work.

So far, when you used the increment and decrement operators, you always placed them before the variables that were being incremented or decremented. So, to increment a variable i, you simply wrote

++i;

Actually, it also is perfectly valid to place the increment operator after the variable, as follows:

i++;

Both expressions are perfectly valid and both achieve the same result—namely, of incrementing the value of i. In the first case, where the ++ is placed before its operand, the increment operation is more precisely identified as a preincrement. In the second case, where the ++ is placed after its operand, the operation is identified as a postincrement.

The same discussion applies to the decrement operator. So the statement

--i;

technically performs a predecrement of i, whereas the statement

i--;

performs a postdecrement of i. Both have the same net result of subtracting 1 from the value of i.

It is when the increment and decrement operators are used in more complex expressions that the distinction between the pre- and post- nature of these operators is realized.

Suppose you have two integers called i and j. If you set the value of i to 0 and then write the statement

j = ++i;

the value that gets assigned to j is 1, and not 0 as you might expect. In the case of the preincrement operator, the variable is incremented before its value is used in the expression. So, in the preceding expression, the value of i is first incremented from 0 to 1 and then its value is assigned toj, as if the following two statements had been written instead:

++i;
j = i;

If you instead use the postincrement operator in the statement

j = i++;

then i is incremented after its value has been assigned to j. So, if i is 0 before the preceding statement is executed, 0 is assigned to j and then i is incremented by 1, as if the statements

j = i;
++i;

were used instead. As another example, if i is equal to 1, then the statement

x = a[--i];

has the effect of assigning the value of a[0] to x because the variable i is decremented before its value is used to index into a. The statement

x = a[i--];

used instead has the effect of assigning the value of a[1] to x because i is decremented after its value has been used to index into a.

As a third example of the distinction between the pre- and post- increment and decrement operators, the function call

printf ("%i\n", ++i);

increments i and then sends its value to the printf() function, whereas the call

printf ("%i\n", i++);

increments i after its value has been sent to the function. So, if i is equal to 100, the first printf() call displays 101, whereas the second printf() call displays 100. In either case, the value of i is equal to 101 after the statement has executed.

As a final example on this topic before presenting Program 10.14, if textPtr is a character pointer, the expression

*(++textPtr)

first increments textPtr and then fetches the character it points to, whereas the expression

*(textPtr++)

fetches the character pointed to by textPtr before its value is incremented. In either case, the parentheses are not required because the * and ++ operators have equal precedence but associate from right to left.

Now go back to the copyString() function from Program 10.13 and rewrite it to incorporate the increment operations directly into the assignment statement.

Because the to and from pointers are incremented each time after the assignment statement inside the for loop is executed, they should be incorporated into the assignment statement as post-increment operations. The revised for loop of Program 10.13 then becomes

for ( ; *from != '\0'; )
*to++ = *from++;

Execution of the assignment statement inside the loop proceeds as follows. The character pointed to by from is retrieved and then from is incremented to point to the next character in the source string. The referenced character is then stored inside the location pointed to by to, and then tois incremented to point to the next location in the destination string.

Study the preceding assignment statement until you fully understand its operation. Statements of this type are so commonly used in C programs, it’s important that you understand it completely before continuing.

The preceding for statement hardly seems worthwhile because it has no initial expression and no looping expression. In fact, the logic would be better served when expressed in the form of a while loop. This has been done in Program 10.14. This program presents your new version of thecopyString() function. The while loop uses the fact that the null character is equal to the value 0, as is commonly done by experienced C programmers.

Program 10.14 Revised Version of the copyString() Function


// Function to copy one string to another. Pointer Ver. 2

#include <stdio.h>

void copyString (char *to, char *from)
{
while ( *from )
*to++ = *from++;

*to = '\0';
}



int main (void)
{
void copyString (char *to, char *from);
char string1[] = "A string to be copied.";
char string2[50];

copyString (string2, string1);
printf ("%s\n", string2);

copyString (string2, "So is this.");
printf ("%s\n", string2);

return 0;
}


Program 10.14 Output


A string to be copied.
So is this.


Operations on Pointers

As you have seen in this chapter, you can add or subtract integer values from pointers. Furthermore, you can compare two pointers to see if they are equal or not, or if one pointer is less than or greater than another pointer. The only other operation that is permitted on pointers is the subtraction of two pointers of the same type. The result of subtracting two pointers in C is the number of elements contained between the two pointers. So, if a points to an array of elements of any type and b points to another element somewhere farther along in the same array, the expression b − arepresents the number of elements between these two pointers. For example, if p points to some element in an array x, the statement

n = p - x;

has the effect of assigning to the variable n (assumed here to be an integer variable) the index number of the element inside x to which p points.4 Therefore, if p is set pointing to the hundredth element in x by a statement such as

4. The actual type of signed integer that is produced by subtracting two pointers (for example, int, long int, or long long int) is ptrdiff_t, which is defined in the standard header file <stddef.h>.

p = &x[99];

the value of n after the previous subtraction is performed is 99.

As a practical application of this newly learned fact about pointer subtraction, take a look at a new version of the stringLength() function from Chapter 9.

In Program 10.15, the character pointer cptr is used to sequence through the characters pointed to by string until the null character is reached. At that point, string is subtracted from cptr to obtain the number of elements (characters) contained in the string. The program’s output verifies that the function is working correctly.

Program 10.15 Using Pointers to Find the Length of a String


// Function to count the characters in a string – Pointer version

#include <stdio.h>

int stringLength (const char *string)
{
const char *cptr = string;

while ( *cptr )
++cptr;
return cptr - string;
}

int main (void)
{
int stringLength (const char *string);

printf ("%i ", stringLength ("stringLength test"));
printf ("%i ", stringLength (""));
printf ("%i\n", stringLength ("complete"));

return 0;
}


Program 10.15 Output


17 0 8


Pointers to Functions

Of a slightly more advanced nature, but presented here for the sake of completeness, is the notion of a pointer to a function. When working with pointers to functions, the C compiler needs to know not only that the pointer variable points to a function, but also the type of value returned by that function as well as the number and types of its arguments. To declare a variable fnPtr to be of type “pointer to function that returns an int and that takes no arguments,” the declaration

int (*fnPtr) (void);

can be written. The parentheses around *fnPtr are required because otherwise the C compiler treats the preceding statement as the declaration of a function called fnPtr that returns a pointer to an int (because the function call operator () has higher precedence than the pointer indirection operator *).

To set your function pointer pointing to a specific function, you simply assign the name of the function to it. So, if lookup is a function that returns an int and that takes no arguments, the statement

fnPtr = lookup;

stores a pointer to this function inside the function pointer variable fnPtr. Writing a function name without a subsequent set of parentheses is treated in an analogous way to writing an array name without a subscript. The C compiler automatically produces a pointer to the specified function. An ampersand is permitted in front of the function name, but it’s not required.

If the lookup() function has not been previously defined in the program, it is necessary to declare the function before the preceding assignment can be made. So, a statement such as

int lookup (void);

is needed before a pointer to this function can be assigned to the variable fnPtr.

You can call the function that is indirectly referenced through a pointer variable by applying the function call operator to the pointer, listing any arguments to the function inside the parentheses. For example,

entry = fnPtr ();

calls the function pointed to by fnPtr, storing the returned value inside the variable entry.

One common application for pointers to functions is in passing them as arguments to other functions. The standard C library uses this, for example, in the function qsort, which performs a “quicksort” on an array of data elements. This function takes as one of its arguments a pointer to a function that is called whenever qsort needs to compare two elements in the array being sorted. In this manner, qsort can be used to sort arrays of any type, as the actual comparison of any two elements in the array is made by a user-supplied function, and not by the qsort function itself. Appendix B, “The Standard C Library,” goes into more detail about qsort and contains an actual example of its use.

Another common application for function pointers is to create what is known as dispatch tables. You can’t store functions themselves inside the elements of an array. However, it is valid to store function pointers inside an array. Given this, you can create tables that contain pointers to functions to be called. For example, you might create a table for processing different commands that will be entered by a user. Each entry in the table could contain both the command name and a pointer to a function to call to process that particular command. Now, whenever the user enters a command, you can look up the command inside the table and invoke the corresponding function to handle it.

Pointers and Memory Addresses

Before ending this discussion of pointers in C, you should note the details of how they are actually implemented. A computer’s memory can be conceptualized as a sequential collection of storage cells. Each cell of the computer’s memory has a number, called an address, associated with it. Typically, the first address of a computer’s memory is numbered 0. On most computer systems, a “cell” is called a byte.

The computer uses memory for storing the instructions of your computer program, and also for storing the values of the variables that are associated with a program. So, if you declare a variable called count to be of type int, the system assigns location(s) in memory to hold the value ofcount while the program is executing. This location might be at address 500, for example, inside the computer’s memory.

Luckily, one of the advantages of higher-level programming languages such as C is that you don’t need to concern yourself with the particular memory addresses that are assigned to variables—they are automatically handled by the system. However, the knowledge that a unique memory address is associated with each variable will help you to understand the way pointers operate.

When you apply the address operator to a variable in C, the value that is generated is the actual address of that variable inside the computer’s memory. (Obviously, this is where the address operator gets its name.) So, the statement

intPtr = &count;

assigns to intPtr the address in the computer’s memory that has been assigned to the variable count. So, if count is located at address 500 and contains the value 10, this statement assigns the value 500 to intPtr, as depicted in Figure 10.11.

Image

Figure 10.11 Pointers and memory addresses.

The address of intPtr is shown in Figure 10.11 as –- because its actual value is irrelevant for this example.

Applying the indirection operator to a pointer variable, as in the expression

*intPtr

has the effect of treating the value contained in the pointer variable as a memory address. The value stored at that memory address is then fetched and interpreted in accordance with the type declared for the pointer variable. So, if intPtr is of type pointer to int, the value stored in the memory address given by *intPtr is interpreted as an integer by the system. In our example, the value stored at memory address 500 is fetched and interpreted as an integer. The result of the expression is 10, and is of type int.

Storing a value in a location reference by a pointer, as in

*intPtr = 20;

proceeds in a like manner. The contents of intPtr is fetched and treated as a memory address. The specified integer value is then stored at that address. In the preceding statement, the integer value of 20 is, therefore, stored at memory address 500.

At times, system programmers must access particular locations inside the computer’s memory. In such cases, this knowledge of the way that pointer variables operate proves helpful.

As you can see from this chapter, the pointer is a very powerful construct in C. The flexibility in defining pointer variables extends beyond those illustrated in this chapter. For example, you can define a pointer to a pointer, and even a pointer to a pointer to a pointer. These types of constructs are beyond the scope of this book, although they are simply logical extensions of everything you’ve learned about pointers in this chapter.

The topic of pointers is probably the hardest for novices to grasp. You should reread any sections of this chapter that still seem unclear before proceeding. Solving the exercises that follow will also help you to understand the material.

Exercises

1. Type in and run the 15 programs presented in this chapter. Compare the output produced by each program with the output presented after each program in the text.

2. Write a function called insertEntry() to insert a new entry into a linked list. Have the procedure take as arguments a pointer to the list entry to be inserted (of type struct entry as defined in this chapter), and a pointer to an element in the list after which the new entry is to be inserted.

3. The function developed in exercise 2 only inserts an element after an existing element in the list, thereby preventing you from inserting a new entry at the front of the list. How can you use this same function and yet overcome this problem? (Hint: Think about setting up a special structure to point to the beginning of the list.)

4. Write a function called removeEntry() to remove an entry from a linked list. The sole argument to the procedure should be a pointer to the list. Have the function remove the entry after the one pointed to by the argument. (Why can’t you remove the entry pointed to by the argument?) You need to use the special structure you set up in exercise 3 to handle the special case of removing the first element from the list.

5. A doubly linked list is a list in which each entry contains a pointer to the preceding entry in the list as well as a pointer to the next entry in the list. Define the appropriate structure definition for a doubly linked list entry and then write a small program that implements a small doubly linked list and prints out the elements of the list.

6. Develop insertEntry() and removeEntry() functions for a doubly linked list that are similar in function to those developed in previous exercises for a singly linked list. Why can your removeEntry() function now take as its argument a direct pointer to the entry to be removed from the list?

7. Write a pointer version of the sort() function from Chapter 7, “Working with Functions.” Be certain that pointers are exclusively used by the function, including index variables in the loops.

8. Write a function called sort3() to sort three integers into ascending order. (This function is not to be implemented with arrays.)

9. Rewrite the readLine() function from Chapter 9 so that it uses a character pointer rather than an array.

10. Rewrite the compareStrings() function from Chapter 9 to use character pointers instead of arrays.

11. Given the definition of a date structure as defined in this chapter, write a function called dateUpdate() that takes a pointer to a date structure as its argument and that updates the structure to the following day (see Program 8.4).

12. Given the following declarations:

char *message = "Programming in C is fun\n";
char message2[] = "You said it\n";
char *format = "x = %i\n";
int x = 100;

determine whether each printf() call from the following sets is valid and produces the same output as other calls from the set.

/*** set 1 ***/
printf ("Programming in C is fun\n");
printf ("%s", "Programming in C is fun\n");
printf ("%s", message);
printf (message);

/*** set 2 ***/
printf ("You said it\n");
printf ("%s", message2);
printf (message2);
printf ("%s", &message2[0]);

/*** set 3 ***/
printf ("said it\n");
printf (message2 + 4);
printf ("%s", message2 + 4);
printf ("%s", &message2[4]);

/*** set 4 ***/
printf ("x = %i\n", x);
printf (format, x);