Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
 iakovlev.org 
 Languages
 С
 GNU С Library 
 Qt 
 STL 
 Threads 
 C++ 
 Samples 
 stanford.edu 
 ANSI C
 Libs
 LD
 Socket
 Pusher
 Pipes
 Encryption
 Plugin
 Inter-Process
 Errors
 Deep C Secrets
 C + UNIX
 Linked Lists / Trees
 Asm
 Perl
 Python
 Shell
 Erlang
 Go
 Rust
 Алгоритмы
NEWS
Последние статьи :
  Тренажёр 16.01   
  Эльбрус 05.12   
  Алгоритмы 12.04   
  Rust 07.11   
  Go 25.12   
  EXT4 10.11   
  FS benchmark 15.09   
  Сетунь 23.07   
  Trees 25.06   
  Apache 03.02   
 
TOP 20
 Linux Kernel 2.6...5164 
 Trees...935 
 Максвелл 3...861 
 Go Web ...814 
 William Gropp...795 
 Ethreal 3...779 
 Ethreal 4...766 
 Gary V.Vaughan-> Libtool...764 
 Rodriguez 6...755 
 Clickhouse...748 
 Steve Pate 1...748 
 Ext4 FS...748 
 Ethreal 1...736 
 Secure Programming for Li...718 
 C++ Patterns 3...711 
 Ulrich Drepper...692 
 Assembler...686 
 DevFS...654 
 Стивенс 9...644 
 MySQL & PosgreSQL...621 
 
  01.01.2024 : 3621733 посещений 

iakovlev.org

C за 21 день

Указатели

As you read through this chapter, the advantages of using pointers might not be clear immediately. The advantages fall into two categories: things that can be done better with pointers than without, and things that can be done only with pointers. The specifics should become clear as you read this and subsequent chapters. At present, just know that you must understand pointers if you want to be a proficient C programmer.

What Is a Pointer?

To understand pointers, you need a basic knowledge of how your computer stores infor-mation in memory. The following is a somewhat simplified account of PC memory storage.

Your Computer's Memory

A PC's RAM consists of many thousands of sequential storage locations, and each location is identified by a unique address. The memory addresses in a given computer range from 0 to a maximum value that depends on the amount of memory installed.

When you're using your computer, the operating system uses some of the system's memory. When you're running a program, the program's code (the machine-language instructions for the program's various tasks) and data (the information the program is using) also use some of the system's memory. This section examines the memory storage for program data.

When you declare a variable in a C program, the compiler sets aside a memory location with a unique address to store that variable. The compiler associates that address with the variable's name. When your program uses the variable name, it automatically accesses the proper memory location. The location's address is used, but it is hidden from you, and you need not be concerned with it.

Figure 9.1 shows this schematically. A variable named rate has been declared and initialized to 100. The compiler has set aside storage at address 1004 for the variable and has associated the name rate with the address 1004.

Figure 9.1. A program variable is stored at a specific memory address.

Creating a Pointer

You should note that the address of the variable rate (or any other variable) is a number and can be treated like any other number in C. If you know a variable's address, you can create a second variable in which to store the address of the first. The first step is to declare a variable to hold the address of rate. Give it the name p_rate, for example. At first, p_rate is uninitialized. Storage has been allocated for p_rate, but its value is undetermined, as shown in Figure 9.2.

Figure 9.2. Memory storage space has been allocated for the variable p_rate.

The next step is to store the address of the variable rate in the variable p_rate. Because p_rate now contains the address of rate, it indicates the location where rate is stored in memory. In C parlance, p_rate points to rate, or is a pointer to rate. This is shown in Figure 9.3.

Figure 9.3. The variable p_rate contains the address of the variable rate and is therefore a pointer to rate.

To summarize, a pointer is a variable that contains the address of another variable. Now you can get down to the details of using pointers in your C programs.

Pointers and Simple Variables

In the example just given, a pointer variable pointed to a simple (that is, nonarray) variable. This section shows you how to create and use pointers to simple variables.

Declaring Pointers

A pointer is a numeric variable and, like all variables, must be declared before it can be used. Pointer variable names follow the same rules as other variables and must be unique. This chapter uses the convention that a pointer to the variable name is called p_name. This isn't necessary, however; you can name pointers anything you want (within C's naming rules).

A pointer declaration takes the following form:

typename *ptrname;
 

typename is any of C's variable types and indicates the type of the variable that the pointer points to. The asterisk (*) is the indirection operator, and it indicates that ptrname is a pointer to type typename and not a variable of type typename. Pointers can be declared along with nonpointer variables. Here are some more examples:

char *ch1, *ch2;         /* ch1 and ch2 both are pointers to type char */
 float *value, percent;   /* value is a pointer to type float, and
                          /* percent is an ordinary float variable */
 


NOTE: The * symbol is used as both the indirection operator and the multiplication operator. Don't worry about the compiler's becoming confused. The context in which * is used always provides enough information so that the compiler can figure out whether you mean indirection or multiplication.

Initializing Pointers

Now that you've declared a pointer, what can you do with it? You can't do anything with it until you make it point to something. Like regular variables, uninitialized pointers can be used, but the results are unpredictable and potentially disastrous. Until a pointer holds the address of a variable, it isn't useful. The address doesn't get stored in the pointer by magic; your program must put it there by using the address-of operator, the ampersand (&). When placed before the name of a variable, the address-of operator returns the address of the variable. Therefore, you initialize a pointer with a statement of the form

pointer = &variable;
 

Look back at the example in Figure 9.3. The program statement to initialize the variable p_rate to point at the variable rate would be

p_rate = &rate;     /* assign the address of rate to p_rate */
 

Before the initialization, p_rate didn't point to anything in particular. After the initialization, p_rate is a pointer to rate.

Using Pointers

Now that you know how to declare and initialize pointers, you're probably wondering how to use them. The indirection operator (*) comes into play again. When the * precedes the name of a pointer, it refers to the variable pointed to.

Let's continue with the previous example, in which the pointer p_rate has been initialized to point to the variable rate. If you write *p_rate, it refers to the variable rate. If you want to print the value of rate (which is 100 in the example), you could write

printf("%d", rate);
 

or this:

printf("%d", *p_rate);
 

In C, these two statements are equivalent. Accessing the contents of a variable by using the variable name is called direct access. Accessing the contents of a variable by using a pointer to the variable is called indirect access or indirection. Figure 9.4 shows that a pointer name preceded by the indirection operator refers to the value of the pointed-to variable.

Figure 9.4. Use of the indirection operator with pointers.

Pause a minute and think about this material. Pointers are an integral part of the C language, and it's essential that you understand them. Pointers have confused many people, so don't worry if you're feeling a bit puzzled. If you need to review, that's fine. Maybe the following summary can help.

If you have a pointer named ptr that has been initialized to point to the variable var, the following are true:

  • *ptr and var both refer to the contents of var (that is, whatever value the program has stored there).

  • ptr and &var refer to the address of var.

As you can see, a pointer name without the indirection operator accesses the pointer value itself, which is, of course, the address of the variable pointed to.

Listing 9.1 demonstrates basic pointer use. You should enter, compile, and run this program.

Listing 9.1. Basic pointer use.

1:  /* Demonstrates basic pointer use. */
 2:
 3:  #include <stdio.h>
 4:
 5:  /* Declare and initialize an int variable */
 6:
 7:  int var = 1;
 8:
 9:  /* Declare a pointer to int */
 10:
 11: int *ptr;
 12:
 13: main()
 14: {
 15:     /* Initialize ptr to point to var */
 16:
 17:     ptr = &var;
 18:
 19:     /* Access var directly and indirectly */
 20:
 21:     printf("\nDirect access, var = %d", var);
 22:     printf("\nIndirect access, var = %d", *ptr);
 23:
 24:     /* Display the address of var two ways */
 25:
 26:     printf("\n\nThe address of var = %d", &var);
 27:     printf("\nThe address of var = %d\n", ptr);
 28:
 29:     return 0;
 30: }
 Direct access, var = 1
 Indirect access, var = 1
 The address of var = 4264228
 The address of var = 4264228
 


The address reported for var might not be 4264228 on your system.

ANALYSIS: In this listing, two variables are declared. In line 7, var is declared as an int and initialized to 1. In line 11, a pointer to a variable of type int is declared and named ptr. In line 17, the pointer ptr is assigned the address of var using the address-of operator (&). The rest of the program prints the values from these two variables to the screen. Line 21 prints the value of var, whereas line 22 prints the value stored in the location pointed to by ptr. In this program, this value is 1. Line 26 prints the address of var using the address-of operator. This is the same value printed by line 27 using the pointer variable, ptr.

This listing is good to study. It shows the relationship between a variable, its address, a pointer, and the dereferencing of a pointer.


DO understand what pointers are and how they work. The mastering of C requires mastering pointers.

DON'T use an uninitialized pointer. Results can be disastrous if you do.


Pointers and Variable Types

The previous discussion ignores the fact that different variable types occupy different amounts of memory. For the more common PC operating systems, an int takes two bytes, a float takes four bytes, and so on. Each individual byte of memory has its own address, so a multibyte variable actually occupies several addresses.

How, then, do pointers handle the addresses of multibyte variables? Here's how it works: The address of a variable is actually the address of the first (lowest) byte it occupies. This can be illustrated with an example that declares and initializes three variables:

int vint = 12252;
 char vchar = 90;
 float vfloat = 1200.156004;
 

These variables are stored in memory as shown in Figure 9.5. In this figure, the int variable occupies two bytes, the char variable occupies one byte, and the float variable occupies four bytes.

Figure 9.5. Different types of numeric variables occupy different amounts of storage space in memory.

Now let's declare and initialize pointers to these three variables:

int *p_vint;
 char *p_vchar;
 float *p_vfloat;
 /* additional code goes here */
 p_vint = &vint;
 p_vchar = &vchar;
 p_vfloat = &vfloat;
 

Each pointer is equal to the address of the first byte of the pointed-to variable. Thus, p_vint equals 1000, p_vchar equals 1003, and p_vfloat equals 1006. Remember, however, that each pointer was declared to point to a certain type of variable. The compiler knows that a pointer to type int points to the first of two bytes, a pointer to type float points to the first of four bytes, and so on. This is illustrated in Figure 9.6.

Figures 9.5 and 9.6 show some empty memory storage locations among the three variables. This is for the sake of visual clarity. In actual practice, the C compiler stores the three variables in adjacent memory locations with no unused bytes between them.

Figure 9.6. The compiler knows the size of the vari-able that a pointer points to.

Pointers and Arrays

Pointers can be useful when you're working with simple variables, but they are more helpful with arrays. There is a special relationship between pointers and arrays in C. In fact, when you use the array subscript notation that you learned on Day 8, "Using Numeric Arrays," you're really using pointers without knowing it. The following sections explain how this works.

The Array Name as a Pointer

An array name without brackets is a pointer to the array's first element. Thus, if you've declared an array data[], data is the address of the first array element.

"Wait a minute," you might be saying. "Don't you need the address-of operator to get an address?" Yes. You can also use the expression &data[0] to obtain the address of the array's first element. In C, the relationship (data == &data[0]) is true.

You've seen that the name of an array is a pointer to the array. Remember that this is a pointer constant; it can't be changed and remains fixed for the duration of program execution. This makes sense: If you changed its value, it would point elsewhere and not to the array (which remains at a fixed location in memory).

You can, however, declare a pointer variable and initialize it to point at the array. For example, the following code initializes the pointer variable p_array with the address of the first element of array[]:

int array[100], *p_array;
 /* additional code goes here */
 p_array = array;
 

Because p_array is a pointer variable, it can be modified to point elsewhere. Unlike array, p_array isn't locked into pointing at the first element of array[]. For example, it could be pointed at other elements of array[]. How would you do this? First, you need to look at how array elements are stored in memory.

Array Element Storage

As you might remember from Day 8, the elements of an array are stored in sequential memory locations with the first element in the lowest address. Subsequent array elements (those with an index greater than 0) are stored in higher addresses. How much higher depends on the array's data type (char, int, float, and so forth).

Take an array of type int. As you learned on Day 3, "Storing Data: Variables and Constants," a single int variable can occupy two bytes of memory. Each array element is therefore located two bytes above the preceding element, and the address of each array element is two higher than the address of the preceding element. A type float, on the other hand, can occupy four bytes. In an array of type float, each array element is located four bytes above the preceding element, and the address of each array element is four higher than the address of the preceding element.

Figure 9.7 illustrates the relationship between array storage and addresses for a six-element int array and a three-element float array.

Figure 9.7. Array storage for different array types.

By looking at Figure 9.7, you should be able to see why the following relationships are true:

1: x == 1000
 2: &x[0] == 1000
 3: &x[1] = 1002
 4: expenses == 1250
 5: &expenses[0] == 1250
 6: &expenses[1] == 1254
 

x without the array brackets is the address of the first element (x[0]). You can also see that x[0] is at the address of 1000. Line 2 shows this too. It can be read like this: "The address of the first element of the array x is equal to 1000." Line 3 shows that the address of the second element (subscripted as 1 in an array) is 1002. Again, Figure 9.7 can confirm this. Lines 4, 5, and 6 are virtually identical to 1, 2, and 3, respectively. They vary in the difference between the addresses of the two array elements. In the type int array x, the difference is two bytes, and in the type float array, expenses, the difference is four bytes.

How do you access these successive array elements using a pointer? You can see from these examples that a pointer must be increased by 2 to access successive elements of a type int array, and by 4 to access successive elements of a type float array. You can generalize and say that to access successive elements of an array of a particular data type, a pointer must be increased by sizeof(datatype). Remember from Day 3 that the sizeof() operator returns the size in bytes of a C data type.

Listing 9.2 illustrates the relationship between addresses and the elements of different type arrays by declaring arrays of type int, float, and double and by displaying the addresses of successive elements.

Listing 9.2. Displaying the addresses of successive array elements.

1:  /* Demonstrates the relationship between addresses and */
 2:  /* elements of arrays of different data types. */
 3:
 4:  #include <stdio.h>
 5:
 6:  /* Declare three arrays and a counter variable. */
 7:
 8:  int i[10], x;
 9:  float f[10];
 10: double d[10];
 11:
 12: main()
 13: {
 14:     /* Print the table heading */
 15:
 16:     printf("\t\tInteger\t\tFloat\t\tDouble");
 17:
 18:     printf("\n================================");
 19:     printf("======================");
 20:
 21:     /* Print the addresses of each array element. */
 22:
 23:     for (x = 0; x < 10; x++)
 24:         printf("\nElement %d:\t%ld\t\t%ld\t\t%ld", x, &i[x],
 25:             &f[x], &d[x]);
 26:
 27:     printf("\n================================");
 28:     printf("======================\n");
 29:
 30:     return 0;
 31: }
                Integer     Float     Double
 ==============================================
 Element 0:     1392        1414      1454
 Element 1:     1394        1418      1462
 Element 2:     1396        1422      1470
 Element 3:     1398        1426      1478
 Element 4:     1400        1430      1486
 Element 5:     1402        1434      1494
 Element 6:     1404        1438      1502
 Element 7:     1406        1442      1510
 Element 8:     1408        1446      1518
 Element 9:     1410        1450      1526
 ==============================================
 

ANALYSIS: The exact addresses that your system displays might be different from these, but the relationships are the same. In this output, there are two bytes between int elements, four bytes between float elements, and eight bytes between double elements. (Note: Some machines use different sizes for variable types. If your machine differs, the preceding output might have different-size gaps; however, they will be consistent gaps.)

This listing takes advantage of the escape characters discussed on Day 7, "Fundamentals of Input and Output." The printf() calls in lines 16 and 24 use the tab escape character (\t) to help format the table by aligning the columns.

Looking more closely at Listing 9.2, you can see that three arrays are created in lines 8, 9, and 10. Line 8 declares array i of type int, line 9 declares array f of type float, and line 10 declares array d of type double. Line 16 prints the column headers for the table that will be displayed. Lines 18 and 19, along with lines 27 and 28, print dashed lines across the top and bottom of the table data. This is a nice touch for a report. Lines 23, 24, and 25 are a for loop that prints each of the table's rows. The number of the element x is printed first. This is followed by the address of the element in each of the three arrays.

Pointer Arithmetic

You have a pointer to the first array element; the pointer must increment by an amount equal to the size of the data type stored in the array. How do you access array elements using pointer notation? You use pointer arithmetic.

"Just what I don't need," you might be thinking, "another kind of arithmetic to learn!" Don't worry. Pointer arithmetic is simple, and it makes using pointers in your programs much easier. You have to be concerned with only two pointer operations: incrementing and decrementing.

Incrementing Pointers

When you increment a pointer, you are increasing its value. For example, when you incre-ment a pointer by 1, pointer arithmetic automatically increases the pointer's value so that it points to the next array element. In other words, C knows the data type that the pointer points to (from the pointer declaration) and increases the address stored in the pointer by the size of the data type.

Suppose that ptr_to_int is a pointer variable to some element of an int array. If you execute the statement

ptr_to_int++;
 

the value of ptr_to_int is increased by the size of type int (usually 2 bytes), and ptr_to_int now points to the next array element. Likewise, if ptr_to_float points to an element of a type float array, the statement

ptr_to_float++;
 

increases the value of ptr_to_float by the size of type float (usually 4 bytes).

The same holds true for increments greater than 1. If you add the value n to a pointer, C increments the pointer by n array elements of the associated data type. Therefore,

ptr_to_int += 4;
 

increases the value stored in ptr_to_int by 8 (assuming that an integer is 2 bytes), so it points four array elements ahead. Likewise,

ptr_to_float += 10;
 

increases the value stored in ptr_to_float by 40 (assuming that a float is 4 bytes), so it points 10 array elements ahead.

Decrementing Pointers

The same concepts that apply to incrementing pointers hold true for decrementing pointers. Decrementing a pointer is actually a special case of incrementing by adding a negative value. If you decrement a pointer with the -- or -= operators, pointer arithmetic automatically adjusts for the size of the array elements.

Listing 9.3 presents an example of how pointer arithmetic can be used to access array elements. By incrementing pointers, the program can step through all the elements of the arrays efficiently.

Listing 9.3. Using pointer arithmetic and pointer notation to access array elements.

1:  /* Demonstrates using pointer arithmetic to access */
 2:  /* array elements with pointer notation. */
 3:
 4:  #include <stdio.h>
 5:  #define MAX 10
 6:
 7:  /* Declare and initialize an integer array. */
 8:
 9:  int i_array[MAX] = { 0,1,2,3,4,5,6,7,8,9 };
 10:
 11: /* Declare a pointer to int and an int variable. */
 12:
 13: int *i_ptr, count;
 14:
 15: /* Declare and initialize a float array. */
 16:
 17: float f_array[MAX] = { .0, .1, .2, .3, .4, .5, .6, .7, .8, .9 };
 18:
 19: /* Declare a pointer to float. */
 20:
 21: float *f_ptr;
 22:
 23: main()
 24: {
 25:     /* Initialize the pointers. */
 26:
 27:     i_ptr = i_array;
 28:     f_ptr = f_array;
 29:
 30:     /* Print the array elements. */
 31:
 32:     for (count = 0; count < MAX; count++)
 33:         printf("%d\t%f\n", *i_ptr++, *f_ptr++);
 34:
 35:     return 0;
 36: }
 0       0.000000
 1       0.100000
 2       0.200000
 3       0.300000
 4       0.400000
 5       0.500000
 6       0.600000
 7       0.700000
 8       0.800000
 9       0.900000
 

ANALYSIS: In this program, a defined constant named MAX is set to 10 in line 5; it is used throughout the listing. In line 9, MAX is used to set the number of elements in an array of ints named i_array. The elements in this array are initialized at the same time that the array is declared. Line 13 declares two additional int variables. The first is a pointer named i_ptr. You know this is a pointer because an indirection operator (*) is used. The other variable is a simple type int variable named count. In line 17, a second array is defined and initialized. This array is of type float, contains MAX values, and is initialized with float values. Line 21 declares a pointer to a float named f_ptr.

The main() function is on lines 23 through 36. The program assigns the beginning address of the two arrays to the pointers of their respective types in lines 27 and 28. Remember, an array name without a subscript is the same as the address of the array's beginning. A for statement in lines 32 and 33 uses the int variable count to count from 0 to the value of MAX. For each count, line 33 dereferences the two pointers and prints their values in a printf() function call. The increment operator then increments each of the pointers so that each points to the next element in the array before continuing with the next iteration of the for loop.

You might be thinking that this program could just as well have used array subscript notation and dispensed with pointers altogether. This is true, and in simple programming tasks like this, the use of pointer notation doesn't offer any major advantages. As you start to write more complex programs, however, you should find the use of pointers advantageous.

Remember that you can't perform incrementing and decrementing operations on pointer constants. (An array name without brackets is a pointer constant.) Also remember that when you're manipulating pointers to array elements, the C compiler doesn't keep track of the start and finish of the array. If you're not careful, you can increment or decrement the pointer so that it points somewhere in memory before or after the array. Something is stored there, but it isn't an array element. You should keep track of pointers and where they're pointing.

Other Pointer Manipulations

The only other pointer arithmetic operation is called differencing, which refers to subtracting two pointers. If you have two pointers to different elements of the same array, you can subtract them and find out how far apart they are. Again, pointer arithmetic automatically scales the answer so that it refers to array elements. Thus, if ptr1 and ptr2 point to elements of an array (of any type), the following expression tells you how far apart the elements are:

ptr1 - ptr2
 

Pointer comparisons are valid only between pointers that point to the same array. Under these circumstances, the relational operators ==, !=, >, <, >=, and <= work properly. Lower array elements (that is, those having a lower subscript) always have a lower address than higher array elements. Thus, if ptr1 and ptr2 point to elements of the same array, the comparison

ptr1 < ptr2
 

is true if ptr1 points to an earlier member of the array than ptr2 does.

This covers all allowed pointer operations. Many arithmetic operations that can be performed with regular variables, such as multiplication and division, don't make sense with pointers. The C compiler doesn't allow them. For example, if ptr is a pointer, the statement

ptr *= 2;
 

generates an error message. As Table 9.1 indicates, you can do a total of six operations with a pointer, all of which have been covered in this chapter.

Table 9.1. Pointer operations.

Operation Description
Assignment You can assign a value to a pointer. The value should be an address, obtained with the address-of operator (&) or from a pointer constant (array name).
Indirection The indirection operator (*) gives the value stored in the pointed-to location.
Address of You can use the address-of operator to find the address of a pointer, so you can have pointers to pointers. This is an advanced topic and is covered on Day 15, "Pointers: Beyond the Basics."
Incrementing You can add an integer to a pointer in order to point to a different memory location.
Decrementing You can subtract an integer from a pointer in order to point to a different memory location.
Differencing You can subtract an integer from a pointer in order to point to a different memory location.
Comparison Valid only with two pointers that point to the same array.

Pointer Cautions

When you're writing a program that uses pointers, you must avoid one serious error: using an uninitialized pointer on the left side of an assignment statement. For example, the following statement declares a pointer to type int:

int *ptr;
 

This pointer isn't yet initialized, so it doesn't point to anything. To be more exact, it doesn't point to anything known. An uninitialized pointer has some value; you just don't know what it is. In many cases, it is zero. If you use an uninitialized pointer in an assignment statement, this is what happens:

*ptr = 12;
 

The value 12 is assigned to whatever address ptr points to. That address can be almost anywhere in memory--where the operating system is stored or somewhere in the program's code. The 12 that is stored there might overwrite some important information, and the result can be anything from strange program errors to a full system crash.

The left side of an assignment statement is the most dangerous place to use an uninitialized pointer. Other errors, although less serious, can also result from using an uninitialized pointer anywhere in your program, so be sure your program's pointers are properly initialized before you use them. You must do this yourself. The compiler won't do this for you!


DON'T try to perform mathematical operations such as division, multiplication, and modulus on pointers. Adding (incrementing) and subtracting (differencing) pointers are acceptable.

DON'T forget that subtracting from or adding to a pointer changes the pointer based on the size of the data type it points to. It doesn't change it by 1 or by the number being added (unless it's a pointer to a one-byte character).

DO understand the size of variable types on your computer. As you can begin to see, you need to know variable sizes when working with pointers and memory.

DON'T try to increment or decrement an array variable. Assign a pointer to the beginning address of the array and increment it (see Listing 9.3).


Array Subscript Notation and Pointers

An array name without brackets is a pointer to the array's first element. Therefore, you can access the first array element using the indirection operator. If array[] is a declared array, the expression *array is the array's first element, *(array + 1) is the array's second element, and so on. If you generalize for the entire array, the following relationships hold true:

*(array) == array[0]
 *(array + 1) == array[1]
 *(array + 2) == array[2]
 ...
 *(array + n) == array[n]
 

This illustrates the equivalence of array subscript notation and array pointer notation. You can use either in your programs; the C compiler sees them as two different ways of accessing array data using pointers.

Passing Arrays to Functions

This chapter has already discussed the special relationship that exists in C between pointers and arrays. This relationship comes into play when you need to pass an array as an argument to a function. The only way you can pass an array to a function is by means of a pointer.

As you learned on Day 5, "Functions: The Basics," an argument is a value that the calling program passes to a function. It can be an int, a float, or any other simple data type, but it must be a single numerical value. It can be a single array element, but it can't be an entire array. What if you need to pass an entire array to a function? Well, you can have a pointer to an array, and that pointer is a single numeric value (the address of the array's first element). If you pass that value to a function, the function knows the address of the array and can access the array elements using pointer notation.

Consider another problem. If you write a function that takes an array as an argument, you want a function that can handle arrays of different sizes. For example, you could write a function that finds the largest element in an integer array. The function wouldn't be much use if it were limited to dealing with arrays of one fixed size.

How does the function know the size of the array whose address it was passed? Remember, the value passed to a function is a pointer to the first array element. It could be the first of 10 elements or the first of 10,000. There are two methods of letting a function know an array's size.

You can identify the last array element by storing a special value there. As the function processes the array, it looks for that value in each element. When the value is found, the end of the array has been reached. The disadvantage of this method is that it forces you to reserve a value as the end-of-array indicator, reducing the flexibility you have for storing real data in the array.

The other method is more flexible and straightforward, and it's the one used in this book: Pass the function the array size as an argument. This can be a simple type int argument. Thus, the function is passed two arguments: a pointer to the first array element, and an integer specifying the number of elements in the array.

Listing 9.4 accepts a list of values from the user and stores them in an array. It then calls a function named largest(), passing the array (both pointer and size). The function finds the largest value in the array and returns it to the calling program.

Listing 9.4. Passing an array to a function.

1:  /* Passing an array to a function. */
 2:
 3:  #include <stdio.h>
 4:
 5:  #define MAX 10
 6:
 7:  int array[MAX], count;
 8:
 9:  int largest(int x[], int y);
 10:
 11: main()
 12: {
 13:     /* Input MAX values from the keyboard. */
 14:
 15:     for (count = 0; count < MAX; count++)
 16:     {
 17:         printf("Enter an integer value: ");
 18:         scanf("%d", &array[count]);
 19:     }
 20:
 21:     /* Call the function and display the return value. */
 22:     printf("\n\nLargest value = %d\n", largest(array, MAX));
 23:
 24:     return 0;
 25: }
 26: /* Function largest() returns the largest value */
 27: /* in an integer array */
 28:
 29: int largest(int x[], int y)
 30: {
 31:     int count, biggest = -12000;
 32:
 33:     for ( count = 0; count < y; count++)
 34:     {
 35:         if (x[count] > biggest)
 36:             biggest = x[count];
 37:     }
 38:
 39:     return biggest;
 40: }
 Enter an integer value: 1
 Enter an integer value: 2
 Enter an integer value: 3
 Enter an integer value: 4
 Enter an integer value: 5
 Enter an integer value: 10
 Enter an integer value: 9
 Enter an integer value: 8
 Enter an integer value: 7
 Enter an integer value: 6
 Largest value = 10
 

ANNALYSIS: A function prototype in line 9 and a function header in line 29 are nearly identical except for a semicolon:

int largest(int x[], int y)
 

Most of this line should make sense to you: largest() is a function that returns an int to the calling program; its second argument is an int represented by the parameter y. The only thing new is the first parameter, int x[], which indicates that the first argument is a pointer to type int, represented by the parameter x. You also could write the function declaration and header as follows:

int largest(int *x, int y);
 

This is equivalent to the first form; both int x[] and int *x mean "pointer to int." The first form might be preferable, because it reminds you that the parameter represents a pointer to an array. Of course, the pointer doesn't know that it points to an array, but the function uses it that way.

Now look at the function largest(). When it is called, the parameter x holds the value of the first argument and is therefore a pointer to the first element of the array. You can use x anywhere an array pointer can be used. In largest(), the array elements are accessed using subscript notation in lines 35 and 36. You also could use pointer notation, rewriting the if loop like this:

for (count = 0; count < y; count++)
 {
    if (*(x+count) > biggest)
       biggest = *(x+count);
 }
 

Listing 9.5 shows the other way of passing arrays to functions.

Listing 9.5. An alternative way of passing an array to a function.

1:  /* Passing an array to a function. Alternative way. */
 2:
 3:  #include <stdio.h>
 4:
 5:  #define MAX 10
 6:
 7:  int array[MAX+1], count;
 8:
 9:  int largest(int x[]);
 10:
 11: main()
 12: {
 13:     /* Input MAX values from the keyboard. */
 14:
 15:     for (count = 0; count < MAX; count++)
 16:     {
 17:         printf("Enter an integer value: ");
 18:         scanf("%d", &array[count]);
 19:
 20:         if ( array[count] == 0 )
 21:             count = MAX;               /* will exit for loop */
 22:     }
 23:     array[MAX] = 0;
 24:
 25:     /* Call the function and display the return value. */
 26:     printf("\n\nLargest value = %d\n", largest(array));
 27:
 28:     return 0;
 29: }
 30: /* Function largest() returns the largest value */
 31: /* in an integer array */
 32:
 33: int largest(int x[])
 34: {
 35:     int count, biggest = -12000;
 36:
 37:     for ( count = 0; x[count] != 0; count++)
 38:     {
 39:         if (x[count] > biggest)
 40:             biggest = x[count];
 41:     }
 42:
 43:     return biggest;
 44: }
 Enter an integer value: 1
 Enter an integer value: 2
 Enter an integer value: 3
 Enter an integer value: 4
 Enter an integer value: 5
 Enter an integer value: 10
 Enter an integer value: 9
 Enter an integer value: 8
 Enter an integer value: 7
 Enter an integer value: 6
 Largest value = 10
 

Here is the output from running the program a second time:

Enter an integer value: 10
 Enter an integer value: 20
 Enter an integer value: 55
 Enter an integer value: 3
 Enter an integer value: 12
 Enter an integer value: 0
 Largest value = 55
 

This program uses a largest() function that has the same functionality as Listing 9.4. The difference is that only the array tag is needed. The for loop in line 37 continues looking for the largest value until it encounters a 0, at which point it knows it is done.

Looking at the early parts of this program, you can see the differences between Listing 9.4 and Listing 9.5. First, in line 7 you need to add an extra element to the array to store the value that indicates the end. In lines 20 and 21, an if statement is added to see whether the user entered 0, thus signaling that he is done entering values. If 0 is entered, count is set to its maximum value so that the for loop can be exited cleanly. Line 23 ensures that the last element is 0 in case the user entered the maximum number of values (MAX).

By adding the extra commands when entering the data, you can make the largest() function work with any size of array; however, there is one catch. What happens if you forget to put a 0 at the end of the array? largest() continues past the end of the array, comparing values in memory until it finds a 0.

As you can see, passing an array to a function is not particularly difficult. You simply pass a pointer to the array's first element. In most situations, you also need to pass the number of elements in the array. In the function, the pointer value can be used to access the array elements with either subscript or pointer notation.

On Day 9, "Understanding Pointers," you were introduced to the basics of pointers, which are an important part of the C programming language. Today you'll go further, exploring some advanced pointer topics that can add flexibility to your programming. Today you will learn

  • How to declare a pointer to a pointer

  • How to use pointers with multidimensional arrays

  • How to declare arrays of pointers

  • How to declare pointers to functions

  • How to use pointers to create linked lists for data storage

Pointers to Pointers

As you learned on Day 9, a pointer is a numeric variable with a value that is the address of another variable. You declare a pointer using the indirection operator (*). For example, the declaration

int *ptr;
 

declares a pointer named ptr that can point to a type int variable. You then use the address-of operator (&) to make the pointer point to a specific variable of the corresponding type. Assuming that x has been declared as a type int variable, the statement

ptr = &x;
 

assigns the address of x to ptr and makes ptr point to x. Again, using the indirection operator, you can access the pointed-to variable by using its pointer. Both of the following statements assign the value 12 to x:

x = 12;
 *ptr = 12;
 

Because a pointer is itself a numeric variable, it is stored in your computer's memory at a particular address. Therefore, you can create a pointer to a pointer, a variable whose value is the address of a pointer. Here's how:

