CHAPTER 7

Pointers

You had a glimpse of pointers in the last chapter and just a small hint at what you can use them for. Here, you'll delve a lot deeper into the subject of pointers and see what else you can do with them.

I'll cover a lot of new concepts here, so you may need to repeat some things a few times. This is a long chapter, so spend some time on it and experiment with the examples. Remember that the basic ideas are very simple, but you can apply them to solving complicated problems. By the end of this chapter, you'll be equipped with an essential element for effective C programming.

In this chapter you'll learn the following:

  • What a pointer is and how it's used
  • What the relationship between pointers and arrays is
  • How to use pointers with strings
  • How you can declare and use arrays of pointers
  • How to write an improved calculator program

A First Look at Pointers

You have now come to one of the most extraordinarily powerful tools in the C language. It's also potentially the most confusing, so it's important you get the ideas straight in your mind at the outset and maintain a clear idea of what's happening as you dig deeper.

Back in Chapters 2 and 5 I discussed memory. I talked about how your computer allocates an area of memory when you declare a variable. You refer to this area in memory using the variable name in your program, but once your program is compiled and running, your computer references it by the address of the memory location. This is the number that the computer uses to refer to the "box" in which the value of the variable is stored.

Look at the following statement:

int number = 5;

Here an area of memory is allocated to store an integer, and you can access it using the name number. The value 5 is stored in this area. The computer references the area using an address. The specific address where this data will be stored depends on your computer and what operating system and compiler you're using. Even though the variable name is fixed in the source program, the address is likely to be different on different systems.

Variables that can store addresses are called pointers, and the address that's stored in a pointer is usually that of another variable, as illustrated in Figure 7-1. You have a pointer P that contains the address of another variable, called number, which is an integer variable containing the value 5. The address that's stored in P is the address of the first byte of number.

image

Figure 7-1. How a pointer works

The first thing to appreciate is that it's not enough to know that a particular variable, such as P, is a pointer. You, and more importantly, the compiler, must know the type of data stored in the variable to which it points. Without this information it's virtually impossible to know how to handle the contents of the memory to which it points. A pointer to a value of type char is pointing to a value occupying 1 byte, whereas a pointer to a value of type long is usually pointing to the first byte of a value occupying 4 bytes. This means that every pointer will be associated with a specific variable type, and it can be used only to point to variables of that type. So pointers of type "pointer to int" can point only to variables of type int, pointers of type "pointer to float" can point only to variables of type float, and so on. In general a pointer of a given type is written type * for any given type name type.

The type name void means absence of any type, so a pointer of type void * can contain the address of a data item of any type. Type void * is often used as an argument type or return value type with functions that deal with data in a type-independent way. Any kind of pointer can be passed around as a value of type void * and then cast to the appropriate type when you come to use it. The address of a variable of type int can be stored in a pointer variable of type void * for example. When you want to access the integer value at the address stored in the void * pointer, you must first cast the pointer to type int *. You'll meet the malloc() library function later in this chapter that returns a pointer of type void *.

Declaring Pointers

You can declare a pointer to a variable of type int with the following statement:

int *pointer;

The type of the variable with the name pointer is int *. It can store the address of any variable of type int. This statement just creates the pointer but doesn't initialize it. Uninitialized pointers are particularly hazardous, so you should always initialize a pointer when you declare it. You can initialize pointer so that it doesn't point to anything by rewriting the declaration like this:

int *pointer = NULL;

NULL is a constant that's defined in the standard library and is the equivalent of zero for a pointer. NULL is a value that's guaranteed not to point to any location in memory. This means that it implicitly prevents the accidental overwriting of memory by using a pointer that doesn't point to anything specific. NULL is defined in the header files <stddef.h>, <stdlib.h>, <stdio.h>, <string.h>, <time.h>, <wchar.h>, and <locale.h>, and you must have at least one of these headers included in your source file for NULL to be recognized by the compiler.

If you want to initialize your variable pointer with the address of a variable that you've already declared, you use the address of operator &:

int number = 10;
int *pointer = &number;

Now the initial value of pointer is the address of the variable number. Note that the declaration of number must precede the declaration of the pointer. If this isn't the case, your code won't compile. The compiler needs to have already allocated space and thus an address for number to use it to initialize the pointer variable.

There's nothing special about the declaration of a pointer. You can declare regular variables and pointers in the same statement, for example

double value, *pVal, fnum;

This statement declares two double precision floating-point variables, value and fnum, and a variable, pVal of type "pointer to double." With this statement it is obvious that only the second variable, pVal, is a pointer, but consider this statement:

int *p, q;

This declares a pointer, p, and a variable, q, that is of type int. It is a common mistake to think that both p and q are pointers.

Accessing a Value Through a Pointer

You use the indirection operator, *, to access the value of the variable pointed to by a pointer. This operator is also referred to as the dereference operator because you use it to "dereference" a pointer. Suppose you declare the following variables:

int number = 15;
int *pointer = &number;
int result = 0;

The pointer variable contains the address of the variable number, so you can use this in an expression to calculate a new value for total, like this:

result = *pointer + 5;

The expression *pointer will evaluate to the value stored at the address contained in the pointer. This is the value stored in number, 15, so result will be set to 15 + 5, which is 20.

So much for the theory. Let's look at a small program that will highlight some of the characteristics of this special kind of variable.

Figure 7-2. Using a pointer

Using Pointers

Because you can access the contents of number through the pointer pointer, you can use a dereferenced pointer in arithmetic statements. For example

*pointer += 25;

This statement increments the value of whatever variable pointer currently addresses by 25. The * indicates you're accessing the contents of whatever the variable called pointer is pointing to. In this case, it's the contents of the variable called number.

The variable pointer can store the address of any variable of type int. This means you can change the variable that pointer points to by a statement such as this:

pointer = &another_number;

If you repeat the same statement that you used previously:

*pointer +=  25;

the statement will operate with the new variable, another_number. This means that a pointer can contain the address of any variable of the same type, so you can use one pointer variable to change the values of many other variables, as long as they're of the same type as the pointer.

I've mentioned the importance of operator precedence again in this discussion. Don't forget that Table 3-2 in Chapter 3 shows the precedence of all the operators in C, so you can always refer back to it when you are uncertain about the precedence of an operator.

