3.1 Overview
This chapter focuses on arrays and structures, which are C’s primary aggregate types. Arrays aggregate variables of the same type, whereas structures can do the same for variables of different types. Structures can be array elements, and a structure may embed arrays. Together these aggregate types make it possible for programmers to define arbitrarily rich data types (e.g., Employee, Game, Species) that meet application needs.
Pointers—address constants and variables—come into play naturally with both arrays and structures, and the code examples throughout the chapter get into the details. Among modern general-purpose languages, C (together with C++) stands out by giving the programmer so much control over—and, therefore, responsibility for—memory addresses and the items stored at these addresses. All of the chapters after this one have examples that, in one way or another, illustrate the power of pointers.
3.2 Arrays
An
array in C is a fixed-size collection of variables—of the
same type—accessible under a single name, the array’s identifier. A code example illustrates.
void main() {
int arr[Size]; /* storage from the stack -- uninitialized */
int i;
for (i = 0; i < Size; i++) /* iterate over the array */
arr[i] = i + 1; /* assign a value to each element */
}
Listing 3-1A simple array
The
array program
(see Listing
3-1) shows the basic syntax for declaring an array and then uses a
for loop to populate the array with values. An array has a
fixed size, in this case specified by the macro
Size. The array’s name, in this case
arr, is a
pointer constant that holds the address of the array’s first element, in this case the element that the
for loop
initializes to 1:
+---+---+---+---+---+---+---+---+
arr--->| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## array elements
+---+---+---+---+---+---+---+---+
[0] [1] [2] [3] [4] [5] [6] [7] ## indexes
Arrays can be
indexed to access elements by using the
square brackets
: legitimate indexes are 0 through the array’s size - 1. The indexes are offsets from the start of the array: the first array element is at offset 0, the second at offset 1, and so on. For the preceding array, here are some of the addresses computed as offsets from the base address
arr:
arr + 0 ---> 1st element ## arr[0] is value of 1st element: 1
arr + 1 ---> 2nd element ## arr[1] is value of 2nd element: 2
...
arr + 7 ---> 8th element ## arr[7] is value of 8th element: 8
A second example builds on the first by introducing pointer variables and showing how C supports pointer arithmetic by having data types for pointers.
3.3 Arrays and Pointer Arithmetic
C supports
typed pointers so that the compiler can perform the required arithmetic when pointers are used to access memory locations. The compiler thereby takes on a task that would be error-prone if left to the programmer. Consider again the
array program
with its array of eight
int elements and a sample index such as 2. The index expression
arr[2] references the
third element in the array, which is two elements over from where the array starts:
arr is the base address, and 2 is the displacement or offset from this base address. However, machine-level addresses are of
bytes, and an
int is a 4-byte element. To reach the array’s third element, it is therefore necessary to move 2
× sizeof(int) bytes from where array
arr starts, which is a move of 8 bytes in all. Yet the programmer refers to the third element as
arr[2] (
int level), not as
arr[8] (byte level). It would be tedious and error-prone for programmers to work at the byte level in accessing array elements of multibyte types. Accordingly, C’s typed pointers allow the programmer to work at the data-type level (e.g.,
int or
Employee), while the compiler then works at the byte level.
#define Size (8) /* Size is a macro that expands into (8) */
void main() {
int arr[Size]; /* storage from the stack -- uninitialized */
int k = 1;
int* ptr = arr; /* point to the start of the array */
int* end = arr + Size; /* points immediately beyond the end of the array */
while (ptr < end) { /* beyond the end yet? */
*ptr = k++; /* assign a value to the array element */
ptr++; /* increment the pointer */
}
}
Listing 3-2Pointer variables and pointer arithmetic
The
arrayPtr program
(see Listing
3-2), which revises the original
array program, has three pointers at work:
The array’s name
arr
, a pointer constant, holds the address of the first element in the array.
The pointer variable
ptr
, assigned to hold the address of the first element in the array.
The pointer variable
end
points just beyond the last element in the array.
The following is a depiction of where
ptr and
end point before the looping begins:
+---+---+---+---+---+---+---+---+---+
ptr--->| ? | ? | ? | ? | ? | ? | ? | ? | ? |<---end
+---+---+---+---+---+---+---+---+---+
[0] [1] [2] [3] [4] [5] [6] [7] ## indexes
A pointer is allowed to point one element beyond the end of the array, although nothing should be stored at that location. In this example, the array’s initialization now uses a
while
rather than a for loop, and the loop’s condition compares the two pointers, ptr and end: looping continues so long as ptr < end. At the bottom of the loop, ptr is incremented by 1—by one int, which is 4 bytes. The pointer
ptr
is a variable, unlike the pointer constant arr, and so can have its value changed. Eventually ptr points to the same location as does end, which makes the loop condition false.
The initialization of each array element uses the
dereference operator
, the star:
*ptr = k++; /* k is 1,2,3,...,8 */
In the declaration of
ptr, the star comes
after the data type
int. In the dereferencing of
ptr, the star comes
before the variable’s name. It would be an
error
to change the code to
because
ptr
then would take on values such as 1,2,3,…,8, which almost surely are not addresses within the program’s address space. The aim is to initialize the array element to which ptr points, not ptr itself.
3.4 More on the Address and Dereference Operators
In the
addPtr example, the pointer variable
ptr is initialized to the array’s name
arr so that both
arr and
ptr point to the array’s first element. An equivalent but less
concise
initialization uses the
address operator &:
int* ptr = &arr[0]; /* alternative to: int* ptr = arr; */
The address operator
computes an in-memory address, in this case the address of array element arr[0].
The
dereference operator uses
an address to access the contents stored at that address. If
ptr points to any cell in the
int array
arr, then
*ptr is the value stored at the address. The dereference operator can be used in the usual ways, for example, to read or to change a value:
int* ptr = &arr[3]; /* address of 4th element, which contains 4 */
*ptr = *ptr + 9; /* equivalent to: arr[3] = arr[3] + 9 */
The examples so far have shown pointers that hold the addresses of
char and
int cells, but not pointers to other pointers. In principle, there can be
pointer to pointer to
…, although in practice, it is unusual to see more than two levels of indirection. The next example illustrates the case of a pointer to a pointer, and later examples
motivate
such a construct.
void main() {
int n = 1234;
int* ptr1 = &n; /* ptr1--->n */
int** ptr2 = &ptr1; /* ptr2--->ptr1 */
printf("%i %p %p
", n, ptr1, ptr2); /* 1234 0x7ffee80dfb5c 0x7ffee80dfb60 */
**ptr2 = *ptr1 + 100; /* increment n by 100 */
printf("%i %i %i
", n, *ptr1, **ptr2); /* 1334 1334 1334 */
}
Listing 3-3The address and dereference operators
The
ptr2ptr program (see Listing
3-3) has an
int variable
n that stores 1234, a pointer
ptr1 that points to
n, and a second pointer
ptr2 that points to
ptr1. Here is a depiction, with fictional addresses written in hex above the storage cells and variable names below these cells:
0xAB 0xEF ## addresses
+------+ +------+ +------+
| 0xAB |--->| OxEF |--->| 1234 | ## contents
+------+ +------+ +------+
ptr2 ptr1 n ## variable names
Given this storage
layout
, any of the variables
n,
ptr1, and
ptr2 can be used to access (including to update) the value stored in variable
n. For example, each of these statements updates
n by one:
n += 1; /* from 1234 to 1235 */
*ptr1 += 1; /* from 1235 to 1236 */
**ptr2 += 1; /* from 1236 to 1237 */
The index syntax used with arrays can be seen as
syntactic sugar, as a short example shows:
int arr[] = {9, 8, 7, 6, 5}; /* compiler figures out the size */
int n = arr[2]; /* n = arr[2] = 7 */
The syntax
arr[2] is straightforward and now is common across programming languages. In C, however, this syntax can be viewed as shorthand for
int n = *(arr + 2); /* n = 7 */
The pointer expression arr + 2 points to two int elements beyond the first in the array, which holds 7. Dereferencing the pointer expression *(arr + 2) yields the int contents, in this case 7.
The same point can be reinforced with some obfuscated C. Consider this code segment:
int arr[] = {9, 8, 7, 6, 5};
int i;
for (i = 0; i < 5; i++)
printf("%i ", i[arr]); /** peculiar syntax **/
In the
printf statement
, the usual syntax for array access would be
arr[i], not
i[arr]. Yet either works, and the compiler does not wince at the second form. The reason can summarized as follows:
arr[i] == *(arr + i) /* syntactic sugar */
*(arr + i) == *(i + arr) /* addition commutes */
*(i + arr) == i[arr] /* more syntactic (but peculiar) sugar */
3.5 Multidimensional Arrays
An array declared with a
single pair of square brackets is
one-dimensional and sometimes called a
vector. An array declared with more than one pair of square brackets is
multidimensional
:
int nums[128]; /* one dimensional array */
int nums_table[4][32]; /* multidimensional array (2-dimensional matrix) */
Arrays of any dimension are possible, but more than three dimensions is unusual. The array
nums_table
is two-dimensional. The arrays
nums
and nums_table hold the same number of integer values (128), but they do not have the same number of elements: array nums has 128 elements, each an int value; by contrast, array
nums_table
has four elements, each a subarray of 32 int values. The sizeof operator, when applied to an array’s name, does the sensible thing: it gives the number of bytes required for all of the array elements, not the size in bytes of the array’s name as pointer. In this case, for example, the sizeof array
nums
is the same as the sizeof array nums_table: 512 because there are 128 int values in each array and each int is 4 bytes.
Multidimensional arrays are yet another example of syntactic sugar in C. All arrays are implemented as one-dimensional, as the next code example illustrates.
void main() {
int table[3][4] = {{1, 2, 3, 4}, /* row 1 */
{9, 8, 7, 6}, /* row 2 */
{3, 5, 7, 9}}; /* row 3 */
int i, j;
for (i = 0; i < 3; i++) /** outer loop: 3 rows **/
for (j = 0; j < 4; j++) /** inner loop: 4 cols per row **/
printf("%i ", table[i][j]);
printf("
");
int* ptr = (int*) table; /** ptr points to an int **/
for (i = 0; i < 12; i++) /** 12 ints (3 rows, 4 cols each) **/
printf("%i ", ptr[i]);
printf("
");
}
Listing 3-4Treating a multidimensional array as a one-dimensional array
The
table program
(see Listing
3-4) highlights critical features about how pointers work in C. The array name
table is, as usual, a pointer constant, and this name points to the first byte of the first
int in the
first element in the array, where the first array element is a
subarray
of four
int values, in this case 1, 2, 3, and 4:
1st row 2nd row 3rd row ## rows
+---+---+---+---|---+---+---+---|---+---+---+---+
table--->| 1 | 2 | 3 | 4 | 9 | 8 | 7 | 6 | 3 | 5 | 7 | 9 | ## contents
+---+---+---+---|---+---+---+---|---+---+---+---+
[0] [1] [2] [3] [0] [1] [2] [3] [0] [1] [2] [3] ## column indexes
The data type of table is pointer to an array of subarrays, each with four integer elements. In memory, the array is laid out contiguously, with the int values in sequence, one table row (subarray) after the other.
The table program traverses the multidimensional array twice. The first traversal uses nested for loops
: the outer for loop iterates over the rows, and the inner for loop iterates over the columns in each row. The C compiler
lays out the table in row-major order: the first row with all of its columns, then the second row with all of its columns, and so on. A Fortran compiler, by contrast, would lay out a multidimensional array in column-major order.
The second traversal of array
table uses only a single
for loop
. The variable
ptr is assigned the value of
table, but with a cast: the cast
(int*) is required because
ptr is of type
int*, whereas
table is not. A revision to the
table example goes into the details.
void print(int (*arr)[4], int n) {
int i, j;
for (i = 0; i < n; i++)
for (j = 0; j < 4; j++)
printf("%i ", arr[i][j]);
printf("
");
}
Listing 3-5A function to print the two-dimensional table of three rows and three columns
To get a better sense of the
table data type, imagine breaking out a
print function
for printing the two-dimensional
table (see Listing
3-5). The first parameter in the
print function could be written in different ways, including the one shown. Another way is this:
void print(int arr[ ][4], int n)
Both versions underscore that the first argument passed to
print, in this case the two-dimensional
array
table, must be an
array of subarrays,
with each subarray of size 4. The second argument
n to the
print function specifies the number of
rows in the array. From the
main function in the
table program, the call would be
print(table, 3); /* 3 rows */
The parameter
arr in function
print then points to the first row, and second parameter
n gives the number of rows. The cast of
table to
int*
in the assignment
acknowledges to the compiler
that pointer constant table and pointer variable ptr may point to the very same byte, but that the two differ in type. As an int* pointer, ptr can be used to iterate over the individual int values in the array, rather than over the four-element subarrays that make up each table row.
Consider the
pointer expressions
table[0],
table[1], and
table[2]. Each of these points to an array of three integers. Here is the output from a sample run that prints out the three
addresses
:
printf("%p (%lu) %p (%lu) %p (%lu)
",
table[0], (long) table[0], /* 0x7ffececccf30 (140730827343600) */
table[1], (long) table[1] , /* 0x7ffececccf40 (140730827343616) */
table[2], (long) table[2]); /* 0x7ffececccf50 (140730827343632) */
}
The first and second addresses differ by 16 bytes, as do the third and fourth. The variable table[0] points to the first of the three rows in the table, and each row has four int values of 4 bytes apiece; hence, table[1] points 16 bytes beyond where table[0] points.
The syntax of multidimensional arrays gives a hint about how various pointer expressions are to be used. The
table array
, which holds
int values, is declared with
two sets of square brackets:
If
index syntax
is used to read or write an
int value, then
two square brackets must be used:
table[1][2] = -999; /* second row, third column set to -999 */
The first index picks out the row, and the second index picks out the column in the row. Any expressions involving table, but with fewer than two pairs of brackets, are pointers rather than int values. In particular, table points
to the first subarray, as does table[0]; pointer table[1] points to the second subarray; and pointer table[2] points to the third subarray. A quick review exercise is to explain, in plain terms or through a code segment, the difference between the data type of table and the data type of table[0]. Both pointer expressions point to the same byte, but the two differ in type.
C arrays promote
efficient modular programming. Consider again a function to print one-dimensional integer
arrays
of arbitrary sizes. As the
table program shows, it is straightforward to treat an
n-dimensional array as if it were one-dimensional. The
print_array function might be declared as follows:
void print_array(int* arr, unsigned n); /* void print_array(int arr[], unsigned n); */
The obvious way to call
print_array
is to pass it, as the first argument, the array’s name—a
pointer:
int arr[100000];
/* fill the array */
print_array(arr, 100000); /* passing a pointer as the 1st arg */
To pass the array’s name as an argument is thus to pass a pointer to the array, not a copy of it. Passing a copy
of 100,000 4-byte integers would be expensive, maybe prohibitively so. It is possible to pass a copy of an array to a function, another issue for later analysis.
3.6 Using Pointers for Return Values
A function in C can take arbitrarily many arguments, but it can return
one value at most. The restriction to just one returned value is not troubling, however. To begin, the single returned
value
could be a
list of values, although this approach requires caution. Later code examples explore the option and go into best practices for returning collections. This section takes on a different approach: using a pointer
argument to store a value that otherwise might be returned explicitly by a function:
int f() { return 100; } /* explicitly returned */
void g(int* arg) { *arg = 100; } /* stored at a provided address */
The
technique
is common in C. A function’s caller provides the
address of some variable, and the callee then stores a value at this address. The effect is to return a value via the pointer. The next code example motivates this approach and also introduces in-line assembly code to check for integer overflow.
#include <stdio.h>
#include <limits.h>
int safe_mult(int n1, int n2, int* product) {
int flag = 0; /* assume no overflow */
*product = n1 * n2; /* potential overflow */
asm("setae %%bl; movzbl %%bl,%0"
: "=r" (flag) /* set flag on overflow */
: /* no other inputs */
: "%rbx"); /* scratchpad */
return flag; /* zero is no overflow, non-zero is overflow */
}
Listing 3-6In-line assembly code to check for integer overflow
The safeMult function
(see Listing 3-6) introduces in-line assembly with a call to the library function asm. The architecture-specific assembly code is in AT&T style and targets an Intel machine; the code detects overflow in integer multiplication, returning a flag to indicate whether overflow occurred.
The syntax of the in-line assembly code
needs a quick analysis. The percentage sign % used to identify a CPU register sometimes occurs twice, in this case to identify the 1-byte, special-purpose register %%bl. The double percentage signs are there to prevent the assembler from confusing this register identifier with something else. One percentage sign might do, but two are safer.
The argument to the
asm function
can be divided into two parts:
The string
"setae %%bl; movzbl %%bl,%0"
contains two instructions, with a semicolon separating them. The setae instruction puts the result of the overflow test in the 1-byte register %bl. This register now flags whether overflow occurs. The movzbl instruction then copies the contents of register %bl into a 32-bit register of the assembler’s own choosing, designated as %0.
The parts that begin with a colon (e.g., : "=r" (flag)) are metadata. For example, the C source code returns the overflow status with the return statement:
return flag; /* zero is no overflow, non-zero is overflow */
Recall that assembly routines return a value in the register %rax or its lower half %eax. The "=r" (flag) clause signals that flag in C is %rax in assembly code. If the assembler is in an optimizing mood, it should make %rax the register designated by %0 shown previously: %rax serves as the overflow
flag returned to the caller. The middle-colon section is empty here but in general could contain other inputs to the assembly code. The third-colon section recommends that the 64-bit register %rbx be used as scratchpad.
When the program executes (see the
main function in the following), the
output
is
No overflow on 16 * 48: returned product == 768
Overflow on INT_MAX * INT_MAX: returned product == 1
The in-line assembly code does its job.
The focus now shifts to the C code, in particular to the
safe_mult function. Here is the challenge:
The safe_mult function
needs to signal its caller whether overflow has occurred. The returned value is used for this purpose: zero (false) means no overflow, and nonzero (true) means overflow.
How, then, is the product of the first two arguments to be returned? The approach taken here is to have safe_mult called with three arguments:
int safe_mult(int n1, int n2, int* product); /* declaration */
The parameters
n1 and
n2 are the numbers to be multiplied, and the parameter
product points to where the result of the multiplication should be stored. The pointer argument
product is the address of a variable declared in the caller
main.
void main() {
int n;
char* msg;
/* no overflow */
int flag = safe_mult(16, 48, &n);
msg = (!flag) ? "Overflow on 16 * 48" : "No overflow on 16 * 48";
printf("%s: returned product == %i
", msg, n);
/* overflow */
flag = safe_mult(INT_MAX, INT_MAX, &n);
msg = (!flag) ? "Overflow on INT_MAX * INT_MAX" : "No overflow on INT_MAX * INT_MAX";
printf("%s: returned product == %i
", msg, n);
}
Listing 3-7Using a pointer argument
to hold a return value
The main function for the safeMult program (see Listing 3-7) makes two calls against the function. The first, with 16 and 48 as the values to be multiplied, does not cause overflow. The second call, however, passes INT_MAX as both arguments, with overflow as the expected and, because of safe_mult, the now detected overflow.
3.7 The void* Data Type and NULL
The term
void is not the name of a data type, although C
syntax
implies as much:
void main() { /* body */ } /* void seems to be the return type */
int some_function(void); /* same as: int some_function(); */
This definition of
main
suggests that the function returns a
void in the same way that another version of
main returns an
int; but the suggestion is misleading. The
void is really shorthand for
returns no value and so is not a data type in the technical sense. For instance, a variable cannot be declared with
void as the type:
void n; /** ERROR: void is not a type **/
In the second example shown previously, the void in the declaration of
some_function
signals only that this function expects no arguments; the void once again is not a type, but another way of writing an empty argument list.
There is a very important data type in C that has void in its name:
void*
, or pointer to void. This type is a generic pointer type: any other pointer type can be converted to and from void* without explicit casting. Why is this useful? A short example provides one answer, and the next section provides another.
Consider this array of
strings
:
char* strings[ ] = {"eins", "zwei", "drei", "vier", "fuenf", "sechs"};
The array happens to hold six strings, each of which is a char* in C. For example, the first array element is a pointer to the “e” in “eins”. To write a loop that traverses this array without going beyond the end requires a count of how many elements are in the array; in this case, there are six.
There is a better, more robust, and more programmer-friendly way to build an array of
strings
:
char* strings[ ] = {"eins", "zwei", "drei", "vier", "fuenf", "sechs", 0};
At first sight, this code looks wrong. An array aggregates elements of the
same data type, and the last element here appears to be an integer value rather than a
char* pointer. But the
0 here is
NULL
, a macro defined in the header file
stdlib.h as follows:
#define NULL ((void*) 0) /* 0 cast as a pointer to void */
Because NULL is of type void*, it can occur in an array of any pointer type, including the char* element type in the strings array. By the way, the 0 as shorthand for NULL is the only numeric value that would work in this case. Were 987 used instead of 0, for instance, the code segment would not compile. C programmers, in order to save on typing, are fond of using 0 for NULL.
Traversing
the revised array is now straightforward and illustrates idiomatic C programming:
int i = 0;
while (strings[i]) /* short for: while (strings[i] != NULL) */
printf("%s
", strings[i++]); /* print current string, then increment i */
The loop condition
is true until strings[i] is NULL, which is 0: the value 0 in C is overloaded, and one of the overloads means false in a test context. The use of NULL to mark the end of pointer arrays is common in C.
A final note is in order. The
NULL used in this most recent example is
not the
null terminator used to mark the end of an individual string. Recall that the
string
“eins” is represented in C as an array with 8-bit zero at the end as the terminator:
+---+---+---+---+--+
| e | i | n | s | | ## is 8-bit zero
----+---+---+---+--+
By contrast, the
NULL that terminates the
strings array is either a 32-bit zero or a 64-bit zero, depending on whether the machine uses 32-bit or 64-bit addresses. To be sure, the
comparison
NULL == ' ' /* evaluates to true */
evaluates to true, but only because the compiler converts the 8-bit null terminator (zero) to the 32-bit or 64-bit zero.
In summary,
zero
has three specific uses in C beyond 0 as a numeric value:
In a boolean context (e.g., an if or while condition), zero means false, and nonzero means true.
In a string context, the 8-bit zero (