© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
M. KalinModern C Up and Runninghttps://doi.org/10.1007/978-1-4842-8676-0_3

3. Aggregates and Pointers

Martin Kalin1  
(1)
Chicago, IL, USA
 

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.
#define Size 8
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-1

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

Does C Provide Bounds Checking on Arrays?
No. The programmer is responsible for ensuring that array indexes are in bounds at runtime. The following code segment compiles without warning and likely blows up when executed because of the out-of-bounds index -9876.
int arr[4];      /* four elements */
int ind = -9876; /* not a good index: 0, 1, 2, and 3 are good indexes */
arr[ind] = 27;   /* out-of-bounds, likely to blow up at run-time */

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

Pointer 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
ptr = k; /** ERROR **/

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.
#include <stdio.h>
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-3

The 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.
#include <stdio.h>
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-4

Treating 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-5

A 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
int* ptr = (int*) table;

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:
int table[3][4] = {...};
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.

How are Arguments Passed to C Functions?

C uses call by value exclusively in passing arguments to functions: the arguments are copied and then accessible in the called function through the parameter names. The compiler can optimize such calls in various ways, including placing arguments in CPU registers rather than on the stack. Addresses (pointers) as well are passed by value. For example, when an array’s name is passed as an argument, a copy of this address is passed. Of course, both the copy and the original address can be used to access the very same array elements.

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

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

    Using 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 () is the code for the nonprinting character that marks the end of a string: the null terminator.

  • In a pointer context, zero is NULL, the address-size null pointer that points nowhere.

C programmers are fond of idioms that conflate these overloads of zero. The
while (strings[i])

test from the preceding example is one such idiom.

3.7.1 The void* Data Type and Higher-Order Callback Functions

The void* type plays an important role in library functions designed to work on arrays of any type. Consider, for example, library functions to initialize, sort, search, and otherwise process arrays. These functions should be generic in that they work on arrays of any data type. It would be impractical to fashion multiple sort functions, each targeted at a specific type. The task presumably would never be completed.