Let's look at an example that will show how pointers work with input from the keyboard.

Testing for a NULL Pointer

The pointer declaration in the last example is the following:

int *pvalue = NULL;

Here, you initialize pvalue with the value NULL. As I said previously, NULL is a special constant in C, and it's the pointer equivalent to 0 with ordinary numbers. The definition of NULL is contained in <stdio.h> as well as a number of other header files, so if you use it, you must ensure that you include one of these header files.

When you assign 0 to a pointer, it's the equivalent of setting it to NULL, so you could write the following:

int *pvalue = 0;

Because NULL is the equivalent of zero, if you want to test whether the pointer pvalue is NULL, you can write this:

if(!pvalue)
{
  ...
}

When pvalue is NULL, !pvalue will be true, so the block of statement will be executed only if pvalue is NULL. Alternatively you can write the test as follows:

if(pvalue == NULL)
{
  ...
}

Pointers to Constants

You can use the const keyword when you declare a pointer to indicate that the value pointed to must not be changed. Here's an example of a declaration of a const pointer:

long value = 9999L;
const long *pvalue = &value;      /* Defines a pointer to a constant */

Because you have declared the value pointed to by pvalue to be const, the compiler will check for any statements that attempt to modify the value pointed to by pvalue and flag such statements as an error. For example, the following statement will now result in an error message from the compiler:

*pvalue = 8888L;                  /* Error - attempt to change const location */

You have only asserted that what pvalue points to must not be changed. You are quite free to do what you want with value:

value = 7777L;

The value pointed to has changed but you did not use the pointer to make the change. Of course, the pointer itself is not constant, so you can still change what it points to:

long number = 8888L;
pvalue = &number;                 /* OK - changing the address in pvalue     */

This will change the address stored in pvalue to point to number. You still cannot use the pointer to change the value that is stored though. You can change the address stored in the pointer as much as you like but using the pointer to change the value pointed to is not allowed.

Constant Pointers

Of course, you might also want to ensure that the address stored in a pointer cannot be changed. You can arrange for this to be the case by using the const keyword slightly differently in the declaration of the pointer. Here's how you could ensure that a pointer always points to the same thing:

int count = 43;
int *const pcount = &count;          /* Defines a constant */

The second statement declares and initializes pnumber and indicates that the address stored must not be changed. The compiler will therefore check that you do not inadvertently attempt to change what the pointer points to elsewhere in your code, so the following statements will result in an error message when you compile:

int item = 34;
pcount = &item;                   /* Error - attempt to change a constant pointer */

You can still change the value that pcount points to using pcount though:

*pcount = 345;                    /* OK - changes the value of count */

This references the value stored in count through the pointer and changes its value to 345. You could also use count directly to change the value.

You can create a constant pointer that points to a value that is also constant:

int item = 25;
const int *const pitem = &item;

pitem is a constant pointer to a constant so everything is fixed. You cannot change the address stored in pitem and you cannot use pitem to modify what it points to.

Naming Pointers

You've already started to write some quite large programs. As you can imagine, when your programs get even bigger, it's going to get even harder to remember which variables are normal variables and which are pointers. Therefore, it's quite a good idea to use names beginning with p for use as pointer names. If you follow this method religiously, you stand a reasonable chance of knowing which variables are pointers.

Arrays and Pointers

You'll need a clear head for this bit. Let's recap for a moment and recall what an array is and what a pointer is:

An array is a collection of objects of the same type that you can refer to using a single name. For example, an array called scores[50] could contain all your basketball scores for a 50-game season. You use a different index value to refer to each element in the array. scores[0] is your first score and scores[49] is your last. If you had ten games each month, you could use a multidimensional array, scores[12][10]. If you start play in January, the third game in June would be referenced by scores[5][2].

A pointer is a variable that has as its value the address of another variable or constant of a given type. You can use a pointer to access different variables at different times, as long as they're all of the same type.

These seem quite different, and indeed they are, but arrays and pointers are really very closely related and they can sometimes be used interchangeably. Let's consider strings. A string is just an array of elements of type char. If you want to input a single character with scanf(), you could use this:

char single;
scanf("%c", &single);

Here you need the address of operator for scanf() to work because scanf() needs the address of the location where the input data is to be stored.

However, if you're reading in a string, you can write this:

char multiple[10];
scanf("%s", multiple);

Here you don't use the & operator. You're using the array name just like a pointer. If you use the array name in this way without an index value, it refers to the address of the first element in the array.

Always keep in mind, though, that arrays are not pointers, and there's an important difference between them. You can change the address contained in a pointer, but you can't change the address referenced by an array name.

Let's go through several examples to see how arrays and pointers work together. The following examples all link together as a progression. With practical examples of how arrays and pointers can work together, you should find it fairly easy to get a grasp of the main ideas behind pointers and their relationship to arrays.

Multidimensional Arrays

So far, you've looked at one-dimensional arrays; but is it the same story with arrays that have two or more dimensions? Well, to some extent it is. However, the differences between pointers and array names start to become more apparent. Let's consider the array that you used for the tic-tac-toe program at the end of Chapter 5. You declared the array as follows:

char board[3][3] = {
                       {'1','2','3'},
                       {'4','5','6'},
                       {'7','8','9'}
                     };

You'll use this array for the examples in this section, to explore multidimensional arrays in relation to pointers.

Multidimensional Arrays and Pointers

So now that you've used the array name using pointer notation for referencing a two-dimensional array, let's use a variable that you've declared as a pointer. As I've already stated, this is where there's a significant difference. If you declare a pointer and assign the address of the array to it, then you can use that pointer to access the members of the array.

Accessing Array Elements

Now you know that, for a two-dimensional array, you have several ways of accessing the elements in that array. Table 7-1 lists these ways of accessing your board array. The left column contains row index values to the board array, and the top row contains column index values. The entry in the table corresponding to a given row index and column index shows the various possible expressions for referring to that element.

Table 7-1. Pointer Expressions for Accessing Array Elements

