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.
|