int x = 12;                   /* x is a type int variable. */
 int *ptr = &x;                /* ptr is a pointer to x. */
 int **ptr_to_ptr = &ptr;      /* ptr_to_ptr is a pointer to a */
                               /* pointer to type int. */
 

Note the use of a double indirection operator (**) when declaring a pointer to a pointer. You also use the double indirection operator when accessing the pointed-to variable with a pointer to a pointer. Thus, the statement

**ptr_to_ptr = 12;
 

assigns the value 12 to the variable x, and the statement

printf("%d", **ptr_to_ptr);
 

displays the value of x on-screen. If you mistakenly use a single indirection operator, you get errors. The statement

*ptr_to_ptr = 12;
 

assigns the value 12 to ptr, which results in ptr's pointing to whatever happens to be stored at address 12. This clearly is a mistake.

Declaring and using a pointer to a pointer is called multiple indirection. Figure 15.1 shows the relationship between a variable, a pointer, and a pointer to a pointer. There's really no limit to the level of multiple indirection possible--you can have a pointer to a pointer to a pointer ad infinitum, but there's rarely any advantage to going beyond two levels; the complexities involved are an invitation to mistakes.

Figure 15.1. A pointer to a pointer.

What can you use pointers to pointers for? The most common use involves arrays of pointers, which are covered later in this chapter. Listing 19.5 on Day 19, "Exploring the C Function Library," presents an example of using multiple indirection.

Pointers and Multidimensional Arrays

Day 8, "Using Numeric Arrays," covers the special relationship between pointers and arrays. Specifically, the name of an array without its following brackets is a pointer to the first element of the array. As a result, it's easier to use pointer notation when you're accessing certain types of arrays. These earlier examples, however, were limited to single-dimensional arrays. What about multidimensional arrays?

Remember that a multidimensional array is declared with one set of brackets for each dimension. For example, the following statement declares a two-dimensional array that contains eight type int variables:

int multi[2][4];
 

You can visualize an array as having a row and column structure--in this case, two rows and four columns. There's another way to visualize a multidimensional array, however, and this way is closer to the manner in which C actually handles arrays. You can consider multi to be a two-element array, with each of these two elements being an array of four integers.

In case this isn't clear to you, Figure 15.2 dissects the array declaration statement into its component parts.

Figure 15.2. The components of a multidimensional array declaration.

Here's how to interpret the components of the declaration:

1. Declare an array named multi.

2. The array multi contains two elements.

3. Each of these two elements contains four elements.

4. Each of the four elements is a type int.

You read a multidimensional array declaration starting with the array name and moving to the right, one set of brackets at a time. When the last set of brackets (the last dimension) has been read, you jump to the beginning of the declaration to determine the array's basic data type.

Under the array-of-arrays scheme, you can visualize a multidimensional array as shown in Figure 15.3.

Figure 15.3. A multidimensional array can be visualized as an array of arrays.

Now, let's get back to the topic of array names as pointers. (This is a chapter about pointers, after all!) As with a one-dimensional array, the name of a multidimensional array is a pointer to the first array element. Continuing with our example, multi is a pointer to the first element of the two-dimensional array that was declared as int multi[2][4]. What exactly is the first element of multi? It isn't the type int variable multi[0][0], as you might think. Remember that multi is an array of arrays, so its first element is multi[0], which is an array of four type int variables (one of the two such arrays contained in multi).

Now, if multi[0] is also an array, does it point to anything? Yes, indeed! multi[0] points to its first element, multi[0][0]. You might wonder why multi[0] is a pointer. Remember that the name of an array without brackets is a pointer to the first array element. The term multi[0] is the name of the array multi[0][0] with the last pair of brackets missing, so it qualifies as a pointer.

If you're a bit confused at this point, don't worry. This material is difficult to grasp. It might help if you remember the following rules for any array of n dimensions used in code:

  • The array name followed by n pairs of brackets (each pair containing an appropriate index, of course) evaluates as array data (that is, the data stored in the specified array element).

  • The array name followed by fewer than n pairs of brackets evaluates as a pointer to an array element.

In the example, therefore, multi evaluates as a pointer, multi[0] evaluates as a pointer, and multi[0][0] evaluates as array data.

Now look at what all these pointers actually point to. Listing 15.1 declares a two-dimensional array--similar to those you've been using in the examples--and then prints the values of the associated pointers. It also prints the address of the first array element.

Listing 15.1. The relationship between a multidimensional array and pointers.

1:    /* Demonstrates pointers and multidimensional arrays. */
 2:
 3:    #include <stdio.h>
 4:
 5:    int multi[2][4];
 6:
 7:    main()
 8:    {
 9:        printf("\nmulti = %u", multi);
 10:       printf("\nmulti[0] = %u", multi[0]);
 11:       printf("\n&multi[0][0] = %u\n", &multi[0][0]);
 13:       return(0);
 12:   }
 multi = 1328
 multi[0] = 1328
 &multi[0][0] = 1328
 

ANALYSIS: The actual value might not be 1328 on your system, but all three values will be the same. The address of the array multi is the same as the address of the array multi[0], and both are equal to the address of the first integer in the array, multi[0][0].

If all three of these pointers have the same value, what is the practical difference between them in terms of your program? Remember from Day 9 that the C compiler knows what a pointer points to. To be more exact, the compiler knows the size of the item a pointer is pointing to.

What are the sizes of the elements you've been using? Listing 15.2 uses the operator sizeof() to display the sizes, in bytes, of these elements.

Listing 15.2. Determining the sizes of elements.

1: /* Demonstrates the sizes of multidimensional array elements. */
 2:
 3: #include <stdio.h>
 4:
 5: int multi[2][4];
 6:
 7: main()
 8: {
 9:     printf("\nThe size of multi = %u", sizeof(multi));
 10:    printf("\nThe size of multi[0] = %u", sizeof(multi[0]));
 11:    printf("\nThe size of multi[0][0] = %u\n", sizeof(multi[0][0]));
 12:    return(0);
 13: }
 

The output of this program (assuming that your compiler uses two-byte integers) is as follows:

The size of multi = 16
 The size of multi[0] = 8
 The size of multi[0][0] = 2
 

ANALYSIS: If you're running a 32-bit operating system, such as IBM's OS/2, your output will be 32, 16, and 4. This is because a type int contains four bytes on these systems.

Think about these size values. The array multi contains two arrays, each of which contains four integers. Each integer requires two bytes of storage. With a total of eight integers, the size of 16 bytes makes sense.

Next, multi[0] is an array containing four integers. Each integer takes two bytes, so the size of eight bytes for multi[0] also makes sense.

Finally, multi[0][0] is an integer, so its size is, of course, two bytes.

Now, keeping these sizes in mind, recall the discussion on Day 9 about pointer arithmetic. The C compiler knows the size of the object being pointed to, and pointer arithmetic takes this size into account. When you increment a pointer, its value is increased by the amount needed to make it point to the "next" of whatever it's pointing to. In other words, it's incremented by the size of the object to which it points.

When you apply this to the example, multi is a pointer to a four-element integer array with a size of 8. If you increment multi, its value should increase by 8 (the size of a four-element integer array). If multi points to multi[0], therefore, (multi + 1) should point to multi[1]. Listing 15.3 tests this theory.

Listing 15.3. Pointer arithmetic with multidimensional arrays.

1: /*  Demonstrates pointer arithmetic with pointers */
 2: /*  to multidimensional arrays. */
 3:
 4: #include <stdio.h>
 5:
 6: int multi[2][4];
 7:
 8: main()
 9: {
 10:     printf("\nThe value of (multi) = %u", multi);
 11:     printf("\nThe value of (multi + 1) = %u", (multi+1));
 12:     printf("\nThe address of multi[1] = %u\n", &multi[1]);
 13:     return(0);
 14: }
 The value of (multi) = 1376
 The value of (multi + 1) = 1384
 The address of multi[1] = 1384
 

ANALYSIS: The precise values might be different on your system, but the relationships are the same. Incrementing multi by 1 increases its value by 8 (or by 16 on a 32-bit system) and makes it point to the next element of the array, multi[1].

In this example, you've seen that multi is a pointer to multi[0]. You've also seen that multi[0] is itself a pointer (to multi[0][0]). Therefore, multi is a pointer to a pointer. To use the expression multi to access array data, you must use double indirection. To print the value stored in multi[0][0], you could use any of the following three statements:

printf("%d", multi[0][0]);
 printf("%d", *multi[0]);
 printf("%d", **multi);
 

These concepts apply equally to arrays with three or more dimensions. Thus, a three-dimensional array is an array with elements that are each a two-dimensional array; each of these elements is itself an array of one-dimensional arrays.

This material on multidimensional arrays and pointers might seem a bit confusing. When you work with multidimensional arrays, keep this point in mind: An array with n dimensions has elements that are arrays with n-1 dimensions. When n becomes 1, that array's elements are variables of the data type specified at the beginning of the array declaration line.

So far, you've been using array names that are pointer constants and that can't be changed. How would you declare a pointer variable that points to an element of a multidimensional array? Let's continue with the previous example, which declared a two-dimensional array as follows:

int multi[2][4];
 

To declare a pointer variable ptr that can point to an element of multi (that is, can point to a four-element integer array), you would write

int (*ptr)[4];
 

You could then make ptr point to the first element of multi by writing

ptr = multi;
 

You might wonder why the parentheses are necessary in the pointer declaration. Brackets ([ ]) have a higher precedence than *. If you wrote

int *ptr[4];
 

you would be declaring an array of four pointers to type int. Indeed, you can declare and use arrays of pointers. This isn't what you want to do now, however.

How can you use pointers to elements of multidimensional arrays? As with single-dimensional arrays, pointers must be used to pass an array to a function. This is illustrated for a multidimensional array in Listing 15.4, which uses two methods of passing a multidimensional array to a function.

Listing 15.4. Passing a multidimensional array to a function using a pointer.

1:  /* Demonstrates passing a pointer to a multidimensional */
 2:  /* array to a function. */
 3:
 4:  #include <stdio.h>
 5:
 6:  void printarray_1(int (*ptr)[4]);
 7:  void printarray_2(int (*ptr)[4], int n);
 8:
 9:  main()
 10: {
 11:    int  multi[3][4] = { { 1, 2, 3, 4 },
 12:                       { 5, 6, 7, 8 },
 13:                       { 9, 10, 11, 12 } };
 14:
 15:    /* ptr is a pointer to an array of 4 ints. */
 16:
 17:    int (*ptr)[4], count;
 18:
 19:    /* Set ptr to point to the first element of multi. */
 20:
 21:    ptr = multi;
 22:
 23:    /* With each loop, ptr is incremented to point to the next */
 24:    /* element (that is, the next 4-element integer array) of multi. */
 25:
 26:    for (count = 0; count < 3; count++)
 27:        printarray_1(ptr++);
 28:
 29:    puts("\n\nPress Enter...");
 30:    getchar();
 31:    printarray_2(multi, 3);
 32:    printf("\n");
 33:    return(0);
 34: }
 35
 36: void printarray_1(int (*ptr)[4])
 37: {
 38: /* Prints the elements of a single four-element integer array. */
 39: /* p is a pointer to type int. You must use a type cast */
 40: /* to make p equal to the address in ptr. */
 41:
 42:      int *p, count;
 43:      p = (int *)ptr;
 44:
 45:      for (count = 0; count < 4; count++)
 46:          printf("\n%d", *p++);
 47: }
 48:
 49: void printarray_2(int (*ptr)[4], int n)
 50: {
 51: /* Prints the elements of an n by four-element integer array. */
 52:
 53:      int *p, count;
 54:      p = (int *)ptr;
 55:
 56:      for (count = 0; count < (4 * n); count++)
 57:          printf("\n%d", *p++);
 58: }
 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 Press Enter...
 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 

ANALYSIS: On lines 11 through 13, the program declares and initializes an array of integers, multi[3][4]. Lines 6 and 7 are the prototypes for the functions printarray_1() and printarray_2(), which print the contents of the array.

The function printarray_1() (lines 36 through 47) is passed only one argument, a pointer to an array of four integers. This function prints all four elements of the array. The first time main() calls printarray_1() on line 27, it passes a pointer to the first element (the first four-element integer array) in multi. It then calls the function two more times, incrementing the pointer each time to point to the second, and then to the third, element of multi. After all three calls are made, the 12 integers in multi are displayed on-screen.

The second function, printarray_2(), takes a different approach. It too is passed a pointer to an array of four integers, but, in addition, it is passed an integer variable that specifies the number of elements (the number of arrays of four integers) that the multidimensional array contains. With a single call from line 31, printarray_2() displays the entire contents of multi.

Both functions use pointer notation to step through the individual integers in the array. The notation (int *)ptr in both functions (lines 43 and 54) might not be clear. The (int *) is a typecast, which temporarily changes the variable's data type from its declared data type to a new one. The typecast is required when assigning the value of ptr to p because they are pointers to different types (p is a pointer to type int, whereas ptr is a pointer to an array of four integers). C doesn't let you assign the value of one pointer to a pointer of a different type. The typecast tells the compiler, "For this statement only, treat ptr as a pointer to type int." Day 20, "Working with Memory," covers typecasts in more detail.


DON'T forget to use the double indirection operator (**) when declaring a pointer to a pointer.

DON'T forget that a pointer increments by the size of the pointer's type (usually what is being pointed to).

DON'T forget to use parentheses when declaring pointers to arrays.
To declare a pointer to an array of characters, use this format:

char (*letters)[26];

 
 
 

To declare an array of pointers to characters, use this format:


char *letters[26];
 

Arrays of Pointers

Recall from Day 8, "Using Numeric Arrays," that an array is a collection of data storage locations that have the same data type and are referred to by the same name. Because pointers are one of C's data types, you can declare and use arrays of pointers. This type of program construct can be very powerful in certain situations.

Perhaps the most common use of an array of pointers is with strings. A string, as you learned on Day 10, "Characters and Strings," is a sequence of characters stored in memory. The start of the string is indicated by a pointer to the first character (a pointer to type char), and the end of the string is marked by a null character. By declaring and initializing an array of pointers to type char, you can access and manipulate a large number of strings using the pointer array. Each element in the array points to a different string, and by looping through the array, you can access each of them in turn.

Strings and Pointers: A Review

This is a good time to review some material from Day 10 regarding string allocation and initialization. One way to allocate and initialize a string is to declare an array of type char as follows:

char message[] = "This is the message.";
 

You could accomplish the same thing by declaring a pointer to type char:

char *message = "This is the message.";
 

Both declarations are equivalent. In each case, the compiler allocates enough space to hold the string along with its terminating null character, and the expression message is a pointer to the start of the string. But what about the following two declarations?

char message1[20];
 char *message2;
 

The first line declares an array of type char that is 20 characters long, with message1 being a pointer to the first array position. Although the array space is allocated, it isn't initialized, and the array contents are undetermined. The second line declares message2, a pointer to type char. No storage space for a string is allocated by this statement--only space to hold the pointer. If you want to create a string and then have message2 point to it, you must allocate space for the string first. On Day 10, you learned how to use the malloc() memory allocation function for this purpose. Remember that any string must have space allocated for it, whether at compilation in a declaration or at runtime with malloc() or a related memory allocation function.

Array of Pointers to Type char

Now that you're done with the review, how do you declare an array of pointers? The following statement declares an array of 10 pointers to type char:

char *message[10];
 

Each element of the array message[] is an individual pointer to type char. As you might have guessed, you can combine the declaration with initialization and allocation of storage space for the strings:

char *message[10] = { "one", "two", "three" };
 