board 0 1 2
0 board[0][0]*board[0]**board board[0][1]*(board[0]+1)*(*board+1) board[0][2]*(board[0]+2)*(*board+2)
1 board[1][0]*(board[0]+3)*board[1]*(*board+3) board[1][1]*(board[0]+4)*(board[1]+1)*(*board+4) board[1][2]*(board[0]+5)*(board[1]+2)*(*board+5)
2 board[2][0]*(board[0]+6)*(board[1]+3)*board[2]*(*board+6) board[2][1]*(board[0]+7)*(board[1]+4)*(board[2]+1)*(*board+7) board[2][2]*(board[0]+8)*(board[1]+5)*(board[2]+2)*(*board+8)

Let's see how you can apply what you've learned so far about pointers in a program that you previously wrote without using pointers. Then you'll be able to see how the pointer-based implementation differs. You'll recall that in Chapter 5 you wrote an example that worked out your hat size. Let's see how you could have done things a little differently.

Using Memory As You Go

Pointers are an extremely flexible and powerful tool for programming over a wide range of applications. The majority of programs in C use pointers to some extent. C also has a further facility that enhances the power of pointers and provides a strong incentive to use them in your code; it permits memory to be allocated dynamically when your program executes. Allocating memory dynamically is possible only because you have pointers available.

Think back to the program in Chapter 5 that calculated the average scores for a group of students. At the moment, it works for only ten students. Suppose you want to write the program so that it works for any number of students without knowing the number of students in the class in advance, and so it doesn't use any more memory than necessary for the number of student scores specified. Dynamic memory allocation allows you to do just that. You can create arrays at runtime that are large enough to hold the precise amount of data that you require for the task.

When you explicitly allocate memory at runtime in a program, space is reserved for you in a memory area called the heap. There's another memory area called the stack in which space to store function arguments and local variables in a function is allocated. When the execution of a function is finished, the space allocated to store arguments and local variables is freed. The memory in the heap is controlled by you. As you'll see in this chapter, when you allocate memory on the heap, it is up to you to keep track of when the memory you have allocated is no longer required and free the space you have allocated to allow it to be reused.

Dynamic Memory Allocation: The malloc() Function

The simplest standard library function that allocates memory at runtime is called malloc(). You need to include the <stdlib.h> header file in your program when you use this function. When you use the malloc() function, you specify the number of bytes of memory that you want allocated as the argument. The function returns the address of the first byte of memory allocated in response to your request. Because you get an address returned, a pointer is a useful place to put it.

A typical example of dynamic memory allocation might be this:

int *pNumber = (int *)malloc(100);

Here, you've requested 100 bytes of memory and assigned the address of this memory block to pNumber. As long as you haven't modified it, any time that you use the variable pNumber, it will point to the first int location at the beginning of the 100 bytes that were allocated. This whole block can hold 25 int values on my computer, where they require 4 bytes each.

Notice the cast, (int *), that you use to convert the address returned by the function to the type "pointer to int." You've done this because malloc() is a general-purpose function that's used to allocate memory for any type of data. The function has no knowledge of what you want to use the memory for, so it actually returns a pointer of type "pointer to void," which, as I indicated earlier, is written as void *. Pointers of type void * can point to any kind of data. However, you can't dereference a pointer of type "pointer to void" because what it points to is unspecified. Many compilers will arrange for the address returned by malloc() to be automatically cast to the appropriate type, but it doesn't hurt to be specific.

You could request any number of bytes, subject only to the amount of free memory on the computer and the limit on malloc() imposed by a particular implementation. If the memory that you request can't be allocated for any reason, malloc() returns a pointer with the value NULL. Remember that this is the equivalent of 0 for pointers. It's always a good idea to check any dynamic memory request immediately using an if statement to make sure the memory is actually there before you try to use it. As with money, attempting to use memory you don't have is generally catastrophic. For that reason, writing

if(pNumber == NULL)
{
  /*Code to deal with no memory allocated */
}

with a suitable action if the pointer is NULL is a good idea. For example, you could at least display a message "Not enough memory" and terminate the program. This would be much better than allowing the program to continue, and crashing when it uses a NULL address to store something. In some instances, though, you may be able to free up a bit of memory that you've been using elsewhere, which might give you enough memory to continue.

Using the sizeof Operator in Memory Allocation

The previous example is all very well, but you don't usually deal in bytes; you deal in data of type int, type double, and so on. It would be very useful to allocate memory for 75 items of type int, for example. You can do this with the following statement:

pNumber = (int *) malloc(75*sizeof(int));

As you've seen already, sizeof is an operator that returns an unsigned integer of type size_t that's the count of the number of bytes required to store its argument. It will accept a type keyword such as int or float as an argument between parentheses, in which case the value it returns will be the number of bytes required to store an item of that type. It will also accept a variable or array name as an argument. With an array name as an argument, it returns the number of bytes required to store the whole array. In the preceding example, you asked for enough memory to store 75 data items of type int. Using sizeof in this way means that you automatically accommodate the potential variability of the space required for a value of type int between one C implementation and another.

Memory Allocation with the calloc() Function

The calloc() function that is declared in the <stdlib.h> header offers a couple of advantages over the malloc() function. First, it allocates memory as an array of elements of a given size, and second, it initializes the memory that is allocated so that all bits are zero. The calloc() function requires you to supply two argument values, the number of elements in the array, and the size of the array element, both arguments being of type size_t. The function still doesn't know the type of the elements in the array so the address of the area that is allocated is returned as type void *.

Here's how you could use calloc() to allocate memory for an array of 75 elements of type int:

int *pNumber = (int *) calloc(75, sizeof(int));

The return value will be NULL if it was not possible to allocate the memory requested, so you should still check for this. This is very similar to using malloc() but the big plus is that you know the memory area will be initialized to zero.

To make Program 7.11 use calloc() instead of malloc() to allocate the memory required, you only need to change one statement, shown in bold. The rest of the code is identical:

/* Allocate sufficient memory to store the number of primes required */
  primes = (unsigned long *)calloc(total, sizeof(unsigned long));
  if (primes == NULL)
  {
     printf(" Not enough memory. Hasta la Vista, baby. ");
     return 1;
  }

Releasing Dynamically Allocated Memory

