Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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]);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sizeof counters      32
sizeof counters[0]   8
how many             4

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

Azvya Education Pvt. Ltd.VLSI Mentor
a dispatch table of counters
#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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reset 2  error 1  frame 3  retry 1

Take (*counters[which])++ apart, since it is the only dense expression in the chapter:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
counters[which]      one element of the array — an int *
*counters[which]     the int object it points at
(*counters[which])++ increment that object

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

Four independent int objects are shown in a row: reset_count, error_count, frame_count and retry_count. Beneath them, the four elements of the array counters, each eight bytes. An arrow runs from each array element up to its own target object, so element zero points at reset_count, element one at error_count, element two at frame_count and element three at retry_count. The four targets are separate objects.reset_countan int objecterror_countan int objectframe_countan int objectretry_countan int objectcounters[0]8 bytescounters[1]8 bytescounters[2]8 bytescounters[3]8 bytes12
Figure 1 — the structure the table creates. The array counters is four contiguous pointer objects, each eight bytes, exactly as Chapter 6.1 would predict for any four-element array. What makes it useful is that each element identifies a different object, and those four objects are independent and not required to be adjacent to one another. Indexing the array selects which target an operation reaches.

3. Reading the Declaration

This is the part worth slowing down for, because one pair of parentheses changes everything.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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 */
Azvya Education Pvt. Ltd.VLSI Mentor
the difference, measured
    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]);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sizeof array_of_pointers   32
sizeof pointer_to_array    8
sizeof *pointer_to_array   16
(*pointer_to_array)[2]     3

Thirty-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 *. So int *name[4] groups as int *(name[4]) — name is an array first, and its elements are pointers. Writing int (*name)[4] forces the * to apply first, making name a 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 int

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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 is3 arrays of 4 int3 pointers to int
Total size48 bytes24 bytes
Storageone contiguous run3 pointer slots; targets anywhere
Rows the same length?necessarilynot necessarily
Rows must exist?yesan 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

  1. int *counters[4] is four objects of type int *, stored end to end — Module 6's array with a pointer element type.
  2. sizeof is 32 and the element is 8; the count idiom works exactly as before.
  3. counters[2] is a pointer; *counters[2] is the int it identifies.
  4. (*counters[i])++ needs its parentheses — indexing binds tighter than *.
  5. The elements point at independent objects, which is what makes it a table of targets.
  6. [] binds tighter than *: int *a[4] is an array of pointers; int (*a)[4] is a pointer to an array.
  7. 32 versus 8 is the measured difference between those two declarations.
  8. Read a declaration from the name outward — right while you can, then left.
  9. An element may be NULL, and = {0} gives a table of null pointers.
  10. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int *a[5];
int (*b)[5];
double *c[3];
char *d[10];

Exercise 2 — predict the output. Do not compile yet.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
*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.
  • sizeof behaves 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.