This declaration does the following:

  • It allocates a 10-element array named message; each element of message is a pointer to type char.

  • It allocates space somewhere in memory (exactly where doesn't concern you) and stores the three initialization strings, each with a terminating null character.

  • It initializes message[0] to point to the first character of the string "one", message[1] to point to the first character of the string "two", and message[2] to point to the first character of the string "three".

This is illustrated in Figure 15.4, which shows the relationship between the array of pointers and the strings. Note that in this example, the array elements message[3] through message[9] aren't initialized to point at anything.

Figure 15.4. An array of pointers to type char.

Now look at Listing 15.5, which is an example of using an array of pointers.

Listing 15.5. Initializing and using an array of pointers to type char.

1: /* Initializing an array of pointers to type char. */
 2:
 3: #include <stdio.h>
 4:
 5: main()
 6: {
 7:     char *message[8] = { "Four", "score", "and", "seven",
 8:                     "years", "ago,", "our", "forefathers" };
 9:   int count;
 10:
 11:     for (count = 0; count < 8; count++)
 12:         printf("%s ", message[count]);
 13:     printf("\n");
 14:     return(0);
 15: }
 
Four score and seven years ago, our forefathers
 

ANALYSIS: This program declares an array of eight pointers to type char and initializes them to point to eight strings (lines 7 and 8). It then uses a for loop on lines 11 and 12 to display each element of the array on-screen.

You probably can see how manipulating the array of pointers is easier than manipulating the strings themselves. This advantage is obvious in more complicated programs, such as the one presented later in this chapter. As you'll see in that program, the advantage is greatest when you're using functions. It's much easier to pass an array of pointers to a function than to pass several strings. This can be illustrated by rewriting the program in Listing 15.5 so that it uses a function to display the strings. The modified program is shown in Listing 15.6.

Listing 15.6. Passing an array of pointers to a function.

1: /* Passing an array of pointers to a function. */
 2:
 3: #include <stdio.h>
 4:
 5: void print_strings(char *p[], int n);
 6:
 7: main()
 8: {
 9:     char *message[8] = { "Four", "score", "and", "seven",
 10:                     "years", "ago,", "our", "forefathers" };
 11:
 12:     print_strings(message, 8);
 13:     return(0);
 14: }
 15:
 16: void print_strings(char *p[], int n)
 17: {
 18:     int count;
 19:
 20:     for (count = 0; count < n; count++)
 21:         printf("%s ", p[count]);
 22:    printf("\n");
 23:  }
 
Four score and seven years ago, our forefathers
 

ANALYSIS: Looking at line 16, you see that the function print_strings() takes two arguments. One is an array of pointers to type char, and the other is the number of elements in the array. Thus, print_strings() could be used to print the strings pointed to by any array of pointers.

You might remember that, in the section on pointers to pointers, you were told that you would see a demonstration later. Well, you've just seen it. Listing 15.6 declared an array of pointers, and the name of the array is a pointer to its first element. When you pass that array to a function, you're passing a pointer (the array name) to a pointer (the first array element).

An Example

Now it's time for a more complicated example. Listing 15.7 uses many of the programming skills you've learned, including arrays of pointers. This program accepts lines of input from the keyboard, allocating space for each line as it is entered and keeping track of the lines by means of an array of pointers to type char. When you signal the end of an entry by entering a blank line, the program sorts the strings alphabetically and displays them on-screen.

If you were writing this program from scratch, you would approach the design of this program from a structured programming perspective. First, make a list of the things the program must do:

1. Accept lines of input from the keyboard one at a time until a blank line is entered.

2. Sort the lines of text into alphabetical order.

3. Display the sorted lines on-screen.

This list suggests that the program should have at least three functions: one to accept input, one to sort the lines, and one to display the lines. Now you can design each function independently. What do you need the input function--called get_lines()--to do? Again, make a list:

1. Keep track of the number of lines entered, and return that value to the calling program after all lines have been entered.

2. Don't allow input of more than a preset maximum number of lines.

3. Allocate storage space for each line.

4. Keep track of all lines by storing pointers to strings in an array.

5. Return to the calling program when a blank line is entered.

Now think about the second function, the one that sorts the lines. It could be called sort(). (Really original, right?) The sort technique used is a simple, brute-force method that compares adjacent strings and swaps them if the second string is less than the first string. More exactly, the function compares the two strings whose pointers are adjacent in the array of pointers and swaps the two pointers if necessary.

To be sure that the sorting is complete, you must go through the array from start to finish, comparing each pair of strings and swapping if necessary. For an array of n elements, you must go through the array n-1 times. Why is this necessary?

Each time you go through the array, a given element can be shifted by, at most, one position. For example, if the string that should be first is actually in the last position, the first pass through the array moves it to the next-to-last position, the second pass through the array moves it up one more position, and so on. It requires n-1 passes to move it to the first position, where it belongs.

Note that this is a very inefficient and inelegant sorting method. However, it's easy to implement and understand, and it's more than adequate for the short lists that the sample program sorts.

The final function displays the sorted lines on-screen. It is, in effect, already written in List- ing 15.6, and it requires only minor modification for use in Listing 15.7.

Listing 15.7. A program that reads lines of text from the keyboard, sorts them alphabetically, and displays the sorted list.

1:  /* Inputs a list of strings from the keyboard, sorts them, */
 2:  /* and then displays them on the screen. */
 3:  #include <stdlib.h>
 4:  #include <stdio.h>
 5:  #include <string.h>
 6:
 7:  #define MAXLINES 25
 8:
 9:  int get_lines(char *lines[]);
 10: void sort(char *p[], int n);
 11: void print_strings(char *p[], int n);
 12:
 13: char *lines[MAXLINES];
 14:
 15: main()
 16: {
 17:     int number_of_lines;
 18:
 19:     /* Read in the lines from the keyboard. */
 20:
 21:     number_of_lines = get_lines(lines);
 22:
 23:     if ( number_of_lines < 0 )
 24:     {
 25:         puts(" Memory allocation error");
 26:         exit(-1);
 27:     }
 28:
 29:     sort(lines, number_of_lines);
 30:     print_strings(lines, number_of_lines);
 31:     return(0);
 32: }
 33:
 34: int get_lines(char *lines[])
 35: {
 36:     int n = 0;
 37:     char buffer[80];  /* Temporary storage for each line. */
 38:
 39:     puts("Enter one line at time; enter a blank when done.");
 40:
 41:     while ((n < MAXLINES) && (gets(buffer) != 0) &&
 42:            (buffer[0] != `\0'))
 43:     {
 44:         if ((lines[n] = (char *)malloc(strlen(buffer)+1)) == NULL)
 45:             return -1;
 46:         strcpy( lines[n++], buffer );
 47:     }
 48:     return n;
 49:
 50: } /* End of get_lines() */
 51:
 52: void sort(char *p[], int n)
 53: {
 54:     int a, b;
 55:     char *x;
 56:
 57:     for (a = 1; a < n; a++)
 58:     {
 59:         for (b = 0; b < n-1; b++)
 60:         {
 61:              if (strcmp(p[b], p[b+1]) > 0)
 62:             {
 63:                  x = p[b];
 64:                  p[b] = p[b+1];
 65:                  p[b+1] = x;
 66:             }
 67:         }
 68:     }
 69: }
 70:
 71: void print_strings(char *p[], int n)
 72: {
 73:     int count;
 74:
 75:     for (count = 0; count < n; count++)
 76:         printf("%s\n ", p[count]);
 77: }
 Enter one line at time; enter a blank when done.
 dog
 apple
 zoo
 program
 merry
 apple
 dog
 merry
 program
 zoo
 

ANALYSIS: It will be worthwhile for you to examine some of the details of this program. Several new library functions are used for various types of string manipulation. They are explained briefly here and in more detail on Day 17, "Manipulating Strings." The header file STRING.H must be included in a program that uses these functions.

In the get_lines() function, input is controlled by the while statement on lines 41 and 42, which read as follows (condensed here onto one line):

while ((n < MAXLINES) && (gets(buffer) != 0) && (buffer[0] != `\0'))
 

The condition tested by the while has three parts. The first part, n < MAXLINES, ensures that the maximum number of lines has not been input yet. The second part, gets(buffer) != 0, calls the gets() library function to read a line from the keyboard into buffer and verifies that end-of-file or some other error has not occurred. The third part, buffer[0] != `\0', verifies that the first character of the line just input is not the null character, which would signal that a blank line had been entered.

If any of these three conditions isn't satisfied, the while loop terminates, and execution returns to the calling program, with the number of lines entered as the return value. If all three conditions are satisfied, the following if statement on line 44 is executed:

if ((lines[n] = (char *)malloc(strlen(buffer)+1)) == NULL)
 

This statement calls malloc() to allocate space for the string that was just input. The strlen() function returns the length of the string passed as an argument; the value is incremented by 1 so that malloc() allocates space for the string plus its terminating null character.

The library function malloc(), you might remember, returns a pointer. The statement assigns the value of the pointer returned by malloc() to the corresponding element of the array of pointers. If malloc() returns NULL, the if loop returns execution to the calling program with a return value of -1. The code in main() tests the return value of get_lines() and checks whether a value less than 0 is returned; lines 23 through 27 report a memory allocation error and terminate the program.

If the memory allocation was successful, the program uses the strcpy() function on line 46 to copy the string from the temporary storage location buffer to the storage space just allocated by malloc(). The while loop then repeats, getting another line of input.

Once execution returns from get_lines() to main(), the following has been accomplished (assuming that a memory allocation error didn't occur):

  • A number of lines of text have been read from the keyboard and stored in memory as null-terminated strings.

  • The array lines[] contains pointers to all the strings. The order of pointers in the array is the order in which the strings were input.

  • The variable number_of_lines holds the number of lines that were input.

Now it's time to sort. Remember, you're not actually going to move the strings around, only the order of the pointers in the array lines[]. Look at the code in the function sort(). It contains one for loop nested inside another (lines 57 through 68). The outer loop executes number_of_lines - 1 times. Each time the outer loop executes, the inner loop steps through the array of pointers, comparing (string n) with (string n+1) for n = 0 to n = number_of_lines - 1. The comparison is performed by the library function strcmp() on line 61, which is passed pointers to two strings. The function strcmp() returns one of the following:

  • A value greater than zero if the first string is greater than the second string.

  • Zero if the two strings are identical.

  • A value less than zero if the second string is greater than the first string.

In the program, a return value from strcmp() that is greater than zero means that the first string is "greater than" the second string, and they must be swapped (that is, their pointers in lines[] must be swapped). This is done using a temporary variable x. Lines 63 through 65 perform the swap.

When program execution returns from sort(), the pointers in lines[] are ordered properly: A pointer to the "lowest" string is in lines[0], a pointer to the "next-lowest" is in lines[1], and so on. Suppose, for example, that you entered the following five lines, in this order:

dog
 apple
 zoo
 program
 merry
 

The situation before calling sort() is illustrated in Figure 15.5, and the situation after the return from sort() is illustrated in Figure 15.6.

Figure 15.5. Before sorting, the pointers are in the same order in which the strings were entered.

Figure 15.6. After sorting, the pointers are ordered according to the alphabetical order of the strings.

Finally, the program calls the function print_strings() to display the sorted list of strings on-screen. This function should be familiar to you from previous examples in this chapter.

The program in Listing 15.7 is the most complex you have yet encountered in this book. It uses many of the C programming techniques that were covered in previous chapters. With the aid of the preceding explanation, you should be able to follow the program's operation and understand each step. If you find areas that are unclear to you, review the related sections of this book until you understand.

Pointers to Functions

Pointers to functions provide another way of calling functions. "Hold on," you might be thinking. "How can you have a pointer to a function? Doesn't a pointer hold the address where a variable is stored?"

Well, yes and no. It's true that a pointer holds an address, but it doesn't have to be the address where a variable is stored. When your program runs, the code for each function is loaded into memory starting at a specific address. A pointer to a function holds the starting address of a function--its entry point.

Why use a pointer to a function? As I mentioned earlier, it provides a more flexible way of calling a function. It lets the program choose from among several functions, selecting the one that is appropriate for the current circumstances.

Declaring a Pointer to a Function

Like other pointers, you must declare a pointer to a function. The general form of the declaration is as follows:

type (*ptr_to_func)(parameter_list);
 

This statement declares ptr_to_func as a pointer to a function that returns type and is passed the parameters in parameter_list. Here are some more concrete examples:

int (*func1)(int x);
 void (*func2)(double y, double z);
 char (*func3)(char *p[]);
 void (*func4)();
 

The first line declares func1 as a pointer to a function that takes one type int argument and returns a type int. The second line declares func2 as a pointer to a function that takes two type double arguments and has a void return type (no return value). The third line declares func3 as a pointer to a function that takes an array of pointers to type char as its argument and returns a type char. The final line declares func4 as a pointer to a function that doesn't take any arguments and has a void return type.

Why do you need parentheses around the pointer name? Why can't you write, for the first example:

int *func1(int x);
 

The reason has to do with the precedence of the indirection operator, *. It has a relatively low precedence, lower than the parentheses surrounding the parameter list. The declaration just given, without the first set of parentheses, declares func1 as a function that returns a pointer to type int. (Functions that return pointers are covered on Day 18, "Getting More from Functions.") When you declare a pointer to a function, always remember to include a set of parentheses around the pointer name and indirection operator, or you will get into trouble.

Initializing and Using a Pointer to a Function

A pointer to a function must not only be declared, but also initialized to point to something. That "something" is, of course, a function. There's nothing special about a function that gets pointed to. The only requirement is that its return type and parameter list match the return type and parameter list of the pointer declaration. For example, the following code declares and defines a function and a pointer to that function:

float square(float x);     /* The function prototype.  */
 float (*p)(float x);       /* The pointer declaration. */
 float square(float x)      /* The function definition. */
 {
 return x * x;
 }
 

Because the function square() and the pointer p have the same parameter and return types, you can initialize p to point to square as follows:

p = square;
 

Then you can call the function using the pointer as follows:

answer = p(x);
 

It's that simple. For a real example, compile and run Listing 15.8, which declares and initializes a pointer to a function and then calls the function twice, using the function name the first time and the pointer the second time. Both calls produce the same result.

Listing 15.8. Using a pointer to a function to call the function.

1:  /* Demonstration of declaring and using a pointer to a function.*/
 2:
 3:  #include <stdio.h>
 4:
 5:  /* The function prototype. */
 6:
 7:  double square(double x);
 8:
 9:  /* The pointer declaration. */
 10:
 11: double (*p)(double x);
 12:
 13: main()
 14: {
 15:     /* Initialize p to point to square(). */
 16:
 17:     p = square;
 18:
 19:     /* Call square() two ways. */
 20:     printf("%f  %f\n", square(6.6), p(6.6));
 21:     return(0);
 22: }
 23:
 24: double square(double x)
 25: {
 26:     return x * x;
 27: }
 
43.559999  43.559999
 


NOTE: Precision of the values might cause some numbers to not display as the exact values entered. For example, the correct answer, 43.56, might appear as 43.559999.

Line 7 declares the function square(), and line 11 declares the pointer p to a function containing a double argument and returning a double value, matching the declaration of square(). Line 17 sets the pointer p equal to square. Notice that parentheses aren't used with square or p. Line 20 prints the return values from calls to square() and p().

A function name without parentheses is a pointer to the function (sounds similar to the situation with arrays, doesn't it?). What's the point of declaring and using a separate pointer to the function? Well, the function name itself is a pointer constant and can't be changed (again, a parallel to arrays). A pointer variable, in contrast, can be changed. Specifically, it can be made to point to different functions as the need arises.

Listing 15.9 calls a function, passing it an integer argument. Depending on the value of the argument, the function initializes a pointer to point to one of three other functions and then uses the pointer to call the corresponding function. Each of these three functions displays a specific message on-screen.

Listing 15.9. Using a pointer to a function to call different functions depending on program circumstances.

1:  /* Using a pointer to call different functions. */
 2:
 3:  #include <stdio.h>
 4:
 5:  /* The function prototypes. */
 6:
 7:  void func1(int x);
 8:  void one(void);
 9:  void two(void);
 10: void other(void);
 11:
 12: main()
 13: {
 14:      int a;
 15:
 16:     for (;;)
 17:     {
 18:         puts("\nEnter an integer between 1 and 10, 0 to exit: ");
 19:          scanf("%d", &a);
 20:
 21:          if (a == 0)
 22:             break;
 23:         func1(a);
 24:     }
 25:     return(0);
 26: }
 27:
 28: void func1(int x)
 29: {
 30:     /* The pointer to function. */
 31:
 32:     void (*ptr)(void);
 33:
 34:     if (x == 1)
 35:         ptr = one;
 36:     else if (x == 2)
 37:         ptr = two;
 38:     else
 39:         ptr = other;
 40:
 41:     ptr();
 42: }
 43:
 44: void one(void)
 45: {
 46:     puts("You entered 1.");
 47: }
 48:
 49: void two(void)
 50: {
 51:     puts("You entered 2.");
 52: }
 53:
 54: void other(void)
 55: {
 56:     puts("You entered something other than 1 or 2.");
 57: }
 Enter an integer between 1 and 10, 0 to exit:
 2
 You entered 2.
 Enter an integer between 1 and 10, 0 to exit:
 9
 You entered something other than 1 or 2.
 Enter an integer between 1 and 10, 0 to exit:
 0
 

ANALYSIS: This program employs an infinite loop starting on line 16 to continue execution until a value of 0 is entered. When a nonzero value is entered, it's passed to func1(). Note that line 32, in func1(), contains a declaration for a pointer ptr to a function. Being declared within a function makes ptr local to func1(), which is appropriate because no other part of the program needs access to it. func1() then uses this value to set ptr equal to the appropriate function (lines 34 through 39). Line 41 then makes a single call to ptr(), which calls the appropriate function.

Of course, this program is for illustration purposes only. You could have easily accomplished the same result without using a pointer to a function.

Now you can learn another way to use pointers to call different functions: passing the pointer as an argument to a function. Listing 15.10 is a revision of Listing 15.9.

Listing 15.10. Passing a pointer to a function as an argument.

1:  /* Passing a pointer to a function as an argument. */
 2:
 3:  #include <stdio.h>
 4:
 5:  /* The function prototypes. The function func1() takes as */
 6:  /* its one argument a pointer to a function that takes no */
 7:  /* arguments and has no return value. */
 8:
 9:  void func1(void (*p)(void));
 10: void one(void);
 11: void two(void);
 12: void other(void);
 13:
 14: main()
 15: {
 16:     /* The pointer to a function. */
 18:     void (*ptr)(void);
 19:     int  a;
 20:
 21:     for (;;)
 22:     {
 23:         puts("\nEnter an integer between 1 and 10, 0 to exit: ");
 24:         scanf("%d", &a);
 25:
 26:         if (a == 0)
 27:             break;
 28:         else if (a == 1)
 29:             ptr = one;
 30:         else if (a == 2)
 31:             ptr = two;
 32:         else
 33:             ptr = other;
 34:         func1(ptr);
 35:     }
 36:     return(0);
 37: }
 38:
 39: void func1(void (*p)(void))
 40: {
 41:     p();
 42: }
 43:
 44: void one(void)
 45: {
 46:     puts("You entered 1.");
 47: }
 48:
 49: void two(void)
 50: {
 51:     puts("You entered 2.");
 52: }
 53:
 54: void other(void)
 55: {
 56:     puts("You entered something other than 1 or 2.");
 57: }
 Enter an integer between 1 and 10, 0 to exit:
 2
 You entered 2.
 Enter an integer between 1 and 10, 0 to exit:
 11
 You entered something other than 1 or 2.
 Enter an integer between 1 and 10, 0 to exit:
 0
 

ANALYSIS: Notice the differences between Listing 15.9 and Listing 15.10. The declaration of the pointer to a function has been moved to line 18 in main(), where it is needed. Code in main() now initializes the pointer to point to the correct function, depending on the value the user entered (lines 26 through 33), and then passes the initialized pointer to func1(). func1() really serves no purpose in Listing 15.10; all it does is call the function pointed to by ptr. Again, this program is for illustration purposes. The same principles can be used in real-world programs, such as the example in the next section.

One programming situation in which you might use pointers to functions is when sorting is required. Sometimes you might want different sorting rules used. For example, you might want to sort in alphabetical order one time and in reverse alphabetical order another time. By using pointers to functions, your program can call the correct sorting function. More precisely, it's usually a different comparison function that's called.

Look back at Listing 15.7. In the sort() function, the actual sort order is determined by the value returned by the strcmp() library function, which tells the program whether a given string is "less than" or "greater than" another string. What if you wrote two comparison functions--one that sorts alphabetically (where A is less than Z), and another that sorts in reverse alphabetical order (where Z is less than A)? The program can ask the user what order he wants and, by using pointers, the sorting function can call the proper comparison function. Listing 15.11 modifies Listing 15.7 to incorporate this feature.

Listing 15.11. Using pointers to functions to control sort order.

1:  /* Inputs a list of strings from the keyboard, sorts them */
 2:  /* in ascending or descending order, and then displays them */
 3:  /* on the screen. */
 4:  #include <stdlib.h>
 5:  #include <stdio.h>
 6:  #include <string.h>
 7:
 8:  #define MAXLINES 25
 9:
 10:  int get_lines(char *lines[]);
 11:  void sort(char *p[], int n, int sort_type);
 12:  void print_strings(char *p[], int n);
 13:  int alpha(char *p1, char *p2);
 14:  int reverse(char *p1, char *p2);
 15:
 16:  char *lines[MAXLINES];
 17:
 18:  main()
 19:  {
 20:     int number_of_lines, sort_type;
 21:
 22:     /* Read in the lines from the keyboard. */
 23:
 24:     number_of_lines = get_lines(lines);
 25:
 26:     if ( number_of_lines < 0 )
 27:     {
 28:        puts("Memory allocation error");
 29:        exit(-1);
 30:     }
 31:
 32:     puts("Enter 0 for reverse order sort, 1 for alphabetical:" );
 33:     scanf("%d", &sort_type);
 34:
 35:     sort(lines, number_of_lines, sort_type);
 36:     print_strings(lines, number_of_lines);
 37:     return(0);
 38:  }
 39:
 40:  int get_lines(char *lines[])
 41:  {
 42:      int n = 0;
 43:      char buffer[80];  /* Temporary storage for each line. */
 44:
 45:      puts("Enter one line at time; enter a blank when done.");
 46:
 47:      while (n < MAXLINES && gets(buffer) != 0 && buffer[0] != `\0')
 48:      {
 49:          if ((lines[n] = (char *)malloc(strlen(buffer)+1)) == NULL)
 50:          return -1;
 51:          strcpy( lines[n++], buffer );
 52:      }
 53:      return n;
 54:
 55:  } /* End of get_lines() */
 56:
 57:  void sort(char *p[], int n, int sort_type)
 58:  {
 59:      int a, b;
 60:      char *x;
 61:
 62:      /* The pointer to function.  */
 63:
 64:      int (*compare)(char *s1, char *s2);
 65:
 66:      /* Initialize the pointer to point to the proper comparison */
 67:      /* function depending on the argument sort_type. */
 68:
 69:      compare = (sort_type) ? reverse : alpha;
 70:
 71:      for (a = 1; a < n; a++)
 72:      {
 73:          for (b = 0; b < n-1; b++)
 74:          {
 75:              if (compare(p[b], p[b+1]) > 0)
 76:              {
 77:                  x = p[b];
 78:                  p[b] = p[b+1];
 79:                  p[b+1] = x;
 80:              }
 81:          }
 82:      }
 83:  }  /* end of sort() */
 84:
 85:  void print_strings(char *p[], int n)
 86:  {
 87:      int count;
 88:
 89:      for (count = 0; count < n; count++)
 90:          printf("%s\n ", p[count]);
 91:  }
 92:
 93:  int alpha(char *p1, char *p2)
 94:  /* Alphabetical comparison. */
 95:  {
 96:      return(strcmp(p2, p1));
 97:  }
 98:
 99:  int reverse(char *p1, char *p2)
 100: /* Reverse alphabetical comparison. */
 101: {
 102:      return(strcmp(p1, p2));
 103: }
 Enter one line at time; enter a blank when done.
 Roses are red
 Violets are blue
 C has been around,
 But it is new to you!
 Enter 0 for reverse order sort, 1 for alphabetical:
 0
 Violets are blue
 Roses are red
 C has been around,
 But it is new to you!
 

ANALYSIS: Lines 32 and 33 in main() prompt the user for the desired sort order. The value entered is placed in sort_type. This value is passed to the sort() function along with the other information described for Listing 15.7. The sort() function contains a couple of changes. Line 64 declares a pointer to a function called compare() that takes two character pointers (strings) as arguments. Line 69 sets compare() equal to one of the two new functions added to the listing based on the value of sort_type. The two new functions are alpha() and reverse(). alpha() uses the strcmp() library function just as it was used in Listing 15.7; reverse() does not. reverse() switches the parameters passed so that a reverse-order sort is done.


DO use structured programming.

DON'T forget to use parentheses when declaring pointers to functions.
Here's how you declare a pointer to a function that takes no arguments and returns a character:

char (*func)();

 
 
 

Here's how you declare a function that returns a pointer to a character:

char *func();

 
 
 

DO initialize a pointer before using it.

DON'T use a function pointer that has been declared with a different return type or different arguments than what you need.


Linked Lists

A linked list is a useful method of data storage that can easily be implemented in C. Why are we covering linked lists in a chapter on pointers? Because, as you will soon see, pointers are central to linked lists.

There are several kinds of linked lists, including single-linked lists, double-linked lists, and binary trees. Each type is suited for certain types of data storage. The one thing that these lists have in common is that the links between data items are defined by information that is contained in the items themselves, in the form of pointers. This is distinctly different from arrays, in which the links between data items result from the layout and storage of the array. This section explains the most fundamental kind of linked list: the single-linked list (which I refer to as simply a linked list).

Basics of Linked Lists

Each data item in a linked list is contained in a structure (you learned about structures on Day 11, "Structures"). The structure contains the data elements needed to hold the data being stored; these depend on the needs of the specific program. In addition, there is one more data element--a pointer. This pointer provides the links in a linked list. Here's a simple example:

struct person {
 char name[20];
 struct person *next;
 };
 

This code defines a structure named person. For the data, person contains only a 20-element array of characters. You generally wouldn't use a linked list for such simple data, but this will serve for an example. The person structure also contains a pointer to type person--in other words, a pointer to another structure of the same type. This means that each structure of type person can not only contain a chunk of data, but also can point to another person structure. Figure 15.7 shows how this lets the structures be linked together in a list.

Figure 15.7. Links in a linked list.

Notice that in Figure 15.7, each person structure points to the next person structure. The last person structure doesn't point to anything. The last element in a linked list is identified by the pointer element being assigned the value of NULL.


NOTE: The structures that make up a link in a linked list can be referred to as links, nodes, or elements of a linked list.

You have seen how the last link in a linked list is identified. What about the first link? This is identified by a special pointer (not a structure) called the head pointer. The head pointer always points to the first element in the linked list. The first element contains a pointer to the second element, the second element contains a pointer to the third, and so on until you encounter an element whose pointer is NULL. If the entire list is empty (contains no links), the head pointer is set to NULL. Figure 15.8 illustrates the head pointer before the list is started and after the first list element is added.

Figure 15.8. A linked list's head pointer.


NOTE: The head pointer is a pointer to the first element in a linked list. The head pointer is sometimes referred to as the first element pointer or top pointer.

Working with Linked Lists

When you're working with a linked list, you can add, delete, or modify elements or links. Modifying an element presents no real challenge; however, adding and deleting elements can. As I stated earlier, elements in a list are connected with pointers. Much of the work of adding and deleting elements consists of manipulating these pointers. Elements can be added to the beginning, middle, or end of a linked list; this determines how the pointers must be changed.

Later in this chapter you'll find a simple linked list demonstration, as well as a more complex working program. Before getting into the nitty-gritty of code, however, it's a good idea to examine some of the actions you need to perform with linked lists. For these sections, we will continue using the person structure that was introduced earlier.

Preliminaries

Before you can start a linked list, you need to define the data structure that will be used for the list, and you also need to declare the head pointer. Since the list starts out empty, the head pointer should be initialized to NULL. You will also need an additional pointer to your list structure type for use in adding records (you might need more than one pointer, as you'll soon see). Here's how you do it:

struct person {
   char name[20];
   struct person *next;
 };
 struct person *new;
 struct person *head;
 head = NULL;
 

Adding an Element to the Beginning of a List

If the head pointer is NULL, the list is empty, and the new element will be its only member. If the head pointer is not NULL, the list already contains one or more elements. In either case, however, the procedure for adding a new element to the start of the list is the same:

1. Create an instance of your structure, allocating memory space using malloc().

2. Set the next pointer of the new element to the current value of the head pointer. This will be NULL if the list is empty, or the address of the current first element otherwise.

3. Make the head pointer point to the new element.

Here is the code to perform this task:

new = (person*)malloc(sizeof(struct person));
 new->next = head;
 head = new
 


WARNING: It's important to switch the pointers in the correct order. If you reassign the head pointer first, you will lose the list!

Figure 15.9 illustrates the procedure for adding a new element to an empty list, and Figure 15.10 illustrates adding a new first element to an existing list.

Figure 15.9. Adding a new element to an empty linked list.

Figure 15.10. Adding a new first element to an existing list.

Notice that malloc() is used to allocate the memory for the new element. As each new element is added, only the memory needed for it is allocated. The calloc() function could also be used. You should be aware of the differences between these two functions. The main difference is that calloc() will initialize the new element; malloc() will not.



WARNING: The malloc() in the preceding code fragment didn't ensure that the memory was allocated. You should always check the return value of a memory allocation function.



TIP: When possible, initialize pointers to NULL when you declare them. Never leave a pointer uninitialized.

Adding an Element to the End of the List

To add an element to the end of a linked list, you need to start at the head pointer and go through the list until you find the last element. After you've found it, follow these steps:

1. Create an instance of your structure, allocating memory space using malloc().

2. Set the next pointer in the last element to point to the new element (whose address is returned by malloc()).

3. Set the next pointer in the new element to NULL to signal that it is the last item in the list.

Here is the code:

person *current;
 ...
 current = head;
 while (current->next != NULL)
     current = current->next;
 new = (person*)malloc(sizeof(struct person));
 current->next = new;
 new->next = NULL;
 

Figure 15.11 illustrates the procedure for adding a new element to the end of a linked list.

Figure 15.11. Adding a new element to the end of a linked list.


Adding an Element to the Middle of the List

When you're working with a linked list, most of the time you will be adding elements somewhere in the middle of the list. Exactly where the new element is placed depends on how you're keeping the list--for example, if it is sorted on one or more data elements. This process, then, requires that you first locate the position in the list where the new element will go, and then add it. Here are the steps to follow:

1. In the list, locate the existing element that the new element will be placed after. Let's call this the marker element.

2. Create an instance of your structure, allocating memory space using malloc().

3. Set the next pointer of the marker element to point to the new element (whose address is returned by malloc()).

4. Set the next pointer of the new element to point to the element that the marker element used to point to.

Here's how the code might look:

person *marker;
 /* Code here to set marker to point to the desired list location. */
 ...
 new = (LINK)malloc(sizeof(PERSON));
 new->next = marker->next;
 marker->next = new;
 

Figure 15.12 illustrates this process.

Figure 15.12. Adding a new element to the middle of a linked list.


Deleting an Element from the List

Deleting an element from a linked list is a simple matter of manipulating pointers. The exact process depends on where in the list the element is located:

  • To delete the first element, set the head pointer to point to the second element in the list.

  • To delete the last element, set the next pointer of the next-to-last element to NULL.

  • To delete any other element, set the next pointer of the element before the one being deleted to point to the element after the one being deleted.

Here's the code to delete the first element in a linked list:

head = head->next;
 

This code deletes the last element in the list:

person *current1, *current2;
 current1 = head;
 current2= current1->next;
 while (current2->next != NULL)
 {
     current1 = current2;
     current2= current1->next;
 }
 current1->next = null;
 if (head == current1)
     head = null;
 

Finally, the following code deletes an element from within the list:

person *current1, *current2;
 /* Code goes here to have current1 point to the */
 /* element just before the one to be deleted. */
 current2 = current1->next;
 current1->next = current2->next;
 

After any of these procedures, the deleted element still exists in memory, but it is removed from the list because there is no pointer in the list pointing to it. In a real-world program, you would want to reclaim the memory occupied by the "deleted" element. This is accomplished with the free() function. You'll learn about this function in detail on Day 20, "Working with Memory."

A Simple Linked List Demonstration

Listing 15.12 demonstrates the basics of using a linked list. This program is clearly for demonstration purposes only, because it doesn't accept user input and doesn't do anything useful other than show the code required for the most basic linked list tasks. The program does the following:

1. It defines a structure and the required pointers for the list.

2. It adds the first element to the list.

3. It adds an element to the end of the list.

4. It adds an element to the middle of the list.

5. It displays the list contents on-screen.

Listing 15.12. The basics of a linked list.

1:  /* Demonstrates the fundamentals of using */
 2:  /* a linked list. */
 3:
 4:  #include <stdlib.h>
 5:  #include <stdio.h>
 6:  #include <string.h>
 7:
 8:  /* The list data structure. */
 9:  struct data {
 10:     char name[20];
 11:     struct data *next;
 12:     };
 13:
 14: /* Define typedefs for the structure */
 15: /* and a pointer to it. */
 16: typedef struct data PERSON;
 17: typedef PERSON *LINK;
 18:
 19: main()
 20: {
 21: /* Head, new, and current element pointers. */
 22: LINK head = NULL;
 23: LINK new = NULL;
 24: LINK current = NULL;
 25:
 26: /* Add the first list element. We do not */
 27: /* assume the list is empty, although in */
 28: /* this demo program it always will be. */
 29:
 30: new = (LINK)malloc(sizeof(PERSON));
 31: new->next = head;
 32: head = new;
 33: strcpy(new->name, "Abigail");
 34:
 35: /* Add an element to the end of the list. */
 36: /* We assume the list contains at least one element. */
 37:
 38: current = head;
 39: while (current->next != NULL)
 40: {
 41:     current = current->next;
 42: }
 43:
 44: new = (LINK)malloc(sizeof(PERSON));
 45: current->next = new;
 46: new->next = NULL;
 47: strcpy(new->name, "Catherine");
 48:
 49: /* Add a new element at the second position in the list. */
 50: new = (LINK)malloc(sizeof(PERSON));
 51: new->next = head->next;
 52: head->next = new;
 53: strcpy(new->name, "Beatrice");
 54:
 55: /* Print all data items in order. */
 56: current = head;
 57: while (current != NULL)
 58: {
 59:     printf("\n%s", current->name);
 60:     current = current->next;
 61: }
 62:
 63: printf("\n");
 64: return(0);
 65: }
 Abigail
 Beatrice
 Catherine
 

ANALYSIS: You can probably figure out at least some of the code. Lines 9 through 12 declare the data structure for the list. Lines 16 and 17 define typedefs for both the data structure and for a pointer to the data structure. Strictly speaking, this isn't necessary, but it simplifies coding by allowing you to write PERSON in place of struct data and LINK in place of struct data *.

Lines 22 through 24 declare a head pointer and a couple of other pointers that will be used when manipulating the list. All of these pointers are initialized to NULL.

Lines 30 through 33 add a new link to the start of the list. Line 30 allocates a new data structure. Note that the successful operation of malloc() is assumed--something you would never do in a real program!

Line 31 sets the next pointer in this new structure to point to whatever the head pointer contains. Why not simply assign NULL to this pointer? That works only if you know that the list is empty. As it is written, the code will work even if the list already contains some elements. The new first element will end up pointing to the element that used to be first, which is just what you want.

Line 32 makes the head pointer point to the new record, and line 33 stores some data in the record.

Adding an element to the end of the list is a bit more complicated. Although in this case you know that the list contains only one element, you can't assume this in a real program. Therefore, it's necessary to loop through the list, starting with the first element, until you find the last element (as indicated by the next pointer's being NULL). Then you know you have found the end of the list. This task is accomplished in lines 38 through 42. Once you have found the last element, it is a simple matter to allocate a new data structure, have the old last element point to it, and set the new element's next pointer to NULL, because it is now the last element in the list. This is done in lines 44 through 47.

The next task is to add an element to the middle of the list--in this case, at the second position. After a new data structure is allocated (line 50), the new element's next pointer is set to point to the element that used to be second and is now third in the list (line 51), and the first element's next pointer is made to point to the new element (line 52).

Finally, the program prints all the records in the linked list. This is a simple matter of starting with the element that the head pointer points to and then progressing through the list until the last list element is found, as indicated by a NULL pointer. Lines 56 through 61 perform this task.

Implementing a Linked List

Now that you have seen the ways to add links to a list, it's time to see them in action. Listing 15.13 is a rather long program that uses a linked list to hold a list of five characters. The characters are stored in memory using a linked list. These characters just as easily could have been names, addresses, or any other data. To keep the example as simple as possible, only a single character is stored in each link.

What makes this linked list program complicated is the fact that it sorts the links as they are added. Of course, this also is what makes this program so valuable. Each link is added to the beginning, middle, or end, depending on its value. The link is always sorted. If you were to write a program that simply added the links to the end, the logic would be much simpler. However, the program also would be less useful.

Listing 15.13. Implementing a linked list of characters.

1:  /*========================================================*
 2:   * Program:  list1513.c                                   *
 3:   * Book:     Teach Yourself C in 21 Days                  *
 4:   * Purpose:  Implementing a linked list                   *
 5:   *========================================================*/
 6:  #include <stdio.h>
 7:  #include <stdlib.h>
 8:
 9:  #ifndef NULL
 10: #define NULL 0
 11: #endif
 12:
 13: /* List data structure */
 14: struct list
 15: {
 16:   int    ch;     /* using an int to hold a char */
 17:   struct list *next_rec;
 18: };
 19:
 20: /* Typedefs for the structure and pointer. */
 21: typedef struct list LIST;
 22: typedef LIST *LISTPTR;
 23:
 24: /* Function prototypes. */
 25: LISTPTR add_to_list( int, LISTPTR );
 26: void show_list(LISTPTR);
 27: void free_memory_list(LISTPTR);
 28:
 29: int main( void )
 30: {
 31:    LISTPTR first = NULL;  /* head pointer */
 32:    int i = 0;
 33:    int ch;
 34:    char trash[256];       /* to clear stdin buffer. */
 35:
 36:    while ( i++ < 5 )      /* build a list based on 5 items given */
 37:    {
 38:       ch = 0;
 39:       printf("\nEnter character %d, ", i);
 40:
 41:       do
 42:       {
 43:           printf("\nMust be a to z: ");
 44:           ch = getc(stdin);  /* get next char in buffer  */
 45:           gets(trash);       /* remove trash from buffer */
 46:       } while( (ch < `a' || ch > `z') && (ch < `A' || ch > `Z'));
 47:
 48:       first = add_to_list( ch, first );
 49:    }
 50:
 51:    show_list( first );           /* Dumps the entire list */
 52:    free_memory_list( first );    /* Release all memory */
 53:    return(0);
 54: }
 55:
 56: /*========================================================*
 57:  * Function: add_to_list()
 58:  * Purpose : Inserts new link in the list
 59:  * Entry   : int ch = character to store
 60:  *           LISTPTR first = address of original head pointer
 61:  * Returns : Address of head pointer (first)
 62:  *========================================================*/
 63:
 64: LISTPTR add_to_list( int ch, LISTPTR first )
 65: {
 66:    LISTPTR new_rec = NULL;       /* Holds address of new rec */
 67:    LISTPTR tmp_rec = NULL;       /* Hold tmp pointer         */
 68:    LISTPTR prev_rec = NULL;
 69:
 70:    /* Allocate memory. */
 71:    new_rec = (LISTPTR)malloc(sizeof(LIST));
 72:    if (!new_rec)      /* Unable to allocate memory */
 73:    {
 74:       printf("\nUnable to allocate memory!\n");
 75:       exit(1);
 76:    }
 77:
 78:    /* set new link's data */
 79:    new_rec->ch = ch;
 80:    new_rec->next_rec = NULL;
 81:
 82:    if (first == NULL)   /* adding first link to list */
 83:    {
 84:        first = new_rec;
 85:        new_rec->next_rec = NULL;  /* redundant but safe */
 86:    }
 87:    else    /* not first record */
 88:    {
 89:       /* see if it goes before the first link */
 90:       if ( new_rec->ch < first->ch)
 91:       {
 92:          new_rec->next_rec = first;
 93:          first = new_rec;
 94:       }
 95:       else   /* it is being added to the middle or end */
 96:       {
 97:          tmp_rec = first->next_rec;
 98:          prev_rec = first;
 99:
 100:          /* Check to see where link is added. */
 101:
 102:          if ( tmp_rec == NULL )
 103:          {
 104:              /* we are adding second record to end */
 105:              prev_rec->next_rec = new_rec;
 106:          }
 107:          else
 108:          {
 109:             /* check to see if adding in middle */
 110:             while (( tmp_rec->next_rec != NULL))
 111:             {
 112:                if( new_rec->ch < tmp_rec->ch )
 113:                {
 114:                   new_rec->next_rec = tmp_rec;
 115:                   if (new_rec->next_rec != prev_rec->next_rec)
 116:                   {
 117:                      printf("ERROR");
 118:                      getc(stdin);
 119:                      exit(0);
 120:                   }
 121:                   prev_rec->next_rec = new_rec;
 122:                   break;   /* link is added; exit while */
 123:                }
 124:                else
 125:                {
 126:                  tmp_rec = tmp_rec->next_rec;
 127:                  prev_rec = prev_rec->next_rec;
 128:                }
 129:             }
 130:
 131:             /* check to see if adding to the end */
 132:             if (tmp_rec->next_rec == NULL)
 133:             {
 134:                if (new_rec->ch < tmp_rec->ch ) /* 1 b4 end */
 135:                {
 136:                   new_rec->next_rec = tmp_rec;
 137:                   prev_rec->next_rec = new_rec;
 138:                }
 139:                else  /* at the end */
 140:                {
 141:                   tmp_rec->next_rec = new_rec;
 142:                   new_rec->next_rec = NULL;  /* redundant */
 143:                }
 144:             }
 145:          }
 146:       }
 147:    }
 148:    return(first);
 149: }
 150:
 151: /*========================================================*
 152:  * Function: show_list
 153:  * Purpose : Displays the information current in the list
 154:  *========================================================*/
 155:
 156: void show_list( LISTPTR first )
 157: {
 158:    LISTPTR cur_ptr;
 159:    int counter = 1;
 160:
 161:    printf("\n\nRec addr  Position  Data  Next Rec addr\n");
 162:    printf("========  ========  ====  =============\n");
 163:
 164:    cur_ptr = first;
 165:    while (cur_ptr != NULL )
 166:    {
 167:       printf("  %X   ", cur_ptr );
 168:       printf("     %2i       %c", counter++, cur_ptr->ch);
 169:       printf("      %X   \n",cur_ptr->next_rec);
 170:       cur_ptr = cur_ptr->next_rec;
 171:    }
 172: }
 173:
 174: /*========================================================*
 175:  * Function: free_memory_list
 176:  * Purpose : Frees up all the memory collected for list
 177:  *========================================================*/
 178:
 179: void free_memory_list(LISTPTR first)
 180: {
 181:    LISTPTR cur_ptr, next_rec;
 182:    cur_ptr = first;                 /* Start at beginning */
 183:
 184:    while (cur_ptr != NULL)          /* Go while not end of list */
 185:    {
 186:       next_rec = cur_ptr->next_rec; /* Get address of next record */
 187:       free(cur_ptr);                /* Free current record */
 188:       cur_ptr = next_rec;           /* Adjust current record*/
 189:    }
 190: }
 Enter character 1,
 Must be a to z: q
 Enter character 2,
 Must be a to z: b
 Enter character 3,
 Must be a to z: z
 Enter character 4,
 Must be a to z: c
 Enter character 5,
 Must be a to z: a
 Rec addr  Position  Data  Next Rec addr
 ========  ========  ====  =============
 C3A         1       a      C22
 C22         2       b      C32
 C32         3       c      C1A
 C1A         4       q      C2A
 C2A         5       z      0
 


NOTE: Your output will probably show different address values.

ANALYSIS: This program demonstrates adding a link to a linked list. It isn't the easiest listing to understand; however, if you walk through it, you'll see that it's a combination of the three methods of adding links that were discussed earlier. This listing can be used to add links to the beginning, middle, or end of a linked list. Additionally, this listing takes into consideration the special cases of adding the first link (the one that gets added to the beginning) and the second link (the one that gets added to the middle).


TIP: The easiest way to fully understand this listing is to step through it line-by-line in your compiler's debugger and to read the following analysis. By seeing the logic executed, you will better understand the listing.

Several items at the beginning of Listing 15.13 should be familiar or easy to understand. Lines 9 through 11 check to see whether the value of NULL is already defined. If it isn't, line 10 defines it to be 0. Lines 14 through 22 define the structure for the linked list and also declare the type definitions to make working with the structure and pointers easier.

The main() function should be easy to follow. A head pointer called first is declared in line 31. Notice that this is initialized to NULL. Remember that you should never let a pointer go uninitialized. Lines 36 through 49 contain a while loop that is used to get five characters from the user. Within this outer while loop, which repeats five times, a do...while is used to ensure that each character entered is a letter. The isalpha() function could have been used just as easily.

After a piece of data is obtained, add_to_list() is called. The pointer to the beginning of the list and the data being added to the list are passed to the function.

The main() function ends by calling show_list() to display the list's data and then free_memory_list() to release all the memory that was allocated to hold the links in the list. Both these functions operate in a similar manner. Each starts at the beginning of the linked list using the head pointer first. A while loop is used to go from one link to the next using the next_ptr value. When next_ptr is equal to NULL, the end of the linked list has been reached, and the functions return.

The most important (and most complicated!) function in this listing is add_to_list() in lines 56 through 149. Lines 66 through 68 declare three pointers that will be used to point to three different links. The new_rec pointer will point to the new link that is to be added. The tmp_rec pointer will point to the current link in the list being evaluated. If there is more than one link in the list, the prev_rec pointer will be used to point to the previous link that was evaluated.

Line 71 allocates memory for the new link that is being added. The new_rec pointer is set to the value returned by malloc(). If the memory can't be allocated, lines 74 and 75 print an error message and exit the program. If the memory was allocated successfully, the program continues.

Line 79 sets the data in the structure to the data passed to this function. This simply consists of assigning the character passed to the function ch to the new record's character field (new_rec->ch). In a more complex program, this could entail the assigning of several fields. Line 80 sets the next_rec in the new record to NULL so that it doesn't point to some random location.

Line 82 starts the "add a link" logic by checking to see whether there are any links in the list. If the link being added is the first link in the list, as indicated by the head pointer first being NULL, the head pointer is simply set equal to the new pointer, and you're done.

If this link isn't the first, the function continues within the else at line 87. Line 90 checks to see whether the new link goes at the beginning of the list. As you should remember, this is one of the three cases for adding a link. If the link does go first, line 92 sets the next_rec pointer in the new link to point to the previous "first" link. Line 93 then sets the head pointer, first, to point to the new link. This results in the new link's being added to the beginning of the list.

If the new link isn't the first link to be added to an empty list, and if it's being added at the first position in an existing list, you know it must be in the middle or at the end of the list. Lines 97 and 98 set up the tmp_rec and prev_rec pointers that were declared earlier. The pointer tmp_rec is set to the address of the second link in the list, and prev_rec is set to the first link in the list.

You should note that if there is only one link in the list, tmp_rec will be equal to NULL. This is because tmp_rec is set to the next_ptr in the first link, which will be equal to NULL. Line 102 checks for this special case. If tmp_rec is NULL, you know that this is the second link being added to the list. Because you know the new link doesn't come before the first link, it can only go at the end. To accomplish this, you simply set prev_rec->next_ptr to the new link, and then you're done.

If the tmp_rec pointer isn't NULL, you know that you already have more than two links in your list. The while statement in lines 110 through 129 loops through the rest of the links to determine where the new link should be placed. Line 112 checks to see whether the new link's data value is less than the link currently being pointed to. If it is, you know this is where you want to add the link. If the new data is greater than the current link's data, you need to look at the next link in the list. Lines 126 and 127 set the pointers tmp_rec and next_rec to the next links.

If the character is "less than" the current link's character, you would follow the logic presented earlier in this chapter for adding to the middle of a linked list. This process can be seen in lines 114 through 122. In line 114, the new link's next pointer is set to equal the current link's address (tmp_rec). Line 121 sets the previous link's next pointer to point to the new link. After this, you're done. The code uses a break statement to get out of the while loop.


NOTE: Lines 115 through 120 contain debugging code that was left in the listing for you to see. These lines could be removed; however, as long as the program is running correctly, they will never be called. After the new link's next pointer is set to the current pointer, it should be equal to the previous link's next pointer, which also points to the current record. If they aren't equal, something went wrong!

The previously covered logic takes care of links being added to the middle of the list. If the end of the list is reached, the while loop in lines 110 through 129 will end without adding the link. Lines 132 through 144 take care of adding the link to the end.

If the last link in the list was reached, tmp_rec->next_rec will equal NULL. Line 132 checks for this condition. Line 134 checks to see whether the link goes before or after the last link. If it goes after the last link, the last link's next_rec is set to the new link (line 132), and the new link's next pointer is set to NULL (line 142).

Improving Listing 15.13

Linked lists are not the easiest thing to learn. As you can see from Listing 15.13, however, they are an excellent way of storing data in a sorted order. Because it's easy to add new data items anywhere in a linked list, the code for keeping a list of data items in sorted order with a linked list is a lot simpler that it would be if you used, say, an array. This listing could easily be converted to sort names, phone numbers, or any other data. Additionally, although this listing sorted in ascending order (A to Z), it just as easily could have sorted in descending order (Z to A).

Deleting from a Linked List

The capability to add information to a linked list is essential, but there will be times when you will want to remove information too. Deleting links, or elements, is similar to adding them. You can delete links from the beginning, middle, or end of linked lists. In each case, the appropriate pointers need to be adjusted. Also, the memory used by the deleted link needs to be freed.


NOTE: Don't forget to free memory when deleting links!

Структуры

A structure is a collection of one or more variables grouped under a single name for easy manipulation. The variables in a structure, unlike those in an array, can be of different variable types. A structure can contain any of C's data types, including arrays and other structures. Each variable within a structure is called a member of the structure. The next section shows a simple example.

You should start with simple structures. Note that the C language makes no distinction between simple and complex structures, but it's easier to explain structures in this way.

Defining and Declaring Structures

If you're writing a graphics program, your code needs to deal with the coordinates of points on the screen. Screen coordinates are written as an x value, giving the horizontal position, and a y value, giving the vertical position. You can define a structure named coord that contains both the x and y values of a screen location as follows:

struct coord {
     int x;
     int y;
 };
 

The struct keyword, which identifies the beginning of a structure definition, must be followed immediately by the structure name, or tag (which follows the same rules as other C variable names). Within the braces following the structure name is a list of the structure's member variables. You must give a variable type and name for each member.

The preceding statements define a structure type named coord that contains two integer variables, x and y. They do not, however, actually create any instances of the structure coord. In other words, they don't declare (set aside storage for) any structures. There are two ways to declare structures. One is to follow the structure definition with a list of one or more variable names, as is done here:

struct coord {
     int x;
     int y;
 } first, second;
 

These statements define the structure type coord and declare two structures, first and second, of type coord. first and second are each instances of type coord; first contains two integer members named x and y, and so does second.

This method of declaring structures combines the declaration with the definition. The second method is to declare structure variables at a different location in your source code from the definition. The following statements also declare two instances of type coord:

struct coord {
     int x;
     int y;
 };
 /* Additional code may go here */
 struct coord first, second;
 

Accessing Structure Members

Individual structure members can be used like other variables of the same type. Structure members are accessed using the structure member operator (.), also called the dot operator, between the structure name and the member name. Thus, to have the structure named first refer to a screen location that has coordinates x=50, y=100, you could write

first.x = 50;
 first.y = 100;
 

To display the screen locations stored in the structure second, you could write

printf("%d,%d", second.x, second.y);
 

At this point, you might be wondering what the advantage is of using structures rather than individual variables. One major advantage is that you can copy information between structures of the same type with a simple equation statement. Continuing with the preceding example, the statement

first = second;
 

is equivalent to this statement:

first.x = second.x;
 first.y = second.y;
 

When your program uses complex structures with many members, this notation can be a great time-saver. Other advantages of structures will become apparent as you learn some advanced techniques. In general, you'll find structures to be useful whenever information of different variable types needs to be treated as a group. For example, in a mailing list database, each entry could be a structure, and each piece of information (name, address, city, and so on) could be a structure member.

The struct Keyword

struct tag {
     structure_member(s);
     /* additional statements may go here */
 } instance;
 

The struct keyword is used to declare structures. A structure is a collection of one or more variables (structure_members) that have been grouped under a single name for easy man-ipulation. The variables don't have to be of the same variable type, nor do they have to be simple variables. Structures also can hold arrays, pointers, and other structures.

The keyword struct identifies the beginning of a structure definition. It's followed by a tag that is the name given to the structure. Following the tag are the structure members, enclosed in braces. An instance, the actual declaration of a structure, can also be defined. If you define the structure without the instance, it's just a template that can be used later in a program to declare structures. Here is a template's format:

struct tag {
     structure_member(s);
     /* additional statements may go here */
 };
 

To use the template, you use the following format:

struct tag instance;
 

To use this format, you must have previously declared a structure with the given tag.

Example 1

/* Declare a structure template called SSN */
 struct SSN {
     int first_three;
     char dash1;
     int second_two;
     char dash2;
     int last_four;
 }
 /* Use the structure template */
 struct SSN customer_ssn;
 

Example 2

/* Declare a structure and instance together */
 struct date {
     char month[2];
     char day[2];
     char year[4];
 } current_date;
 

Example 3

/* Declare and initialize a structure */
 struct time {
     int hours;
     int minutes;
     int seconds;
 } time_of_birth = { 8, 45, 0 };
 

More-Complex Structures

Now that you have been introduced to simple structures, you can get to the more interesting and complex types of structures. These are structures that contain other structures as members and structures that contain arrays as members.

Structures That Contain Structures

As mentioned earlier, a C structure can contain any of C's data types. For example, a structure can contain other structures. The previous example can be extended to illustrate this.

Assume that your graphics program needs to deal with rectangles. A rectangle can be defined by the coordinates of two diagonally opposite corners. You've already seen how to define a structure that can hold the two coordinates required for a single point. You need two such structures to define a rectangle. You can define a structure as follows (assuming, of course, that you have already defined the type coord structure):

struct rectangle {
     struct coord topleft;
     struct coord bottomrt;
 };
 

This statement defines a structure of type rectangle that contains two structures of type coord. These two type coord structures are named topleft and bottomrt.

The preceding statement defines only the type rectangle structure. To declare a structure, you must then include a statement such as

struct rectangle mybox;
 

You could have combined the definition and declaration, as you did before for the type coord:

struct rectangle {
     struct coord topleft;
     struct coord bottomrt;
 } mybox;
 

To access the actual data locations (the type int members), you must apply the member operator (.) twice. Thus, the expression

mybox.topleft.x
 

refers to the x member of the topleft member of the type rectangle structure named mybox. To define a rectangle with coordinates (0,10),(100,200), you would write

mybox.topleft.x = 0;
 mybox.topleft.y = 10;
 mybox.bottomrt.x = 100;
 mybox.bottomrt.y = 200;
 

Maybe this is getting a bit confusing. You might understand better if you look at Figure 11.1, which shows the relationship between the type rectangle structure, the two type coord structures it contains, and the two type int variables each type coord structure contains. These structures are named as in the preceding example.

Let's look at an example of using structures that contain other structures. Listing 11.1 takes input from the user for the coordinates of a rectangle and then calculates and displays the rectangle's area. Note the program's assumptions, given in comments near the start of the program (lines 3 through 8).

Figure 11.1. The relationship between a structure, structures within a structure, and the structure members.

Listing 11.1. A demonstration of structures that contain other structures.

1:  /* Demonstrates structures that contain other structures. */
 2:
 3:  /* Receives input for corner coordinates of a rectangle and
 4:     calculates the area. Assumes that the y coordinate of the
 5:     upper-left corner is greater than the y coordinate of the
 6:     lower-right corner, that the x coordinate of the lower-
 7:     right corner is greater than the x coordinate of the upper-
 8:     left corner, and that all coordinates are positive. */
 9:
 10: #include <stdio.h>
 11:
 12: int length, width;
 13: long area;
 14:
 15: struct coord{
 16:     int x;
 17:     int y;
 18: };
 19:
 20: struct rectangle{
 21:     struct coord topleft;
 22:     struct coord bottomrt;
 23: } mybox;
 24:
 25: main()
 26: {
 27:     /* Input the coordinates */
 28:
 29:     printf("\nEnter the top left x coordinate: ");
 30:     scanf("%d", &mybox.topleft.x);
 31:
 32:     printf("\nEnter the top left y coordinate: ");
 33:     scanf("%d", &mybox.topleft.y);
 34:
 35:     printf("\nEnter the bottom right x coordinate: ");
 36:     scanf("%d", &mybox.bottomrt.x);
 37:
 38:     printf("\nEnter the bottom right y coordinate: ");
 39:     scanf("%d", &mybox.bottomrt.y);
 40:
 41:     /* Calculate the length and width */
 42:
 43:     width = mybox.bottomrt.x - mybox.topleft.x;
 44:     length = mybox.bottomrt.y - mybox.topleft.y;
 45:
 46:     /* Calculate and display the area */
 47:
 48:     area = width * length;
 49:     printf("\nThe area is %ld units.\n", area);
 50:
 51:     return 0;
 52: }
 Enter the top left x coordinate: 1
 Enter the top left y coordinate: 1
 Enter the bottom right x coordinate: 10
 Enter the bottom right y coordinate: 10
 The area is 81 units.
 

ANALYSIS: The coord structure is defined in lines 15 through 18 with its two members, x and y. Lines 20 through 23 declare and define an instance, called mybox, of the rectangle structure. The two members of the rectangle structure are topleft and bottomrt, both structures of type coord.

Lines 29 through 39 fill in the values in the mybox structure. At first it might seem that there are only two values to fill, because mybox has only two members. However, each of mybox's members has its own members. topleft and bottomrt have two members each, x and y from the coord structure. This gives a total of four members to be filled. After the members are filled with values, the area is calculated using the structure and member names. When using the x and y values, you must include the structure instance name. Because x and y are in a structure within a structure, you must use the instance names of both structures--mybox.bottomrt.x, mybox.bottomrt.y, mybox.topleft.x, and mybox.topleft.y--in the calculations.

C places no limits on the nesting of structures. While memory allows, you can define structures that contain structures that contain structures that contain structures--well, you get the idea! Of course, there's a limit beyond which nesting becomes unproductive. Rarely are more than three levels of nesting used in any C program.

Structures That Contain Arrays

You can define a structure that contains one or more arrays as members. The array can be of any C data type (int, char, and so on). For example, the statements

struct data{
     int  x[4];
     char y[10];
 };
 

define a structure of type data that contains a four-element integer array member named x and a 10-element character array member named y. You can then declare a structure named record of type data as follows:

struct data record;
 

The organization of this structure is shown in Figure 11.2. Note that, in this figure, the elements of array x take up twice as much space as the elements of array y. This is because a type int typically requires two bytes of storage, whereas a type char usually requires only one byte (as you learned on Day 3, "Storing Data: Variables and Constants").

Figure 11.2. The organization of a structure that contains arrays as members.

You access individual elements of arrays that are structure members using a combination of the member operator and array subscripts:

record.x[2] = 100;
 record.y[1] = `x';
 

You probably remember that character arrays are most frequently used to store strings. You should also remember (from Day 9, "Understanding Pointers") that the name of an array, without brackets, is a pointer to the array. Because this holds true for arrays that are structure members, the expression

record.y
 

is a pointer to the first element of array y[] in the structure record. Therefore, you could print the contents of y[] on-screen using the statement

puts(record.y);
 

Now look at another example. Listing 11.2 uses a structure that contains a type float variable and two type char arrays.

Listing 11.2. A structure that contains array members.

1:  /* Demonstrates a structure that has array members. */
 2:
 3:  #include <stdio.h>
 4:
 5:  /* Define and declare a structure to hold the data. */
 6:  /* It contains one float variable and two char arrays. */
 7:
 8:  struct data{
 9:      float amount;
 10:     char fname[30];
 11:     char lname[30];
 12: } rec;
 13:
 14: main()
 15: {
 16:     /* Input the data from the keyboard. */
 17:
 18:     printf("Enter the donor's first and last names,\n");
 19:     printf("separated by a space: ");
 20:     scanf("%s %s", rec.fname, rec.lname);
 21:
 22:     printf("\nEnter the donation amount: ");
 23:     scanf("%f", &rec.amount);
 24:
 25:     /* Display the information. */
 26:     /* Note: %.2f specifies a floating-point value */
 27:     /* to be displayed with two digits to the right */
 28:     /* of the decimal point. */
 29:
 30:     /* Display the data on the screen. */
 31:
 32:     printf("\nDonor %s %s gave $%.2f.\n", rec.fname, rec.lname,
 33:             rec.amount);
 34:
 35:     return 0;
 36: }
 Enter the donor's first and last names,
 separated by a space: Bradley Jones
 Enter the donation amount: 1000.00
 Donor Bradley Jones gave $1000.00.
 

ANALYSIS: This program includes a structure that contains array members named fname[30] and lname[30]. Both are arrays of characters that hold a person's first name and last name, respectively. The structure declared in lines 8 through 12 is called data. It contains the fname and lname character arrays with a type float variable called amount. This structure is ideal for holding a person's name (in two parts, first name and last name) and a value, such as the amount the person donated to a charitable organization.

An instance of the array, called rec, has also been declared in line 12. The rest of the program uses rec to get values from the user (lines 18 through 23) and then print them (lines 32 and 33).

Arrays of Structures

If you can have structures that contain arrays, can you also have arrays of structures? You bet you can! In fact, arrays of structures are very powerful programming tools. Here's how it's done.

You've seen how a structure definition can be tailored to fit the data your program needs to work with. Usually a program needs to work with more than one instance of the data. For example, in a program to maintain a list of phone numbers, you can define a structure to hold each person's name and number:

struct entry{
     char fname[10];
     char lname[12];
     char phone[8];
 };
 

A phone list must hold many entries, however, so a single instance of the entry structure isn't of much use. What you need is an array of structures of type entry. After the structure has been defined, you can declare an array as follows:

struct entry list[1000];
 

This statement declares an array named list that contains 1,000 elements. Each element is a structure of type entry and is identified by subscript like other array element types. Each of these structures has three elements, each of which is an array of type char. This entire complex creation is diagrammed in Figure 11.3.

Figure 11.3. The organization of the array of structures defined in the text.

When you have declared the array of structures, you can manipulate the data in many ways. For example, to assign the data in one array element to another array element, you would write

list[1] = list[5];
 

This statement assigns to each member of the structure list[1] the values contained in the corresponding members of list[5]. You can also move data between individual structure members. The statement

strcpy(list[1].phone, list[5].phone);
 

copies the string in list[5].phone to list[1].phone. (The strcpy() library function copies one string to another string. You'll learn the details of this on Day 17, "Manipulating Strings.") If you want to, you can also move data between individual elements of the structure member arrays:

list[5].phone[1] = list[2].phone[3];
 

This statement moves the second character of list[5]'s phone number to the fourth position in list[2]'s phone number. (Don't forget that subscripts start at offset 0.)

Listing 11.3 demonstrates the use of arrays of structures. Moreover, it demonstrates arrays of structures that contain arrays as members.

Listing 11.3. Arrays of structures.

1:  /* Demonstrates using arrays of structures. */
 2:
 3:  #include <stdio.h>
 4:
 5:  /* Define a structure to hold entries. */
 6:
 7:  struct entry {
 8:      char fname[20];
 9:      char lname[20];
 10:     char phone[10];
 11: };
 12:
 13: /* Declare an array of structures. */
 14:
 15: struct entry list[4];
 16:
 17: int i;
 18:
 19: main()
 20: {
 21:
 22:     /* Loop to input data for four people. */
 23:
 24:     for (i = 0; i < 4; i++)
 25:     {
 26:         printf("\nEnter first name: ");
 27:         scanf("%s", list[i].fname);
 28:         printf("Enter last name: ");
 29:         scanf("%s", list[i].lname);
 30:         printf("Enter phone in 123-4567 format: ");
 31:         scanf("%s", list[i].phone);
 32:     }
 33:
 34:     /* Print two blank lines. */
 35:
 36:     printf("\n\n");
 37:
 38:     /* Loop to display data. */
 39:
 40:     for (i = 0; i < 4; i++)
 41:     {
 42:         printf("Name: %s %s", list[i].fname, list[i].lname);
 43:         printf("\t\tPhone: %s\n", list[i].phone);
 44:     }
 45:
 46:     return 0;
 47: }
 Enter first name: Bradley
 Enter last name: Jones
 Enter phone in 123-4567 format: 555-1212
 Enter first name: Peter
 Enter last name: Aitken
 Enter phone in 123-4567 format: 555-3434
 Enter first name: Melissa
 Enter last name: Jones
 Enter phone in 123-4567 format: 555-1212
 Enter first name: Deanna
 Enter last name: Townsend
 Enter phone in 123-4567 format: 555-1234
 Name: Bradley Jones             Phone: 555-1212
 Name: Peter Aitken              Phone: 555-3434
 Name: Melissa Jones             Phone: 555-1212
 Name: Deanna Townsend           Phone: 555-1234
 

ANALYSIS: This listing follows the same general format as most of the other listings. It starts with the comment in line 1 and, for the input/output functions, the #include file STDIO.H in line 3. Lines 7 through 11 define a template structure called entry that contains three character arrays: fname, lname, and phone. Line 15 uses the template to define an array of four entry structure variables called list. Line 17 defines a variable of type int to be used as a counter throughout the program. main() starts in line 19. The first function of main() is to perform a loop four times with a for statement. This loop is used to get information for the array of structures. This can be seen in lines 24 through 32. Notice that list is being used with a subscript in the same way as the array variables on Day 8, "Using Numeric Arrays," were subscripted.

Line 36 provides a break from the input before starting with the output. It prints two new lines in a manner that shouldn't be new to you. Lines 40 through 44 display the data that the user entered in the preceding step. The values in the array of structures are printed with the subscripted array name followed by the member operator (.) and the structure member name.

Familiarize yourself with the techniques used in Listing 11.3. Many real-world programming tasks are best accomplished by using arrays of structures containing arrays as members.


DON'T forget the structure instance name and member operator (.) when using a structure's members.

DON'T confuse a structure's tag with its instances! The tag is used to declare the structure's template, or format. The instance is a variable declared using the tag.

DON'T forget the struct keyword when declaring an instance from a previously defined structure.

DO declare structure instances with the same scope rules as other variables. (Day 12, "Understanding Variable Scope," covers this topic fully.)


Initializing Structures

Like other C variable types, structures can be initialized when they're declared. This procedure is similar to that for initializing arrays. The structure declaration is followed by an equal sign and a list of initialization values separated by commas and enclosed in braces. For example, look at the following statements:

1: struct sale {
 2:     char customer[20];
 3:     char item[20];
 4:     float amount;
 5: } mysale = { "Acme Industries",
 6:              "Left-handed widget",
 7:               1000.00
 8:            };
 

When these statements are executed, they perform the following actions:

1. Define a structure type named sale (lines 1 through 5).

2. Declare an instance of structure type sale named mysale (line 5).

3. Initialize the structure member mysale.customer to the string "Acme Industries" (line 5).

4. Initialize the structure member mysale.item to the string "Left-handed widget" (line 6).

5. Initialize the structure member mysale.amount to the value 1000.00 (line 7).

For a structure that contains structures as members, list the initialization values in order. They are placed in the structure members in the order in which the members are listed in the structure definition. Here's an example that expands on the previous one:

1:  struct customer {
 2:      char firm[20];
 3:      char contact[25];
 4:  }
 5:
 6:  struct sale {
 7:     struct customer buyer;
 8:     char item[20];
 9:     float amount;
 10: } mysale = { { "Acme Industries", "George Adams"},
 11:                "Left-handed widget",
 12:                 1000.00
 13:            };
 

These statements perform the following initializations:

1. The structure member mysale.buyer.firm is initialized to the string "Acme Industries" (line 10).

2. The structure member mysale.buyer.contact is initialized to the string "George Adams" (line 10).

3. The structure member mysale.item is initialized to the string "Left-handed widget" (line 11).

4. The structure member mysale.amount is initialized to the amount 1000.00 (line 12).

You can also initialize arrays of structures. The initialization data that you supply is applied, in order, to the structures in the array. For example, to declare an array of structures of type sale and initialize the first two array elements (that is, the first two structures), you could write

1:  struct customer {
 2:      char firm[20];
 3:      char contact[25];
 4:  };
 5:
 6:  struct sale {
 7:      struct customer buyer;
 8:      char item[20];
 9:      float amount;
 10: };
 11:
 12:
 13: struct sale y1990[100] = {
 14:      { { "Acme Industries", "George Adams"},
 15:          "Left-handed widget",
 16:           1000.00
 17:      }
 18:      { { "Wilson & Co.", "Ed Wilson"},
 19:          "Type 12 gizmo",
 20:           290.00
 21:      }
 22: };
 

This is what occurs in this code:

1. The structure member y1990[0].buyer.firm is initialized to the string "Acme Industries" (line 14).

2. The structure member y1990[0].buyer.contact is initialized to the string "George Adams" (line 14).

3. The structure member y1990[0].item is initialized to the string "Left-handed widget" (line 15).

4. The structure member y1990[0].amount is initialized to the amount 1000.00 (line 16).

5. The structure member y1990[1].buyer.firm is initialized to the string "Wilson & Co." (line 18).

6. The structure member y1990[1].buyer.contact is initialized to the string "Ed Wilson" (line 18).

7. The structure member y1990[1].item is initialized to the string "Type 12 gizmo" (line 19).

8. The structure member y1990[1].amount is initialized to the amount 290.00 (line 20).

Structures and Pointers

Given that pointers are such an important part of C, you shouldn't be surprised to find that they can be used with structures. You can use pointers as structure members, and you can also declare pointers to structures. These topics are covered in the following sections.

Pointers as Structure Members

You have complete flexibility in using pointers as structure members. Pointer members are declared in the same manner as pointers that aren't members of structures--that is, by using the indirection operator (*). Here's an example:

struct data {
     int *value;
     int *rate;
 } first;
 

These statements define and declare a structure whose two members are both pointers to type int. As with all pointers, declaring them is not enough; you must, by assigning them the address of a variable, initialize them to point to something. If cost and interest have been declared to be type int variables, you could write

first.value = &cost;
 first.rate = &interest;
 

Now that the pointers have been initialized, you can use the indirection operator (*), as explained on Day 9. The expression *first.value evaluates to the value of cost, and the expression *first.rate evaluates to the value of interest.

Perhaps the type of pointer most frequently used as a structure member is a pointer to type char. Recall from Day 10, "Characters and Strings," that a string is a sequence of characters delineated by a pointer that points to the string's first character and a null character that indicates the end of the string. To refresh your memory, you can declare a pointer to type char and initialize it to point at a string as follows:

char *p_message;
 p_message = "Teach Yourself C In 21 Days";
 

You can do the same thing with pointers to type char that are structure members:

struct msg {
     char *p1;
     char *p2;
 } myptrs;
 myptrs.p1 = "Teach Yourself C In 21 Days";
 myptrs.p2 = "By SAMS Publishing";
 

Figure 11.4 illustrates the result of executing these statements. Each pointer member of the structure points to the first byte of a string, stored elsewhere in memory. Contrast this with Figure 11.3, which shows how data is stored in a structure that contains arrays of type char.

You can use pointer structure members anywhere a pointer can be used. For example, to print the pointed-to strings, you would write

printf("%s %s", myptrs.p1, myptrs.p2);
 

Figure 11.4. A structure that contains pointers to type char.

What's the difference between using an array of type char as a structure member and using a pointer to type char? These are both methods for "storing" a string in a structure, as shown here in the structure msg, which uses both methods:

struct msg {
     char p1[30];
     char *p2;
 } myptrs;
 

Recall that an array name without brackets is a pointer to the first array element. Therefore, you can use these two structure members in similar fashion:

strcpy(myptrs.p1, "Teach Yourself C In 21 Days");
 strcpy(myptrs.p2, "By SAMS Publishing");
 /* additional code goes here */
 puts(myptrs.p1);
 puts(myptrs.p2);
 

What's the difference between these methods? It is this: If you define a structure that contains an array of type char, every instance of that structure type contains storage space for an array of the specified size. Furthermore, you're limited to the specified size; you can't store a larger string in the structure. Here's an example:

struct msg {
     char p1[10];
     char p2[10];
 } myptrs;
 ...
 strcpy(p1, "Minneapolis"); /* Wrong! String longer than array.*/
 strcpy(p2, "MN");          /* OK, but wastes space because    */
                            /* string shorter than array.      */
 

If, on the other hand, you define a structure that contains pointers to type char, these restrictions don't apply. Each instance of the structure contains storage space for only the pointer. The actual strings are stored elsewhere in memory (but you don't need to worry about where in memory). There's no length restriction or wasted space. The actual strings aren't stored as part of the structure. Each pointer in the structure can point to a string of any length. That string becomes part of the structure, even though it isn't stored in the structure.

Pointers to Structures

A C program can declare and use pointers to structures, just as it can declare pointers to any other data storage type. As you'll see later in this chapter, pointers to structures are often used when passing a structure as an argument to a function. Pointers to structures are also used in a very powerful data storage method known as linked lists. Linked lists are explored on Day 15, "Pointers: Beyond the Basics."

For now, take a look at how your program can create and use pointers to structures. First, define a structure:

struct part {
     int number;
     char name[10];
 };
 

Now declare a pointer to type part:

struct part *p_part;
 

Remember, the indirection operator (*) in the declaration says that p_part is a pointer to type part, not an instance of type part.

Can the pointer be initialized now? No, because even though the structure part has been defined, no instances of it have been declared. Remember that it's a declaration, not a definition, that sets aside storage space in memory for a data object. Because a pointer needs a memory address to point to, you must declare an instance of type part before anything can point to it. Here's the declaration:

struct part gizmo;
 

Now you can perform the pointer initialization:

p_part = &gizmo;
 

This statement assigns the address of gizmo to p_part. (Recall the address-of operator, &, from Day 9.) Figure 11.5 shows the relationship between a structure and a pointer to the structure.

Figure 11.5. A pointer to a structure points to the structure's first byte.

Now that you have a pointer to the structure gizmo, how do you make use of it? One method uses the indirection operator (*). Recall from Day 9 that if ptr is a pointer to a data object, the expression *ptr refers to the object pointed to.

Applying this to the current example, you know that p_part is a pointer to the structure gizmo, so *p_part refers to gizmo. You then apply the structure member operator (.) to access individual members of gizmo. To assign the value 100 to gizmo.number, you could write

(*p_part).number = 100;
 

*p_part must be enclosed in parentheses because the (.) operator has a higher precedence than the (*) operator.

A second way to access structure members using a pointer to the structure is to use the indirect membership operator, which consists of the characters -> (a hyphen followed by the greater-than symbol). (Note that when they are used together in this way, C treats them as a single operator, not two.) This symbol is placed between the pointer name and the member name. For example, to access the number member of gizmo with the p_part pointer, you would write

p_part->number
 

Looking at another example, if str is a structure, p_str is a pointer to str, and memb is a member of str, you can access str.memb by writing

p_str->memb
 

Therefore, there are three ways to access a structure member:

  • Using the structure name

  • Using a pointer to the structure with the indirection operator (*)

  • Using a pointer to the structure with the indirect membership operator (->)

If p_str is a pointer to the structure str, the following expressions are all equivalent:

str.memb
 (*p_str).memb
 p_str->memb
 

Pointers and Arrays of Structures

You've seen that arrays of structures can be a very powerful programming tool, as can pointers to structures. You can combine the two, using pointers to access structures that are array elements.

To illustrate, here is a structure definition from an earlier example:

struct part {
     int number;
     char name[10];
 };
 

After the part structure is defined, you can declare an array of type part:

struct part data[100];
 

Next you can declare a pointer to type part and initialize it to point to the first structure in the array data:

struct part *p_part;
 p_part = &data[0];
 

Recall that the name of an array without brackets is a pointer to the first array element, so the second line could also have been written as

p_part = data;
 

You now have an array of structures of type part and a pointer to the first array element (that is, the first structure in the array). For example, you could print the contents of the first element using the statement

printf("%d %s", p_part->number, p_part->name);
 

What if you wanted to print all the array elements? You would probably use a for loop, printing one array element with each iteration of the loop. To access the members using pointer notation, you must change the pointer p_part so that with each iteration of the loop it points to the next array element (that is, the next structure in the array). How do you do this?

C's pointer arithmetic comes to your aid. The unary increment operator (++) has a special meaning when applied to a pointer: It means "increment the pointer by the size of the object it points to." Put another way, if you have a pointer ptr that points to a data object of type obj, the statement

ptr++;
 

has the same effect as

ptr += sizeof(obj);
 

This aspect of pointer arithmetic is particularly relevant to arrays because array elements are stored sequentially in memory. If a pointer points to array element n, incrementing the pointer with the (++) operator causes it to point to element n + 1. This is illustrated in Figure 11.6, which shows an array named x[] that consists of four-byte elements (for example, a structure containing two type int members, each two bytes long). The pointer ptr was initialized to point to x[0]; each time ptr is incremented, it points to the next array element.

Figure 11.6. With each increment, a pointer steps to the next array element.

What this means is that your program can step through an array of structures (or an array of any other data type, for that matter) by incrementing a pointer. This sort of notation is usually easier to use and more concise than using array subscripts to perform the same task. Listing 11.4 shows how you do this.

Listing 11.4. Accessing successive array elements by incrementing a pointer.

1:  /* Demonstrates stepping through an array of structures */
 2:  /* using pointer notation. */
 3:
 4:  #include <stdio.h>
 5:
 6:  #define MAX 4
 7:
 8:  /* Define a structure, then declare and initialize */
 9:  /* an array of four structures. */
 10:
 11: struct part {
 12:     int number;
 13:     char name[10];
 14: } data[MAX] = {1, "Smith",
 15:                2, "Jones",
 16:                3, "Adams",
 17:                4, "Wilson"
 18:                };
 19:
 20: /* Declare a pointer to type part, and a counter variable. */
 21:
 22: struct part *p_part;
 23: int count;
 24:
 25: main()
 26: {
 27:     /* Initialize the pointer to the first array element. */
 28:
 29:     p_part = data;
 30:
 31:     /* Loop through the array, incrementing the pointer */
 32:     /* with each iteration. */
 33:
 34:     for (count = 0; count < MAX; count++)
 35:     {
 36:         printf("At address %d: %d %s\n", p_part, p_part->number,
 37:                 p_part->name);
 38:         p_part++;
 39:     }
 40:
 41:     return 0;
 42: }
 At address 96: 1 Smith
 At address 108: 2 Jones
 At address 120: 3 Adams
 At address 132: 4 Wilson
 

ANALYSIS: First, in lines 11 through 18, this program declares and initializes an array of structures called data. A pointer called p_part is then defined in line 22 to be used to point to the data structure. The main() function's first task in line 29 is to set the pointer, p_part, to point to the part structure that was declared. All the elements are then printed using a for loop in lines 34 through 39 that increments the pointer to the array with each iteration. The program also displays the address of each element.

Look closely at the addresses displayed. The precise values might differ on your system, but they are in equal-sized increments--just the size of the structure part (most systems will have an increment of 12). This clearly illustrates that incrementing a pointer increases it by an amount equal to the size of the data object it points to.

Passing Structures as Arguments to Functions

Like other data types, a structure can be passed as an argument to a function. Listing 11.5 shows how to do this. This program is a modification of the program shown in Listing 11.2. It uses a function to display data on the screen, whereas Listing 11.2 uses statements that are part of main().

Listing 11.5. Passing a structure as a function argument.

1:  /* Demonstrates passing a structure to a function. */
 2:
 3:  #include <stdio.h>
 4:
 5:  /* Declare and define a structure to hold the data. */
 6:
 7:  struct data{
 8:      float amount;
 9:      char fname[30];
 10:     char lname[30];
 11: } rec;
 12:
 13: /* The function prototype. The function has no return value, */
 14: /* and it takes a structure of type data as its one argument. */
 15:
 16: void print_rec(struct data x);
 17:
 18: main()
 19: {
 20:     /* Input the data from the keyboard. */
 21:
 22:     printf("Enter the donor's first and last names,\n");
 23:     printf("separated by a space: ");
 24:     scanf("%s %s", rec.fname, rec.lname);
 25:
 26:     printf("\nEnter the donation amount: ");
 27:     scanf("%f", &rec.amount);
 28:
 29:     /* Call the display function. */
 30:     print_rec( rec );
 31:
 32:     return 0;
 33: }
 34: void print_rec(struct data x)
 35: {
 36:     printf("\nDonor %s %s gave $%.2f.\n", x.fname, x.lname,
 37:             x.amount);
 38: }
 Enter the donor's first and last names,
 separated by a space: Bradley Jones
 Enter the donation amount: 1000.00
 Donor Bradley Jones gave $1000.00.
 

ANALYSIS: Looking at line 16, you see the function prototype for the function that is to receive the structure. As you would with any other data type that was going to be passed, you need to include the proper arguments. In this case, it is a structure of type data. This is repeated in the header for the function in line 34. When calling the function, you only need to pass the structure instance name--in this case, rec (line 30). That's all there is to it. Passing a structure to a function isn't very different from passing a simple variable.

You can also pass a structure to a function by passing the structure's address (that is, a pointer to the structure). In fact, in older versions of C, this was the only way to pass a structure as an argument. It's not necessary now, but you might see older programs that still use this method. If you pass a pointer to a structure as an argument, remember that you must use the indirect membership operator (->) to access structure members in the function.


DON'T confuse arrays with structures!

DO take advantage of declaring a pointer to a structure--especially when using arrays of structures.

DON'T forget that when you increment a pointer, it moves a distance equivalent to the size of the data to which it points. In the case of a pointer to a structure, this is the size of the structure.

DO use the indirect membership operator (->) when working with a pointer to a structure.


Unions

Unions are similar to structures. A union is declared and used in the same ways that a structure is. A union differs from a structure in that only one of its members can be used at a time. The reason for this is simple. All the members of a union occupy the same area of memory. They are laid on top of each other.

Defining, Declaring, and Initializing Unions

Unions are defined and declared in the same fashion as structures. The only difference in the declarations is that the keyword union is used instead of struct. To define a simple union of a char variable and an integer variable, you would write the following:

union shared {
     char c;
     int i;
 };
 

This union, shared, can be used to create instances of a union that can hold either a character value c or an integer value i. This is an OR condition. Unlike a structure that would hold both values, the union can hold only one value at a time. Figure 11.7 illustrates how the shared union would appear in memory.

A union can be initialized on its declaration. Because only one member can be used at a time, only one can be initialized. To avoid confusion, only the first member of the union can be initialized. The following code shows an instance of the shared union being declared and initialized:

union shared generic_variable = {`@'};
 

Notice that the generic_variable union was initialized just as the first member of a structure would be initialized.

Figure 11.7. The union can hold only one value at a time.

Accessing Union Members

Individual union members can be used in the same way that structure members can be used--by using the member operator (.). However, there is an important difference in accessing union members. Only one union member should be accessed at a time. Because a union stores its members on top of each other, it's important to access only one member at a time. Listing 11.6 presents an example.

Listing 11.6. An example of the wrong use of unions.

1:   /* Example of using more than one union member at a time */
 2:   #include <stdio.h>
 3:
 4:   main()
 5:   {
 6:       union shared_tag {
 7:           char   c;
 8:           int    i;
 9:           long   l;
 10:          float  f;
 11:          double d;
 12:      } shared;
 13:
 14:      shared.c = `$';
 15:
 16:      printf("\nchar c   = %c",  shared.c);
 17:      printf("\nint i    = %d",  shared.i);
 18:      printf("\nlong l   = %ld", shared.l);
 19:      printf("\nfloat f  = %f",  shared.f);
 20:      printf("\ndouble d = %f",  shared.d);
 21:
 22:      shared.d = 123456789.8765;
 23:
 24:      printf("\n\nchar c   = %c",  shared.c);
 25:      printf("\nint i    = %d",  shared.i);
 26:      printf("\nlong l   = %ld", shared.l);
 27:      printf("\nfloat f  = %f",  shared.f);
 28:      printf("\ndouble d = %f\n",  shared.d);
 29:
 30:      return 0;
 31:  }
 char c   = $
 int i    = 4900
 long l   = 437785380
 float f  = 0.000000
 double d = 0.000000
 char c   = 7
 int i    = -30409
 long l   = 1468107063
 float f  = 284852666499072.000000
 double d = 123456789.876500
 

ANALYSIS: In this listing, you can see that a union named shared is defined and declared in lines 6 through 12. shared contains five members, each of a different type. Lines 14 and 22 initialize individual members of shared. Lines 16 through 20 and 24 through 28 then present the values of each member using printf() statements.

Note that, with the exceptions of char c = $ and double d = 123456789.876500, the output might not be the same on your computer. Because the character variable, c, was initialized in line 14, it is the only value that should be used until a different member is initialized. The results of printing the other union member variables (i, l, f, and d) can be unpredictable (lines 16 through 20). Line 22 puts a value into the double variable, d. Notice that the printing of the variables again is unpredictable for all but d. The value entered into c in line 14 has been lost because it was overwritten when the value of d in line 22 was entered. This is evidence that the members all occupy the same space.

The union Keyword

union tag {
     union_member(s);
     /* additional statements may go here */
 }instance;
 

The union keyword is used for declaring unions. A union is a collection of one or more variables (union_members) that have been grouped under a single name. In addition, each of these union members occupies the same area of memory.

The keyword union identifies the beginning of a union definition. It's followed by a tag that is the name given to the union. Following the tag are the union members enclosed in braces. An instance, the actual declaration of a union, also can be defined. If you define the structure without the instance, it's just a template that can be used later in a program to declare structures. The following is a template's format:

union tag {
     union_member(s);
     /* additional statements may go here */
 };
 

To use the template, you would use the following format:

union tag instance;
 

To use this format, you must have previously declared a union with the given tag.

Example 1

/* Declare a union template called tag */
 union tag {
     int nbr;
     char character;
 }
 /* Use the union template */
 union tag mixed_variable;
 

Example 2

/* Declare a union and instance together */
 union generic_type_tag {
     char c;
     int i;
     float f;
     double d;
 } generic;
 

Example 3

/* Initialize a union. */
 union date_tag {
     char full_date[9];
     struct part_date_tag {
         char month[2];
         char break_value1;
         char day[2];
         char break_value2;
         char year[2];
     } part_date;
 }date = {"01/01/97"};
 

Listing 11.7 shows a more practical use of a union. Although this use is simplistic, it's one of the more common uses of a union.

Listing 11.7. A practical use of a union.

1:   /* Example of a typical use of a union */
 2:
 3:   #include <stdio.h>
 4:
 5:   #define CHARACTER   `C'
 6:   #define INTEGER     `I'
 7:   #define FLOAT       `F'
 8:
 9:   struct generic_tag{
 10:      char type;
 11:      union shared_tag {
 12:          char   c;
 13:          int    i;
 14:          float  f;
 15:      } shared;
 16:  };
 17:
 18:  void print_function( struct generic_tag generic );
 19:
 20:  main()
 21:  {
 22:      struct generic_tag var;
 23:
 24:      var.type = CHARACTER;
 25:      var.shared.c = `$';
 26:      print_function( var );
 27:
 28:      var.type = FLOAT;
 29:      var.shared.f = (float) 12345.67890;
 30:      print_function( var );
 31:
 32:      var.type = `x';
 33:      var.shared.i = 111;
 34:      print_function( var );
 35:      return 0;
 36:  }
 37:  void print_function( struct generic_tag generic )
 38:  {
 39:      printf("\n\nThe generic value is...");
 40:      switch( generic.type )
 41:      {
 42:          case CHARACTER: printf("%c",  generic.shared.c);
 43:                          break;
 44:          case INTEGER:   printf("%d",  generic.shared.i);
 45:                          break;
 46:          case FLOAT:     printf("%f",  generic.shared.f);
 47:                          break;
 48:          default:        printf("an unknown type: %c\n",
 49:                                  generic.type);
 50:                          break;
 51:      }
 52:  }
 The generic value is...$
 The generic value is...12345.678711
 The generic value is...an unknown type: x
 

ANALYSIS: This program is a very simplistic version of what could be done with a union. This program provides a way of storing multiple data types in a single storage space. The generic_tag structure lets you store either a character, an integer, or a floating-point number within the same area. This area is a union called shared that operates just like the examples in Listing 11.6. Notice that the generic_tag structure also adds an additional field called type. This field is used to store information on the type of variable contained in shared. type helps prevent shared from being used in the wrong way, thus helping to avoid erroneous data such as that presented in Listing 11.6.

A formal look at the program shows that lines 5, 6, and 7 define constants CHARACTER, INTEGER, and FLOAT. These are used later in the program to make the listing more readable. Lines 9 through 16 define a generic_tag structure that will be used later. Line 18 presents a prototype for the print_function(). The structure var is declared in line 22 and is first initialized to hold a character value in lines 24 and 25. A call to print_function() in line 26 lets the value be printed. Lines 28 through 30 and 32 through 34 repeat this process with other values.

The print_function() is the heart of this listing. Although this function is used to print the value from a generic_tag variable, a similar function could have been used to initialize it. print_function() will evaluate the type variable in order to print a statement with the appropriate variable type. This prevents getting erroneous data such as that in Listing 11.6.


DON'T try to initialize more than the first union member.

DO remember which union member is being used. If you fill in a member of one type and then try to use a different type, you can get unpredictable results.

DON'T forget that the size of a union is equal to its largest member.

DO note that unions are an advanced C topic.


typedef and Structures

You can use the typedef keyword to create a synonym for a structure or union type. For example, the following statements define coord as a synonym for the indicated structure:

typedef struct {
     int x;
     int y;
 } coord;
 

You can then declare instances of this structure using the coord identifier:

coord topleft, bottomright;
 

Note that a typedef is different from a structure tag, as described earlier in this chapter. If you write

struct coord {
     int x;
     int y;
 };
 

the identifier coord is a tag for the structure. You can use the tag to declare instances of the structure, but unlike with a typedef, you must include the struct keyword:

struct coord topleft, bottomright;
 

Whether you use typedef or a structure tag to declare structures makes little difference. Using typedef results in slightly more concise code, because the struct keyword doesn't need to be used. On the other hand, using a tag and having the struct keyword explicit makes it clear that it is a structure being declared.

Работа с файлами

As you learned on Day 14, "Working with the Screen, Printer, and Keyboard," C performs all input and output, including disk files, by means of streams. You saw how to use C's predefined streams that are connected to specific devices such as the keyboard, screen, and (on DOS systems) the printer. Disk file streams work essentially the same way. This is one of the advantages of stream input/output--techniques for using one stream can be used with little or no change for other streams. The major difference with disk file streams is that your program must explicitly create a stream associated with a specific disk file.

Types of Disk Files

On Day 14, you saw that C streams come in two flavors: text and binary. You can associate either type of stream with a file, and it's important that you understand the distinction in order to use the proper mode for your files.

Text streams are associated with text-mode files. Text-mode files consist of a sequence of lines. Each line contains zero or more characters and ends with one or more characters that signal end-of-line. The maximum line length is 255 characters. It's important to remember that a "line" isn't a C string; there is no terminating NULL character (\0). When you use a text-mode stream, translation occurs between C's newline character (\n) and whatever character(s) the operating system uses to mark end-of-line on disk files. On DOS systems, it's a carriage-return linefeed (CR-LF) combination. When data is written to a text-mode file, each \n is translated to a CR-LF; when data is read from a disk file, each CR-LF is translated to a \n. On UNIX systems, no translation is done--newline characters remain unchanged.

Binary streams are associated with binary-mode files. Any and all data is written and read unchanged, with no separation into lines and no use of end-of-line characters. The NULL and end-of-line characters have no special significance and are treated like any other byte of data.

Some file input/output functions are restricted to one file mode, whereas other functions can use either mode. This chapter teaches you which mode to use with which functions.

Filenames

Every disk file has a name, and you must use filenames when dealing with disk files. Filenames are stored as strings, just like other text data. The rules as to what is acceptable for filenames and what is not differ from one operating system to another. In DOS and Windows 3.x, a complete filename consists of a name that has from one to eight characters, optionally followed by a period and an extension that has from one to three characters. In contrast, the Windows 95 and Windows NT operating systems, as well as most UNIX systems, permit filenames up to 256 characters long.

Operating systems also differ in the characters that are permitted in filenames. In Windows 95, for example, the following characters are not permitted:

/ \ : * ? " < > |

You must be aware of the filename rules of whichever operating system you're writing for.

A filename in a C program also can contain path information. The path specifies the drive and/or directory (or folder) where the file is located. If you specify a filename without a path, it will be assumed that the file is located at whatever location the operating system currently designates as the default. It's good programming practice to always specify path information as part of your filenames.

On PCs, the backslash character is used to separate directory names in a path. For example, to DOS and Windows, the name

c:\data\list.txt
 

refers to a file named LIST.TXT in the directory \DATA on drive C. Remember that the backslash character has a special meaning to C when it's in a string. To represent the backslash character itself, you must precede it with another backslash. Thus, in a C program, you would represent the filename as follows:

char *filename = "c:\\data\\list.txt";
 

If you're entering a filename using the keyboard, however, enter only a single backslash.

Not all systems use the backslash as the directory separator. For example, UNIX uses the forward slash (/).

Opening a File

The process of creating a stream linked to a disk file is called opening the file. When you open a file, it becomes available for reading (meaning that data is input from the file to the program), writing (meaning that data from the program is saved in the file), or both. When you're done using the file, you must close it. Closing a file is covered later in this chapter.

To open a file, you use the fopen() library function. The prototype of fopen() is located in STDIO.H and reads as follows:

FILE *fopen(const char *filename, const char *mode);
 

This prototype tells you that fopen() returns a pointer to type FILE, which is a structure declared in STDIO.H. The members of the FILE structure are used by the program in the various file access operations, but you don't need to be concerned about them. However, for each file that you want to open, you must declare a pointer to type FILE. When you call fopen(), that function creates an instance of the FILE structure and returns a pointer to that structure. You use this pointer in all subsequent operations on the file. If fopen() fails, it returns NULL. Such a failure could be caused, for example, by a hardware error or by trying to open a file on a diskette that hasn't been formatted.

The argument filename is the name of the file to be opened. As noted earlier, filename can--and should--contain a path specification. The filename argument can be a literal string enclosed in double quotation marks or a pointer to a string variable.

The argument mode specifies the mode in which to open the file. In this context, mode controls whether the file is binary or text and whether it is for reading, writing, or both. The permitted values for mode are listed in Table 16.1.

Table 16.1. Values of mode for the fopen() function.

mode Meaning
r Opens the file for reading. If the file doesn't exist, fopen() returns NULL.
w Opens the file for writing. If a file of the specified name doesn't exist, it is created. If a file of the specified name does exist, it is deleted without warning, and a new, empty file is created.
a Opens the file for appending. If a file of the specified name doesn't exist, it is created. If the file does exist, new data is appended to the end of the file.
r+ Opens the file for reading and writing. If a file of the specified name doesn't exist, it is created. If the file does exist, new data is added to the beginning of the file, overwriting existing data.
w+ Opens the file for reading and writing. If a file of the specified name doesn't exist, it is created. If the file does exist, it is overwritten.
a+ Opens a file for reading and appending. If a file of the specified name doesn't exist, it is created. If the file does exist, new data is appended to the end of the file.

The default file mode is text. To open a file in binary mode, you append a b to the mode argument. Thus, a mode argument of a would open a text-mode file for appending, whereas ab would open a binary-mode file for appending.

Remember that fopen() returns NULL if an error occurs. Error conditions that can cause a return value of NULL include the following:

  • Using an invalid filename.

  • Trying to open a file on a disk that isn't ready (the drive door isn't closed or the disk isn't formatted, for example).

  • Trying to open a file in a nonexistent directory or on a nonexistent disk drive.

  • Trying to open a nonexistent file in mode r.

Whenever you use fopen(), you need to test for the occurrence of an error. There's no way to tell exactly which error occurred, but you can display a message to the user and try to open the file again, or you can end the program. Most C compilers include non-ANSI extensions that let you obtain information about the nature of the error; refer to your compiler documentation for information.

Listing 16.1 demonstrates fopen().

Listing 16.1. Using fopen() to open disk files in various modes.

1:  /* Demonstrates the fopen() function. */
 2:  #include <stdlib.h>
 3:  #include <stdio.h>
 4:
 5:  main()
 6:  {
 7:      FILE *fp;
 8:      char filename[40], mode[4];
 9:
 10:     while (1)
 11:     {
 12:
 13:         /* Input filename and mode. */
 14:
 15:         printf("\nEnter a filename: ");
 16:         gets(filename);
 17:         printf("\nEnter a mode (max 3 characters): ");
 18:         gets(mode);
 19:
 20:         /* Try to open the file. */
 21:
 22:         if ( (fp = fopen( filename, mode )) != NULL )
 23:         {
 24:             printf("\nSuccessful opening %s in mode %s.\n",
 25:                     filename, mode);
 26:             fclose(fp);
 27:             puts("Enter x to exit, any other to continue.");
 28:             if ( (getc(stdin)) == `x')
 29:                 break;
 30:             else
 31:                 continue;
 32:         }
 33:         else
 34:         {
 35:             fprintf(stderr, "\nError opening file %s in mode %s.\n",
 36:                     filename, mode);
 37:             puts("Enter x to exit, any other to try again.");
 38:             if ( (getc(stdin)) == `x')
 39:                 break;
 40:             else
 41:                 continue;
 42:         }
 43:     }
 44: }
 Enter a filename: junk.txt
 Enter a mode (max 3 characters): w
 Successful opening junk.txt in mode w.
 Enter x to exit, any other to continue.
 j
 Enter a filename: morejunk.txt
 Enter a mode (max 3 characters): r
 Error opening morejunk.txt in mode r.
 Enter x to exit, any other to try again.
 x
 

]ANALYSIS: This program prompts you for both the filename and the mode specifier on lines 15 through 18. After getting the names, line 22 attempts to open the file and assign its file pointer to fp. As an example of good programming practice, the if statement on line 22 checks to see that the opened file's pointer isn't equal to NULL. If fp isn't equal to NULL, a message stating that the open was successful and that the user can continue is printed. If the file pointer is NULL, the else condition of the if loop executes. The else condition on lines 33 through 42 prints a message stating that there was a problem. It then prompts the user to determine whether the program should continue.

You can experiment with different names and modes to see which ones give you an error. In the output just shown, you can see that trying to open MOREJUNK.TXT in mode r resulted in an error because the file didn't exist on the disk. If an error occurs, you're given the choice of entering the information again or quitting the program. To force an error, you could enter an invalid filename such as [].

Writing and Reading File Data

A program that uses a disk file can write data to a file, read data from a file, or a combination of the two. You can write data to a disk file in three ways:

  • You can use formatted output to save formatted data to a file. You should use formatted output only with text-mode files. The primary use of formatted output is to create files containing text and numeric data to be read by other programs such as spreadsheets or databases. You rarely, if ever, use formatted output to create a file to be read again by a C program.

  • You can use character output to save single characters or lines of characters to a file. Although technically it's possible to use character output with binary-mode files, it can be tricky. You should restrict character-mode output to text files. The main use of character output is to save text (but not numeric) data in a form that can be read by C, as well as other programs such as word processors.

  • You can use direct output to save the contents of a section of memory directly to a disk file. This method is for binary files only. Direct output is the best way to save data for later use by a C program.

When you want to read data from a file, you have the same three options: formatted input, character input, or direct input. The type of input you use in a particular case depends almost entirely on the nature of the file being read. Generally, you will read data in the same mode that it was saved in, but this is not a requirement. However, reading a file in a mode different from the one it was written in requires a thorough knowledge of C and file formats.

The previous descriptions of the three types of file input and output suggest tasks best suited for each type of output. This is by no means a set of strict rules. The C language is very flexible (this is one of its advantages!), so a clever programmer can make any type of file output suit almost any need. As a beginning programmer, it might make things easier if you follow these guidelines, at least initially.

Formatted File Input and Output

Formatted file input/output deals with text and numeric data that is formatted in a specific way. It is directly analogous to formatted keyboard input and screen output done with the printf() and scanf() functions, as described on Day 14. I'll discuss formatted output first, followed by input.

Formatted File Output

Formatted file output is done with the library function fprintf(). The prototype of fprintf() is in the header file STDIO.H, and it reads as follows:

int fprintf(FILE *fp, char *fmt, ...);
 

The first argument is a pointer to type FILE. To write data to a particular disk file, you pass the pointer that was returned when you opened the file with fopen().

The second argument is the format string. You learned about format strings in the discussion of printf() on Day 14. The format string used by fprintf() follows exactly the same rules as printf(). Refer to Day 14 for details.

The final argument is .... What does that mean? In a function prototype, ellipses represent a variable number of arguments. In other words, in addition to the file pointer and the format string arguments, fprintf() takes zero, one, or more additional arguments. This is just like printf(). These arguments are the names of the variables to be output to the specified stream.

Remember, fprintf() works just like printf(), except that it sends its output to the stream specified in the argument list. In fact, if you specify a stream argument of stdout, fprintf() is identical to printf().

Listing 16.2 demonstrates the use of fprintf().

Listing 16.2. The equivalence of fprintf() formatted output to both a file and to stdout.

1:  /* Demonstrates the fprintf() function. */
 2:  #include <stdlib.h>
 3:  #include <stdio.h>
 4:
 5:  void clear_kb(void);
 6:
 7:  main()
 8:  {
 9:      FILE *fp;
 10:     float data[5];
 11:     int count;
 12:     char filename[20];
 13:
 14:     puts("Enter 5 floating-point numerical values.");
 15:
 16:     for (count = 0; count < 5; count++)
 17:         scanf("%f", &data[count]);
 18:
 19:     /* Get the filename and open the file. First clear stdin */
 20:     /* of any extra characters. */
 21:
 22:     clear_kb();
 23:
 24:     puts("Enter a name for the file.");
 25:     gets(filename);
 26:
 27:     if ( (fp = fopen(filename, "w")) == NULL)
 28:     {
 29:         fprintf(stderr, "Error opening file %s.", filename);
 30:         exit(1);
 31:     }
 32:
 33:     /* Write the numerical data to the file and to stdout. */
 34:
 35:     for (count = 0; count < 5; count++)
 36:     {
 37:         fprintf(fp, "\ndata[%d] = %f", count, data[count]);
 38:         fprintf(stdout, "\ndata[%d] = %f", count, data[count]);
 39:     }
 40:     fclose(fp);
 41:     printf("\n");
 42:     return(0);
 43: }
 44:
 45: void clear_kb(void)
 46: /* Clears stdin of any waiting characters. */
 47: {
 48:     char junk[80];
 49:     gets(junk);
 50: }
 Enter 5 floating-point numerical values.
 3.14159
 9.99
 1.50
 3.
 1000.0001
 Enter a name for the file.
 numbers.txt
 data[0] = 3.141590
 data[1] = 9.990000
 data[2] = 1.500000
 data[3] = 3.000000
 data[4] = 1000.000122
 

ANALYSIS: You might wonder why the program displays 1000.000122 when the value you entered was 1000.0001. This isn't an error in the program. It's a normal consequence of the way C stores numbers internally. Some floating-point values can't be stored exactly, so minor inaccuracies such as this one sometimes result.

This program uses fprintf() on lines 37 and 38 to send some formatted text and numeric data to stdout and to the disk file whose name you specified. The only difference between the two lines is the first argument--that is, the stream to which the data is sent. After running the program, use your editor to look at the contents of the file NUMBERS.TXT (or whatever name you assigned to it), which will be in the same directory as the program files. You'll see that the text in the file is an exact copy of the text that was displayed on-screen.

Note that Listing 16.2 uses the clear_kb() function discussed on Day 14. This is necessary to remove from stdin any extra characters that might be left over from the call to scanf(). If you don't clear stdin, these extra characters (specifically, the newline) are read by the gets() that inputs the filename, and the result is a file creation error.

Formatted File Input

For formatted file input, use the fscanf() library function, which is used like scanf() (see Day 14), except that input comes from a specified stream instead of from stdin. The prototype for fscanf() is

int fscanf(FILE *fp, const char *fmt, ...);
 

The argument fp is the pointer to type FILE returned by fopen(), and fmt is a pointer to the format string that specifies how fscanf() is to read the input. The components of the format string are the same as for scanf(). Finally, the ellipses (...) indicate one or more additional arguments, the addresses of the variables where fscanf() is to assign the input.

Before getting started with fscanf(), you might want to review the section on scanf() on Day 14. The function fscanf() works exactly the same as scanf(), except that characters are taken from the specified stream rather than from stdin.

To demonstrate fscanf(), you need a text file containing some numbers or strings in a format that can be read by the function. Use your editor to create a file named INPUT.TXT, and enter five floating-point numbers with some space between them (spaces or newlines). For example, your file might look like this:

123.45     87.001
 100.02
 0.00456    1.0005
 

Now, compile and run Listing 16.3.

Listing 16.3. Using fscanf() to read formatted data from a disk file.

1:  /* Reading formatted file data with fscanf(). */
 2:  #include <stdlib.h>
 3:  #include <stdio.h>
 4:
 5:  main()
 6:  {
 7:      float f1, f2, f3, f4, f5;
 8:      FILE *fp;
 9:
 10:     if ( (fp = fopen("INPUT.TXT", "r")) == NULL)
 11:     {
 12:         fprintf(stderr, "Error opening file.\n");
 13:         exit(1);
 14:     }
 15:
 16:     fscanf(fp, "%f %f %f %f %f", &f1, &f2, &f3, &f4, &f5);
 17:     printf("The values are %f, %f, %f, %f, and %f\n.",
 18:             f1, f2, f3, f4, f5);
 19:
 20:     fclose(fp);
 21:      return(0);
 22: }
 
The values are 123.45, 87.0001, 100.02, 0.00456, and 1.0005.
 


NOTE: The precision of the values might cause some numbers to not display as the exact values you entered. For example, 100.02 might appear as 100.01999.

ANALYSIS: This program reads the five values from the file you created and then displays them on-screen. The fopen() call on line 10 opens the file for read mode. It also checks to see that the file opened correctly. If the file wasn't opened, an error message is displayed on line 12, and the program exits (line 13). Line 16 demonstrates the use of the fscanf() function. With the exception of the first parameter, fscanf() is identical to scanf(), which you have been using throughout this book. The first parameter points to the file that you want the program to read. You can do further experiments with fscanf(), creating input files with your programming editor and seeing how fscanf() reads the data.

Character Input and Output

When used with disk files, the term character I/O refers to single characters as well as lines of characters. Remember, a line is a sequence of zero or more characters terminated by the newline character. Use character I/O with text-mode files. The following sections describe character input/output functions, and then you'll see a demonstration program.

Character Input

There are three character input functions: getc() and fgetc() for single characters, and fgets() for lines.

The getc() and fgetc() Functions

The functions getc() and fgetc() are identical and can be used interchangeably. They input a single character from the specified stream. Here is the prototype of getc(), which is in STDIO.H:

int getc(FILE *fp);
 

The argument fp is the pointer returned by fopen() when the file is opened. The function returns the character that was input or EOF on error.

You've seen getc() used in earlier programs to input a character from the keyboard. This is another example of the flexibility of C's streams--the same function can be used for keyboard or file input.

If getc() and fgetc() return a single character, why are they prototyped to return a type int? The reason is that, when reading files, you need to be able to read in the end-of-file marker, which on some systems isn't a type char but a type int. You'll see getc() in action later, in Listing 16.10.

The fgets() Function

To read a line of characters from a file, use the fgets() library function. The prototype is

char *fgets(char *str, int n, FILE *fp);
 

The argument str is a pointer to a buffer in which the input is to be stored, n is the maximum number of characters to be input, and fp is the pointer to type FILE that was returned by fopen() when the file was opened.

When called, fgets() reads characters from fp into memory, starting at the location pointed to by str. Characters are read until a newline is encountered or until n-1 characters have been read, whichever occurs first. By setting n equal to the number of bytes allocated for the buffer str, you prevent input from overwriting memory beyond allocated space. (The n-1 is to allow space for the terminating \0 that fgets() adds to the end of the string.) If successful, fgets() returns str. Two types of errors can occur, as indicated by the return value of NULL:

  • If a read error or EOF is encountered before any characters have been assigned to str, NULL is returned, and the memory pointed to by str is unchanged.

  • If a read error or EOF is encountered after one or more characters have been assigned to str, NULL is returned, and the memory pointed to by str contains garbage.

You can see that fgets() doesn't necessarily input an entire line (that is, everything up to the next newline character). If n-1 characters are read before a newline is encountered, fgets() stops. The next read operation from the file starts where the last one leaves off. To be sure that fgets() reads in entire strings, stopping only at newlines, be sure that the size of your input buffer and the corresponding value of n passed to fgets() are large enough.

Character Output

You need to know about two character output functions: putc() and fputs().

The putc() Function

The library function putc() writes a single character to a specified stream. Its prototype in STDIO.H reads

int putc(int ch, FILE *fp);
 

The argument ch is the character to output. As with other character functions, it is formally called a type int, but only the lower-order byte is used. The argument fp is the pointer associated with the file (the pointer returned by fopen() when the file was opened). The function putc() returns the character just written if successful or EOF if an error occurs. The symbolic constant EOF is defined in STDIO.H, and it has the value -1. Because no "real" character has that numeric value, EOF can be used as an error indicator (with text-mode files only).

The fputs() Function

To write a line of characters to a stream, use the library function fputs(). This function works just like puts(), covered on Day 14. The only difference is that with fputs() you can specify the output stream. Also, fputs() doesn't add a newline to the end of the string; if you want it, you must explicitly include it. Its prototype in STDIO.H is

char fputs(char *str, FILE *fp);
 

The argument str is a pointer to the null-terminated string to be written, and fp is the pointer to type FILE returned by fopen() when the file was opened. The string pointed to by str is written to the file, minus its terminating \0. The function fputs() returns a nonnegative value if successful or EOF on error.

Direct File Input and Output

You use direct file I/O most often when you save data to be read later by the same or a different C program. Direct I/O is used only with binary-mode files. With direct output, blocks of data are written from memory to disk. Direct input reverses the process: A block of data is read from a disk file into memory. For example, a single direct-output function call can write an entire array of type double to disk, and a single direct-input function call can read the entire array from disk back into memory. The direct I/O functions are fread() and fwrite().

The fwrite() Function

The fwrite() library function writes a block of data from memory to a binary-mode file. Its prototype in STDIO.H is

int fwrite(void *buf, int size, int count, FILE *fp);
 

The argument buf is a pointer to the region of memory holding the data to be written to the file. The pointer type is void; it can be a pointer to anything.

The argument size specifies the size, in bytes, of the individual data items, and count specifies the number of items to be written. For example, if you wanted to save a 100-element integer array, size would be 2 (because each int occupies 2 bytes) and count would be 100 (because the array contains 100 elements). To obtain the size argument, you can use the sizeof() operator.

The argument fp is, of course, the pointer to type FILE, returned by fopen() when the file was opened. The fwrite() function returns the number of items written on success; if the value returned is less than count, it means that an error has occurred. To check for errors, you usually program fwrite() as follows:

if( (fwrite(buf, size, count, fp)) != count)
 fprintf(stderr, "Error writing to file.");
 

Here are some examples of using fwrite(). To write a single type double variable x to a file, use the following:

fwrite(&x, sizeof(double), 1, fp);
 

To write an array data[] of 50 structures of type address to a file, you have two choices:

fwrite(data, sizeof(address), 50, fp);
 fwrite(data, sizeof(data), 1, fp);
 

The first method writes the array as 50 elements, with each element having the size of a single type address structure. The second method treats the array as a single element. The two methods accomplish exactly the same thing.

The following section explains fread() and then presents a program demonstrating fread() and fwrite().

The fread() Function

The fread() library function reads a block of data from a binary-mode file into memory. Its prototype in STDIO.H is

int fread(void *buf, int size, int count, FILE *fp);
 

The argument buf is a pointer to the region of memory that receives the data read from the file. As with fwrite(), the pointer type is void.

The argument size specifies the size, in bytes, of the individual data items being read, and count specifies the number of items to read. Note how these arguments parallel the arguments used by fwrite(). Again, the sizeof() operator is typically used to provide the size argu-ment. The argument fp is (as always) the pointer to type FILE that was returned by fopen() when the file was opened. The fread() function returns the number of items read; this can be less than count if end-of-file was reached or an error occurred.

Listing 16.4 demonstrates the use of fwrite() and fread().

Listing 16.4. Using fwrite() and fread() for direct file access.

1:  /* Direct file I/O with fwrite() and fread(). */
 2:  #include <stdlib.h>
 3:  #include <stdio.h>
 4:
 5:  #define SIZE 20
 6:
 7:  main()
 8:  {
 9:      int count, array1[SIZE], array2[SIZE];
 10:     FILE *fp;
 11:
 12:     /* Initialize array1[]. */
 13:
 14:     for (count = 0; count < SIZE; count++)
 15:         array1[count] = 2 * count;
 16:
 17:     /* Open a binary mode file. */
 18:
 19:     if ( (fp = fopen("direct.txt", "wb")) == NULL)
 20:     {
 21:         fprintf(stderr, "Error opening file.");
 22:         exit(1);
 23:     }
 24:     /* Save array1[] to the file. */
 25:
 26:     if (fwrite(array1, sizeof(int), SIZE, fp) != SIZE)
 27:     {
 28:         fprintf(stderr, "Error writing to file.");
 29:         exit(1);
 30:     }
 31:
 32:     fclose(fp);
 33:
 34:     /* Now open the same file for reading in binary mode. */
 35:
 36:     if ( (fp = fopen("direct.txt", "rb")) == NULL)
 37:     {
 38:         fprintf(stderr, "Error opening file.");
 39:         exit(1);
 40:     }
 41:
 42:     /* Read the data into array2[]. */
 43:
 44:     if (fread(array2, sizeof(int), SIZE, fp) != SIZE)
 45:     {
 46:         fprintf(stderr, "Error reading file.");
 47:         exit(1);
 48:     }
 49:
 50:     fclose(fp);
 51:
 52:     /* Now display both arrays to show they're the same. */
 53:
 54:     for (count = 0; count < SIZE; count++)
 55:         printf("%d\t%d\n", array1[count], array2[count]);
 56:     return(0);
 57: }
 0       0
 2       2
 4       4
 6       6
 8       8
 10      10
 12      12
 14      14
 16      16
 18      18
 20      20
 22      22
 24      24
 26      26
 28      28
 30      30
 32      32
 34      34
 36      36
 38      38
 

ANALYSIS: Listing 16.4 demonstrates the use of the fwrite() and fread() functions. This program initializes an array on lines 14 and 15. It then uses fwrite() on line 26 to save the array to disk. The program uses fread() on line 44 to read the data into a different array. Finally, it displays both arrays on-screen to show that they now hold the same data (lines 54 and 55).

When you save data with fwrite(), not much can go wrong besides some type of disk error. With fread(), you need to be careful, however. As far as fread() is concerned, the data on the disk is just a sequence of bytes. The function has no way of knowing what it represents. For example, on a 16-bit system, a block of 100 bytes could be 100 char variables, 50 int variables, 25 long variables, or 25 float variables. If you ask fread() to read that block into memory, it obediently does so. However, if the block was saved from an array of type int and you retrieve it into an array of type float, no error occurs, but you get strange results. When writing programs, you must be sure that fread() is used properly, reading data into the appropriate types of variables and arrays. Notice that in Listing 16.4, all calls to fopen(), fwrite(), and fread() are checked to ensure that they worked correctly.

File Buffering: Closing and Flushing Files

When you're done using a file, you should close it using the fclose() function. You saw fclose() used in programs presented earlier in this chapter. Its prototype is

int fclose(FILE *fp);
 

The argument fp is the FILE pointer associated with the stream; fclose() returns 0 on success or -1 on error. When you close a file, the file's buffer is flushed (written to the file). You can also close all open streams except the standard ones (stdin, stdout, stdprn, stderr, and stdaux) by using the fcloseall() function. Its prototype is

int fcloseall(void);
 

This function also flushes any stream buffers and returns the number of streams closed.

When a program terminates (either by reaching the end of main() or by executing the exit() function), all streams are automatically flushed and closed. However, it's a good idea to close streams explicitly--particularly those linked to disk files--as soon as you're finished with them. The reason has to do with stream buffers.

When you create a stream linked to a disk file, a buffer is automatically created and associated with the stream. A buffer is a block of memory used for temporary storage of data being written to and read from the file. Buffers are needed because disk drives are block-oriented devices, which means that they operate most efficiently when data is read and written in blocks of a certain size. The size of the ideal block differs, depending on the specific hardware in use. It's typically on the order of a few hundred to a thousand bytes. You don't need to be concerned about the exact block size, however.

The buffer associated with a file stream serves as an interface between the stream (which is character-oriented) and the disk hardware (which is block-oriented). As your program writes data to the stream, the data is saved in the buffer until the buffer is full, and then the entire contents of the buffer are written, as a block, to the disk. An analogous process occurs when reading data from a disk file. The creation and operation of the buffer are handled by the operating system and are entirely automatic; you don't have to be concerned with them. (C does offer some functions for buffer manipulation, but they are beyond the scope of this book.)

In practical terms, this buffer operation means that, during program execution, data that your program wrote to the disk might still be in the buffer, not on the disk. If your program hangs up, if there's a power failure, or if some other problem occurs, the data that's still in the buffer might be lost, and you won't know what's contained in the disk file.

You can flush a stream's buffers without closing it by using the fflush() or flushall() library functions. Use fflush() when you want a file's buffer to be written to disk while still using the file. Use flushall() to flush the buffers of all open streams. The prototypes of these two functions are as follows:

int fflush(FILE *fp);
 int flushall(void);
 

The argument fp is the FILE pointer returned by fopen() when the file was opened. If a file was opened for writing, fflush() writes its buffer to disk. If the file was opened for reading, the buffer is cleared. The function fflush() returns 0 on success or EOF if an error occurred. The function flushall() returns the number of open streams.


DO open a file before trying to read or write to it.

DON'T assume that a file access is okay. Always check after doing a read, write, or open to ensure that the function worked.

DO use the sizeof() operator with the fwrite() and fread() functions.

DO close all files that you've opened.

DON'T use fcloseall() unless you have a reason to close all the streams.


Sequential Versus Random File Access

Every open file has a file position indicator associated with it. The position indicator specifies where read and write operations take place in the file. The position is always given in terms of bytes from the beginning of the file. When a new file is opened, the position indicator is always at the beginning of the file, position 0. (Because the file is new and has a length of 0, there's no other location to indicate.) When an existing file is opened, the position indicator is at the end of the file if the file was opened in append mode, or at the beginning of the file if the file was opened in any other mode.

The file input/output functions covered earlier in this chapter make use of the position indicator, although the manipulations go on behind the scenes. Writing and reading operations occur at the location of the position indicator and update the position indicator as well. For example, if you open a file for reading, and 10 bytes are read, you input the first 10 bytes in the file (the bytes at positions 0 through 9). After the read operation, the position indicator is at position 10, and the next read operation begins there. Thus, if you want to read all the data in a file sequentially or write data to a file sequentially, you don't need to be concerned about the position indicator, because the stream I/O functions take care of it automatically.

When you need more control, use the C library functions that let you determine and change the value of the file position indicator. By controlling the position indicator, you can per-form random file access. Here, random means that you can read data from, or write data to, any position in a file without reading or writing all the preceding data.

The ftell() and rewind() Functions

To set the position indicator to the beginning of the file, use the library function rewind(). Its prototype, in STDIO.H, is

void rewind(FILE *fp);
 

The argument fp is the FILE pointer associated with the stream. After rewind() is called, the file's position indicator is set to the beginning of the file (byte 0). Use rewind() if you've read some data from a file and you want to start reading from the beginning of the file again without closing and reopening the file.

To determine the value of a file's position indicator, use ftell(). This function's prototype, located in STDIO.H, reads

long ftell(FILE *fp);
 

The argument fp is the FILE pointer returned by fopen() when the file was opened. The function ftell() returns a type long that gives the current file position in bytes from the start of the file (the first byte is at position 0). If an error occurs, ftell() returns -1L (a type long -1).

To get a feel for the operation of rewind() and ftell(), look at Listing 16.5.

Listing 16.5. Using ftell() and rewind().

1:  /* Demonstrates ftell() and rewind(). */
 2:  #include <stdlib.h>
 3:  #include <stdio.h>
 4:
 5:  #define BUFLEN 6
 6:
 7:  char msg[] = "abcdefghijklmnopqrstuvwxyz";
 8:
 9:  main()
 10: {
 11:     FILE *fp;
 12:     char buf[BUFLEN];
 13:
 14:     if ( (fp = fopen("TEXT.TXT", "w")) == NULL)
 15:     {
 16:         fprintf(stderr, "Error opening file.");
 17:         exit(1);
 18:     }
 19:
 20:     if (fputs(msg, fp) == EOF)
 21:     {
 22:         fprintf(stderr, "Error writing to file.");
 23:         exit(1);
 24:     }
 25:
 26:     fclose(fp);
 27:
 28:     /* Now open the file for reading. */
 29:
 30:     if ( (fp = fopen("TEXT.TXT", "r")) == NULL)
 31:     {
 32:         fprintf(stderr, "Error opening file.");
 33:         exit(1);
 34:     }
 35:     printf("\nImmediately after opening, position = %ld", ftell(fp));
 36:
 37:     /* Read in 5 characters. */
 38:
 39:     fgets(buf, BUFLEN, fp);
 40:     printf("\nAfter reading in %s, position = %ld", buf, ftell(fp));
 41:
 42:     /* Read in the next 5 characters. */
 43:
 44:     fgets(buf, BUFLEN, fp);
 45:     printf("\n\nThe next 5 characters are %s, and position now = %ld",
 46:             buf, ftell(fp));
 47:
 48:     /* Rewind the stream. */
 49:
 50:     rewind(fp);
 51:
 52:     printf("\n\nAfter rewinding, the position is back at %ld",
 53:             ftell(fp));
 54:
 55:     /* Read in 5 characters. */
 56:
 57:     fgets(buf, BUFLEN, fp);
 58:     printf("\nand reading starts at the beginning again: %s\n", buf);
 59:     fclose(fp);
 60:     return(0);
 61: }
 Immediately after opening, position = 0
 After reading in abcde, position = 5
 The next 5 characters are fghij, and position now = 10
 After rewinding, the position is back at 0
 and reading starts at the beginning again: abcde
 

ANALYSIS: This program writes a string, msg, to a file called TEXT.TXT. The message consists of the 26 letters of the alphabet, in order. Lines 14 through 18 open TEXT.TXT for writing and test to ensure that the file was opened successfully. Lines 20 through 24 write msg to the file using fputs() and check to ensure that the write was successful. Line 26 closes the file with fclose(), completing the process of creating a file for the rest of the program to use.

Lines 30 through 34 open the file again, only this time for reading. Line 35 prints the return value of ftell(). Notice that this position is at the beginning of the file. Line 39 performs a gets() to read five characters. The five characters and the new file position are printed on line 40. Notice that ftell() returns the correct offset. Line 50 calls rewind() to put the pointer back at the beginning of the file, before line 52 prints the file position again. This should confirm for you that rewind() resets the position. An additional read on line 57 further confirms that the program is indeed back at the beginning of the file. Line 59 closes the file before ending the program.

The fseek() Function

More precise control over a stream's position indicator is possible with the fseek() library function. By using fseek(), you can set the position indicator anywhere in the file. The function prototype, in STDIO.H, is

int fseek(FILE *fp, long offset, int origin);
 

The argument fp is the FILE pointer associated with the file. The distance that the position indicator is to be moved is given by offset in bytes. The argument origin specifies the move's relative starting point. There can be three values for origin, with symbolic constants defined in IO.H, as shown in Table 16.2.

Table 16.2. Possible origin values for fseek().

Constant Value Description
SEEK_SET 0 Moves the indicator offset bytes from the beginning of the file.
SEEK_CUR 1 Moves the indicator offset bytes from its current position.
SEEK_END 2 Moves the indicator offset bytes from the end of the file.

The function fseek() returns 0 if the indicator was successfully moved or nonzero if an error occurred. Listing 16.6 uses fseek() for random file access.

Listing 16.6. Random file access with fseek().

1:  /* Random access with fseek(). */
 2:
 3:  #include <stdlib.h>
 4:  #include <stdio.h>
 5:
 6:  #define MAX 50
 7:
 8:  main()
 9:  {
 10:     FILE *fp;
 11:     int data, count, array[MAX];
 12:     long offset;
 13:
 14:     /* Initialize the array. */
 15:
 16:     for (count = 0; count < MAX; count++)
 17:         array[count] = count * 10;
 18:
 19:     /* Open a binary file for writing. */
 20:
 21:     if ( (fp = fopen("RANDOM.DAT", "wb")) == NULL)
 22:     {
 23:         fprintf(stderr, "\nError opening file.");
 24:         exit(1);
 25:     }
 26:
 27:     /* Write the array to the file, then close it. */
 28:
 29:     if ( (fwrite(array, sizeof(int), MAX, fp)) != MAX)
 30:     {
 31:         fprintf(stderr, "\nError writing data to file.");
 32:         exit(1);
 33:     }
 34:
 35:     fclose(fp);
 36:
 37:     /* Open the file for reading. */
 38:
 39:     if ( (fp = fopen("RANDOM.DAT", "rb")) == NULL)
 40:     {
 41:         fprintf(stderr, "\nError opening file.");
 42:         exit(1);
 43:     }
 44:
 45:     /* Ask user which element to read. Input the element */
 46:     /* and display it, quitting when -1 is entered. */
 47:
 48:     while (1)
 49:     {
 50:         printf("\nEnter element to read, 0-%d, -1 to quit: ",MAX-1);
 51:         scanf("%ld", &offset);
 52:
 53:         if (offset < 0)
 54:             break;
 55:         else if (offset > MAX-1)
 56:             continue;
 57:
 58:         /* Move the position indicator to the specified element. */
 59:
 60:         if ( (fseek(fp, (offset*sizeof(int)), SEEK_SET)) != 0)
 61:         {
 62:             fprintf(stderr, "\nError using fseek().");
 63:             exit(1);
 64:         }
 65:
 66:         /* Read in a single integer. */
 67:
 68:         fread(&data, sizeof(int), 1, fp);
 69:
 70:         printf("\nElement %ld has value %d.", offset, data);
 71:     }
 72:
 73:     fclose(fp);
 74:     return(0);
 75: }
 Enter element to read, 0-49, -1 to quit: 5
 Element 5 has value 50.
 Enter element to read, 0-49, -1 to quit: 6
 Element 6 has value 60.
 Enter element to read, 0-49, -1 to quit: 49
 Element 49 has value 490.
 Enter element to read, 0-49, -1 to quit: 1
 Element 1 has value 10.
 Enter element to read, 0-49, -1 to quit: 0
 Element 0 has value 0.
 Enter element to read, 0-49, -1 to quit: -1
 

ANALYSIS: Lines 14 through 35 are similar to Listing 16.5. Lines 16 and 17 initialize an array called data with 50 type int values. The value stored in each array element is equal to 10 times the index. Then the array is written to a binary file called RANDOM.DAT. You know it is binary because the file was opened with mode "wb" on line 21.

Line 39 reopens the file in binary read mode before going into an infinite while loop. The while loop prompts users to enter the number of the array element that they want to read. Notice that lines 53 through 56 check to see that the entered element is within the range of the file. Does C let you read an element that is beyond the end of the file? Yes. Like going beyond the end of an array with values, C also lets you read beyond the end of a file. If you do read beyond the end (or before the beginning), your results are unpredictable. It's always best to check what you're doing (as lines 53 through 56 do in this listing).

After you have input the element to find, line 60 jumps to the appropriate offset with a call to fseek(). Because SEEK_SET is being used, the seek is done from the beginning of the file. Notice that the distance into the file is not just offset, but offset multiplied by the size of the elements being read. Line 68 then reads the value, and line 70 prints it.

Detecting the End of a File

Sometimes you know exactly how long a file is, so there's no need to be able to detect the file's end. For example, if you used fwrite() to save a 100-element integer array, you know the file is 200 bytes long (assuming 2-byte integers). At other times, however, you don't know how long the file is, but you still want to read data from the file, starting at the beginning and proceeding to the end. There are two ways to detect end-of-file.

When reading from a text-mode file character-by-character, you can look for the end-of-file character. The symbolic constant EOF is defined in STDIO.H as -1, a value never used by a "real" character. When a character input function reads EOF from a text-mode stream, you can be sure that you've reached the end of the file. For example, you could write the following:

while ( (c = fgetc( fp )) != EOF )
 

With a binary-mode stream, you can't detect the end-of-file by looking for -1, because a byte of data from a binary stream could have that value, which would result in premature end of input. Instead, you can use the library function feof(), which can be used for both binary- and text-mode files:

int feof(FILE *fp);
 

The argument fp is the FILE pointer returned by fopen() when the file was opened. The function feof() returns 0 if the end of file fp hasn't been reached, or a nonzero value if end-of-file has been reached. If a call to feof() detects end-of-file, no further read operations are permitted until a rewind() has been done, fseek() is called, or the file is closed and reopened.

Listing 16.7 demonstrates the use of feof(). When you're prompted for a filename, enter the name of any text file--one of your C source files, for example, or a header file such as STDIO.H. Just be sure that the file is in the current directory, or else enter a path as part of the filename. The program reads the file one line at a time, displaying each line on stdout, until feof() detects end-of-file.

Listing 16.7. Using feof() to detect the end of a file.

1:  /* Detecting end-of-file. */
 2:  #include <stdlib.h>
 3:  #include <stdio.h>
 4:
 5:  #define BUFSIZE 100
 6:
 7:  main()
 8:  {
 9:      char buf[BUFSIZE];
 10:     char filename[60];
 11:     FILE *fp;
 12:
 13:     puts("Enter name of text file to display: ");
 14:     gets(filename);
 15:
 16:     /* Open the file for reading. */
 17:     if ( (fp = fopen(filename, "r")) == NULL)
 18:     {
 19:         fprintf(stderr, "Error opening file.");
 20:         exit(1);
 21:     }
 22:
 23:     /* If end of file not reached, read a line and display it. */
 24:
 25:     while ( !feof(fp) )
 26:     {
 27:         fgets(buf, BUFSIZE, fp);
 28:         printf("%s",buf);
 29:     }
 30:
 31:     fclose(fp);
 32:     return(0);
 33: }
 Enter name of text file to display:
 hello.c
 #include <stdio.h>
 main()
 {
     printf("Hello, world.");
     return(0);
 }
 

ANALYSIS: The while loop in this program (lines 25 through 29) is typical of a while used in more complex programs that do sequential processing. As long as the end of the file hasn't been reached, the code within the while statement (lines 27 and 28) continues to execute repeatedly. When the call to feof() returns a nonzero value, the loop ends, the file is closed, and the program ends.


DO check your position within a file so that you don't read beyond the end or before the beginning of a file.
DO use either rewind() or fseek( fp, SEEK_SET, 0 ) to reset the file position to the beginning of the file.
DO use feof() to check for the end of the file when working with binary files.
DON'T use EOF with binary files.

File Management Functions

The term file management refers to dealing with existing files--not reading from or writing to them, but deleting, renaming, and copying them. The C standard library contains functions for deleting and renaming files, and you can also write your own file-copying program.

Deleting a File

To delete a file, you use the library function remove(). Its prototype is in STDIO.H, as follows:

int remove( const char *filename );
 

The variable *filename is a pointer to the name of the file to be deleted. (See the section on filenames earlier in this chapter.) The specified file must not be open. If the file exists, it is deleted (just as if you used the DEL command from the DOS prompt or the rm command in UNIX), and remove() returns 0. If the file doesn't exist, if it's read-only, if you don't have sufficient access rights, or if some other error occurs, remove() returns -1.

Listing 16.8 demonstrates the use of remove(). Be careful: If you remove() a file, it's gone forever.

Listing 16.8. Using the remove() function to delete a disk file.

1:  /* Demonstrates the remove() function. */
 2:
 3:  #include <stdio.h>
 4:
 5:  main()
 6:  {
 7:      char filename[80];
 8:
 9:      printf("Enter the filename to delete: ");
 10:     gets(filename);
 11:
 12:     if ( remove(filename) == 0)
 13:         printf("The file %s has been deleted.\n", filename);
 14:     else
 15:         fprintf(stderr, "Error deleting the file %s.\n", filename);
 16:     return(0);
 17: }
 Enter the filename to delete: *.bak
 Error deleting the file *.bak.
 Enter the filename to delete: list1414.bak
 The file list1414.bak has been deleted.
 

ANALYSIS: This program prompts the user on line 9 for the name of the file to be deleted. Line 12 then calls remove() to delete the entered file. If the return value is 0, the file was removed, and a message is displayed stating this fact. If the return value is not zero, an error occurred, and the file was not removed.

Renaming a File

The rename() function changes the name of an existing disk file. The function prototype, in STDIO.H, is as follows:

int rename( const char *oldname, const char *newname );
 

The filenames pointed to by oldname and newname follow the rules given earlier in this chapter. The only restriction is that both names must refer to the same disk drive; you can't rename a file to a different disk drive. The function rename() returns 0 on success, or -1 if an error occurs. Errors can be caused by the following conditions (among others):

  • The file oldname does not exist.

  • A file with the name newname already exists.

  • You try to rename to another disk.

Listing 16.9 demonstrates the use of rename().

Listing 16.9. Using rename() to change the name of a disk file.

1:  /* Using rename() to change a filename. */
 2:
 3:  #include <stdio.h>
 4:
 5:  main()
 6:  {
 7:      char oldname[80], newname[80];
 8:
 9:      printf("Enter current filename: ");
 10:     gets(oldname);
 11:     printf("Enter new name for file: ");
 12:     gets(newname);
 13:
 14:     if ( rename( oldname, newname ) == 0 )
 15:         printf("%s has been renamed %s.\n", oldname, newname);
 16:     else
 17:         fprintf(stderr, "An error has occurred renaming %s.\n", oldname);
 18:     return(0);
 19: }
 Enter current filename: list1609.c
 Enter new name for file: rename.c
 list1609.c has been renamed rename.c.
 

ANALYSIS: Listing 16.9 shows how powerful C can be. With only 18 lines of code, this program replaces an operating system command, and it's a much friendlier function. Line 9 prompts for the name of the file to be renamed. Line 11 prompts for the new filename. The call to the rename() function is wrapped in an if statement on line 14. The if statement checks to ensure that the renaming of the file was carried out correctly. If so, line 15 prints an affirmative message; otherwise, line 17 prints a message stating that there was an error.

Copying a File

It's frequently necessary to make a copy of a file--an exact duplicate with a different name (or with the same name but in a different drive or directory). In DOS, you do this with the COPY command, and other operating systems have equivalents. How do you copy a file in C? There's no library function, so you need to write your own.

This might sound a bit complicated, but it's really quite simple thanks to C's use of streams for input and output. Here are the steps you follow:

1. Open the source file for reading in binary mode. (Using binary mode ensures that the function can copy all sorts of files, not just text files.)

2. Open the destination file for writing in binary mode.

3. Read a character from the source file. Remember, when a file is first opened, the pointer is at the start of the file, so there's no need to position the file pointer explicitly.

4. If the function feof() indicates that you've reached the end of the source file, you're done and can close both files and return to the calling program.

5. If you haven't reached end-of-file, write the character to the destination file, and then loop back to step 3.

Listing 16.10 contains a function, copy_file(), that is passed the names of the source and destination files and then performs the copy operation just as the preceding steps outlined. If there's an error opening either file, the function doesn't attempt the copy operation and returns -1 to the calling program. When the copy operation is complete, the program closes both files and returns 0.

Listing 16.10. A function that copies a file.

1:  /* Copying a file. */
 2:
 3:  #include <stdio.h>
 4:
 5:  int file_copy( char *oldname, char *newname );
 6:
 7:  main()
 8:  {
 9:      char source[80], destination[80];
 10:
 11:     /* Get the source and destination names. */
 12:
 13:     printf("\nEnter source file: ");
 14:     gets(source);
 15:     printf("\nEnter destination file: ");
 16:     gets(destination);
 17:
 18:     if ( file_copy( source, destination ) == 0 )
 19:         puts("Copy operation successful");
 20:     else
 21:         fprintf(stderr, "Error during copy operation");
 22:     return(0);
 23: }
 24: int file_copy( char *oldname, char *newname )
 25: {
 26:     FILE *fold, *fnew;
 27:     int c;
 28:
 29:     /* Open the source file for reading in binary mode. */
 30:
 31:     if ( ( fold = fopen( oldname, "rb" ) ) == NULL )
 32:         return -1;
 33:
 34:     /* Open the destination file for writing in binary mode. */
 35:
 36:     if ( ( fnew = fopen( newname, "wb" ) ) == NULL  )
 37:     {
 38:         fclose ( fold );
 39:         return -1;
 40:     }
 41:
 42:     /* Read one byte at a time from the source; if end of file */
 43:     /* has not been reached, write the byte to the */
 44:     /* destination. */
 45:
 46:     while (1)
 47:     {
 48:         c = fgetc( fold );
 49:
 50:         if ( !feof( fold ) )
 51:             fputc( c, fnew );
 52:         else
 53:             break;
 54:     }
 55:
 56:     fclose ( fnew );
 57:     fclose ( fold );
 58:
 59:     return 0;
 60: }
 Enter source file: list1610.c
 Enter destination file: tmpfile.c
 Copy operation successful
 

ANALYSIS: The function copy_file() works perfectly well, letting you copy anything from a small text file to a huge program file. It does have limitations, however. If the destination file already exists, the function deletes it without asking. A good programming exercise for you would be to modify copy_file() to check whether the destination file already exists, and then query the user as to whether the old file should be overwritten.

main() in Listing 16.10 should look very familiar. It's nearly identical to the main() in Listing 16.9, with the exception of line 14. Instead of rename(), this function uses copy(). Because C doesn't have a copy function, lines 24 through 60 create a copy function. Lines 31 and 32 open the source file, fold, in binary read mode. Lines 36 through 40 open the destination file, fnew, in binary write mode. Notice that line 38 closes the source file if there is an error opening the destination file. The while loop in lines 46 through 54 does the actual copying of the file. Line 48 gets a character from the source file, fold. Line 50 checks to see whether the end-of-file marker was read. If the end of the file has been reached, a break statement is executed in order to get out of the while loop. If the end of the file has not been reached, the character is written to the destination file, fnew. Lines 56 and 57 close the two files before returning to main().

Using Temporary Files

Some programs make use of one or more temporary files during execution. A temporary file is a file that is created by the program, used for some purpose during program execution, and then deleted before the program terminates. When you create a temporary file, you don't really care what its name is, because it gets deleted. All that is necessary is that you use a name that isn't already in use for another file. The C standard library includes a function tmpnam() that creates a valid filename that doesn't conflict with any existing file. Its prototype in STDIO.H is as follows:

char *tmpnam(char *s);
 

The argument s must be a pointer to a buffer large enough to hold the filename. You can also pass a null pointer (NULL), in which case the temporary name is stored in a buffer internal to tmpnam(), and the function returns a pointer to that buffer. Listing 16.11 demonstrates both methods of using tmpnam() to create temporary filenames.

Listing 16.11. Using tmpnam() to create temporary filenames.

1:  /* Demonstration of temporary filenames. */
 2:
 3:  #include <stdio.h>
 4:
 5:  main()
 6:  {
 7:      char buffer[10], *c;
 8:
 9:      /* Get a temporary name in the defined buffer. */
 10:
 11:     tmpnam(buffer);
 12:
 13:     /* Get another name, this time in the function's */
 14:     /* internal buffer. */
 15:
 16:     c = tmpnam(NULL);
 17:
 18:     /* Display the names. */
 19:
 20:     printf("Temporary name 1: %s", buffer);
 21:     printf("\nTemporary name 2: %s\n", c);
 22: }
 Temporary name 1: TMP1.$$$
 Temporary name 2: TMP2.$$$
 

ANALYSIS: The temporary names generated on your system will probably be different from these. This program only generates and prints the temporary names; it doesn't actually create any files. Line 11 stores a temporary name in the character array, buffer. Line 16 assigns the character pointer to the name returned by tmpnam() to c. Your program would have to use the generated name to open the temporary file and then delete the file before program execution terminates. The following code fragment illustrates:

char tempname[80];
 FILE *tmpfile;
 tmpnam(tempname);
 tmpfile = fopen(tempname, "w");  /* Use appropriate mode */
 fclose(tmpfile);
 remove(tempname);
 


DON'T remove a file that you might need again.

DON'T try to rename files across drives.

DON'T forget to remove temporary files that you create. They aren't deleted automatically.


Строки

You should remember from earlier chapters that, in C programs, a string is a sequence of characters, with its beginning indicated by a pointer and its end marked by the null character \0. At times, you need to know the length of a string (the number of characters between the start and the end of the string). This length is obtained with the library function strlen(). Its prototype, in STRING.H, is

size_t strlen(char *str);
 

You might be puzzling over the size_t return type. This type is defined in STRING.H as unsigned, so the function strlen() returns an unsigned integer. The size_t type is used with many of the string functions. Just remember that it means unsigned.

The argument passed to strlen is a pointer to the string of which you want to know the length. The function strlen() returns the number of characters between str and the next null character, not counting the null character. Listing 17.1 demonstrates strlen().

Listing 17.1. Using the strlen() function to determine the length of a string.

1:  /* Using the strlen() function. */
 2:
 3:  #include <stdio.h>
 4:  #include <string.h>
 5:
 6:  main()
 7:  {
 8:      size_t length;
 9:      char buf[80];
 10:
 11:     while (1)
 12:     {
 13:         puts("\nEnter a line of text; a blank line terminates.");
 14:         gets(buf);
 15:
 16:         length = strlen(buf);
 17:
 18:         if (length != 0)
 19:             printf("\nThat line is %u characters long.", length);
 20:         else
 21:             break;
 22:     }
 23:      return(0);
 24: }
 Enter a line of text; a blank line terminates.
 Just do it!
 That line is 11 characters long.
 Enter a line of text; a blank line terminates.
 

ANALYSIS:: This program does little more than demonstrate the use of strlen(). Lines 13 and 14 display a message and get a string called buf. Line 16 uses strlen() to assign the length of buf to the variable length. Line 18 checks whether the string was blank by checking for a length of 0. If the string wasn't blank, line 19 prints the string's size.

Copying Strings

The C library has three functions for copying strings. Because of the way C handles strings, you can't simply assign one string to another, as you can in some other computer languages. You must copy the source string from its location in memory to the memory location of the destination string. The string-copying functions are strcpy(), strncpy(), and strdup(). All of the string-copying functions require the header file STRING.H.

The strcpy() Function

The library function strcpy() copies an entire string to another memory location. Its prototype is as follows:

char *strcpy( char *destination, char *source );
 

The function strcpy() copies the string (including the terminating null character \0) pointed to by source to the location pointed to by destination. The return value is a pointer to the new string, destination.

When using strcpy(), you must first allocate storage space for the destination string. The function has no way of knowing whether destination points to allocated space. If space hasn't been allocated, the function overwrites strlen(source) bytes of memory, starting at destination; this can cause unpredictable problems. The use of strcpy() is illustrated in Listing 17.2.


NOTE: When a program uses malloc() to allocate memory, as Listing 17.2 does, good programming practice requires the use of the free() function to free up the memory when the program is finished with it. You'll learn about free() on Day 20, "Working with Memory."

Listing 17.2. Before using strcpy(), you must allocate storage space for the destination string.

1:  /* Demonstrates strcpy(). */
 2:  #include <stdlib.h>
 3:  #include <stdio.h>
 4:  #include <string.h>
 5:
 6:  char source[] = "The source string.";
 7:
 8:  main()
 9:  {
 10:     char dest1[80];
 11:     char *dest2, *dest3;
 12:
 13:     printf("\nsource: %s", source );
 14:
 15:     /* Copy to dest1 is okay because dest1 points to */
 16:     /* 80 bytes of allocated space. */
 17:
 18:     strcpy(dest1, source);
 19:     printf("\ndest1:  %s", dest1);
 20:
 21:     /* To copy to dest2 you must allocate space. */
 22:
 23:     dest2 = (char *)malloc(strlen(source) +1);
 24:     strcpy(dest2, source);
 25:     printf("\ndest2:  %s\n", dest2);
 26:
 27:     /* Copying without allocating destination space is a no-no. */
 28:     /* The following could cause serious problems. */
 29:
 30:     /* strcpy(dest3, source); */
 31:     return(0);
 32: }
 source: The source string.
 dest1:  The source string.
 dest2:  The source string.
 

ANALYSIS: This program demonstrates copying strings both to character arrays such as dest1 (declared on line 10) and to character pointers such as dest2 (declared along with dest3 on line 11). Line 13 prints the original source string. This string is then copied to dest1 with strcpy() on line 18. Line 24 copies source to dest2. Both dest1 and dest2 are printed to show that the function was successful. Notice that line 23 allocates the appropriate amount of space for dest2 with the malloc() function. If you copy a string to a character pointer that hasn't been allocated memory, you get unpredictable results.

The strncpy() Function

The strncpy() function is similar to strcpy(), except that strncpy() lets you specify how many characters to copy. Its prototype is

char *strncpy(char *destination, char *source, size_t n);
 

The arguments destination and source are pointers to the destination and source strings. The function copies, at most, the first n characters of source to destination. If source is shorter than n characters, enough null characters are added to the end of source to make a total of n characters copied to destination. If source is longer than n characters, no terminating \0 is added to destination. The function's return value is destination.

Listing 17.3 demonstrates the use of strncpy().

Listing 17.3. The strncpy() function.

1:  /* Using the strncpy() function. */
 2:
 3:  #include <stdio.h>
 4:  #include <string.h>
 5:
 6:  char dest[] = "..........................";
 7:  char source[] = "abcdefghijklmnopqrstuvwxyz";
 8:
 9:  main()
 10: {
 11:     size_t n;
 12:
 13:     while (1)
 14:     {
 15:         puts("Enter the number of characters to copy (1-26)");
 16:         scanf("%d", &n);
 17:
 18:         if (n > 0 && n< 27)
 19:             break;
 20:     }
 21:
 22:     printf("\nBefore strncpy destination = %s", dest);
 23:
 24:     strncpy(dest, source, n);
 25:
 26:     printf("\nAfter strncpy destination = %s\n", dest);
 27:     return(0);
 28: }
 Enter the number of characters to copy (1-26)
 15
 Before strncpy destination = ..........................
 After strncpy destination = abcdefghijklmno...........
 

ANALYSIS: In addition to demonstrating the strncpy() function, this program also illustrates an effective way to ensure that only correct information is entered by the user. Lines 13 through 20 contain a while loop that prompts the user for a number from 1 to 26. The loop continues until a valid value is entered, so the program can't continue until the user enters a valid value. When a number between 1 and 26 is entered, line 22 prints the original value of dest, line 24 copies the number of characters specified by the user from source to dest, and line 26 prints the final value of dest.


WARNING: Be sure that the number of characters copied doesn't exceed the allocated size of the destination.

The strdup() Function

The library function strdup() is similar to strcpy(), except that strdup() performs its own memory allocation for the destination string with a call to malloc(). In effect, it does what you did in Listing 17.2, allocating space with malloc() and then calling strcpy(). The prototype for strdup() is

char *strdup( char *source );
 

The argument source is a pointer to the source string. The function returns a pointer to the destination string--the space allocated by malloc()--or NULL if the needed memory couldn't be allocated. Listing 17.4 demonstrates the use of strdup(). Note that strdup() isn't an ANSI-standard function. It is included in the Microsoft, Borland, and Symantec C libraries, but it might not be present (or it might be different) in other C compilers.

Listing 17.4. Using strdup() to copy a string with automatic memory allocation.

1:  /* The strdup() function. */
 2:  #include <stdlib.h>
 3:  #include <stdio.h>
 4:  #include <string.h>
 5:
 6:  char source[] = "The source string.";
 7:
 8:  main()
 9:  {
 10:     char *dest;
 11:
 12:     if ( (dest = strdup(source)) == NULL)
 13:     {
 14:         fprintf(stderr, "Error allocating memory.");
 15:         exit(1);
 16:     }
 17:
 18:     printf("The destination = %s\n", dest);
 19:     return(0);
 20: }
 
The destination = The source string.
 

ANALYSIS: In this listing, strdup() allocates the appropriate memory for dest. It then makes a copy of the passed string, source. Line 18 prints the duplicated string.

Concatenating Strings

If you're not familiar with the term concatenation, you might be asking, "What is it?" and "Is it legal?" Well, it means to join two strings--to tack one string onto the end of another--and, in most states, it is legal. The C standard library contains two string concatenation functions--strcat() and strncat()--both of which require the header file STRING.H.

The strcat() Function

The prototype of strcat() is

char *strcat(char *str1, char *str2);
 

The function appends a copy of str2 onto the end of str1, moving the terminating null character to the end of the new string. You must allocate enough space for str1 to hold the resulting string. The return value of strcat() is a pointer to str1. Listing 17.5 demonstrates strcat().

Listing 17.5. Using strcat() to concatenate strings.

1:  /* The strcat() function. */
 2:
 3:  #include <stdio.h>
 4:  #include <string.h>
 5:
 6:  char str1[27] = "a";
 7:  char str2[2];
 8:
 9:  main()
 10: {
 11:     int n;
 12:
 13:     /* Put a null character at the end of str2[]. */
 14:
 15:     str2[1] = `\0';
 16:
 17:     for (n = 98; n< 123; n++)
 18:     {
 19:         str2[0] = n;
 20:         strcat(str1, str2);
 21:         puts(str1);
 22:     }
 23:      return(0);
 24: }
 ab
 abc
 abcd
 abcde
 abcdef
 abcdefg
 abcdefgh
 abcdefghi
 abcdefghij
 abcdefghijk
 abcdefghijkl
 abcdefghijklm
 abcdefghijklmn
 abcdefghijklmno
 abcdefghijklmnop
 abcdefghijklmnopq
 abcdefghijklmnopqr
 abcdefghijklmnopqrs
 abcdefghijklmnopqrst
 abcdefghijklmnopqrstu
 abcdefghijklmnopqrstuv
 abcdefghijklmnopqrstuvw
 abcdefghijklmnopqrstuvwx
 abcdefghijklmnopqrstuvwxy
 abcdefghijklmnopqrstuvwxyz
 

ANALYSIS: The ASCII codes for the letters b through z are 98 to 122. This program uses these ASCII codes in its demonstration of strcat(). The for loop on lines 17 through 22 assigns these values in turn to str2[0]. Because str2[1] is already the null character (line 15), the effect is to assign the strings "b", "c", and so on to str2. Each of these strings is concatenated with str1 (line 20), and then str1 is displayed on-screen (line 21).

The strncat() Function

The library function strncat() also performs string concatenation, but it lets you specify how many characters of the source string are appended to the end of the destination string. The prototype is

char *strncat(char *str1, char *str2, size_t n);
 

If str2 contains more than n characters, the first n characters are appended to the end of str1. If str2 contains fewer than n characters, all of str2 is appended to the end of str1. In either case, a terminating null character is added at the end of the resulting string. You must allocate enough space for str1 to hold the resulting string. The function returns a pointer to str1. Listing 17.6 uses strncat() to produce the same output as Listing 17.5.

Listing 17.6. Using the strncat() function to concatenate strings.

1:  /* The strncat() function. */
 2:
 3:  #include <stdio.h>
 4:  #include <string.h>
 5:
 6:  char str2[] = "abcdefghijklmnopqrstuvwxyz";
 7:
 8:  main()
 9:  {
 10:     char str1[27];
 11:     int n;
 12:
 13:     for (n=1; n< 27; n++)
 14:     {
 15:         strcpy(str1, "");
 16:         strncat(str1, str2, n);
 17:         puts(str1);
 18:     }
 19: }
 a
 ab
 abc
 abcd
 abcde
 abcdef
 abcdefg
 abcdefgh
 abcdefghi
 abcdefghij
 abcdefghijk
 abcdefghijkl
 abcdefghijklm
 abcdefghijklmn
 abcdefghijklmno
 abcdefghijklmnop
 abcdefghijklmnopq
 abcdefghijklmnopqr
 abcdefghijklmnopqrs
 abcdefghijklmnopqrst
 abcdefghijklmnopqrstu
 abcdefghijklmnopqrstuv
 abcdefghijklmnopqrstuvw
 abcdefghijklmnopqrstuvwx
 abcdefghijklmnopqrstuvwxy
 abcdefghijklmnopqrstuvwxyz
 

ANALYSIS: You might wonder about the purpose of line 15, strcpy(str1, "");. This line copies to str1 an empty string consisting of only a single null character. The result is that the first character in str1--str1[0]--is set equal to 0 (the null character). The same thing could have been accomplished with the statements str1[0] = 0; or str1[0] = `\0';.

Comparing Strings

Strings are compared to determine whether they are equal or unequal. If they are unequal, one string is "greater than" or "less than" the other. Determinations of "greater" and "less" are made with the ASCII codes of the characters. In the case of letters, this is equivalent to alphabetical order, with the one seemingly strange exception that all uppercase letters are "less than" the lowercase letters. This is true because the uppercase letters have ASCII codes 65 through 90 for A through Z, while lowercase a through z are represented by 97 through 122. Thus, "ZEBRA" would be considered to be less than "apple" by these C functions.

The ANSI C library contains functions for two types of string comparisons: comparing two entire strings, and comparing a certain number of characters in two strings.

Comparing Two Entire Strings

The function strcmp() compares two strings character by character. Its prototype is

int strcmp(char *str1, char *str2);
 

The arguments str1 and str2 are pointers to the strings being compared. The function's return values are given in Table 17.1. Listing 17.7 demonstrates strcmp().

Table 17.1. The values returned by strcmp().

Return Value Meaning
< 0 str1 is less than str2.
0 str1 is equal to str2.
> 0 str1 is greater than str2.

Listing 17.7. Using strcmp() to compare strings.

1:  /* The strcmp() function. */
 2:
 3:  #include <stdio.h>
 4:  #include <string.h>
 5:
 6:  main()
 7:  {
 8:      char str1[80], str2[80];
 9:      int x;
 10:
 11:     while (1)
 12:     {
 13:
 14:         /* Input two strings. */
 15:
 16:         printf("\n\nInput the first string, a blank to exit: ");
 17:         gets(str1);
 18:
 19:         if ( strlen(str1) == 0 )
 20:             break;
 21:
 22:         printf("\nInput the second string: ");
 23:         gets(str2);
 24:
 25:         /* Compare them and display the result. */
 26:
 27:         x = strcmp(str1, str2);
 28:
 29:         printf("\nstrcmp(%s,%s) returns %d", str1, str2, x);
 30:     }
 31:     return(0);
 32: }
 Input the first string, a blank to exit: First string
 Input the second string: Second string
 strcmp(First string,Second string) returns -1
 Input the first string, a blank to exit: test string
 Input the second string: test string
 strcmp(test string,test string) returns 0
 Input the first string, a blank to exit: zebra
 Input the second string: aardvark
 strcmp(zebra,aardvark) returns 1
 Input the first string, a blank to exit:
 


NOTE: On some UNIX systems, string comparison functions don't necessarily return -1 when the strings aren't the same. They will, however, always return a nonzero value for unequal strings.

ANALYSIS: This program demonstrates strcmp(), prompting the user for two strings (lines 16, 17, 22, and 23) and displaying the result returned by strcmp() on line 29. Experiment with this program to get a feel for how strcmp() compares strings. Try entering two strings that are identical except for case, such as Smith and SMITH. You'll see that strcmp() is case-sensitive, meaning that the program considers uppercase and lowercase letters to be different.

Comparing Partial Strings

The library function strncmp() compares a specified number of characters of one string to another string. Its prototype is

int strncmp(char *str1, char *str2, size_t n);
 

The function strncmp() compares n characters of str2 to str1. The comparison proceeds until n characters have been compared or the end of str1 has been reached. The method of comparison and return values are the same as for strcmp(). The comparison is case-sensitive. Listing 17.8 demonstrates strncmp().

Listing 17.8. Comparing parts of strings with strncmp().

1:  /* The strncmp() function. */
 2:
 3:  #include <stdio.h>
 4:  #include[Sigma]>=tring.h>
 5:
 6:  char str1[] = "The first string.";
 7:  char str2[] = "The second string.";
 8:
 9:  main()
 10: {
 11:     size_t n, x;
 12:
 13:     puts(str1);
 14:     puts(str2);
 15:
 16:     while (1)
 17:     {
 18:         puts("\n\nEnter number of characters to compare, 0 to exit.");
 19:         scanf("%d", &n);
 20:
 21:         if (n <= 0)
 22:             break;
 23:
 24:         x = strncmp(str1, str2, n);
 25:
 26:         printf("\nComparing %d characters, strncmp() returns %d.", n, x);
 27:     }
 28:     return(0);
 29: }
 The first string.
 The second string.
 Enter number of characters to compare, 0 to exit.
 3
 Comparing 3 characters, strncmp() returns .©]
 Enter number of characters to compare, 0 to exit.
 6
 Comparing 6 characters, strncmp() returns -1.
 Enter number of characters to compare, 0 to exit.
 0
 

ANALYSIS: This program compares two strings defined on lines 6 and 7. Lines 13 and 14 print the strings to the screen so that the user can see what they are. The program executes a while loop on lines 16 through 27 so that multiple comparisons can be done. If the user asks to compare zero characters on lines 18 and 19, the program breaks on line 22; otherwise, a strncmp() executes on line 24, and the result is printed on line 26.

Comparing Two Strings While Ignoring Case

Unfortunately, the ANSI C library doesn't include any functions for case-insensitive string comparison. Fortunately, most C compilers provide their own "in-house" functions for this task. Symantec uses the function strcmpl(). Microsoft uses a function called _stricmp(). Borland has two functions--strcmpi() and stricmp(). You need to check your library reference manual to determine which function is appropriate for your compiler. When you use a function that isn't case-sensitive, the strings Smith and SMITH compare as equal. Modify line 27 in Listing 17.7 to use the appropriate case-insensitive compare function for your compiler, and try the program again.

Searching Strings

The C library contains a number of functions that search strings. To put it another way, these functions determine whether one string occurs within another string and, if so, where. You can choose from six string-searching functions, all of which require the header file STRING.H.

The strchr() Function

The strchr() function finds the first occurrence of a specified character in a string. The prototype is

char *strchr(char *str, int ch);
 

The function strchr() searches str from left to right until the character ch is found or the terminating null character is found. If ch is found, a pointer to it is returned. If not, NULL is returned.

When strchr() finds the character, it returns a pointer to that character. Knowing that str is a pointer to the first character in the string, you can obtain the position of the found character by subtracting str from the pointer value returned by strchr(). Listing 17.9 illustrates this. Remember that the first character in a string is at position 0. Like many of C's string functions, strchr() is case-sensitive. For example, it would report that the character F isn't found in the string raffle.

Listing 17.9. Using strchr() to search a string for a single character.

1:  /* Searching for a single character with strchr(). */
 2:
 3:  #include <stdio.h>
 4:  #include <string.h>
 5:
 6:  main()
 7:  {
 8:      char *loc, buf[80];
 9:      int ch;
 10:
 11:     /* Input the string and the character. */
 12:
 13:     printf("Enter the string to be searched: ");
 14:     gets(buf);
 15:     printf("Enter the character to search for: ");
 16:     ch = getchar();
 17:
 18:     /* Perform the search. */
 19:
 20:     loc = strchr(buf, ch);
 21:
 22:     if ( loc == NULL )
 23:         printf("The character %c was not found.", ch);
 24:     else
 25:         printf("The character %c was found at position %d.\n",
 26:                 ch, loc-buf);
 27:     return(0);
 28: }
 Enter the string to be searched: How now Brown Cow?
 Enter the character to search for: C
 The character C was found at position 14.
 

ANALYSIS: This program uses strchr() on line 20 to search for a character within a string. strchr() returns a pointer to the location where the character is first found, or NULL if the character isn't found. Line 22 checks whether the value of loc is NULL and prints an appropriate message. As just mentioned, the position of the character within the string is determined by subtracting the string pointer from the value returned by the function.

The strrchr() Function

The library function strrchr() is identical to strchr(), except that it searches a string for the last occurrence of a specified character in a string. Its prototype is

char *strrchr(char *str, int ch);
 

The function strrchr() returns a pointer to the last occurrence of ch in str and NULL if it finds no match. To see how this function works, modify line 20 in Listing 17.9 to use strrchr() instead of strchr().

The strcspn() Function

The library function strcspn() searches one string for the first occurrence of any of the characters in a second string. Its prototype is

size_t strcspn(char *str1, char *str2);
 

The function strcspn() starts searching at the first character of str1, looking for any of the individual characters contained in str2. This is important to remember. The function doesn't look for the string str2, but only the characters it contains. If the function finds a match, it returns the offset from the beginning of str1, where the matching character is located. If it finds no match, strcspn() returns the value of strlen(str1). This indicates that the first match was the null character terminating the string. Listing 17.10 shows you how to use strcspn().

Listing 17.10. Searching for a set of characters with strcspn().

1:  /* Searching with strcspn(). */
 2:
 3:  #include <stdio.h>
 4:  #include <string.h>
 5:
 6:  main()
 7:  {
 8:      char  buf1[80], buf2[80];
 9:      size_t loc;
 10:
 11:     /* Input the strings. */
 12:
 13:     printf("Enter the string to be searched: ");
 14:     gets(buf1);
 15:     printf("Enter the string containing target characters: ");
 16:     gets(buf2);
 17:
 18:     /* Perform the search. */
 19:
 20:     loc = strcspn(buf1, buf2);
 21:
 22:     if ( loc ==  strlen(buf1) )
 23:         printf("No match was found.");
 24:     else
 25:         printf("The first match was found at position %d.\n", loc);
 26:     return(0);
 27: }
 Enter the string to be searched: How now Brown Cow?
 Enter the string containing target characters: Cat
 The first match was found at position 14.
 

ANALYSIS: This listing is similar to Listing 17.10. Instead of searching for the first occurrence of a single character, it searches for the first occurrence of any of the characters entered in the second string. The program calls strcspn() on line 20 with buf1 and buf2. If any of the characters in buf2 are in buf1, strcspn() returns the offset from the beginning of buf1 to the location of the first occurrence. Line 22 checks the return value to determine whether it is NULL. If the value is NULL, no characters were found, and an appropriate message is displayed on line 23. If a value was found, a message is displayed stating the character's position in the string.

The strspn() Function

This function is related to the previous one, strcspn(), as the following paragraph explains. Its prototype is

size_t strspn(char *str1, char *str2);
 

The function strspn() searches str1, comparing it character by character with the characters contained in str2. It returns the position of the first character in str1 that doesn't match a character in str2. In other words, strspn() returns the length of the initial segment of str1 that consists entirely of characters found in str2. The return is 0 if no characters match. Listing 17.11 demonstrates strspn().

Listing 17.11. Searching for the first nonmatching character with strspn().

1: /* Searching with strspn(). */
 2:
 3: #include <stdio.h>
 4: #include <string.h>
 5:
 6: main()
 7: {
 8:     char  buf1[80], buf2[80];
 9:     size_t loc;
 10:
 11:     /* Input the strings. */
 12:
 13:     printf("Enter the string to be searched: ");
 14:     gets(buf1);
 15:     printf("Enter the string containing target characters: ");
 16:     gets(buf2);
 17:
 18:     /* Perform the search. */
 19:
 20:     loc = strspn(buf1, buf2);
 21:
 22:     if ( loc ==  0 )
 23:         printf("No match was found.\n");
 24:     else
 25:         printf("Characters match up to position %d.\n", loc-1);
 26:
 27: }
 Enter the string to be searched: How now Brown Cow?
 Enter the string containing target characters: How now what?
 Characters match up to position 7.
 

ANALYSIS: This program is identical to the previous example, except that it calls strspn() instead of strcspn() on line 20. The function returns the offset into buf1, where the first character not in buf2 is found. Lines 22 through 25 evaluate the return value and print an appropriate message.

The strpbrk() Function

The library function strpbrk() is similar to strcspn(), searching one string for the first occurrence of any character contained in another string. It differs in that it doesn't include the terminating null characters in the search. The function prototype is

char *strpbrk(char *str1, char *str2);
 

The function strpbrk() returns a pointer to the first character in str1 that matches any of the characters in str2. If it doesn't find a match, the function returns NULL. As previously explained for the function strchr(), you can obtain the offset of the first match in str1 by subtracting the pointer str1 from the pointer returned by strpbrk() (if it isn't NULL, of course). For example, replace strcspn() on line 20 of Listing 17.10 with strpbrk().

The strstr() Function

The final, and perhaps most useful, C string-searching function is strstr(). This function searches for the first occurrence of one string within another, and it searches for the entire string, not for individual characters within the string. Its prototype is

char *strstr(char *str1, char *str2);
 

The function strstr() returns a pointer to the first occurrence of str2 within str1. If it finds no match, the function returns NULL. If the length of str2 is 0, the function returns str1. When strstr() finds a match, you can obtain the offset of str2 within str1 by pointer subtraction, as explained earlier for strchr(). The matching procedure that strstr() uses is case-sensitive. Listing 17.12 demonstrates how to use strstr().

Listing 17.12. Using strstr() to search for one string within another.

1:  /* Searching with strstr(). */
 2:
 3:  #include <stdio.h>
 4:  #include <string.h>
 5:
 6:  main()
 7:  {
 8:      char *loc, buf1[80], buf2[80];
 9:
 10:     /* Input the strings. */
 11:
 12:     printf("Enter the string to be searched: ");
 13:     gets(buf1);
 14:     printf("Enter the target string: ");
 15:     gets(buf2);
 16:
 17:     /* Perform the search. */
 18:
 19:     loc = strstr(buf1, buf2);
 20:
 21:     if ( loc ==  NULL )
 22:         printf("No match was found.\n");
 23:     else
 24:         printf("%s was found at position %d.\n", buf2, loc-buf1);
 25:     return(0);
 26: }
 Enter the string to be searched: How now brown cow?
 Enter the target string: cow
 Cow was found at position 14.
 

ANALYSIS: This function provides an alternative way to search a string. This time you can search for an entire string within another string. Lines 12 through 15 prompt for two strings. Line 19 uses strstr() to search for the second string, buf2, within the first string, buf1. A pointer to the first occurrence is returned, or NULL is returned if the string isn't found. Lines 21 through 24 evaluate the returned value, loc, and print an appropriate message.


DO remember that for many of the string functions, there are equivalent functions that let you specify a number of characters to manipulate. The functions that allow specification of the number of characters are usually named strnxxx(), where xxx is specific to the function.

DON'T forget that C is case-sensitive. A and a are different.


String Conversions

Many C libraries contain two functions that change the case of characters within a string. These functions aren't ANSI standard and therefore might differ or not even exist in your compiler. Because they can be quite useful, they are included here. Their prototypes, in STRING.H, are as follows for the Microsoft C compiler (if you use a different compiler, they should be similar):

char *strlwr(char *str);
 char *strupr(char *str);
 

The function strlwr() converts all the letter characters in str from uppercase to lowercase; strupr() does the reverse, converting all the characters in str to uppercase. Nonletter characters aren't affected. Both functions return str. Note that neither function actually creates a new string but modifies the existing string in place. Listing 17.13 demonstrates these functions. Remember that to compile a program that uses non-ANSI functions, you might need to tell your compiler not to enforce the ANSI standards.

Listing 17.13. Converting the case of characters in a string with strlwr() and strupr().

1:  /* The character conversion functions strlwr() and strupr(). */
 2:
 3:  #include <stdio.h>
 4:  #include <string.h>
 5:
 6:  main()
 7:  {
 8:      char buf[80];
 9:
 10:     while (1)
 11:     {
 12:         puts("Enter a line of text, a blank to exit.");
 13:         gets(buf);
 14:
 15:         if ( strlen(buf) == 0 )
 16:             break;
 17:
 18:         puts(strlwr(buf));
 19:         puts(strupr(buf));
 20:     }
 21:     return(0);
 22: }
 Enter a line of text, a blank to exit.
 Bradley L. Jones
 bradley l. jones
 BRADLEY L. JONES
 Enter a line of text, a blank to exit.
 

ANALYSIS: This listing prompts for a string on line 12. It then checks to ensure that the string isn't blank (line 15). Line 18 prints the string after converting it to lowercase. Line 19 prints the string in all uppercase.

These functions are a part of the Symantec, Microsoft, and Borland C libraries. You need to check the Library Reference for your compiler before using these functions. If portability is a concern, you should avoid using non-ANSI functions such as these.

Miscellaneous String Functions

This section covers a few string functions that don't fall into any other category. They all require the header file STRING.H.

The strrev() Function

The function strrev() reverses the order of all the characters in a string. Its prototype is

char *strrev(char *str);
 

The order of all characters in str is reversed, with the terminating null character remaining at the end. The function returns str. After strset() and strnset() are defined in the next section, strrev() is demonstrated in Listing 17.14.

The strset() and strnset() Functions

Like the previous function, strrev(), strset() and strnset() aren't part of the ANSI C standard library. These functions change all characters (strset()) or a specified number of characters (strnset()) in a string to a specified character. The prototypes are

char *strset(char *str, int ch);
 char *strnset(char *str, int ch, size_t n);
 

The function strset() changes all the characters in str to ch except the terminating null character. The function strnset() changes the first n characters of str to ch. If n >= strlen(str), strnset() changes all the characters in str. Listing 17.14 demonstrates all three functions.

Listing 17.14. A demonstration of strrev(), strnset(), and strset().

1:  /* Demonstrates strrev(), strset(), and strnset(). */
 2:  #include <stdio.h>
 3:  #include <string.h>
 4:
 5:  char str[] = "This is the test string.";
 6:
 7:  main()
 8:  {
 9:      printf("\nThe original string: %s", str);
 10:     printf("\nCalling strrev(): %s", strrev(str));
 11:     printf("\nCalling strrev() again: %s", strrev(str));
 12:     printf("\nCalling strnset(): %s", strnset(str, `!', 5));
 13:     printf("\nCalling strset(): %s", strset(str, `!'));
 14:     return(0);
 15: }
 The original string: This is the test string.
 Calling strrev(): .gnirts tset eht si sihT
 Calling strrev() again: This is the test string.
 Calling strnset(): !!!!!is the test string.
 Calling strset(): !!!!!!!!!!!!!!!!!!!!!!!!
 

ANALYSIS: This program demonstrates the three different string functions. The demonstrations are done by printing the value of a string, str. Line 9 prints the string normally. Line 10 prints the string after it has been reversed with strrev(). Line 11 reverses it back to its original state. Line 12 uses the strnset() function to set the first five characters of str to exclamation marks. To finish the program, line 13 changes the entire string to exclamation marks.

Although these functions aren't ANSI standard, they are included in the Symantec, Microsoft, and Borland C compiler function libraries. You should check your compiler's Library Reference manual to determine whether your compiler supports these functions.

String-to-Number Conversions

Sometimes you will need to convert the string representation of a number to an actual numeric variable. For example, the string "123" can be converted to a type int variable with the value 123. Three functions can be used to convert a string to a number. They are explained in the following sections; their prototypes are in STDLIB.H.

The atoi() Function

The library function atoi() converts a string to an integer. The prototype is

int atoi(char *ptr);
 

The function atoi() converts the string pointed to by ptr to an integer. Besides digits, the string can contain leading white space and a + or -- sign. Conversion starts at the beginning of the string and proceeds until an unconvertible character (for example, a letter or punctuation mark) is encountered. The resulting integer is returned to the calling program. If it finds no convertible characters, atoi() returns 0. Table 17.2 lists some examples.

Table 17.2. String-to-number conversions with atoi().

String Value Returned by atoi()
"157" 157
"-1.6" -1
"+50x" 50
"twelve" 0
"x506" 0

The first example is straightforward. In the second example, you might be confused about why the ".6" didn't translate. Remember that this is a string-to-integer conversion. The third example is also straightforward; the function understands the plus sign and considers it a part of the number. The fourth example uses "twelve". The atoi() function can't translate words; it sees only characters. Because the string didn't start with a number, atoi() returns 0. This is true of the last example also.

The atol() Function

The library function atol() works exactly like atoi(), except that it returns a type long. The function prototype is

long atol(char *ptr);
 

The values returned by atol() would be the same as shown for atoi() in Table 17.2, except that each return value would be a type long instead of a type int.

The atof() Function

The function atof() converts a string to a type double. The prototype is

double atof(char *str);
 

The argument str points to the string to be converted. This string can contain leading white space and a + or -- character. The number can contain the digits 0 through 9, the decimal point, and the exponent indicator E or e. If there are no convertible characters, atof() returns 0. Table 17.3 lists some examples of using atof().

Table 17.3. String-to-number conversions with atof().

String Value Returned by atof()
"12" 12.000000
"-0.123" -0.123000
"123E+3" 123000.000000
"123.1e-5" 0.001231

Listing 17.15 lets you enter your own strings for conversion.

Listing 17.15. Using atof() to convert strings to type double numeric variables.

1:  /* Demonstration of atof(). */
 2:
 3:  #include <string.h>
 4:  #include <stdio.h>
 5:  #include <stdlib.h>
 6:
 7:  main()
 8:  {
 9:      char buf[80];
 10:     double d;
 11:
 12:     while (1)
 13:     {
 14:         printf("\nEnter the string to convert (blank to exit):     ");
 15:         gets(buf);
 16:
 17:         if ( strlen(buf) == 0 )
 18:             break;
 19:
 20:         d = atof( buf );
 21:
 22:         printf("The converted value is %f.", d);
 23:     }
 24:     return(0);
 25: }
 Enter the string to convert (blank to exit):     1009.12
 The converted value is 1009.120000.
 Enter the string to convert (blank to exit):     abc
 The converted value is 0.000000.
 Enter the string to convert (blank to exit):     3
 The converted value is 3.000000.
 Enter the string to convert (blank to exit):
 

ANALYSSIS: The while loop on lines 12 through 23 lets you keep running the program until you enter a blank line. Lines 14 and 15 prompt for the value. Line 17 checks whether a blank line was entered. If it was, the program breaks out of the while loop and ends. Line 20 calls atof(), converting the value entered (buf) to a type double, d. Line 22 prints the final result.

Character Test Functions

The header file CTYPE.H contains the prototypes for a number of functions that test characters, returning TRUE or FALSE depending on whether the character meets a certain condition. For example, is it a letter or is it a numeral? The isxxxx() functions are actually macros, defined in CTYPE.H. You'll learn about macros on Day 21, "Advanced Compiler Use," at which time you might want to look at the definitions in CTYPE.H to see how they work. For now, you only need to see how they're used.

The isxxxx() macros all have the same prototype:

int isxxxx(int ch);
 

In the preceding line, ch is the character being tested. The return value is TRUE (nonzero) if the condition is met or FALSE (zero) if it isn't. Table 17.4 lists the complete set of isxxxx() macros.

Table 17.4. The isxxxx() macros.

Macro Action
isalnum() Returns TRUE if ch is a letter or a digit.
isalpha() Returns TRUE if ch is a letter.
isascii() Returns TRUE if ch is a standard ASCII character (between 0 and 127).
iscntrl() Returns TRUE if ch is a control character.
isdigit() Returns TRUE if ch is a digit.
isgraph() Returns TRUE if ch is a printing character (other than a space).
islower() Returns TRUE if ch is a lowercase letter.
isprint() Returns TRUE if ch is a printing character (including a space).
ispunct() Returns TRUE if ch is a punctuation character.
isspace() Returns TRUE if ch is a whitespace character (space, tab, vertical tab, line feed, form feed, or carriage return).
isupper() Returns TRUE if ch is an uppercase letter.
isxdigit() Returns TRUE if ch is a hexadecimal digit (0 through 9, a through f, A through F).

You can do many interesting things with the character-test macros. One example is the function get_int(), shown in Listing 17.16. This function inputs an integer from stdin and returns it as a type int variable. The function skips over leading white space and returns 0 if the first nonspace character isn't a numeric character.

Listing 17.16. Using the isxxxx() macros to implement a function that inputs an integer.

1:  /* Using character test macros to create an integer */
 2:  /* input function. */
 3:
 4:  #include <stdio.h>
 5:  #include <ctype.h>
 6:
 7:  int get_int(void);
 8:
 9:  main()
 10: {
 11:     int x;
 12:     x =  get_int();
 13:
 14:     printf("You entered %d.\n", x);
 15: }
 16:
 17: int get_int(void)
 18: {
 19:     int ch, i, sign = 1;
 20:
 21:     /* Skip over any leading white space. */
 22:
 23:     while ( isspace(ch = getchar()) )
 24:         ;
 25:
 26:     /* If the first character is nonnumeric, unget */
 27:     /* the character and return 0. */
 28:
 29:     if (ch != `-' && ch != `+' && !isdigit(ch) && ch != EOF)
 30:     {
 31:         ungetc(ch, stdin);
 32:         return 0;
 33:     }
 34:
 35:     /* If the first character is a minus sign, set */
 36:     /* sign accordingly. */
 37:
 38:     if (ch == `-')
 39:         sign = -1;
 40:
 41:     /* If the first character was a plus or minus sign, */
 42:     /* get the next character. */
 43:
 44:     if (ch == `+' || ch == `-')
 45:         ch = getchar();
 46:
 47:     /* Read characters until a nondigit is input. Assign */
 48:     /* values, multiplied by proper power of 10, to i. */
 49:
 50:     for (i = 0; isdigit(ch); ch = getchar() )
 51:         i = 10 * i + (ch - `0');
 52:
 53:     /* Make result negative if sign is negative. */
 54:
 55:     i *= sign;
 56:
 57:     /* If EOF was not encountered, a nondigit character */
 58:     /* must have been read in, so unget it. */
 59:
 60:     if (ch != EOF)
 61:         ungetc(ch, stdin);
 62:
 63:     /* Return the input value. */
 64:
 65:     return i;
 66: }
 -100
 You entered -100.
 abc3.145
 You entered 0.
 9 9 9
 You entered 9.
 2.5
 You entered 2.
 

ANALYSIS: This program uses the library function ungetc() on lines 31 and 61, which you learned about on Day 14, "Working with the Screen, Printer, and Keyboard." Remember that this function "ungets," or returns, a character to the specified stream. This returned character is the first one input the next time the program reads a character from that stream. This is necessary because when the function get_int() reads a nonnumeric character from stdin, you want to put that character back in case the program needs to read it later.

In this program, main() is simple. An integer variable, x, is declared (line 11), assigned the value of the get_int() function (line 12), and printed to the screen (line 14). The get_int() function makes up the rest of the program.

The get_int() function isn't so simple. To remove leading white space that might be entered, line 23 loops with a while command. The isspace() macro tests a character, ch, obtained with the getchar() function. If ch is a space, another character is retrieved, until a nonwhitespace character is received. Line 29 checks whether the character is one that can be used. Line 29 could be read, "If the character input isn't a negative sign, a plus sign, a digit, or the end of the file(s)." If this is true, ungetc() is used on line 31 to put the character back, and the function returns to main(). If the character is usable, execution continues.

Lines 38 through 45 handle the sign of the number. Line 38 checks to see whether the character entered was a negative sign. If it was, a variable (sign) is set to -1. sign is used to make the final number either positive or negative (line 55). Because positive numbers are the default, after you have taken care of the negative sign, you are almost ready to continue. If a sign was entered, the program needs to get another character. Lines 44 and 45 take care of this.

The heart of the function is the for loop on lines 50 and 51, which continues to get characters as long as the characters retrieved are digits. Line 51 might be a little confusing at first. This line takes the individual character entered and turns it into a number. Subtracting the character `0' from your number changes a character number to a real number. (Remember the ASCII values.) When the correct numerical value is obtained, the numbers are multiplied by the proper power of 10. The for loop continues until a nondigit number is entered. At that point, line 55 applies the sign to the number, making it complete.

Before returning, the program needs to do a little cleanup. If the last number wasn't the end of file, it needs to be put back in case it's needed elsewhere. Line 61 does this before line 65 returns.


DON'T use non-ANSI functions if you plan to port your application to other platforms.

DO take advantage of the string functions that are available.

DON'T confuse characters with numbers. It's easy to forget that the character "1" isn't the same thing as the number 1.


Память

All of C's data objects have a specific type. A numeric variable can be an int or a float, a pointer can be a pointer to a double or char, and so on. Programs often require that different types be combined in expressions and statements. What happens in such cases? Sometimes C automatically handles the different types, so you don't need to be concerned. Other times, you must explicitly convert one data type to another to avoid erroneous results. You've seen this in earlier chapters when you had to convert or cast a type void pointer to a specific type before using it. In this and other situations, you need a clear understanding of when explicit type conversions are necessary and what types of errors can result when the proper conversion isn't applied. The following sections cover C's automatic and explicit type conversions.

Automatic Type Conversions

As the name implies, automatic type conversions are performed automatically by the C compiler without your needing to do anything. However, you should be aware of what's going on so that you can understand how C evaluates expressions.

Type Promotion in Expressions

When a C expression is evaluated, the resulting value has a particular data type. If all the components in the expression have the same type, the resulting type is that type as well. For example, if x and y are both type int, the following expression is type int also:

x + y
 

What if the components of an expression have different types? In that case, the expression has the same type as its most comprehensive component. From least-comprehensive to most-comprehensive, the numerical data types are

char

int

long

float

double

Thus, an expression containing an int and a char evaluates to type int, an expression containing a long and a float evaluates to type float, and so on.

Within expressions, individual operands are promoted as necessary to match the associated operands in the expression. Operands are promoted, in pairs, for each binary operator in the expression. Of course, promotion isn't needed if both operands are the same type. If they aren't, promotion follows these rules:

  • If either operand is a double, the other operand is promoted to type double.

  • If either operand is a float, the other operand is promoted to type float.

  • If either operand is a long, the other operand is converted to type long.

For example, if x is an int and y is a float, evaluating the expression x/y causes x to be promoted to type float before the expression is evaluated. This doesn't mean that the type of variable x is changed. It means that a type float copy of x is created and used in the expression evaluation. The value of the expression is, as you just learned, type float. Likewise, if x is a type double and y is a type float, y will be promoted to double.

Conversion by Assignment

Promotions also occur with the assignment operator. The expression on the right side of an assignment statement is always promoted to the type of the data object on the left side of the assignment operator. Note that this might cause a "demotion" rather than a promotion. If f is a type float and i is a type int, i is promoted to type float in this assignment statement:

f = i;
 

In contrast, the assignment statement

i = f;
 

causes f to be demoted to type int. Its fractional part is lost on assignment to i. Remember that f itself isn't changed at all; promotion affects only a copy of the value. Thus, after the following statements are executed:

float f = 1.23;
 int i;
 i = f;
 

the variable i has the value 1, and f still has the value 1.23. As this example illustrates, the fractional part is lost when a floating-point number is converted to an integer type.

You should be aware that when an integer type is converted to a floating-point type, the resulting floating-point value might not exactly match the integer value. This is because the floating-point format used internally by the computer can't accurately represent every possible integer number. For example, the following code could result in display of 2.999995 instead of 3:

float f;
 int i = 3;
 f = i;
 printf("%f", f);
 

In most cases, any loss of accuracy caused by this would be insignificant. To be sure, however, keep integer values in type int or type long variables.

Explicit Conversions Using Typecasts

A typecast uses the cast operator to explicitly control type conversions in your program. A typecast consists of a type name, in parentheses, before an expression. Casts can be performed on arithmetic expressions and pointers. The result is that the expression is converted to the type specified by the cast. In this manner, you can control the type of expressions in your program rather than relying on C's automatic conversions.

Casting Arithmetic Expressions

Casting an arithmetic expression tells the compiler to represent the value of the expression in a certain way. In effect, a cast is similar to a promotion, which was discussed earlier. However, a cast is under your control, not the compiler's. For example, if i is a type int, the expression

(float)i
 

casts i to type float. In other words, the program makes an internal copy of the value of i in floating-point format.

When would you use a typecast with an arithmetic expression? The most common use is to avoid losing the fractional part of the answer in an integer division. Listing 20.1 illustrates this. You should compile and run this program.

Listing 20.1. When one integer is divided by another, any fractional part of the answer is lost.

1: #include <stdio.h>
 2:
 3: main()
 4: {
 5:     int i1 = 100, i2 = 40;
 6:     float f1;
 7:
 8:     f1 = i1/i2;
 9:     printf("%lf\n", f1);
 10:    return(0);
 11: }
 
2.000000
 

ANALYSIS: The answer displayed by the program is 2.000000, but 100/40 evaluates to 2.5. What happened? The expression i1/i2 on line 8 contains two type int variables. Following the rules explained earlier in this chapter, the value of the expression is type int itself. As such, it can represent only whole numbers, so the fractional part of the answer is lost.

You might think that assigning the result of i1/i2 to a type float variable promotes it to type float. This is correct, but now it's too late; the fractional part of the answer is already gone.

To avoid this sort of inaccuracy, you must cast one of the type int variables to type float. If one of the variables is cast to type float, the previous rules tell you that the other variable is promoted automatically to type float, and the value of the expression is also type float. The fractional part of the answer is thus preserved. To demonstrate this, change line 8 in the source code so that the assignment statement reads as follows:

f1 = (float)i1/i2;
 

The program will then display the correct answer.

Casting Pointers

You have already been introduced to the casting of pointers. As you saw on Day 18, "Getting More from Functions," a type void pointer is a generic pointer; it can point to anything. Before you can use a void pointer, you must cast it to the proper type. Note that you don't need to cast a pointer in order to assign a value to it or to compare it with NULL. However, you must cast it before dereferencing it or performing pointer arithmetic with it. For more details on casting void pointers, review Day 18.


DO use a cast to promote or demote variable values when necessary.

DON'T use a cast just to prevent a compiler warning. You might find that using a cast gets rid of a warning, but before removing the warning this way, be sure you understand why you're getting the warning.


Allocating Memory Storage Space

The C library contains functions for allocating memory storage space at runtime, a process called dynamic memory allocation. This technique can have significant advantages over explicitly allocating memory in the program source code by declaring variables, structures, and arrays. This latter method, called static memory allocation, requires you to know when you're writing the program exactly how much memory you need. Dynamic memory allocation allows the program to react, while it's executing, to demands for memory, such as user input. All the functions for handling dynamic memory allocation require the header file STDLIB.H; with some compilers, MALLOC.H is required as well. Note that all allocation functions return a type void pointer. As you learned on Day 18, a type void pointer must be cast to the appropriate type before being used.

Before we move on to the details, a few words are in order about memory allocation. What exactly does it mean? Each computer has a certain amount of memory (random access memory, or RAM) installed. This amount varies from system to system. When you run a program, whether a word processor, a graphics program, or a C program you wrote yourself, the program is loaded from disk into the computer's memory. The memory space the program occupies includes the program code as well as space for all the program's static data--that is, data items that are declared in the source code. The memory left over is what's available for allocation using the functions in this section.

How much memory is available for allocation? It all depends. If you're running a large program on a system with only a modest amount of memory installed, the amount of free memory will be small. Conversely, when a small program is running on a multimegabyte system, plenty of memory will be available. This means that your programs can't make any assumptions about memory availability. When a memory allocation function is called, you must check its return value to ensure that the memory was allocated successfully. In addition, your programs must be able to gracefully handle the situation when a memory allocation request fails. Later in this chapter, you'll learn a technique for determining exactly how much memory is available.

Also note that your operating system might have an effect on memory availability. Some operating systems make only a portion of physical RAM available. DOS 6.x and earlier falls into this category. Even if your system has multiple megabytes of RAM, a DOS program will have direct access to only the first 640 KB. (Special techniques can be used to access the other memory, but these are beyond the scope of this book.) In contrast, UNIX usually will make all physical RAM available to a program. To complicate matters further, some operating systems, such as Windows and OS/2, provide virtual memory that permits storage space on the hard disk to be allocated as if it were RAM. In this situation, the amount of memory available to a program includes not only the RAM installed, but also the virtual-memory space on the hard disk.

For the most part, these operating system differences in memory allocation should be transparent to you. If you use one of the C functions to allocate memory, the call either succeeds or fails, and you don't need to worry about the details of what's happening.

The malloc() Function

In earlier chapters, you learned how to use the malloc() library function to allocate storage space for strings. The malloc() function isn't limited to allocating memory for strings, of course; it can allocate space for any storage need. This function allocates memory by the byte. Recall that malloc()'s prototype is

void *malloc(size_t num);
 

The argument size_t is defined in STDLIB.H as unsigned. The malloc() function allocates num bytes of storage space and returns a pointer to the first byte. This function returns NULL if the requested storage space couldn't be allocated or if num == 0. Review the section called "The malloc() Function" on Day 10, "Characters and Strings," if you're still a bit unclear on its operation.

Listing 20.2 shows you how to use malloc() to determine the amount of free memory available in your system. This program works fine under DOS, or in a DOS box under Windows. Be warned, however, that you might get strange results on systems such as OS/2 and UNIX, which use hard disk space to provide "virtual" memory. The program might take a very long time to exhaust available memory.

Listing 20.2. Using malloc() to determine how much memory is free.

1: /* Using malloc() to determine free memory.*/
 2:
 3: #include <stdio.h>
 4: #include <stdlib.h>
 5:
 6: /* Definition of a structure that is
 7:    1024 bytes (1 kilobyte) in size.) */
 8:
 9: struct kilo {
 10:    struct kilo *next;
 11:    char dummy[1022];
 12: };
 13:
 14: int FreeMem(void);
 15:
 16: main()
 17: {
 18:
 19:    printf("You have %d kilobytes free.\n", FreeMem());
 20:    return(0);
 21: }
 22:
 23: int FreeMem(void)
 24: {
 25:    /*Returns the number of kilobytes (1024 bytes)
 26:    of free memory. */
 27:
 28:    int counter;
 29:    struct kilo *head, *current, *nextone;
 30:
 31:    current = head = (struct kilo*) malloc(sizeof(struct kilo));
 32:
 33:    if (head == NULL)
 34:       return 0;      /*No memory available.*/
 35:
 36:    counter = 0;
 37:    do
 38:    {
 39:       counter++;
 40:       current->next = (struct kilo*) malloc(sizeof(struct kilo));
 41:       current = current->next;
 42:    } while (current != NULL);
 43:
 44:    /* Now counter holds the number of type kilo
 45:       structures we were able to allocate. We
 46:       must free them all before returning. */
 47:
 48:    current = head;
 49:
 50:    do
 51:    {
 52:       nextone = current->next;
 53:       free(current);
 54:       current = nextone;
 55:    } while (nextone != NULL);
 56:
 57:    return counter;
 58: }
 
You have 60 kilobytes free.
 

ANALYSIS: Listing 20.2 operates in a brute-force manner. It simply loops, allocating blocks of memory, until the malloc() function returns NULL, indicating that no more memory is available. The amount of available memory is then equal to the number of blocks allocated multiplied by the block size. The function then frees all the allocated blocks and returns the number of blocks allocated to the calling program. By making each block one kilobyte, the returned value indicates directly the number of kilobytes of free memory. As you may know, a kilobyte is not exactly 1000 bytes, but rather 1024 bytes (2 to the 10th power). We obtain a 1024-byte item by defining a structure, which we cleverly named kilo, that contains a 1022-byte array plus a 2-byte pointer.

The function FreeMem() uses the technique of linked lists, which was covered in more detail on Day 15, "Pointers: Beyond the Basics." In brief, a linked list consists of structures that contain a pointer to their own type (in addition to other data members). There is also a head pointer that points to the first item in the list (the variable head, a pointer to type kilo). The first item in the list points to the second, the second points to the third, and so on. The last item in the list is identified by a NULL pointer member. See Day 15 for more information.

The calloc() Function

The calloc() function also allocates memory. Rather than allocating a group of bytes as malloc() does, calloc() allocates a group of objects. The function prototype is

void *calloc(size_t num, size_t size);
 

Remember that size_t is a synonym for unsigned on most compilers. The argument num is the number of objects to allocate, and size is the size (in bytes) of each object. If allocation is successful, all the allocated memory is cleared (set to 0), and the function returns a pointer to the first byte. If allocation fails or if either num or size is 0, the function returns NULL.

Listing 20.3 illustrates the use of calloc().

Listing 20.3. Using the calloc() function to allocate memory storage space dynamically.

1: /* Demonstrates calloc(). */
 2:
 3: #include <stdlib.h>
 4: #include <stdio.h>
 5:
 6: main()
 7: {
 8:     unsigned num;
 9:     int *ptr;
 10:
 11:     printf("Enter the number of type int to allocate: ");
 12:     scanf("%d", &num);
 13:
 14:     ptr = (int*)calloc(num, sizeof(int));
 15:
 16:     if (ptr != NULL)
 17:         puts("Memory allocation was successful.");
 18:     else
 19:         puts("Memory allocation failed.");
 20:     return(0);
 21: }
 Enter the number of type int to allocate: 100
 Memory allocation was successful.
 Enter the number of type int to allocate: 99999999
 Memory allocation failed.
 

This program prompts for a value on lines 11 and 12. This number determines how much space the program will attempt to allocate. The program attempts to allocate enough memory (line 14) to hold the specified number of int variables. If the allocation fails, the return value from calloc() is NULL; otherwise, it's a pointer to the allocated memory. In the case of this program, the return value from calloc() is placed in the int pointer, ptr. An if statement on lines 16 through 19 checks the status of the allocation based on ptr's value and prints an appropriate message.

Enter different values and see how much memory can be successfully allocated. The maximum amount depends, to some extent, on your system configuration. Some systems can allocate space for 25,000 occurrences of type int, whereas 30,000 fails. Remember that the size of an int depends on your system.

The realloc() Function

The realloc() function changes the size of a block of memory that was previously allocated with malloc() or calloc(). The function prototype is

void *realloc(void *ptr, size_t size);
 

The ptr argument is a pointer to the original block of memory. The new size, in bytes, is specified by size. There are several possible outcomes with realloc():

  • If sufficient space exists to expand the memory block pointed to by ptr, the additional memory is allocated and the function returns ptr.

  • If sufficient space does not exist to expand the current block in its current location, a new block of the size for size is allocated, and existing data is copied from the old block to the beginning of the new block. The old block is freed, and the function returns a pointer to the new block.

  • If the ptr argument is NULL, the function acts like malloc(), allocating a block of size bytes and returning a pointer to it.

  • If the argument size is 0, the memory that ptr points to is freed, and the function returns NULL.

  • If memory is insufficient for the reallocation (either expanding the old block or allocating a new one), the function returns NULL, and the original block is unchanged.

Listing 20.4 demonstrates the use of realloc().

Listing 20.4. Using realloc() to increase the size of a block of dynamically allocated memory.

1: /* Using realloc() to change memory allocation. */
 2:
 3: #include <stdio.h>
 4: #include <stdlib.h>
 5: #include <string.h>
 6:
 7: main()
 8: {
 9:     char buf[80], *message;
 10:
 11:     /* Input a string. */
 12:
 13:     puts("Enter a line of text.");
 14:     gets(buf);
 15:
 16:     /* Allocate the initial block and copy the string to it. */
 17:
 18:     message = realloc(NULL, strlen(buf)+1);
 19:     strcpy(message, buf);
 20:
 21:     /* Display the message. */
 22:
 23:     puts(message);
 24:
 25:     /* Get another string from the user. */
 26:
 27:     puts("Enter another line of text.");
 28:     gets(buf);
 29:
 30:     /* Increase the allocation, then concatenate the string to it. */
 31:
 32:     message = realloc(message,(strlen(message) + strlen(buf)+1));
 33:     strcat(message, buf);
 34:
 35:     /* Display the new message. */
 36:     puts(message);
 37:     return(0);
 38: }
 Enter a line of text.
 This is the first line of text.
 This is the first line of text.
 Enter another line of text.
 This is the second line of text.
 This is the first line of text.This is the second line of text.
 

ANALYSSIS: This program gets an input string on line 14, reading it into an array of characters called buf. The string is then copied into a memory location pointed to by message (line 19). message was allocated using realloc() on line 18. realloc() was called even though there was no previous allocation. By passing NULL as the first parameter, realloc() knows that this is a first allocation.

Line 28 gets a second string in the buf buffer. This string is concatenated to the string already held in message. Because message is just big enough to hold the first string, it needs to be reallocated to make room to hold both the first and second strings. This is exactly what line 32 does. The program concludes by printing the final concatenated string.

The free() Function

When you allocate memory with either malloc() or calloc(), it is taken from the dynamic memory pool that is available to your program. This pool is sometimes called the heap, and it is finite. When your program finishes using a particular block of dynamically allocated memory, you should deallocate, or free, the memory to make it available for future use. To free memory that was allocated dynamically, use free(). Its prototype is

void free(void *ptr);
 

The free() function releases the memory pointed to by ptr. This memory must have been allocated with malloc(), calloc(), or realloc(). If ptr is NULL, free() does nothing. Listing 20.5 demonstrates the free() function. (It was also used in Listing 20.2.)

Listing 20.5. Using free() to release previously allocated dynamic memory.

1: /* Using free() to release allocated dynamic memory. */
 2:
 3: #include <stdio.h>
 4: #include <stdlib.h>
 5: #include <string.h>
 6:
 7: #define BLOCKSIZE 30000
 8:
 9: main()
 10: {
 11:     void *ptr1, *ptr2;
 12:
 13:     /* Allocate one block. */
 14:
 15:     ptr1 = malloc(BLOCKSIZE);
 16:
 17:     if (ptr1 != NULL)
 18:         printf("\nFirst allocation of %d bytes successful.",BLOCKSIZE);
 19:     else
 20:     {
 21:         printf("\nAttempt to allocate %d bytes failed.\n",BLOCKSIZE);
 22:         exit(1);
 23:     }
 24:
 25:     /* Try to allocate another block. */
 26:
 27:     ptr2 = malloc(BLOCKSIZE);
 28:
 29:     if (ptr2 != NULL)
 30:     {
 31:         /* If allocation successful, print message and exit. */
 32:
 33:         printf("\nSecond allocation of %d bytes successful.\n",
 34:                BLOCKSIZE);
 35:         exit(0);
 36:     }
 37:
 38:     /* If not successful, free the first block and try again.*/
 39:
 40:     printf("\nSecond attempt to allocate %d bytes failed.",BLOCKSIZE);
 41:     free(ptr1);
 42:     printf("\nFreeing first block.");
 43:
 44:     ptr2 = malloc(BLOCKSIZE);
 45:
 46:     if (ptr2 != NULL)
 47:         printf("\nAfter free(), allocation of %d bytes successful.\n",
 48:                BLOCKSIZE);
 49:     return(0);
 50: }
 First allocation of 30000 bytes successful.
 Second allocation of 30000 bytes successful.
 

AANALYSIS: This program tries to dynamically allocate two blocks of memory. It uses the defined constant BLOCKSIZE to determine how much to allocate. Line 15 does the first allocation using malloc(). Lines 17 through 23 check the status of the allocation by checking to see whether the return value was equal to NULL. A message is displayed, stating the status of the allocation. If the allocation failed, the program exits. Line 27 tries to allocate a second block of memory, again checking to see whether the allocation was successful (lines 29 through 36). If the second allocation was successful, a call to exit() ends the program. If it was not successful, a message states that the attempt to allocate memory failed. The first block is then freed with free() (line 41), and a new attempt is made to allocate the second block.

You might need to modify the value of the symbolic constant BLOCKSIZE. On some systems, the value of 30000 produces the following program output:

First allocation of 30000 bytes successful.
 Second attempt to allocate 30000 bytes failed.
 Freeing first block.
 After free(), allocation of 30000 bytes successful.
 

On systems with virtual memory, of course, allocation will always succeed.


DO free allocated memory when you're done with it.

DON'T assume that a call to malloc(), calloc(), or realloc() was successful. In other words, always check to see that the memory was indeed allocated.


Manipulating Memory Blocks

So far today, you've seen how to allocate and free blocks of memory. The C library also contains functions that can be used to manipulate blocks of memory--setting all bytes in a block to a specified value, and copying and moving information from one location to another.

The memset() Function

To set all the bytes in a block of memory to a particular value, use memset(). The function prototype is

void * memset(void *dest, int c, size_t count);
 

The argument dest points to the block of memory. c is the value to set, and count is the number of bytes, starting at dest, to be set. Note that while c is a type int, it is treated as a type char. In other words, only the low-order byte is used, and you can specify values of c only in the range 0 through 255.

Use memset() to initialize a block of memory to a specified value. Because this function can use only a type char as the initialization value, it is not useful for working with blocks of data types other than type char, except when you want to initialize to 0. In other words, it wouldn't be efficient to use memset() to initialize an array of type int to the value 99, but you could initialize all array elements to the value 0. memset() will be demonstrated in Listing 20.6.

The memcpy() Function

memcpy() copies bytes of data between memory blocks, sometimes called buffers. This function doesn't care about the type of data being copied--it simply makes an exact byte-for-byte copy. The function prototype is

void *memcpy(void *dest, void *src, size_t count);
 

The arguments dest and src point to the destination and source memory blocks, respectively. count specifies the number of bytes to be copied. The return value is dest. If the two blocks of memory overlap, the function might not operate properly--some of the data in src might be overwritten before being copied. Use the memmove() function, discussed next, to handle overlapping memory blocks. memcpy() will be demonstrated in Listing 20.6.

The memmove() Function

memmove() is very much like memcpy(), copying a specified number of bytes from one memory block to another. It's more flexible, however, because it can handle overlapping memory blocks properly. Because memmove() can do everything memcpy() can do with the added flexibility of dealing with overlapping blocks, you rarely, if ever, should have a reason to use memcpy(). The prototype is

void *memmove(void *dest, void *src, size_t count);
 

dest and src point to the destination and source memory blocks, and count specifies the number of bytes to be copied. The return value is dest. If the blocks overlap, this function ensures that the source data in the overlapped region is copied before being overwritten. Listing 20.6 demonstrates memset(), memcpy(), and memmove().

Listing 20.6. A demonstration of memset(), memcpy(), and memmove().

1: /* Demonstrating memset(), memcpy(), and memmove(). */
 2:
 3: #include <stdio.h>
 4: #include <string.h>
 4:
 5: char message1[60] = "Four score and seven years ago ...";
 6: char message2[60] = "abcdefghijklmnopqrstuvwxyz";
 7: char temp[60];
 8:
 9: main()
 10: {
 11:    printf("\nmessage1[] before memset():\t%s", message1);
 12:    memset(message1 + 5, `@', 10);
 13:    printf("\nmessage1[] after memset():\t%s", message1);
 14:
 15:    strcpy(temp, message2);
 16:    printf("\n\nOriginal message: %s", temp);
 17:    memcpy(temp + 4, temp + 16, 10);
 18:    printf("\nAfter memcpy() without overlap:\t%s", temp);
 19:    strcpy(temp, message2);
 20:    memcpy(temp + 6, temp + 4, 10);
 21:    printf("\nAfter memcpy() with overlap:\t%s", temp);
 22:
 23:    strcpy(temp, message2);
 24:    printf("\n\nOriginal message: %s", temp);
 25:    memmove(temp + 4, temp + 16, 10);
 26:    printf("\nAfter memmove() without overlap:\t%s", temp);
 27:    strcpy(temp, message2);
 28:    memmove(temp + 6, temp + 4, 10);
 29:    printf("\nAfter memmove() with overlap:\t%s\n", temp);
 30:
 31: }
 message1[] before memset():     Four score and seven years ago ...
 message1[] after memset():      Four @@@@@@@@@@seven years ago ...
 Original message: abcdefghijklmnopqrstuvwxyz
 After memcpy() without overlap: abcdqrstuvwxyzopqrstuvwxyz
 After memcpy() with overlap:    abcdefefefefefefqrstuvwxyz
 Original message: abcdefghijklmnopqrstuvwxyz
 After memmove() without overlap:        abcdqrstuvwxyzopqrstuvwxyz
 After memmove() with overlap:   abcdefefghijklmnqrstuvwxyz
 

ANALYSIS: The operation of memset() is straightforward. Note how the pointer notation message1 + 5 is used to specify that memset() is to start setting characters at the sixth character in message1[] (remember, arrays are zero-based). As a result, the 6th through 15th characters in message1[] have been changed to @.

When source and destination do not overlap, memcpy() works fine. The 10 characters of temp[] starting at position 17 (the letters q through z) have been copied to positions 5 though 14, where the letters e though n were originally located. If, however, the source and destination overlap, things are different. When the function tries to copy 10 characters starting at position 4 to position 6, an overlap of 8 positions occurs. You might expect the letters e through n to be copied over the letters g through p. Instead, the letters e and f are repeated five times.

If there's no overlap, memmove() works just like memcpy(). With overlap, however, memmove() copies the original source characters to the destination.


DO use memmove() instead of memcpy() in case you're dealing with overlapping memory regions.

DON'T try to use memset() to initialize type int, float, or double arrays to any value other than 0.


Working with Bits

As you may know, the most basic unit of computer data storage is the bit. There are times when being able to manipulate individual bits in your C program's data is very useful. C has several tools that let you do this.

The C bitwise operators let you manipulate the individual bits of integer variables. Remember, a bit is the smallest possible unit of data storage, and it can have only one of two values: 0 or 1. The bitwise operators can be used only with integer types: char, int, and long. Before continuing with this section, you should be familiar with binary notation--the way the computer internally stores integers. If you need to review binary notation, refer to Appendix C, "Working with Binary and Hexadecimal Numbers."

The bitwise operators are most frequently used when your C program interacts directly with your system's hardware--a topic that is beyond the scope of this book. They do have other uses, however, which this chapter introduces.

The Shift Operators

Two shift operators shift the bits in an integer variable by a specified number of positions. The << operator shifts bits to the left, and the >> operator shifts bits to the right. The syntax for these binary operators is

x << n
 

and

x >> n
 

Each operator shifts the bits in x by n positions in the specified direction. For a right shift, zeros are placed in the n high-order bits of the variable; for a left shift, zeros are placed in the n low-order bits of the variable. Here are a few examples:

Binary 00001100 (decimal 12) right-shifted by 2 evaluates to binary 00000011 (decimal 3).

Binary 00001100 (decimal 12) left-shifted by 3 evaluates to binary 01100000 (decimal 96).

Binary 00001100 (decimal 12) right-shifted by 3 evaluates to binary 00000001 (decimal 1).

Binary 00110000 (decimal 48) left-shifted by 3 evaluates to binary 10000000 (decimal 128).

Under certain circumstances, the shift operators can be used to multiply and divide an integer variable by a power of 2. Left-shifting an integer by n places has the same effect as multiplying it by 2n, and right-shifting an integer has the same effect as dividing it by 2n. The results of a left-shift multiplication are accurate only if there is no overflow--that is, if no bits are "lost" by being shifted out of the high-order positions. A right-shift division is an integer division, in which any fractional part of the result is lost. For example, if you right-shift the value 5 (binary 00000101) by one place, intending to divide by 2, the result is 2 (binary 00000010) instead of the correct 2.5, because the fractional part (the .5) is lost. Listing 20.7 demonstrates the shift operators.

Listing 20.7. Using the shift operators.

1:  /* Demonstrating the shift operators. */
 2:
 3:  #include <stdio.h>
 4:
 5:  main()
 6:  {
 7:       unsigned int y, x = 255;
 8:       int count;
 9:
 10:      printf("Decimal\t\tshift left by\tresult\n");
 11:
 12:      for (count = 1; count < 8; count++)
 13:      {
 14:          y = x << count;
 15:          printf("%d\t\t%d\t\t%d\n", x, count, y);
 16:      }
 17:      printf("\n\nDecimal\t\tshift right by\tresult\n");
 18:
 19:      for (count = 1; count < 8; count++)
 20:      {
 21:           y = x >> count;
 22:           printf("%d\t\t%d\t\t%d\n", x, count, y);
 23:      }
 24:      return(0);
 25: }
 Decimal         shift left by   result
 255             1               254
 255             2               252
 255             3               248
 255             4               240
 255             5               224
 255             6               192
 255             7               128
 Decimal         shift right by  result
 255             1               127
 255             2               63
 255             3               31
 255             4               15
 255             5               7
 255             6               3
 255             7               1
 

The Bitwise Logical Operators

Three bitwise logical operators are used to manipulate individual bits in an integer data type, as shown in Table 20.1. These operators have names similar to the TRUE/FALSE logical operators you learned about in earlier chapters, but their operations differ.

Table 20.1. The bitwise logical operators.

Operator Description
& AND
| Inclusive OR
^ Exclusive OR

These are all binary operators, setting bits in the result to 1 or 0 depending on the bits in the operands. They operate as follows:

  • Bitwise AND sets a bit in the result to 1 only if the corresponding bits in both operands are 1; otherwise, the bit is set to 0. The AND operator is used to turn off, or clear, one or more bits in a value.

  • Bitwise inclusive OR sets a bit in the result to 0 only if the corresponding bits in both operands are 0; otherwise, the bit is set to 1. The OR operator is used to turn on, or set, one or more bits in a value.

  • Bitwise exclusive OR sets a bit in the result to 1 if the corresponding bits in the operands are different (if one is 1 and the other is 0); otherwise, the bit is set to 0.

The following are examples of how these operators work:

Operation Example
AND 11110000
& 01010101
----------------
01010000
Inclusive OR 11110000
| 01010101
----------------
11110101
Exclusive OR 11110000
^ 01010101
----------------
10100101

You just read that bitwise AND and bitwise inclusive OR can be used to clear or set, respectively, specified bits in an integer value. Here's what that means. Suppose you have a type char variable, and you want to ensure that the bits in positions 0 and 4 are cleared (that is, equal to 0) and that the other bits stay at their original values. If you AND the variable with a second value that has the binary value 11101110, you'll obtain the desired result. Here's how this works:

In each position where the second value has a 1, the result will have the same value, 0 or 1, as was present in that position in the original variable:

0 & 1 == 0
 1 & 1 == 1
 

In each position where the second value has a 0, the result will have a 0 regardless of the value that was present in that position in the original variable:

0 & 0 == 0
 1 & 0 == 0
 

Setting bits with OR works in a similar way. In each position where the second value has a 1, the result will have a 1, and in each position where the second value has a 0, the result will be unchanged:

0 | 1 == 1
 1 | 1 == 1
 0 | 0 == 0
 1 | 0 == 1
 

The Complement Operator

The final bitwise operator is the complement operator, ~. This is a unary operator. Its action is to reverse every bit in its operand, changing all 0s to 1s, and vice versa. For example, ~254 (binary 11111110) evaluates to 1 (binary 00000001).

All the examples in this section have used type char variables containing 8 bits. For larger variables, such as type int and type long, things work exactly the same.

Bit Fields in Structures

The final bit-related topic is the use of bit fields in structures. On Day 11, "Structures," you learned how to define your own data structures, customizing them to fit your program's data needs. By using bit fields, you can accomplish even greater customization and save memory space as well.

A bit field is a structure member that contains a specified number of bits. You can declare a bit field to contain one bit, two bits, or whatever number of bits are required to hold the data stored in the field. What advantage does this provide?

Suppose that you're programming an employee database program that keeps records on your company's employees. Many of the items of information that the database stores are of the yes/no variety, such as "Is the employee enrolled in the dental plan?" or "Did the employee graduate from college?" Each piece of yes/no information can be stored in a single bit, with 1 representing yes and 0 representing no.

Using C's standard data types, the smallest type you could use in a structure is a type char. You could indeed use a type char structure member to hold yes/no data, but seven of the char's eight bits would be wasted space. By using bit fields, you could store eight yes/no values in a single char.

Bit fields aren't limited to yes/no values. Continuing with this database example, imagine that your firm has three different health insurance plans. Your database needs to store data about the plan in which each employee is enrolled (if any). You could use 0 to represent no health insurance and use the values 1, 2, and 3 to represent the three plans. A bit field containing two bits is sufficient, because two binary bits can represent values of 0 through 3. Likewise, a bit field containing three bits could hold values in the range 0 through 7, four bits could hold values in the range 0 through 15, and so on.

Bit fields are named and accessed like regular structure members. All bit fields have type unsigned int, and you specify the size of the field (in bits) by following the member name with a colon and the number of bits. To define a structure with a one-bit member named dental, another one-bit member named college, and a two-bit member named health, you would write the following:

struct emp_data {
 unsigned dental      : 1;
 unsigned college     : 1;
 unsigned health      : 2;
 ...
 };
 

The ellipsis (...) indicates space for other structure members. The members can be bit fields or fields made up of regular data types. Note that bit fields must be placed first in the structure definition, before any nonbit field structure members. To access the bit fields, use the structure member operator just as you do with any structure member. For the example, you can expand the structure definition to something more useful:

struct emp_data {
 unsigned dental      : 1;
 unsigned college     : 1;
 unsigned health      : 2;
 char fname[20];
 char lname[20];
 char ssnumber[10];
 };
 

You can then declare an array of structures:

struct emp_data workers[100];
 

To assign values to the first array element, write something like this:

workers[0].dental = 1;
 workers[0].college = 0;
 workers[0].health = 2;
 strcpy(workers[0].fname, "Mildred");
 

Your code would be clearer, of course, if you used symbolic constants YES and NO with values of 1 and 0 when working with one-bit fields. In any case, you treat each bit field as a small, unsigned integer with the given number of bits. The range of values that can be assigned to a bit field with n bits is from 0 to 2n-1. If you try to assign an out-of-range value to a bit field, the compiler won't report an error, but you will get unpredictable results.


DO use defined constants YES and NO or TRUE and FALSE when working with bits. These are much easier to read and understand than 1 and 0.

DON'T define a bit field that takes 8 or 16 bits. These are the same as other available variables such as type char or int.


Оставьте свой комментарий !

Ваше имя:
Комментарий:
Оба поля являются обязательными

 Автор  Комментарий к данной статье