When you allocate memory dynamically, you should always release the memory when it is no longer required. Memory that you allocate on the heap will be automatically released when your program ends, but it is better to explicitly release the memory when you are done with it, even if it's just before you exit from the program. In more complicated situations, you can easily have a memory leak. A memory leak occurs when you allocate some memory dynamically and you do not retain the reference to it, so you are unable to release the memory. This often occurs within a loop, and because you do not release the memory when it is no longer required, your program consumes more and more of the available memory and eventually may occupy it all.

Of course, to free memory that you have allocated using malloc() or calloc(), you must still be able to use the address that references the block of memory that the function returned. To release the memory for a block of dynamically allocated memory whose address you have stored in the pointer pNumber, you just write the statement:

free(pNumber);

The free() function has a formal parameter of type void *, and because any pointer type can be automatically converted to this type, you can pass a pointer of any type as the argument to the function. As long as pNumber contains the address that was returned by malloc() or calloc() when the memory was allocated, the entire block of memory that was allocated will be freed for further use.

If you pass a null pointer to the free() function the function does nothing. You should avoid attempting to free the same memory area twice, as the behavior of the free() function is undefined in this instance and therefore unpredictable. You are most at risk of trying to free the same memory twice when you have more than one pointer variable that references the memory you have allocated, so take particular care when you are doing this.

Let's modify the previous example so that it uses calloc() and frees the memory at the end of the program.

Reallocating Memory

The realloc() function enables you to reuse memory that you previously allocated using malloc() or calloc() (or realloc()). The realloc() function expects two argument values to be supplied: a pointer containing an address that was previously returned by a call to malloc(), calloc() or realloc(), and the size in bytes of the new memory that you want allocated.

The realloc() function releases the previously allocated memory referenced by the pointer that you supply as the first argument, then reallocates the same memory area to fulfill the new requirement specified by the second argument. Obviously the value of the second argument should not exceed the number of bytes that was previously allocated. If it is, you will only get a memory area allocated that is equal to the size of the previous memory area.

Here's a code fragment illustrating how you might use the realloc() function:

