C Part I · Module 7 · Pointers
Arrays of Pointers
Nothing in this chapter is new. Module 6 established that an array is a fixed run of objects all of one type, and this module has spent eight chapters establishing that a pointer is an ordinary object with a type of its own. Putting them together requires no additional rule: an array can have pointers as its elements, exactly as it can have ints. What the combination buys is a table of independent targets that a program can select among by index, which turns out to be one of the most useful structures in C and is the shape behind two things you will meet later — the list of arguments a program is started with, and a table of text. The one genuinely new skill is reading the declarations, because two of them differ by a single pair of parentheses and mean entirely different things.
Intermediate16 min readCPart IFundamentalsPointersArrays
Part I · Module 7 · Chapter 7.9 · Arrays of Pointers
1. An Array Whose Elements Are Pointers
int *counters[4];Read it with Chapter 6.1's definition of an array and Chapter 7.1's definition of a pointer, and it is unremarkable: four objects, all of type int *, stored end to end.
Everything Module 6 established applies without modification:
printf("sizeof counters %zu\n", sizeof counters);
printf("sizeof counters[0] %zu\n", sizeof counters[0]);
printf("how many %zu\n", sizeof counters / sizeof counters[0]);sizeof counters 32
sizeof counters[0] 8
how many 4Thirty-two bytes, which is four times eight — Chapter 6.1's contiguity result, with int * as the element type. The count idiom works for the same reason it worked there, and will stop working at a function boundary for the same reason Chapter 7.7 gave.
counters[2] is one element, of type int *. *counters[2] is the int it points at — two operations, in that order, because indexing binds tighter than *.
2. A Table of Targets
The reason to want this is that each element can point at a different object, so an index selects which object an operation applies to. Chapter 7.2 §8 did this with one pointer and two choices; an array scales it.
#include <stdio.h>
#include <stddef.h>
int main(void)
{
int reset_count = 0;
int error_count = 0;
int frame_count = 0;
int retry_count = 0;
int *counters[4] = { &reset_count, &error_count, &frame_count, &retry_count };
int events[7] = {2, 0, 2, 3, 1, 2, 0};
size_t event_count = sizeof events / sizeof events[0];
for (size_t i = 0; i < event_count; i++) {
int which = events[i];
(*counters[which])++;
}
printf("reset %d error %d frame %d retry %d\n",
reset_count, error_count, frame_count, retry_count);
return 0;
}Output:
reset 2 error 1 frame 3 retry 1Take (*counters[which])++ apart, since it is the only dense expression in the chapter:
counters[which] one element of the array — an int *
*counters[which] the int object it points at
(*counters[which])++ increment that objectThe parentheses are required for Chapter 7.2 §7's reason. Without them, *counters[which]++ would increment the pointer element and then dereference, which is a different and wrong operation.
Note what the alternative would have been: a four-branch if or switch with four nearly identical increments. The table replaces branching with indexing, the increment is written once, and adding a fifth counter means adding one initialiser and one object rather than a fifth branch.
3. Reading the Declaration
This is the part worth slowing down for, because one pair of parentheses changes everything.
int *array_of_pointers[4]; /* an array of 4 pointers to int */
int (*pointer_to_array)[4]; /* a pointer to an array of 4 int */ printf("sizeof array_of_pointers %zu\n", sizeof array_of_pointers);
printf("sizeof pointer_to_array %zu\n", sizeof pointer_to_array);
printf("sizeof *pointer_to_array %zu\n", sizeof *pointer_to_array);
printf("(*pointer_to_array)[2] %d\n", (*pointer_to_array)[2]);sizeof array_of_pointers 32
sizeof pointer_to_array 8
sizeof *pointer_to_array 16
(*pointer_to_array)[2] 3Thirty-two versus eight. The first is four pointers; the second is one.
The rule that produces the difference is a precedence rule, and it is Chapter 3.10's:
[]binds tighter than*. Soint *name[4]groups asint *(name[4])—nameis an array first, and its elements are pointers. Writingint (*name)[4]forces the*to apply first, makingnamea pointer whose target is an array.
The reading technique that works for any declaration: start at the name, read right while you can, then left.
int *array_of_pointers[4]
array_of_pointers start at the name
array_of_pointers[4] right: it is an array of 4
* array_of_pointers[4] left: of pointers
int * array_of_pointers[4] left: to int
-> an array of 4 pointers to int
int (*pointer_to_array)[4]
pointer_to_array start at the name
*pointer_to_array the parentheses force left first: a pointer
(*pointer_to_array)[4] right: to an array of 4
int (*pointer_to_array)[4] left: of int
-> a pointer to an array of 4 intThe second form is the type Chapter 7.6 §3 produced with &arr, which is the main place you will meet it. sizeof *pointer_to_array being 16 is the same 16 that chapter measured.
4. Traversing the Table
An array of pointers is traversed with Chapter 6.3's loop, unchanged:
for (size_t i = 0; i < how_many; i++) {
printf("%d ", *counters[i]);
}Each element is a pointer; each dereference reaches a different object. Chapter 7.4's rule applies here with more force than usual: an element of a pointer array can be NULL, and a table with holes in it is a perfectly reasonable design.
int *optional[4] = { &a, NULL, &c, NULL };
for (size_t i = 0; i < 4u; i++) {
if (optional[i] != NULL) {
printf("%d ", *optional[i]);
}
}Chapter 6.2's zero rule makes this convenient: int *table[8] = {0}; gives you eight null pointers, because a null pointer constant is what 0 means in a pointer context — Chapter 7.4 §2. A table initialised that way is immediately safe to traverse with a null check, before any entry has been filled in.
5. Not a Two-Dimensional Array
Readers who have just finished Chapter 6.5 tend to conflate these, and the difference is structural.
int grid[3][4] | int *rows[3] | |
|---|---|---|
| What it is | 3 arrays of 4 int | 3 pointers to int |
| Total size | 48 bytes | 24 bytes |
| Storage | one contiguous run | 3 pointer slots; targets anywhere |
| Rows the same length? | necessarily | not necessarily |
| Rows must exist? | yes | an element may be NULL |
grid[1][2] / rows[1][2] | both compile, and mean different things |
Both support the [i][j] notation, which is why they get confused — rows[1][2] works because rows[1] is a pointer and Chapter 7.8 established that pointers can be indexed. But Chapter 6.5's grid is one object with a known total size, and a table of pointers is a set of independent targets that happen to be listed together.
Neither is better. A grid is the right model for a table whose shape is fixed and whose storage should be one block; an array of pointers is the right model when the targets are separate objects, when they vary in size, or when some of them may be absent.
6. Where This Leads
Two structures you will meet are exactly this chapter's shape with a different element type.
A table of text. const char *messages[4] is an array of four pointers to characters — the same picture as Figure 1, with text at the other end of each arrow instead of an int. Strings are Chapter 8.1's subject, and Chapter 8.2 covers what the pointers point at.
A program's arguments. When a program is started with arguments, it receives them as an array of pointers, one per argument. That is Chapter 12.6's subject.
Both are worth knowing are coming, because when you meet char *argv[] it will be a combination of three things you already have — Chapter 7.7's array parameter, this chapter's array of pointers, and Module 8's strings — rather than a new piece of syntax.
7. What You Should Be Able to Say Now
int *counters[4]is four objects of typeint *, stored end to end — Module 6's array with a pointer element type.sizeofis 32 and the element is 8; the count idiom works exactly as before.counters[2]is a pointer;*counters[2]is theintit identifies.(*counters[i])++needs its parentheses — indexing binds tighter than*.- The elements point at independent objects, which is what makes it a table of targets.
[]binds tighter than*:int *a[4]is an array of pointers;int (*a)[4]is a pointer to an array.- 32 versus 8 is the measured difference between those two declarations.
- Read a declaration from the name outward — right while you can, then left.
- An element may be
NULL, and= {0}gives a table of null pointers. - This is not a two-dimensional array: Chapter 6.5's grid is one contiguous run with equal rows.
8. Exercises
Exercise 1 — read the declarations. Say in words what each declares, and give sizeof for each assuming 8-byte pointers and 4-byte int.
int *a[5];
int (*b)[5];
double *c[3];
char *d[10];Exercise 2 — predict the output. Do not compile yet.
int x = 1, y = 2, z = 3;
int *t[3] = { &x, &y, &z };
printf("%d %d %d\n", *t[0], *t[1], *t[2]);
*t[1] = 20;
printf("%d %d\n", y, *t[1]);
t[1] = &z;
printf("%d %d\n", y, *t[1]);The last line is the one to think about — relate it to Chapter 7.2 §4.
Exercise 3 — add the parentheses. For int *t[3] with i a valid index, say what each expression does.
*t[i] (*t[i])++ *t[i]++ t[i]++Exercise 4 — build a table. Write a dispatch table of three int counters and a loop that processes the event list {0, 2, 2, 1, 0, 2}, then give the three final counts without compiling.
Exercise 5 — handle the holes. Given int *entries[6] = {0}; with entries 1 and 4 later set to point at real objects, write the traversal that prints only the entries that exist, and say what Chapter 6.2's rule guarantees about the other four.
Exercise 6 — contrast the structures. For int grid[2][3] and int *rows[2], give the total size of each, say which stores its data contiguously, and say which one allows the two rows to have different lengths.
Exercise 7 — find the wrong declaration. A colleague wants a pointer to an array of ten int and writes int *p[10];. Say what they actually declared, what sizeof p would be, and give the correct declaration with the reading technique from §3 applied to it.
Exercise 8 — predict the future. Without looking ahead, say what char *messages[3] declares, how many pointer objects exist, and what you would expect each one to identify. Then say which module will tell you what is at the other end.
9. Summary
- An array of pointers is Module 6's array with a pointer element type — no new rule.
sizeofbehaves exactly as before: 32 bytes for four 8-byte elements.- The elements identify independent objects, which makes the array a table of targets.
(*table[i])++needs its parentheses, because[]binds tighter than*.int *a[4]is an array of pointers;int (*a)[4]is a pointer to an array — 32 bytes versus 8.- Read declarations from the name outward, right then left.
- Elements may be
NULL;= {0}produces a table of null pointers. - It is not Chapter 6.5's two-dimensional array, which is one contiguous run with equal rows.
- The same structure with text at the targets is Chapter 8.2's, and with program arguments Chapter 12.6's.
An array of pointers is a run of pointer objects, and Chapter 7.6 established that an array in an expression yields a pointer to its first element. Applying that rule here produces something this module has not yet had a name for: a pointer whose target is itself a pointer. That type is not exotic, it is what counters becomes the moment it is passed to a function, and it solves two problems that nothing so far can — the next chapter identifies both of them precisely, because double pointers are introduced far more often than they are justified.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