Among the generic library functions is qsort, which can sort an array of Employee instances, or int instances, or double instances, and so on. The first argument to qsort is a pointer that specifies where, in the array, the sort should begin, which is typically but not necessarily the first element: qsort can sort arbitrary subarrays, or the whole array, with only small changes to the arguments passed to this function. For now, the other arguments can be ignored, as the emphasis is on the type of first argument to qsort. This type is void* because it satisfies the requirement that qsort should work on any array of any type. Here is how the declaration of qsort begins:
void qsort(void* start,... /* 4 arguments in all */
A full sorting example fleshes out the details of the remaining three arguments.
#include <stdio.h>
#include <stdlib.h> /* rand, qsort */
#define Size 12
void print_array(int* array, unsigned n) {
  unsigned i;
  for (i = 0; i < n; i++) printf("%i ", array[i]);
  printf(" ");
}
int comp(const void* p1, const void* p2) {
  int n1 = *((int*) p1);  /* cast p1 to int*, then dereference */
  int n2 = *((int*) p2);  /* same for p2 */
  return n2 - n1;         /* descending order */
}
void main() {
  int arr[Size], i;
  for (i = 0; i < Size; i++) arr[i] = rand() % 100; /* values < 100 */
  print_array(arr, Size); /* 83 86 77 15 93 35 84 92 49 21 62 27 */
  qsort(arr, Size, sizeof(int), comp); /* comp is a pointer to a function */
  print_array(arr, Size); /* 93 92 86 84 83 77 62 49 35 27 21 15 */
}
Listing 3-8

Sorting an array with qsort

The sort program (see Listing 3-8) does the following :
  1. 1.

    Populates an int array with pseudorandomly generated values

     
  2. 2.

    Prints the array

     
  3. 3.

    Sorts the array in descending order using the library function qsort

     
  4. 4.

    Prints the sorted array

     
The qsort function has a comparison semantics used throughout modern programming languages. Here is the full declaration for qsort:
void qsort(void* start,
           size_t nmemb,
           size_t size,
           int (*comp) (const void*, const void*));
The arguments can be clarified as follows:
  • The first argument, of type void*, points to where in the array the sorting should begin. This is typically, but not necessarily, the start of the array. The qsort function can sort only part of array, if required. Because the argument is of type void*, any type of array can be sorted using qsort.

  • The second argument, of unsigned integer type size_t, specifies the number of elements to be sorted.

  • The third argument (also of type size_t) is the sizeof each element.

  • The fourth argument is a pointer to a function that matches this prototype:
    • Returns an int value.

    • Takes two arguments of type const void*, which are pointers to two elements that qsort needs to compare and, perhaps, move. The const indicates that the pointers are not used to change the values to which they point.

The critical fourth argument makes qsort a higher-order function, one that takes a (pointer to a) function as an argument.

A function’s name , like an array’s name, is a pointer constant. A function’s name points to the first statement in a function’s body; in assembly language, the function’s name is thus a label.

The comparison function used in qsort can have any name so long as the function matches the prototype. In the sort program, the comparison function is named comp. The comparison function is a callback, a function that a programmer writes for some other function to call, in this case, qsort itself. In the course of doing the sort, qsort must do pairwise element comparisons in order to determine how to rearrange the array. The sort is destructive in that the sort occurs in place: the array being sorted is rearranged unless it is already sorted.

Here are the details for the comparison. Each argument passed to the comparison function points at an array element. Assume that the first argument points to array element E1 and the second argument points to array element E2. The value returned from the comparison function then has the following semantics :
  • If E1 and E2 are considered equal, 0 is returned.

  • If E1 is considered to precede E2, a negative value is returned (e.g., -1).

  • If E2 is considered to precede E1, a positive value is returned (e.g., +1).

These semantics are remarkably simple and flexible. The author of the comparison function determines the details. Here, for review, is the comparison function for the sort program :
int comp(const void* p1, const void* p2) {
  int n1 = *((int*) p1); /* cast p1 to int*, then dereference */
  int n2 = *((int*) p2); /* same for p2 */
  return n2 - n1;        /* descending order */
}
The function’s body could be reduced to a single return statement, but at the cost of clarity. Since the array being sorted has int elements, the void* arguments are cast to pointers of type int*. Each int* pointer then is dereferenced to get the int value pointed to. Variables n1 and n2 hold these values. Suppose that n1 is 20 and that n2 is 99. The returned value of
n2 - n1
is then 79, a positive value signaling that 99 should precede 20 in the sorted order. The sort is thus in descending order. If the returned value were changed to
n1 - n2

then the resulting sort would be in ascending order. If the int array had the same values throughout, then 0 would be returned for every comparison, leaving the array unchanged by the sort.

The usefulness of void* is undoubtedly evident to programmers from object-oriented languages such as Java and C#. In these languages, a reference (pointer) to Object can point to anything. Here is a segment of Java to illustrate:
Object ptr = new String("Hello, world!"); /* string */
ptr = 99;                                 /* integer: boxed as new Integer(99) */
ptr = new int[ ] {1, 2, 3, 4};            /* array of integers */

Generic types such as void* in C, and Object in Java, make languages flexible.

The second code example uses a typedef to describe the type of function suitable as an argument to another function. A typedef creates an alias for an existing type:
typedef unsigned boolean;  /* unsigned is existing type, boolean is the alias */
boolean flag;              /* use the type in a variable's declaration */

Pointers to functions, like other C pointers, have data types, and the typedef is useful in defining the appropriate type, a type that will satisfy the compiler. It is easy to get a pointer to a function; the function’s name is just such a pointer. It can be challenging to pass an appropriate function pointer as an argument in another function.

What’s an enum?
An enum (enumerated type) gives names to integer values. The enumerated type itself can but need not be named:
enum { false, true };                    /* false is 0, true is 1, and so on */
enum TruthValue { true = 1, false = 0 }; /* tagged and explicit assignments */

The enumerated values start at 0 and continue in series unless explicit values are given, as in the second example shown previously. In the second example, false would default to 2 if not explicitly assigned 0 as its value.

Constructs such as typedef and enum promote readable code:
typedef unsigned boolean ;
boolean continue_to_loop = true;
The next example uses a typedef to specify the prototype of a function passed as an argument to the higher-order reduce function. The reduce function takes two additional arguments : an array of integer values and the array’s length.
/* pointer to function with two arguments (int array and length), returns an int */
typedef unsigned (*reducer)(unsigned list[], unsigned len);                                      /* type name is reducer */
unsigned sum(unsigned list[], unsigned len) {
   unsigned sum = 0, i;
   for (i = 0; i < len; i++) sum += list[i];
   return sum;
}
unsigned product(unsigned list[], unsigned len) {
   unsigned prod = 1, i;
   for (i = 0; i < len; i++) prod *= list[i];
   return prod;
}
unsigned reduce(reducer func, unsigned list[], unsigned len) { /* 1st arg: ptr to func */
   return func(list, len); /** invoking a function in the usual way **/
}
Listing 3-9

Another example of pointers to functions

The reducer program (see Listing 3-9) has two functions, sum and product, that reduce a list of integers to a single value, in this case a sum and product, respectively. The third function is higher order and named reduce. This function takes a (pointer to a) function as its first argument, an array of values as its second, and the array’s length as its third.

The typedef in the reducer program is the tricky part:
typedef unsigned (*reducer)(unsigned list[], unsigned len);
The data type alias is reducer, and it can point to any function that meets these conditions :
  • The function takes two arguments: an array of unsigned integers and a single unsigned integer (the length) in that order.

  • The function returns an unsigned integer.

The declaration of the reduce function uses the typedef data type in the first argument position:
unsigned reduce(reducer func, unsigned list[], unsigned len);
Applying a particular reducer function, in this case sum or product, through the function pointer func requires no special syntax :
unsigned n = func(list, len); /* invoking a function through a pointer argument func */
Normally, a function is invoked using its name, a pointer constant; in this case, a function is invoked using a pointer variable instead, func of type reducer. Invoking reduce also is straightforward:
reduce(sum, nums, Size);     /* sum is a function */
reduce(product, nums, Size); /* product is a function */
#include <stdio.h>
#define Size 30
int main() {
   unsigned nums[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
                      11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
                      21, 22, 23, 24, 25, 26, 27, 28, 29, 30};
   printf("Sum of list: %i ", reduce(sum, nums, Size));   /* 465 */
   printf("Product of list: %i ", reduce(product, nums, Size));   /* 1,409,286,144 */
   return 0;
}
Listing 3-10

The main function in the reducer program

The main function in the reducer program (see Listing 3-10) shows two calls to the reduce function: the first using sum as its first argument and the second using product as this argument.

The reducer program illustrates that higher-order functions are routine in C. Such functions, used judiciously, make programs easier to understand. The reduce function maps a list of integers to a single value, and the first argument—the function pointer—specifies the kind of mapping involved, in this case reducing the list to either a sum or a product.

3.8 Structures

Arrays aggregate variables of the same data type, whereas structures can aggregate variables of different types. The variables in a structure are known as its fields. There can be arrays of structures, and structures that embed arrays and even other structures. As a result, programmer-defined data structures can be arbitrarily rich.

The syntax of structures can be introduced in short code examples. Here’s a start:
struct {
   int n;
   double k;
} s1;
s1.n = -999;
s1.k = 44.4;

The data type is struct {...}, and variable s1 is of this structure type; hence, s1 has two fields: an int named n and a double named k. The member operator, the period, is used to access the structure’s fields, in this case the int field n and the double field k. The compiler is not bound to lay out storage for the fields in a way that matches the structure’s declaration. Although field n occurs before field k in the structure declaration shown previously, this may not be the case after compilation. The member operator should be used to access the fields by name.

A second code segment adds a tag to the structure so that the structure type has a name:
struct TwoNums { /* TwoNums is the tag */
   int n;
   double k;
};
struct TwoNums s2; /* the data type is struct TwoNums: struct plus the tag */
A third example shows the popular approach, which uses a typedef to name a structure type:
typedef struct {  /* tag is optional, could be same as typedef name TwoNums */
   int n;
   double k;
} TwoNums;        /* TwoNums is now an alias for this struct type */
TwoNums s3;       /* Note: the word 'struct' is not needed anymore */
The name of a structure, unlike the name of an array, is not a pointer. Caution is thus required when structures are passed as arguments to functions.
#include <stdio.h>
#define Size 100000
typedef struct { /* Declare the structure using a typedef for convenience. */
  double nums1[Size]; /* 8 bytes per double */
  double nums2[Size]; /* 8 bytes per double */
  int nums3[Size];    /* 4 bytes per int */
  float nums4[Size];  /* 4 bytes per float */
  float nums5[Size];  /* 4 bytes per float */
  int n; /* for demo purposes */
} BigNumsStruct;
void good(BigNumsStruct* ptr) {
  printf("%lu ", sizeof(ptr));        /* 8 on my machine */
  printf("%i %i ", (*ptr).n, ptr->n); /* -9876 -9876 */
}
void bad(BigNumsStruct arg) {
  printf("Argument size is: %lu ", sizeof(arg)); /* 2,800,008 bytes */
}
void main() {
  BigNumsStruct bns;
  bns.n = -9876;
  bad(bns);   /** CAUTION **/
  good(&bns); /* right approach: pass an address */
}
Listing 3-11

Passing a structure as an argument

The bigStruct program (see Listing 3-11) declares a structure, five of whose fields are large arrays. The function main then creates a local variable bns of this structure type and passes the variable to function bad. Recall that C uses call by value in function calls; hence, a byte-per-byte copy of bns is passed to function bad, a copy that is about 2.8MB (megabytes) in size.

By contrast, main then calls function good by passing the address of bns rather than a copy of this BigNumsStruct instance. The address is 4 or 8 bytes, depending on whether the underlying machine uses 32-bit or 64-bit addresses.

The second printf in function good shows how C syntax supports two ways of accessing structure fields:
  • The first way uses the member operator (the period) but is clumsy because the expression contains the pointer ptr:

    (*ptr).n
The parentheses are necessary because the period has higher precedence than the star. Without the parentheses, the deference operator would apply to ptr.n, but n is a nonpointer field.
  • The second way uses the arrow operator (a minus symbol followed by a greater-than symbol):

    ptr->n

This syntax is cleaner and is idiomatic in C.

In the bigStruct program, the sizeof of the BigNumsStruct is reported to be 2,800,008 bytes. The arrays account for 2,800,000 of these bytes, and int field n requires only 4 bytes. What accounts for the extra 4 bytes? A simpler example explains.

Consider this structure:
struct {
   int n;    /* sizeof(int) == 4 */
   char c;   /* sizeof(char) == 1 */
   double d; /* sizeof(double) == 8 */
} test;

The minimum storage required for a variable such as test is 13 bytes, but most implementations would report sizeof(test) to be 16 rather than 13. Modern C compilers typically align storage for scalar variables on multibyte boundaries, for example, on 4-byte (32-bit) boundaries. The char field named c thus is implemented with four bytes rather than just one.

3.8.1 Sorting Pointers to Structures

An earlier discussion noted that pointers to pointers are common in C. The current discussion, on structures , is an opportunity to show how such pointers can be put to use.

Imagine an array of structure elements, perhaps of Employee instances, each of which is roughly 8KB (kilobytes) in size and all of which differ in whatever field (for instance, an ID field) might be used as a sort key. Suppose, then, that the Employee array is to be sorted by employee ID.

Sorting the Employee array with qsort would require moving 8KB chunks around in the array in order to get the desired sorted order. Such moves are inefficient, given the chunk size. A first principle of programming is not to move large data chunks unless the reasons are compelling.

There is another way, one that brings pointers to pointers into the picture. Given an array of relatively large structure elements, it is straightforward to create an index array for the Employee array, where the index array is a second array whose elements are pointers to elements in the first array :
   0x0004     0x1f44      0x3e84         ## addresses, 8KB bytes or sizeof(Employee) apart
+-----------+-----------+-----------+
| Employee1 | Employee2 | Employee3 |... ## 8KB Employee elements
+-----------+-----------+-----------+
+--------+--------+--------+
| 0x0004 | 0x1f44 | 0x3e84 |...## index array for Employee array
+--------+--------+--------+

In this depiction, the elements in the top or data array are Employee instances, whereas the elements in the bottom or index array are Employee* pointers. In short, each index element points to an Employee element. The addresses in the index array are 8KB (kilobytes) apart because sizeof(Employee) is 8,000 bytes, and addresses are of bytes. Given the significant difference in size between elements in the Employee array and the index array, it would be more efficient to sort the index than the Employee array. Indeed, several index arrays might be created and then sorted to obtain various orders : employees sorted by ID, by salary, by years in service, and so on. To print or otherwise process the Employee elements in the desired order, a program would traverse one of the indexes. The Employee elements would remain in their initial positions.

This approach does bring a challenge to the programmer, however. Consider the arguments passed to the qsort comparison function when an index is sorted on some Employee feature such as ID or years in service. Each such argument is of type const void*, which in this case is really of type Employee**: a pointer to a pointer to an Employee. The arguments to the comparison function thus must be dereferenced twice in order to access the Employee feature to be used in the comparison. A full code example goes into the details.
#include <stdio.h>
#include <stdlib.h> /* rand */
#define SizeS 1000
#define SizeA 100
typedef struct {
  double nums[SizeS]; /* 8 bytes per */
  int n;              /* for demo purposes */
} BigNumsStruct;
int comp(const void* p1, const void* p2) {
  BigNumsStruct* ptr1 = *((BigNumsStruct**) p1);                                 /* p1 points to a pointer */
  BigNumsStruct* ptr2 = *((BigNumsStruct**) p2);                                 /* p2 points to a pointer */
  return ptr1->n - ptr2->n;                      /* ascending order */
}
void main() {
  BigNumsStruct big_nums[SizeA];
  BigNumsStruct* pointers[SizeA];
  int i;
  for (i = 0; i < SizeA; i++) {
    big_nums[i].n = rand();
    pointers[i] = big_nums + i;   /* base address (big_nums) plus offset (index i) */
}
qsort(pointers, SizeA, sizeof(BigNumsStruct*), comp);                                       /** sort the pointers **/
for (i = 0; i < SizeA; i++)
   printf("%i ", pointers[i]->n);
}
Listing 3-12

Sorting pointers rather than data

The sortPtrs program (see Listing 3-12) revises the earlier example of the BigNumsStruct. The size of this structure is reduced to a more realistic number, and a local array of such structures is declared, which means that storage for the array comes from the stack. The int field named n remains and now is initialized to a random value.

Although a BigNumsStruct is slimmer than before, its sizeof remains an impressive 8,008 bytes on my machine. By contrast, a pointer to such a structure instance requires only 8 bytes on the same machine. In the sortPtrs program, sorting the big_nums array would require moving 8KB (kilobytes) chunks, whereas sorting pointers to the elements in this array would require moving only 8-byte chunks. The resulting gain in efficiency is compelling. The printf loop at the end confirms that the pointers array has been sorted as desired, in ascending order by the BigNumsStruct field named n.

The cost for this efficiency is a complicated comparison function , again named comp. Recall that each argument in the comparison callback is of type const void*. Because an array of pointers is being sorted, the two arguments to comp, named p1 and p2, are indeed pointers to pointers. Each of these pointers is therefore cast to its actual type, BigNumsStrut**: a pointer to a pointer to a BigNumsStruct. A dereference of each point provides what is needed: a pointer to a BigNumsStruct, which then can be used with the arrow operator to access the field n. Here, for review, is the body of the comparison function :
BigNumsStruct* ptr1 = *((BigNumsStruct**) p1); /* p1 points to a pointer */
BigNumsStruct* ptr2 = *((BigNumsStruct**) p2); /* p2 points to a pointer */
return ptr1->n - ptr2->n; /* access the field n, sort in ascending order */

3.8.2 Unions

There is a specialized type of structure called a union, which is designed for memory efficiency. A short example highlights the difference between a struct and a union.

The following structure has two fields: a double and a long. The sizeof(v1)
struct {
  double d;
  long l;
} v1;

is 16: both the double and the long are 8 bytes in size.

By contrast, a union with exactly the same fields would require only half the bytes. The sizeof(v2)
union {
  double d;
  long l;
} v2;
is 8 bytes. A union provides enough storage for the largest of its fields, and all of the fields then share this storage. For example, the struct variable v1 can store both a double and a long at the same time:
v1.d = 44.44;
v1.l = 1234L;
By contrast , the union variable v2 stores either the one or the other:
v2.d = 44.44; /* the double is stored */
v2.l = 1234L; /* initializing the long overwrites the double */

3.9 String Conversions with Pointers to Pointers

Earlier examples illustrated very simple conversions involving basic data types. For example, even the statement
char c = 65; /* 65 is ASCII/Unicode for uppercase A */
involves a conversion: from the 32-bit int constant 65 to the 8-bit char value stored in variable c. Converting from one single value to another is routine in C: an explicit cast can be used for clarity, but in general, the compiler can be counted on to do the converting without complaint. For example, the compiler does not even warn against this conversion:
short n = 3.1415; /* 64-bit floating-point value stored in 16-bit integer variable */
The conversion goes from a three-field, 64-bit floating-point source to a two-field, 16-bit signed-integer destination. In examples such as these, explicit casts can be used to enhance clarity, but this remains a recommendation rather than a requirement:
char c = (char) 65;
short n = (short) 3.1415;

The challenge arises in converting between strings, an aggregate rather than a scalar type, and other basic types. Because a string in C is an array, converting an array to a single integer or floating-point value is nontrivial. C provides library functions to do the heavy lifting.

The stdlib.h header file declares functions for converting strings to integers and floating-point values:
int atoi(const char* nptr);        /* string to 32-bit int */
long atol(const char* nptr);       /* string to 64-bit long */
long long atoll(const char* nptr); /* string to long long, probably 64-bits */
float atof(const char* nptr);      /* string to 32-bit float */

The const qualifier signals that the pointer argument is not used to change the string itself, only to convert the string to a numeric value. The a in atoi and the others is for ASCII, the default character encoding in C.

None of the ato functions are especially helpful in determining why an attempted conversion failed. To that end, the stdlib.h header file also includes functions with names that start out with strto, for example, strtol (string to long integer) and strtod (string to double). The strto functions check the string for inappropriate characters and have a mechanism for separating out the converted part of the source string, if any, from the rest. A code example clarifies.
#include <stdio.h>
#include <stdlib.h> /* atoi, etc. */
void main() {
  const char* s1 = "27";
  const char* s2 = "27.99";
  const char* s3 = " 123";      /* whitespace to begin */
  const char* e1 = "1z2q";      /* bad characters */
  const char* e2 = "4m3.abc!#"; /* ditto */
  printf("%s + 3 is %i. ",      s1, atoi(s1) + 3);   /* 27 + 3 is 30. */
  printf("%s + 3 is %f. ",      s2, atof(s2) + 3.0); /* 27.99 + 3 is 30.990000. */
  printf("%s to int is %i. ",   s3, atoi(s3));       /* 123 to int is 123. */
  printf("%s to int is %i. ",   e1, atoi(e1));       /* 1z2q to int is 1. */
  printf("%s to float is %f. ", e2, atof(e2));       /* 4m3.abc to float is 4.000000. */
  char* bad_chars = NULL;
  const char* e3 = "9876 !!foo bar";
  long num = strtol(e3, &bad_chars, 10);             /* 10 is the base, for decimal */
  printf("Number: %li Junk: %s ", num, bad_chars); /* Number: 9876 Junk: !!foo bar */
}
Listing 3-13

Converting strings to numeric values

The str2num program (see Listing 3-13) has three examples of strings that convert straightforwardly. The pointers to these are s1, s2, and s3. The string to which s3 points is the most interesting in that it begins with blanks; but the atoi functions ignore the leading whitespace. The challenging cases are the strings to which e1 and e2 point, as these strings contain nonnumeric characters other than whitespace. (Numeric characters include the numerals, the plus and minus signs, and the decimal point.)

For strings with nonnumeric characters such as the sharp sign, the ato functions convert until the first such character is encountered and then stop. This is why function atoi converts the string “1z2q” to 1: the function converts as long as it can and then halts abruptly on the first inappropriate character. If a string starts with a nonnumeric character, then the ato functions return 0:
int n = atoi("foo123"); /* n == 0 after the conversion */
The strto functions are more powerful than their ato counterparts, and they use a pointer-to-pointer type to gain this power. Here is the declaration for strtol:
long int strtol(const char* nptr, char** endptr, int base);
The first argument is again a pointer to the source string, and the return value is a long. The last argument specifies the base to be used in the conversion: 2 for binary, 10 for decimal, and so on. The middle argument is the tricky one, as its type is pointer-to-pointer-to-char. Here, for review, is the code segment in the str2num program that sets up and then calls the strtol function:
char* bad_chars = NULL;
const char* e3 = "9876!!foo bar";
long num = strtol(e3, &bad_chars, 10);

The strtol function determines where to break the source string to which e3 points: at the first ! character. The library function then sets pointer bad_chars to this character. In an idiom analyzed earlier, the strtol function thus uses an argument, in this case the pointer-to-pointer variable bad_chars, in order to return a value—the first character (the !) that cannot be used in the string-to-number conversion. The return value for strtol is, of course, the converted number. A pointer-to-pointer type allows the strtol function to return two pieces of information.

The ato and strto functions are convenient for converting strings to integer and floating-point types. There is also a more general approach. The printf function, for type-sensitive printing, has been used in many examples. This function prints to the standard output, the screen by default. The inverse function is scanf, which scans the standard input (the keyboard by default) for strings that then are converted into the specified type. Two variants of these functions are useful for converting from and to strings: sprintf, which prints to a buffer (char array) rather than to the standard output, and sscanf, which reads from a buffer instead of from the standard input. A code example clarifies.
#include <stdio.h>
void main() {
  char* s1 = "123456";
  char* s2 = "123.45";
  int n1;
  float n2;
  /** string to other types: sscanf **/
  sscanf(s1, "%i", &n1); /* address of n1, not n1 */
  sscanf(s2, "%f", &n2); /* address of n2, not n2 */
  printf("%i %f ", n1 + 3, n2 + 8.7f); /* 123459 132.149994 */
  /** other types to string: sprintf **/
  char buffer[64]; /* stack storage, buffer its address */
  sprintf(buffer, "%i", n1 + 3);
  printf("%s ", buffer); /* 123459 */
}
Listing 3-14

A general approach to converting to and from strings

The scanPrint program (see Listing 3-14) illustrates the basics of converting to and from strings using the printing and scanning functions. The print and scan functions differ markedly in their arguments. The print functions (printf, sprintf, and fprintf for printing to a file) take nonpointer values as the arguments after the format string. By contrast, the scan functions (scanf, sscanf, and fscanf for scanning data from a file) take pointers as the arguments after the format string. The scanning functions require a pointer to indicate where a scanned (and perhaps converted) value should be stored. For functions in both families, the format string specifies the desired type for either printing or scanning. As even this short code example shows, sprintf and sscanf provide a general-purpose solution to the problem of converting to and from strings.

Finally, the header file ctype.h has various functions for determining properties of individual characters. For instance, the library function isdigit(c) checks whether character c is a decimal digit, function isprint(c) checks whether character c is printable, and so on.

3.10 Heap Storage and Pointers

A program in execution (process) has access to three areas of memory:
  • A static area that stores string literals, global variables, and executable code. The traditional name for the area that holds the executable code is text, as earlier assembly-code examples illustrate; the term text is meant to suggest read-only, but this static area can store read/write variables as well.

  • The stack , which provides scratchpad storage for parameters and local variables. The stack acts as a backup for CPU registers, which are quite limited in number (e.g., roughly 16 on standard handheld, laptop, and desktop machines).

  • The heap , which provides storage that the program explicitly allocates and, in the case of C, deallocates. Pointers come into play with heap storage.

The examples so far have not covered the third category, the heap. The compiler figures out how much storage is required for the read-only area and the stack; hence, the details about such storage are determined at compile time—no extra programmer intervention is required. By contrast, the programmer uses designated operators (e.g., new in many modern languages) or functions (e.g., malloc and its relatives in C) to allocate storage from the heap, an allocation traditionally described as dynamic because it is done explicitly at runtime.

The programmer plays a more active role with heap as opposed to stack storage. The compiler determines the mix of stack and CPU registers required for program execution, thereby off-loading this responsibility from the programmer. By contrast, the programmer manages heap storage through system calls to allocate and, in the case of C, to deallocate this storage. A review of stack storage through a code example sets the scene for a code-based analysis of heap storage.
#include <stdio.h>
#define Size 9
int sum_array(int arr[], unsigned n) {
  int sum = 0;
  unsigned i;
  for (i = 0; i < n; i++) sum += arr[i];
  return sum;
}
void main() {
  int nums[ ] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
  int n = sum_array(nums, Size);
  printf("The sum is: %i ", n); /* The sum is: 45 */
}
Listing 3-15

Summing an array in C

The sumArray program (see Listing 3-15) has two functions, main and sum_array, each of which needs stack storage for scratchpad. The main function has a local array of nine elements, each an int; these elements are stored on the stack. This function also has a local variable n to store the value returned from a call to the sum_array function. Depending on how optimizing the compiler happens to be, variable n could be implemented as a CPU register instead of as a stack location.

The sum_array function works with a pointer to the array declared and populated in main, but sum_array does need some local storage of its own: the integers sum and i, the loop counter. Both sum and i are scalars rather than aggregates, and so CPU registers would be ideal; but the stack is the fallback for the compiler.

The assembly code for the sumArray program is generated in the usual way:
% gcc -S -O1 sumArray.c ## capital letter O for optimization level, 1 in this case
Here is a quick overview of how the assembly code handles summing the array. The assembly code
  • Stores the array nums on the stack. The assembly code grows the stack by 56 bytes for this purpose, although only 36 bytes are needed for the nine int values.

  • Stores the array’s size in a CPU register for efficiency.

For readability, the resulting assembly code has been pared down; for instance, most of the directives are omitted. The first code display is the assembly code for main, and the following display is the assembly code for sum_array. To begin, however, a look at the syntax for pointers in assembly code will be useful.

Recall the assembly opcode movq , which copies 64 bits (a quadword) from a source to a destination:
movq $0, %rax   ## copy zero into %rax
A comparable C statement is
unsigned long n = 0; /* a long is 64 bits */
Consider a more complicated example:
movq $1, (%rax)
The parentheses are the dereference operator in assembly code. Accordingly, this statement implies that %rax holds an address, and 1 is to be copied to wherever %rax points, not into %rax itself. In C, a counterpart would be
*ptr = 1; /* copy 1 to where ptr points, not into ptr itself */
A common variant of pointer syntax in assembly language is
movq $1, 16(%rax)
The parentheses with an integer value to the left indicate base-displacement addressing: inside the parentheses is the base address, in this case the contents of %rax. To the left of the left parenthesis is the displacement, the number of bytes added to the base address. (The displacement can be positive or negative.) In C, a counterpart would be
*(ptr + 16) = 1; /* assuming ptr is of type char* because a char is a byte */
With this background, the assembly code for the function main in the sumArray program should make sense.
.LC0:                         ## address of format string
   .string "The sum is: %i " ## format string
main:
   subq $56, %rsp    ## grow the stack by 56 bytes (stack grows high to low)
   movl $1, (%rsp)   ## store 1 to where the stack pointer points (the TOP)
   movl $2, 4(%rsp)  ## store 2 four bytes _up_
   movl $3, 8(%rsp)  ## and so on
...
   movl $9, 32(%rsp) ## 9 is stored 32 bytes up from the stack pointer
   movl $9, %esi     ## this 9 is Size: put in a CPU 32-bit register %esi
   movq %rsp, %rdi   ## copy stack pointer in %rdi, which now points to 9 in the array
   call sum_array    ## call the subroutine
   movl %eax, %edx   ## save the value returned from sum_array
   movl $.LC0, %esi  ## copy address of format string into %esi
   movl $1, %edi     ## copy 1 into %edi: number of values to format, 1 in this case
   movl $0, %eax     ## clear %eax for the print routine
   call __printf_chk ## call print routine (special arg-checking version of printf)
   addq $56, %rsp    ## restore the stack pointer by reclaiming the 56 bytes
   ret               ## return to caller in exec family
Listing 3-16

The assembly code for main in the sumArray program

The high points of the assembly code for the main block (see Listing 3-16), the assembly-language counterpart of the main function in C, can be summarized as follows:
  • The block begins by growing the scratchpad storage on the stack: 56 is subtracted from the 64-bit stack pointer %rsp, which has the effect of growing the stack scratchpad by 56 bytes because the Intel stack grows from high to low addresses. Moving the stack pointer %rsp down by 56 bytes means, in other words, that there are now 56 newly available bytes above where the stack pointer currently points. Shrinking the scratchpad storage on the stack is done by adding to the stack pointer, as occurs in the second-to-last statement in the main block:

    addq $56, %rsp ## cleanup from the earlier subq %56, %rsp
  • The nine-integer array elements 1,2,…,9 in the array nums from the C code are stored on the stack. Most of the values are stored up from the stack pointer. For example, 1 is stored at where the stack pointer currently points, 2 is stored 4 bytes up from this position at 4(%rsp), and so on. In general, the compiler stores arrays on the stack, even very small arrays . There are simply too few general-purpose registers to store arrays, and addressing array elements is simplified by having these elements be stored contiguously. Registers are used for scalar values, not for aggregates.

  • The array’s size, 9, is not stored on the stack, but rather in the 32-bit CPU register %esi. Recall that on a 64-bit machine, the name %esi refers to the lower-order 32 bits of the 64-bit register %rsi. The sum_array routine accesses the array’s size from register %esi.

  • The value returned from sum_array in 32-bit register %eax is copied to register %edx, the address of the format string is copied to register %esi, and the number of values to be formatted (in this case, one) is copied into register %edi. At this pointer, the main module is ready to call the print routine printf_chk, which does an integrity check on the arguments , where chk stands for “check.” As the example shows, the underscore can be used even to start an identifier.

  • After shrinking the stack back to its size before the call to main, the main routine returns to its caller. Recall that main in the C source does not return a value; hence, the assembly routine does not place a value in %eax immediately before returning.

    sum_array:
       testl %esi, %esi  ## is the array size 0?
       je .L4            ## if so, return to caller
       movl $0, %edx     ## otherwise, set loop counter to 0
       movl $0, %eax     ## initialize sum to 0
    .L3:
       addl (%rdi,%rdx,4), %eax  ## increment the running sum by the next value (sum += arr[i])
       addq $1, %rdx             ## increment loop counter by 1 (integer)
       cmpl %edx, %esi           ## compare loop counter with array size
       ja .L3                    ## keep looping if size is bigger (ja = jump if above)
       rep ret           ## otherwise, AMD-specific version of ret for return
    .L4:                 ## return 0 as the sum because array is empty
       movl $0, %eax     ## copy 0 into returned-value register
       ret               ## return to caller
    Listing 3-17

    The assembly code for sum_array

The sum_array routine in assembly code (see Listing 3-17) is complicated because of the control structure. The code basically handles two cases:
  • If the array’s size is zero (the array is empty), then return 0.

  • Otherwise, initialize a loop counter (32-bit register %edx) to 0, and loop until the array’s size is no longer greater than the loop counter. The running sum is stored in 32-bit register %eax, and %eax also serves as the returned-value register.

Several points about the code deserve mention. For one thing, the code sometimes references the 64-bit register %rdx but sometimes references only the lower 32 bits of this register under the name %edx. This can be confusing but works just fine because the upper-order bits in register %rdx have been zeroed out.

Another point of interest is the most complicated instruction in the sum_array routine :
addl (%rdi,%rdx,4), %eax   ## in C: sum += arr[i]
First, consider the instruction that follows the addl instruction :
addq $1, %rdx   ## in C: i = i + 1

This instruction updates the loop counter %rdx by one integer, not by 4 bytes. Accordingly, the addl instruction’s first operand is the expression (%rdi,%rdx,4). Register %rdi points to the start of the array; in the C code, this is the parameter arr in the function sum_array. The offset from this base address is %rdx × 4, where %rdx is the loop counter (in C, the index i) and 4 is sizeof(int).

The assembly code confirms that the stack requirements for the sumArray program are determined at compile time. The stack management is thus automatic from the programmer’s perspective: the programmer declares local variables and parameters, makes a function call, executes a print statement, and so on. The compiler manages the details when it comes to providing scratchpad storage on the stack and, in this example, in CPU registers as well.

This analysis of the sumArray program sets up a contrast between stack and heap storage. C has functions for allocating heap storage, with the malloc and the calloc functions as the primary ones. There is also a realloc function for growing or shrinking previously allocated heap memory. The free function deallocates the memory allocated by any of these functions. The general rule for avoiding memory leaks is this: for every malloc or calloc, there should be a matching free. The programmer is fully responsible for the calls to these functions. A first code example covers the basics.
#include <stdio.h>
#include <stdlib.h> /* malloc, calloc, realloc */
#include <string.h> /* memset */
#define Size 20
void dump(int* ptr, unsigned size) {
  if (!ptr) return; /* do nothing if ptr is NULL */
  int i;
  for (i = 0; i < size; i++) printf("%i ", ptr[i]); /* *(ptr + i) */
  printf(" ");
}
void main() {
  /* allocate */
  int* mptr = malloc(Size * sizeof(int)); /* 20 ints, 80 bytes */
  if (mptr) /* malloc returns NULL (0) if it cannot allocate the storage */
    memset(mptr, -1, Size * sizeof(int)); /* set each byte to -1 */
  dump(mptr, Size);
  /* realloc */
  mptr = realloc(mptr, (Size + 8) * sizeof(int)); /* request 8 more */
  if (mptr) dump(mptr, Size + 8);
  /* deallocate */
  free(mptr);
  /* calloc */
  mptr = calloc(Size, sizeof(int)); /* calloc initializes the storage to zero */
  if (mptr) {
    dump(mptr, Size);
    free(mptr);
  }
}
Listing 3-18

Basic heap allocation and deallocation

The program memalloc (see Listing 3-18) shows the basic API for allocating and deallocating memory from the heap. The simplest and most basic function is malloc, which tries to allocate the number of bytes given as its single argument. The return type from malloc is the same for calloc and realloc:
  • If the memory can be allocated, a pointer to the first byte is returned.

  • If the memory cannot be allocated, NULL is returned.

The malloc function could be used to allocate as little as 1 byte but typically is used to allocate aggregates. In the case of malloc, the allocated storage is not initialized. The memalloc program therefore initializes the allocated memory to -1 by using the memset library function:
memset(mptr, -1, Size * sizeof(int)); /* mptr returned from malloc */

This function takes three arguments: a pointer to the storage to be initialized, the value to be stored in each byte, and the number of bytes to be initialized. The memset function is yet another library routine that works at the byte level.

The calloc function takes two arguments: the first is the number of elements to allocate (e.g., 10), and the second is the sizeof each element (e.g., 4 for an int). The calloc function thus can be used to allocate storage for multibyte types such as int and double. This function, unlike malloc, initializes the allocated storage to all 0s. In general, malloc is faster than calloc because malloc does no memory initialization.

The realloc function can be used to grow or shrink previously allocated storage. In this example, the function is used to grow the allocated storage by 8 × sizeof(int) bytes. If realloc succeeds, it leaves the previously allocated storage unchanged and adds or removes the requested number of bytes. In the memalloc program, realloc is called to request an additional 32 bytes (8 int values) to a collection of 20 int values already initialized to -1; hence, the original bytes still have -1 as their value after the reallocation, but the added bytes have arbitrary values.

As the name suggests, the free function deallocates storage allocated with the malloc and calloc functions. The realloc function presupposes a previous call to one of these other functions. To avoid memory leaks, it is critical for a program to free explicitly allocated storage.

Does C Have Garbage Collection?

No. Library functions such as malloc and calloc allocate specified amounts of storage from the heap, but the programmer then is responsible for explicitly deallocating (freeing) this heap storage. Allocation without deallocation causes memory leaks, which can dramatically degrade system performance. Freeing no longer needed heap storage is, indeed, one of the major challenges in writing sound C programs.

3.11 The Challenge of Freeing Heap Storage

Recall the rule of thumb for freeing heap storage: for every malloc or calloc, there should be a free. Putting the rule into practice can be challenging, in particular when dealing with functions that return a pointer to a structure that, in turn, has, among its fields, pointers to heap storage. In short, the heap storage allocation may be nested. If the allocation is nested, then the freeing should be so as well. The documentation on library functions is worth reading carefully, in particular for functions that return a pointer to heap storage. There are different ways for memory-allocating library functions to guard against memory leakage, for example, by providing a utility function that does the freeing to whatever level is appropriate.

Two code examples get into the details of the challenge. The first example focuses on how to return an aggregate to a caller, and the second focuses on nested freeing.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BuffSize 128
void get_name1(char buffer[ ], size_t len) {  /* safest: buffer passed in as arg */
  strncpy(buffer, "Gandalf", len);            /* user-supplied buffer */
}
void* get_name2() {                          /* ok, but invoker must free */
  void* ptr = malloc(BuffSize + 1);
  if (!ptr) return 0;
  strcpy(ptr, "Sam");
  return ptr;
}
char* get_name3() {                          /* VERY BAD (compiler warning) */
  char buffer[BuffSize + 1];
  strcpy(buffer, "Frodo");
  return buffer;
}
Listing 3-19

Three ways of returning a string to a caller

The getname program (see Listing 3-19) contrasts three ways to return a string—an aggregate—from a function. The compiler generates an apt warning about one of the ways. Three functions represent the three different approaches. For each approach, imagine that a user is prompted for, and then enters, a name. To keep the code short, the example hard-wires the names. Here is a summary of the three approaches , with recommendations:
  • The get_name1 function represents the safest approach. The function takes two arguments: an array to hold the name and the array’s length. The function then uses the library function strncpy to copy a name into this array. The n in strncpy specifies the maximum number of characters to be copied, thereby protecting against the notorious buffer overflow problem. A buffer overflow occurs if the array is not big enough to hold all of the elements placed in it. In the case of get_name1, the invoker of the function is responsible for providing a buffer at least as big as the len argument specifies. The first three lines of main illustrate a proper call to get_name1.

A cautionary note is in order. Suppose that the first two lines of main change from
char buffer[BuffSize + 1];                  /* + 1 for null terminator */
get_name1(buffer, BuffSize);
to
char* buffer; /* storage for a pointer, but not for any characters pointed to */
get_name1(buffer, BuffSize);              /* false promise: the buffer's length is zero */
The getname program still compiles because the compiler treats these two data types as being equivalent:
char* buffer    ## the argument's type in main
char buffer[ ]  ## the first parameter's type in get_name1
Nonetheless, the program is likely to crash at runtime because there is no storage provided for the characters in the string; there is storage only for a single pointer to a char. Increasing the length of the string increases the likelihood of a crash.
  • The get_name2 function takes no arguments and instead allocates heap storage to store a string of BuffSize characters, where BuffSize is a macro defined as 64; a pointer to this storage is returned. The call to malloc requests an additional byte for the null terminator, so BuffSize + 1 bytes in all. The get_name2 function returns ptr, which holds the value returned from malloc. (If malloc returns NULL, so does get_name2.) This approach makes the caller, in this case main, responsible for freeing the allocated storage. There is a division of labor: one function allocates the required heap storage, but a different function (its invoker) must free these allocated bytes when they are no longer needed.

  • The get_name3 function is done badly, and the compiler points out the shortcoming. The function declares a local variable buffer of BuffSize + 1 bytes. This, in itself, is fine. The function then returns the array’s name—a pointer to the first char in the array. This is risky because the storage for the array comes from the stack, and that very area of the stack is open for reuse once get_name3 returns. Some other function might place other data in this very area. The general principle is clear: never return a pointer to local storage.

    /** headers and macro above **/
    void main() {
      char buffer[BuffSize + 1];         /* + 1 for null terminator */
      get_name1(buffer, BuffSize);
      printf("%s ", buffer);
      void* retval2 = get_name2();
      printf("%s ", (char*) retval2);   /* cast for the %s */
      free(retval2);                     /* safeguard against memory leak */
      const char* retval1 = get_name3(); /* not a good idea */
      printf("%s ", retval1);           /* unpredictable output */
    }
    Listing 3-20

    Calling the three functions in the getname program

The main function for the getname program (see Listing 3-20) declares a char buffer, which is used in the call to function get_name1. The responsibility falls squarely on the caller to provide enough storage for the string to be stored. The second argument, BuffSize, guards against buffer overflow because the char array is of size BuffSize + 1, with the added byte for the null terminator.

The call to get_name2 returns a pointer to the heap storage provided for the name. In this case, the main function does call free, but the logic is complicated: one function allocates, another function frees. The approach works, but it is error-prone.

The last call, to get_name3 , provokes a compiler warning because a pointer to local storage is being returned to main. In this case, the storage for the name is local to the call frame for get_name3. Once the function get_name3 returns to main, the call frame for get_name3 should not be accessed. It is unpredictable whether this third approach works.

3.12 Nested Heap Storage

It is relatively straightforward to handle nonnested cases of allocating and freeing, as in the previous examples of heap storage . Here is a review of the pattern:
int* some_nums = malloc(5000 * sizeof(int));
/* ... application logic ... */
free(some_nums);

This code segment allocates heap storage for 5,000 int values, does whatever logic is appropriate, and then frees the storage. The challenge increases when, for example, structure instances are allocated from the heap—and such instances contain fields that are themselves pointers to heap storage. If the heap allocation is nested, the freeing must be nested as well.

As a common example of the challenge, C has various library functions that return a pointer to heap storage. Here is a typical scenario:
  1. 1.

    The C program invokes a library function that returns a pointer to heap-based storage, typically an aggregate such as an array or a structure:

    SomeStructure* ptr = lib_function(); /* returns pointer to heap storage */
     
  2. 2.

    The program then uses the allocated storage.

     
  3. 3.

    For cleanup, the issue is whether a single call to free will clean up all of the heap-allocated storage that the library function allocates. For example, the SomeStructure instance may have fields that, in turn, point to heap-allocated storage. A particularly troublesome case would be a dynamically allocated array of structures, each of which has a field pointing to more dynamically allocated storage.

     
The next code example (see Listing 3-21) illustrates the problem and focuses on how to design a library that safely provides heap-allocated storage to clients.
#include <stdio.h>
#include <stdlib.h>
typedef struct {
  unsigned id;
  unsigned len;
  float* heap_nums;
} HeapStruct;
unsigned structId = 1;
HeapStruct* get_heap_struct(unsigned n) {
  /* Try to allocate a HeapStruct. */
  HeapStruct* heap_struct = malloc(sizeof(HeapStruct));
  if (NULL == heap_struct) /* failure? */
    return NULL;           /* if so, return NULL */
  /* Try to allocate floating-point aggregate within HeapStruct. */
  heap_struct->heap_nums = malloc(sizeof(float) * n);
  if (NULL == heap_struct->heap_nums) { /* failure? */
    free(heap_struct);                  /* if so, first free the HeapStruct */
    return NULL;                        /* then return NULL */
  }
  /* Success: set fields */
  heap_struct->id = structId++;
  heap_struct->len = n;
  return heap_struct; /* return pointer to allocated HeapStruct */
}
void free_all(HeapStruct* heap_struct) {
  if (NULL == heap_struct)  /* NULL pointer? */
    return;                 /* if so, do nothing */
  free(heap_struct->heap_nums); /* first free encapsulated aggregate */
  free(heap_struct);            /* then free containing structure */
}
int main() {
  const unsigned n = 100;
  HeapStruct* hs = get_heap_struct(n); /* get structure with N floats */
  /* Do some (meaningless) work for demo. */
  unsigned i;
  for (i = 0; i < n; i++) hs->heap_nums[i] = 3.14 + (float) i;
  for (i = 0; i < n; i += 10) printf("%12f ", hs->heap_nums[i]);
  free_all(hs); /* free dynamically allocated storage */
  return 0;
}
Listing 3-21

Nested heap storage

The nestedHeap example (see Listing 3-21) centers on a structure HeapStruct with a pointer field named heap_nums :
typedef struct {
  unsigned id;
  unsigned len;
  float* heap_nums; /** pointer **/
} HeapStruct;
The function get_heap_struct tries to allocate heap storage for a HeapStruct instance, which entails allocating heap storage for a specified number of float variables to which the field heap_nums points. The result of a successful call to get_heap_struct can be depicted as follows, with hs as the pointer to the heap-allocated structure:
hs-->HeapStruct instance
        id
        len
        heap_nums-->N contiguous float elements
In the get_heap_struct function , the first heap allocation is straightforward:
HeapStruct* heap_struct = malloc(sizeof(HeapStruct));
if (NULL == heap_struct)  /* failure? */
  return NULL;            /* if so, return NULL */

The sizeof(HeapStruct) includes the bytes (four on a 32-bit machine, eight on a 64-bit machine) for the heap_nums field, which is a pointer to the float elements in a dynamically allocated array. At issue, then, is whether the malloc delivers the bytes for this structure or NULL to signal failure; if NULL, the get_heap_struct function returns NULL to notify the caller that the heap allocation failed.

The second attempted heap allocation is more complicated because, at this step, heap storage for the HeapStruct has been allocated:
heap_struct->heap_nums = malloc(sizeof(float) * n);
if (NULL == heap_struct->heap_nums) { /* failure? */
  free(heap_struct);                  /* if so, first free the HeapStruct */
  return NULL;                        /* and then return NULL */
}
The argument n sent to the get_heap_struct function indicates how many float elements should be in the dynamically allocated heap_nums array. If the required float elements can be allocated, then the function sets the structure’s id and len fields before returning the heap address of the HeapStruct. If the attempted allocation fails, however, two steps are necessary to meet best practice :
  1. 1.

    The storage for the HeapStruct must be freed to avoid memory leakage. Without the dynamic heap_nums array, the HeapStruct is presumably of no use to the client function that calls get_heap_struct; hence, the bytes for the HeapStruct instance should be explicitly deallocated so that the system can reclaim these bytes for future heap allocations.

     
  2. 2.

    NULL is returned to signal failure.

     
If the call to the get_heap_struct function succeeds, then freeing the heap storage is also tricky because it involves two free operations in the proper order. Accordingly, the program includes a free_all function instead of requiring the programmer to figure out the proper two-step deallocation. For review, here’s the free_all function :
void free_all(HeapStruct* heap_struct) {
  if (NULL == heap_struct)  /* NULL pointer? */
    return;                 /* if so, do nothing */
  free(heap_struct->heap_nums);  /* first free encapsulated aggregate */
  free(heap_struct);             /* then free containing structure */
}

After checking that the argument heap_struct is not NULL, the function first frees the heap_nums array, which requires that the heap_struct pointer is still valid. It would be an error to free the heap_struct first. Once the heap_nums have been deallocated, the heap_struct can be freed as well. If heap_struct were freed but heap_nums were not, then the float elements in the array would be leakage: still allocated bytes but with no possibility of access—hence, of deallocation. The leakage would persist until the nestedHeap program exited and the system reclaimed the leaked bytes.

A few cautionary notes on the free library function are in order. Recall the earlier sample calls:
free(heap_struct->heap_nums);  /* first free encapsulated aggregate */
free(heap_struct);             /* then free containing structure */

These calls free the allocated storage—but they do not set their arguments to NULL. (The free function gets a copy of an address as an argument; hence, changing the copy to NULL would leave the original unchanged.) For example, after a successful call to free, the pointer heap_struct still holds a heap address of some heap-allocated bytes, but using this address now would be an error because the call to free gives the system the right to reclaim and then reuse the allocated bytes.

Calling free with a NULL argument is pointless but harmless. Calling free repeatedly on a non-NULL address is an error with indeterminate results:
free(heap_struct);  /* 1st call: ok */
free(heap_struct);  /* 2nd call: ERROR */

3.12.1 Memory Leakage and Heap Fragmentation

As the previous code examples illustrate, the phrase memory leakage” refers to dynamically allocated heap storage that is no longer accessible. Here is a refresher code segment:
float* nums = malloc(sizeof(float) * 10); /* 10 floats */
nums[0] = 3.14f;                          /* and so on */
nums = malloc(sizeof(float) * 25);        /* 25 new floats */

Assume that the first malloc succeeds. The second malloc resets the nums pointer, either to NULL (allocation failure) or to the address of the first float among newly allocated 25. Heap storage for the initial ten float elements remains allocated but is now inaccessible because the nums pointer either points elsewhere or is NULL. The result is 40 bytes (sizeof(float) * 10) of leakage.

Before the second call to malloc, the initially allocated storage should be freed:
float* nums = malloc(sizeof(float) * 10); /* 10 floats */
nums[0] = 3.14f;                          /* and so on */
free(nums);                               /** good **/
nums = malloc(sizeof(float) * 25);        /* no leakage */

Even without leakage, the heap can fragment over time, which then requires system defragmentation. For example, suppose that the two biggest heap chunks are currently of sizes 200MB and 100MB. However, the two chunks are not contiguous, and process P needs to allocate 250MB of contiguous heap storage. Before the allocation can be made, the system must defragment the heap to provide 250MB contiguous bytes for P. Defragmentation is complicated and, therefore, time-consuming.

Memory leakage promotes fragmentation by creating allocated but inaccessible heap chunks. Freeing no-longer-needed heap storage is, therefore, one way that a programmer can help to reduce the need for defragmentation.

3.12.2 Tools to Diagnose Memory Leakage

Various tools are available for profiling memory efficiency and safety. My favorite is valgrind ( www.valgrind.org/ ). The leaky program (see Listing 3-22) illustrates the problem and the valgrind solution.
#include <stdio.h>
#include <stdlib.h>
int* get_ints(unsigned n) {
  int* ptr = malloc(n * sizeof(int));
  if (ptr != NULL) {
    unsigned i;
    for (i = 0; i < n; i++) ptr[i] = i + 1;
  }
  return ptr;
}
void print_ints(int* ptr, unsigned n) {
  unsigned i;
  for (i = 0; i < n; i++) printf("%3i ", ptr[i]);
}
int main() {
  const unsigned n = 32;
  int* arr = get_ints(n);
  if (arr != NULL) print_ints(arr, n);
  /** heap storage not yet freed... **/
  return 0;
}
Listing 3-22

The leaky program

The function main calls get_ints, which tries to malloc 32 four-byte integers from the heap and then initializes the dynamic array if the malloc succeeds. On success, the main function then calls print_ints. There is no call to free to match the call to malloc; hence, memory leaks.

With the valgrind toolbox installed, the following command checks the leaky program for memory leaks:
% valgrind  --leak-check=full  ./leaky
In the following code segment, most of the output is shown. The number of the left, 207683, is the process identifier of the executing leaky program. The report provides details of where the leak occurs, in this case, from the call to malloc within the get_ints function that main calls.
==207683== HEAP SUMMARY:
==207683==   in use at exit: 128 bytes in 1 blocks
==207683==   total heap usage: 2 allocs, 1 frees, 1,152 bytes allocated
==207683==
==207683== 128 bytes in 1 blocks are definitely lost in loss record 1 of 1
==207683==   at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/...-linux.so)
==207683==   by 0x109186: get_ints (in /home/marty/gc/leaky)
==207683==   by 0x109236: main (in /home/marty/gc/leaky)
==207683==
==207683== LEAK SUMMARY:
==207683==   definitely lost: 128 bytes in 1 blocks
==207683==   indirectly lost: 0 bytes in 0 blocks
==207683==   possibly lost: 0 bytes in 0 blocks
==207683==   still reachable: 0 bytes in 0 blocks
==207683==   suppressed: 0 bytes in 0 blocks
If function main is revised to include a call to free right after the one to print_ints, then valgrind gives the leaky program a clean bill of health:
==218462==  All heap blocks were freed -- no leaks are possible

3.13 What’s Next?

C variables can be defined inside and outside of blocks, where a block is the by-now-familiar construct that begins with a left brace { and ends with a matching right brace }. Where a variable is defined determines, within options, where its value is stored, how long the variable persists, and where the variable is visible. Every variable has a storage class (with auto and extern as examples) that determines the variable’s persistence and scope. Functions too have a storage class: either extern (the default) or static. The next chapter gets into the details of storage classes.

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

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