long *pData = NULL;                    /* Stores the data                  */
size_t count = 0;                      /* Number of data items             */
size_t oldCount = 0;                   /* previous count value             */
while(true)
{
  oldCount = count;                    /* Save previous count value        */
  printf("How many values would you like?  ");
  scanf("%u", &count);                /* Total is how many we need to find */

  if(count == 0)                      /* If none required, we are done     */
  {
    if(!pData)                        /* If memory is allocated            */
      free(pData);                    /* release it                        */
    break;                            /* Exit the loop                     */
  }

  /* Allocate sufficient memory to store count values */
  if((pData && (count <= oldCount)  /* If there's big enough old memory... */
    pData = (long *)realloc(pData, sizeof(long)*count); /* reallocate it.  */
else
  {                                   /* There wasn't enough old memory   */
    if(pData)                         /* If there's old memory...         */
      free(pData);                    /* release it.                      */

    /* Allocate a new block of memory */
    pData = (long *)calloc(count, sizeof(long));
  }
  if (pData == NULL)                  /* If no memory was allocated...   */
  {
     printf(" Not enough memory. ");
     return 1;                        /* abandon ship!                   */
  }
  /* Read and process the data and output the result... */
}

This should be easy to follow from the comments. The loop reads an arbitrary number of items of data, the number being supplied by the user. Space is allocated dynamically by reusing the previously allocated block if it exists and if it is large enough to accommodate the new requirement. If the old block is not there, or is not big enough, the code allocates a new block using calloc().

As you see from the code fragment, there's quite a lot of work involved in reallocating memory because you typically need to be sure that an existing block is large enough for the new requirement. Most of the time in such situations it will be best to just free the old memory block explicitly and allocate a completely new block.

Here are some basic guidelines for working with memory that you allocate dynamically:

  • Avoid allocating lots of small amounts of memory. Allocating memory on the heap carries some overhead with it, so allocating many small blocks of memory will carry much more overhead than allocating fewer larger blocks.
  • Only hang on to the memory as long as you need it. As soon as you are finished with a block of memory on the heap, release the memory.
  • Always ensure that you provide for releasing memory that you have allocated. Decide where in you code you will release the memory when you write the code that allocates it.
  • Make sure you do not inadvertently overwrite the address of memory you have allocated on the heap before you have released it; otherwise your program will have a memory leak. You need to be especially careful when allocating memory within a loop.

Handling Strings Using Pointers

You've used array variables of type char to store strings up to now, but you can also use a variable of type "pointer to char" to reference a string. This approach will give you quite a lot of flexibility in handling strings, as you'll see. You can declare a variable of type "pointer to char" with a statement such as this:

char *pString = NULL;

At this point, it's worth noting yet again that a pointer is just a variable that can store the address of another memory location. So far, you've created a pointer but not a place to store a string. To store a string, you need to allocate some memory. You can declare a block of memory that you intend to use to store string data and then use pointers to keep track of where in this block you've stored the strings.

String Input with More Control

It's often desirable to read text with more control than you get with the scanf() function. The getchar() function that's declared in <stdio.h> provides a much more primitive operation in that it reads only a single character at a time, but it does enable you to control when you stop reading characters. This way, you can be sure that you don't exceed the memory you have allocated to store the input.

The getchar() function reads a single character from the keyboard and returns it as type int. You can read a string terminated by ' ' into an array, buffer, like this:

char buffer[100];                           /* String input buffer */
char *pbuffer = buffer;                     /* Pointer to buffer   */
while((*pbuffer++ = getchar()) != ' '),

*pbuffer = '';                            /* Add null terminator */

All the input is done in the while loop condition. The getchar() function reads a character and stores it in the current address in pbuffer. The address in pbuffer is then incremented to point to the next character. The value of the assignment expression, ((*pbuffer++ = getchar()), is the value that was stored in the operation. As long as the character that was stored isn't ' ', the loop will continue. After the loop ends, the '' character is added in the next available position. Note that this retains the ' ' character as part of the string. If you don't want to do this, you can adjust the address where you store the '' to overwrite the ' '.

This doesn't prevent the possibility of exceeding the 100 bytes available in the array, so you can use this safely only when you're sure that the array is large enough. However, you could rewrite the loop to check for this:

size_t index = 0;
for(; index<sizeof(buffer) ; i++)
  if((*(pbuffer+index) = getchar()) == ' ')
  {
    *(pbuffer + index++) = '';
    break;
  }
if( (index ==sizeof(buffer) &&( (*(pbuffer+index-1) != ') )
    {
      printf(" You ran out of space in the buffer.");
      return 1;
    }

The index variable indicates the next available element in the buffer array. The read operations now take place in a for loop that terminates, either when the end of the buffer array is reached, or when a ' ' character is read and stored. The ' ' character is replaced by '' within the loop. Note that index is incremented after '' is stored. This ensures that index still reflects the next available position in buffer, although of course, if you fill the buffer, this will be beyond the last element in the array.

When the loop ends, you have to determine why; it could be because you finished reading the string, but it also could be because you ran out of space in buffer. When you run out of space, index will be equal to the number of elements in buffer and the last element in buffer will not be a terminating null. Therefore the left operand of the && operation in the if expression will be true if you have filled buffer, and the right operand will be true if the last element in buffer is not a terminating null. It is possible that you read a string that exactly fits, in which case the last element will be a terminating null, in which case the if expression will be false, which is the way it should be.

Using Arrays of Pointers

Of course, when you are dealing with several strings, you can use an array of pointers to store references to the strings on the heap. Suppose that you wanted to read three strings from the keyboard and store them in the buffer array. You could create an array of pointers to store the locations of the three strings:

char *pS[3] = { NULL };

This declares an array, pS, of three pointers. You learned in Chapter 5 that if you supply fewer initial values than elements in an array initializer list, the remaining elements will be initialized with 0. Thus just putting a list with one value, NULL, will initialize all the elements of an array of pointers of any size to NULL.

Let's see how this works in an example.

Designing a Program

Congratulations! You made it through a really tough part of the C language, and now I can show you an application using some of what you've learned. I'll follow the usual process, taking you through the analysis and design, and writing the code step by step. Let's look at the final program for this chapter.

The Problem

The problem you'll address is to rewrite the calculator program that you wrote in Chapter 3 with some new features, but this time using pointers. The main improvements are as follows:

  • Allow the use of signed decimal numbers, including a decimal point with an optional leading sign, - or +, as well as signed integers.
  • Permit expressions to combine multiple operations such as 2.5 + 3.7 - 6/6.
  • Add the ^ operator, which will be raised to a power, so 2 ^ 3 will produce 8.
  • Allow a line to operate on the previous result. If the previous result is 2.5, then writing =*2 + 7 will produce the result 12. Any input line that starts with an assignment operator will automatically assume the left operand is the previous result.

You're also going to cheat a little by not taking into consideration the precedence of the operators. You'll simply evaluate the expression that's entered from left to right, applying each operator to the previous result and the right operand. This means that the expression

1 + 2*3 - 4*-5

will be evaluated as

((1 + 2)*3 - 4)*(-5)

The Analysis

You don't know in advance how long an expression is going to be or how many operands are going to be involved. You'll get a complete string from the user and then analyze this to see what the numbers and operators are. You'll evaluate each intermediate result as soon as you have an operator with a left and a right operand.

The steps are as follows:

  1. Read an input string entered by the user and exit if it is quit.
  2. Check for an = operator, and if there is one skip looking for the first operand.
  3. Search for an operator followed by an operand, executing each operator in turn until the end of the input string.
  4. Display the result and go back to step 1.

The Solution

This section outlines the steps you'll take to solve the problem.

step 1

As you saw earlier in this chapter, the scanf() function doesn't allow you to read a complete string that contains spaces, as it stops at the first whitespace character. You'll therefore read the input expression using the gets() function that's declared in the <stdio.h> library header file. This will read an entire line of input, including spaces. You can actually combine the input and the overall program loop together as follows:

/* Program 7.15 An improved calculator */
#include <stdio.h>                             /* Standard input/output  */
#include <string.h>                            /* For string functions   */
#define BUFFER_LEN 256                         /* Length of input buffer */

int main(void)
{
  char input[BUFFER_LEN];                      /* Input expression       */
while(strcmp(fgets(input, BUFFER_LEN, stdin), "quit ") != 0)
  {
    /* Code to implement the calculator */
  }
  return 0;
}

You can do this because the function strcmp() expects to receive an argument that's a pointer to a string, and the function fgets() actually returns a pointer to the string that the user has typed inβ€”&input[0] in this case. The strcmp() function will compare the string that's entered with "quit " and will return 0 if they're equal. This will end the loop.

You set the input string to a length of 256. This should be enough because most computers keyboard buffers are 255 characters. (This refers to the maximum number of characters that you can type in before having to press Enter.)

Once you have your string, you could start analyzing it right away, but it would be better if you removed any spaces from the string. Because the input string is well-defined, you don't need spaces to separate the operator from the operands. Let's add code inside the while loop to remove any spaces:

/* Program 7.15 An improved calculator */
#include <stdio.h>                             /* Standard input/output  */
#include <string.h>                            /* For string functions   */
#define BUFFER_LEN 256                         /* Length of input buffer */

int main(void)
{
  char input[BUFFER_LEN];                      /* Input expression       */
  unsigned int index = 0;   /* Index of the current character in input   */
  unsigned int to = 0;    /* To index for copying input to itself      */
  size_t input_length = 0;       /* Length of the string in input      */
  while(strcmp(fgets(input, BUFFER_LEN, stdin), "quit ") != 0)
  {
    input_length = strlen(input);       /* Get the input string length */
    input[--input_length] = '';       /* Remove newline at the end   */

    /* Remove all spaces from the input by copy the string to itself */
    /* including the string terminating character                    */
    for(to = 0, index = 0 ; index<=input_length ; index++)
    if(*(input+index) != ' ')               /* If it is not a space    */
      *(input+to++) = *(input+index);       /* Copy the character      */

    input_length = strlen(input);      /* Get the new string length    */
    index = 0;                         /* Start at the first character */

    /* Code to implement the calculator */
  }
  return 0;
}

You've added declarations for the additional variables that you'll need. The variable input_length has been declared as type size_t to be compatible with the type returned by the strlen() function. This avoids possible warning messages from the compiler.

The fgets() function stores a newline character when you press the Enter to end entry of as line. You don't want the code that analyzes the string to be looking at the newline character, so you overwrite it with ''. The index expression for the input array decrements the length value returned by the strlen() and uses the result to reference the element containing newline.

You remove spaces by copying the string stored in input to itself. You need to keep track of two indexes in the copy loop: one for the position in input where the next nonspace character is to be copied to, and one for the position of the next character to be copied. In the loop you don't copy spaces; you just increment index to move to the next character. The to index gets incremented only when a character is copied. After the loop is entered, you store the new string length in input_length and reset index to reference to the first character in input.

You could equally well write the loop here using array notation:

for(to = 0, index = 0 ; index<=input_length ; index++)
      if(input[index] != ' ')                  /* If it is not a space */
        input[to++] = input[index];            /* Copy the character   */

For my taste, the code is clearer using array notation, but you'll continue using pointer notation as you need the practice.

step 2

The input expression has two possible forms. It can start with an assignment operator, indicating that the last result is to be taken as the left operand, or it can start with a number with or without a sign. You can differentiate these two situations by looking for the '=' character first. If you find one, the left operand is the previous result.

The code you need to add next in the while loop will look for an '=', and if it doesn't find one it will look for a substring that is numeric that will be the left operand:

/* Program 7.15 An improved calculator */
#include <stdio.h>                /* Standard input/output                    */
#include <string.h>               /* For string functions                     */
#include <ctype.h>                /* For classifying characters               */ #include <stdlib.h>               /* For converting strings to numeric values */ #define BUFFER_LEN 256            /* Length of input buffer                   */

int main(void)
{
  char input[BUFFER_LEN];                      /* Input expression       */
  char number_string[30];        /* Stores a number string from input         */

  unsigned int index = 0;        /* Index of the current character in input   */
  unsigned int to = 0;           /* To index for copying input to itself      */
  size_t input_length = 0;       /* Length of the string in input             */
  unsigned int number_length = 0;    /* Length of the string in number_string */   double result = 0.0;           /* The result of an operation                */
  while(strcmp(fgets(input, BUFFER_LEN, stdin), "quit ") != 0)
  {
    input_length = strlen(input);              /* Get the input string length */
    input[--input_length] = '';              /* Remove newline at the end   */

    /* Remove all spaces from the input by copying the string to itself */
    /* including the string terminating character                       */
    for(to = 0, index = 0 ; index<=input_length ; index++)
    if(*(input+index) != ' ')                      /* If it is not a space    */
*(input+to++) = *(input+index);              /* Copy the character      */

    input_length = strlen(input);             /* Get the new string length    */
    index = 0;                                /* Start at the first character */

    if(input[index]== '=')                  /* Is there =?                    */
      index++;                              /* Yes so skip over it            */
    else
    {                                     /* No - look for the left operand */
      /* Look for a number that is the left operand for */
      /* the first operator                             */

      /* Check for sign and copy it */
      number_length = 0;                             /* Initialize length      */
      if(input[index]=='+' || input[index]=='-')     /* Is it + or -?          */
        *(number_string+number_length++) = *(input+index++); /* Yes so copy it */

      /* Copy all following digits */
      for( ; isdigit(*(input+index)) ; index++)             /* Is it a digit? */
       *(number_string+number_length++) = *(input+index);   /* Yes - Copy it  */

      /* copy any fractional part */
      if(*(input+index)=='.')                        /* Is it decimal point? */
      { /* Yes so copy the decimal point and the following digits */
        *(number_string+number_length++) = *(input+index++);    /* Copy point */

        for( ; isdigit(*(input+index)) ; index++)           /* For each digit */
             *(number_string+number_length++) = *(input+index);    /* copy it */
      }
      *(number_string+number_length) = '';      /* Append string terminator */

      /* If we have a left operand, the length of number_string */
      /* will be > 0. In this case convert to a double so we    */
      /* can use it in the calculation                          */
      if(number_length>0)
        result = atof(number_string);         /* Store first number as result */
    }

    /* Code to analyze the operator and right operand */
    /* and produce the result                         */
  }
  return 0;
}

You include the <ctype.h> header for the character analysis functions and the <stdlib.h> header because you use the function atof(), which converts a string passed as an argument to a floating-point value. You've added quite a chunk of code here, but it consists of a number of straightforward steps.

The if statement checks for '=' as the first character in the input:

if(input[index]== '=')                  /* Is there =?                    */
  index++;                              /* Yes so skip over it            */

If you find one, you increment index to skip over it and go straight to looking for the operand. If '=' isn't found, you execute the else, which looks for a numeric left operand.

You copy all the characters that make up the number to the array number_string. The number may start with a unary sign, '-' or '+', so you first check for that in the else block. If you find it, then you copy it to number_string with the following statement:

if(input[index]=='+' || input[index]=='-')             /* Is it + or -?  */
  *(number_string+number_length++) = *(input+index++); /* Yes so copy it */

If a sign isn't found, then index value, recording the current character to be analyzed in input, will be left exactly where it is. If a sign is found, it will be copied to number_string and the value of index will be incremented to point to the next character.

One or more digits should be next, so you have a for loop that copies however many digits there are to number_string:

for( ; isdigit(*(input+index)) ; index++)             /* Is it a digit? */
*(number_string+number_length++) = *(input+index);     /* Yes - Copy it  */

This will copy all the digits of an integer and increment the value of index accordingly. Of course, if there are no digits, the value of index will be unchanged.

The number might not be an integer. In this case, there must be a decimal point next, which may be followed by more digits. The if statement checks for the decimal point. If there is one, then the decimal point and any following digits will also be copied:

if(*(input+index)=='.')                         /* Is it decimal point? */
{ /* Yes so copy the decimal point and the following digits */
  *(number_string+number_length++) = *(input+index++);    /* Copy point */

  for( ; isdigit(*(input+index)) ; index++)           /* For each digit */
       *(number_string+number_length++) = *(input+index);    /* copy it */
}

You must have finished copying the string for the first operand now, so you append a string-terminating character to number_string.

*(number_string+number_length) = '';      /* Append string terminator */

While there may not be a value found, if you've copied a string representing a number to number_string, the value of number_length must be positive because there has to be at least one digit. Therefore, you use the value of number_length as an indicator that you have a number:

if(number_length>0)
  result = atof(number_string);         /* Store first number as result */

The string is converted to a floating-point value of type double by the atof() function. Note that you store the value of the string in result. You'll use the same variable later to store the result of an operation. This will ensure that result always contains the result of an operation, including that produced at the end of an entire string. If you haven't stored a value here, because there is no left operand, result will already contain the value from the previous input string.

step 3

At this point, what follows in the input string is very well-defined. It must be an operator followed by a number. The operator will have the number that you found previously as its left operand, or the previous result. This "op-number" combination may also be followed by another, so you have a possible succession of op-number combinations through to the end of the string. You can deal with this in a loop that will look for these combinations:

/* Program 7.15 An improved calculator */
#include <stdio.h>                /* Standard input/output                    */
#include <string.h>               /* For string functions                     */
#include <ctype.h>                /* For classifying characters               */
#include <stdlib.h>               /* For converting strings to numeric values */
#define BUFFER_LEN 256            /* Length of input buffer                   */

int main(void)
{
  char input[BUFFER_LEN];               /* Input expression                  */
  char number_string[30];               /* Stores a number string from input */
  char op = 0;                          /* Stores an operator                */
  unsigned int index = 0;        /* Index of the current character in input   */
  unsigned int to = 0;           /* To index for copying input to itself      */
  size_t input_length = 0;       /* Length of the string in input             */
  unsigned int number_length = 0;    /* Length of the string in number_string */
  double result = 0.0;           /* The result of an operation                */
  double number = 0.0;           /* Stores the value of number_string         */
  while(strcmp(fgets(input, BUFFER_LEN, stdin), "quit ") != 0)
  {
    input_length = strlen(input);             /* Get the input string length */
    input[--input_length] = '';             /* Remove newline at the end   */

    /* Remove all spaces from the input by copying the string to itself */
    /* including the string terminating character                       */
    /* Code to remove spaces as before...                               */
!
    /* Code to check for '=' and analyze & store the left operand as before.. */

    /* Now look for 'op number' combinations */
    for(;index < input_length;)
    {
      op = *(input+index++);                             /* Get the operator */
      /* Copy the next operand and store it in number */
      number_length = 0;                           /* Initialize the length  */

      /* Check for sign and copy it */
      if(input[index]=='+' || input[index]=='-')            /* Is it + or -? */
      *(number_string+number_length++) = *(input+index++); /* Yes - copy it. */

      /* Copy all following digits */
      for( ; isdigit(*(input+index)) ; index++)            /* For each digit */
       *(number_string+number_length++) = *(input+index);   /* copy it.       */

      /* copy any fractional part */
      if(*(input+index)=='.')                      /* Is it a decimal point? */
      { /* Copy the  decimal point and the following digits */
        *(number_string+number_length++) = *(input+index++);   /* Copy point */
        for( ; isdigit(*(input+index)) ; index++)          /* For each digit */
          *(number_string+number_length++) = *(input+index);   /* copy it.   */
      }
      *(number_string+number_length) = '';             /* terminate string */

      /* Convert to a double so we can use it in the calculation */
      number = atof(number_string);     }
  /* code to produce result */
  }
  return 0;
}

In the interest of not repeating the same code ad nauseam, there are some comments indicating where the previous bits of code that you added are located in the program. I'll list the complete source code with the next addition to the program.

The for loop continues until you reach the end of the input string, which will be when you have incremented index to be equal to input_length. On each iteration of the loop, you store the operator in the variable op of type char:

op = *(input+index++);                             /* Get the operator */

With the operator out of the way, you then extract the characters that form the next number. This will be the right operand for the operator. You haven't verified that the operator is valid here, so the code won't spot an invalid operator at this point.

The extraction of the string for the number that's the right operand is exactly the same as that for the left operand. The same code is repeated. This time, though, the double value for the operand is stored in number:

number = atof(number_string);

You now have the left operand stored in result, the operator stored in op, and the right operand stored in number. Consequently, you're now prepared to execute an operation of the form

result=(result op number)

When you've added the code for this, the program will be complete.

step 4

You can use a switch statement to select the operation to be carried out based on the operand. This is essentially the same code that you used in the previous calculator. You'll also display the output and add a prompt at the beginning of the program on how the calculator is used. Here's the complete code for the program, with the last code you're adding in bold:

/* Program 7.15 An improved calculator */
#include <stdio.h>                /* Standard input/output                    */
#include <string.h>               /* For string functions                     */
#include <ctype.h>                /* For classifying characters               */
#include <stdlib.h>               /* For converting strings to numeric values */
#include <math.h>                 /* For power() function                     */ #define BUFFER_LEN 256            /* Length of input buffer                   */

int main(void)
{
  char input[BUFFER_LEN];               /* Input expression                  */
  char number_string[30];               /* Stores a number string from input */
  char op = 0;                          /* Stores an operator                */
unsigned int index = 0;        /* Index of the current character in input   */
  unsigned int to = 0;           /* To index for copying input to itself      */
  size_t input_length = 0;       /* Length of the string in input             */
  unsigned int number_length = 0;  /* Length of the string in number_string   */
  double result = 0.0;           /* The result of an operation                */
  double number = 0.0;           /* Stores the value of number_string         */

  printf(" To use this calculator, enter any expression with"
                                          " or without spaces");
  printf(" An expression may include the operators:");
  printf("           +, -, *, /, %%, or ^(raise to a power).");
  printf(" Use = at the beginning of a line to operate on ");
  printf(" the result of the previous calculation.");
  printf(" Use quit by itself to stop the calculator. ");

  /* The main calculator loop */
  while(strcmp(fgets(input, BUFFER_LEN, stdin), "quit ") != 0)
  {
    input_length = strlen(input);             /* Get the input string length */
    input[--input_length] = '';             /* Remove newline at the end   */

    /* Remove all spaces from the input by copying the string to itself */
    /* including the string terminating character                       */
    for(to = 0, index = 0 ; index<=input_length ; index++)
    if(*(input+index) != ' ')                       /* If it is not a space  */
      *(input+to++) = *(input+index);               /* Copy the character    */

    input_length = strlen(input);               /* Get the new string length */
    index = 0;                               /* Start at the first character */

   if(input[index]== '=')                    /* Is there =?                  */      index++;                                /* Yes so skip over it          */    else    {                                       /* No - look for the left operand */      /* Look for a number that is the left operand for the 1st operator */

     /* Check for sign and copy it */
     number_length = 0;                            /* Initialize length      */
     if(input[index]=='+' || input[index]=='-')             /* Is it + or -?  */
       *(number_string+number_length++) = *(input+index++); /* Yes so copy it */

     /* Copy all following digits */
     for( ; isdigit(*(input+index)) ; index++)             /* Is it a digit? */
      *(number_string+number_length++) = *(input+index);   /* Yes - Copy it  */

     /* copy any fractional part */
     if(*(input+index)=='.')                         /* Is it decimal point? */
     { /* Yes so copy the decimal point and the following digits */
       *(number_string+number_length++) = *(input+index++);    /* Copy point */

       for( ; isdigit(*(input+index)) ; index++)           /* For each digit */
         *(number_string+number_length++) = *(input+index);  /* copy it      */
     }
*(number_string+number_length) = '';      /* Append string terminator */

     /* If we have a left operand, the length of number_string */
     /* will be > 0. In this case convert to a double so we    */
     /* can use it in the calculation                          */
     if(number_length>0)
       result = atof(number_string);         /* Store first number as result */
   }

    /* Now look for 'op number' combinations */
    for(;index < input_length;)
    {
      op = *(input+index++);                             /* Get the operator */
      /* Copy the next operand and store it in number */
      number_length = 0;                           /* Initialize the length  */

      /* Check for sign and copy it */
      if(input[index]=='+' || input[index]=='-')           /* Is it + or -?  */
      *(number_string+number_length++) = *(input+index++); /* Yes - copy it. */

      /* Copy all following digits */
      for( ; isdigit(*(input+index)) ; index++)            /* For each digit */
        *(number_string+number_length++) = *(input+index); /* copy it.       */

      /* copy any fractional part */
      if(*(input+index)=='.')                      /* Is it a decimal point? */
      { /* Copy the  decimal point and the following digits */
        /* Copy point     */
        *(number_string+number_length++) = *(input+index++);
        for( ; isdigit(*(input+index)) ; index++)          /* For each digit */
          *(number_string+number_length++) = *(input+index); /* copy it.     */
      }
      *(number_string+number_length) = '';             /* terminate string */

      /* Convert to a double so we can use it in the calculation */
      number = atof(number_string);

      /* Execute operation, as 'result op= number' */
      switch(op)
      {
         case '+':                                        /* Addition        */
           result += number;
           break;
         case '-':                                        /* Subtraction     */
           result -= number;
           break;
         case '*':                                        /* Multiplication  */
           result *= number;
           break;
         case '/':                                        /* Division         */
           /* Check second operand for zero */
           if(number == 0)
             printf(" aDivision by zero error! ");

           else
             result /= number;
           break;
         case '%':                          /* Modulus operator - remainder  */
           /* Check second operand for zero */
           if((long)number == 0)
!             printf(" aDivision by zero error! ");
          else
             result = (double)((long)result % (long)number);
           break;
         case '^':
                             /* Raise to a power          */
           result = pow(result, number);
           break;
         default:                          /* Invalid operation or bad input */
           printf(" aIllegal operation! ");
           break;
       }
    }
    printf("= %f ", result);                          /* Output the result */

 }
 return 0;
}

The switch statement is essentially the same as in the previous calculator program, but with some extra cases. Because you use the power function pow() to calculate resultnumber, you have to add an #include directive for the header file math.h.

Typical output from the calculator program is as follows:


To use this calculator, enter any expression with or without spaces
An expression may include the operators:
                 +, -, *, /, %, or ^(raise to a power).
Use = at the beginning of a line to operate on
the result of the previous calculation.
Use quit by itself to stop the calculator.

2.5+3.3/2
= 2.900000
= *3
= 8.700000
= ^4
= 5728.976100
1.3+2.4-3.5+-7.8
= βˆ’7.600000
=*-2
= 15.200000
= *-2
= βˆ’30.400000
= +2
= βˆ’28.400000
quit

And there you have it!

Summary

This chapter covered a lot of ground. You explored pointers in detail. You should now understand the relationship between pointers and arrays (both one-dimensional and multidimensional arrays) and have a good grasp of their uses. I introduced the malloc(), calloc(), and realloc() functions for dynamically allocating memory, which provides the potential for your programs to use just enough memory for the data being processed in each run. You also saw the complementary function free() that you use to release memory previously allocated by malloc(), calloc(), or realloc(). You should have a clear idea of how you can use pointers with strings and how you can use arrays of pointers.

The topics I've discussed in this chapter are fundamental to a lot of what follows in the rest of the book, and of course to writing C programs effectively, so you should make sure that you're quite comfortable with the material in this chapter before moving on to the next chapter. The next chapter is all about structuring your programs.

Exercises

The following exercises enable you to try out what you've learned in this chapter. If you get stuck, look back over the chapter for help. If you're still stuck, you can download the solutions from the Source Code area of the Apress web site (http://www.apress.com), but that really should be a last resort.

Exercise 7-1 Write a program to calculate the average for an arbitrary number of floating-point values that are entered from the keyboard. Store all values in memory that's allocated dynamically before calculating and displaying the average. The user shouldn't be required to specify in advance how many values there will be.

Exercise 7-2 Write a program that will read an arbitrary number of proverbs from the keyboard and store them in memory that's allocated at runtime. The program should then output the proverbs ordered by their length, starting with the shortest and ending with the longest.

Exercise 7-3 Write a program that will read a string from the keyboard and display it after removing all spaces and punctuation characters. All operations should use pointers.

Exercise 7-4. Write a program that will read a series of temperature recordings as floating-point values for an arbitrary number of days, in which six recordings are made per day. The temperature readings should be stored in an array that's allocated dynamically and that's the correct dimensions for the number of temperature values that are entered. Calculate the average temperature per day, and then output the recordings for each day together, with the average on a single line with one decimal place after the point.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.51.145