Pointers · about 55 min

Pointers to pointers and const

Why this matters

A pointer is a variable, so it has an address too, and that address can be stored in another pointer. This sounds like a puzzle for its own sake, but it answers a practical question: how does a function change the caller's pointer (rather than the thing it points to)? Library functions like strtol and getline need exactly that, and argv is a pointer to pointers. This lesson also sorts out const with pointers, where the position of one keyword changes the meaning completely, so you can read real-world declarations like const char *const *names.

A pointer to a pointer

#include <stdio.h>

int main(void)
{
    int x = 5;
    int *p = &x;
    int **pp = &p;

    printf("x = %d, *p = %d, **pp = %d\n", x, *p, **pp);
    **pp = 6;
    printf("after **pp = 6: x = %d\n", x);

    int y = 50;
    *pp = &y;                     /* changes p itself */
    printf("after *pp = &y: *p = %d, x = %d\n", *p, x);
    return 0;
}
x = 5, *p = 5, **pp = 5
after **pp = 6: x = 6
after *pp = &y: *p = 50, x = 6

int **pp declares "pointer to pointer to int": **pp is an int, *pp is an int *. Each * follows one arrow:

   pp               p                x
  +------+        +------+        +-----+
  |  *---+------->|  *---+------->|  5  |
  +------+        +------+        +-----+
  int **          int *           int

  pp      the address of p
  *pp     p itself (the address of x)
  **pp    x

After *pp = &y; the middle arrow moves:

   pp               p                x
  +------+        +------+        +-----+
  |  *---+------->|  *   |        |  6  |
  +------+        +--+---+        +-----+
                     |             y
                     |            +-----+
                     +----------->| 50  |
                                  +-----+

Why: letting a function move the caller's pointer

Suppose you want a function that skips leading spaces in a string by advancing the caller's pointer. A first attempt:

#include <stdio.h>

void skip_spaces_wrong(const char *s)
{
    while (*s == ' ') {
        s++;
    }
}

int main(void)
{
    const char *cur = "   x";
    skip_spaces_wrong(cur);
    printf("[%s]\n", cur);
    return 0;
}
[   x]

Nothing happened, for the same reason swap_broken failed two lessons ago: s is a copy of cur, and moving the copy does not move the original. The fix is the same too: to change a variable of type T in the caller, pass a T *. Here T is const char *, so the parameter is const char **:

#include <stdio.h>

/* Moves the caller's pointer past any leading spaces. */
void skip_spaces(const char **s)
{
    while (**s == ' ') {
        (*s)++;
    }
}

int main(void)
{
    const char *text = "    indented";
    const char *cur = text;

    skip_spaces(&cur);
    printf("[%s] -> [%s], skipped %td\n", text, cur, cur - text);
    return 0;
}
[    indented] -> [indented], skipped 4

Inside the function, *s is the caller's cur and **s is the character it points at. (*s)++ advances cur. The parentheses are essential: postfix ++ binds tighter than unary *, so *s++ would mean *(s++), moving the local s and reading through the old value.

you want the function to changethe caller passesthe parameter type
an int&nint *
an int * (where it points)&pint **
a const char *&curconst char **

The standard library does this

strtol (string to long), which the I/O module covers in detail, converts the start of a string to a number and reports where it stopped through a char ** output parameter:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    const char *input = "42 apples";
    char *end;
    long n = strtol(input, &end, 10);
    printf("number: %ld, rest: \"%s\"\n", n, end);
    return 0;
}
number: 42, rest: " apples"

You pass &end, the address of your pointer; strtol stores into it a pointer to the first character it did not use.

argv is a char **

The parameter char *argv[] is, like every array parameter, really a pointer: char **argv. Both spellings of main are equivalent. argv points at the first element of the array of string pointers; argv[1] is *(argv + 1), a char *; argv[1][0] is the first character of the first argument.

  argv (char **)
  +------+       +---------+
  |  *---+------>| argv[0] |----> "./prog\0"
  +------+       +---------+
                 | argv[1] |----> "pear\0"
                 +---------+
                 | argv[2] |----> "apple\0"
                 +---------+
                 |  NULL   |
                 +---------+

Sorting such an array means rearranging the pointers in the middle column; the strings never move. That is the last exercise of this lesson.

const and pointers

With a pointer there are two things that could be read-only: the object pointed to, and the pointer variable itself. Where you put const decides which. The reliable trick is to read the declaration from right to left, starting at the name:

declarationread as*p = ...p = ...
const int *pp is a pointer to an int that is constnoyes
int const *p(same as above; const before or after the type)noyes
int *const pp is a const pointer to an intyesno
const int *const pp is a const pointer to a const intnono
int x = 1, y = 2;
const int *a = &x;
int *const b = &x;
*a = 5;      /* error */
a = &y;      /* fine */
*b = 5;      /* fine */
b = &y;      /* error */
main.c:6:8: error: assignment of read-only location '*a'
main.c:9:7: error: assignment of read-only variable 'b'

In practice, const T * ("pointer to const") is by far the most common: it is what a function parameter uses to promise "I only read through this". T *const is rarer; it is a pointer you promise not to re-aim, like any other const variable.

const int *a = &x; does not make x itself constant. It only means you will not change x through a. x can still change by other means.

Conversions follow the safety rule: you may add const implicitly (int * to const int *), but not drop it. Passing a const char * to a parameter of type char * makes gcc report passing argument 1 of 'f' discards 'const' qualifier from pointer target type, because the function could then modify read-only data.

Common mistakes

Recap

Going further: man 3 getline describes a function that takes char **lineptr and may replace your buffer with a bigger one, which needs malloc (next module). Read its description and notice why it needs a pointer to your pointer.

Exercises

Exercise 1 · quiz

Predict the output

Draw the boxes and arrows. What does this print?

int x = 1, y = 2;
int *p = &x;
int **pp = &p;
**pp = 10;
*pp = &y;
**pp = 20;
printf("%d %d %d\n", x, y, *p);
Answer
  • 10 20 20

pp points at p, which points at x. pp = 10 follows both arrows and writes x = 10. *pp = &y follows one arrow and changes p itself: p now points at y. pp = 20 follows both arrows again, now ending at y. So x = 10, y = 20, and *p (which is y) is 20. Each * follows one arrow.

Exercise 2 · quiz

What does const protect?

Given

int x = 1, y = 2;
const int *a = &x;
int *const b = &x;

which of these statements compile? Select all that apply.

Answer
  • a = &y;

  • *b = 5;

Read declarations right to left: a is a "pointer to const int", so the int cannot be changed through a (*a = 5 fails with "assignment of read-only location") but a itself can point elsewhere. b is a "const pointer to int": the pointer is fixed (b = &y fails with "assignment of read-only variable 'b'"), but the int it points to can be changed. const int *const c would lock both.

Exercise 3 · write code

A tokenizer that moves the caller's pointer

Write

int read_number(const char **p)

*p points at a digit in a string. The function converts the run of decimal digits starting there into an int, moves the caller's pointer just past the last digit, and returns the value. For example, if cur points at "123abc", then after int n = read_number(&cur);, n is 123 and cur points at "abc".

The starter's main uses it to add up every number that appears in a line of text (it skips non-digits itself and calls read_number at each digit), and prints total = <sum>. For input a12b3 45 that prints total = 60. The check also calls read_number directly.

File: main.c

Starting point

#include <stdio.h>
#include <ctype.h>

int read_number(const char **p)
{
    /* your code here */
    return 0;
}

int main(void)
{
    char line[256];
    if (fgets(line, sizeof line, stdin) == NULL) {
        return 1;
    }

    int total = 0;
    const char *cur = line;
    while (*cur != '\0') {
        if (isdigit((unsigned char)*cur)) {
            total += read_number(&cur);
        } else {
            cur++;
        }
    }
    printf("total = %d\n", total);
    return 0;
}

Example

input

a12b3 45

expected output

total = 60

Example

input

nothing here

expected output

total = 0
Hint 1

*p is the caller's pointer; **p is the character it points at.

Hint 2

Digit value: **p - '0' (the digit characters have consecutive codes). Build the number with value = value * 10 + digit.

Hint 3

Advance the caller's pointer with (*p)++. The parentheses matter: *p++ would move the local p instead.

Reference solution
#include <stdio.h>
#include <ctype.h>

int read_number(const char **p)
{
    int value = 0;
    while (isdigit((unsigned char)**p)) {
        value = value * 10 + (**p - '0');
        (*p)++;
    }
    return value;
}

int main(void)
{
    char line[256];
    if (fgets(line, sizeof line, stdin) == NULL) {
        return 1;
    }

    int total = 0;
    const char *cur = line;
    while (*cur != '\0') {
        if (isdigit((unsigned char)*cur)) {
            total += read_number(&cur);
        } else {
            cur++;
        }
    }
    printf("total = %d\n", total);
    return 0;
}

Exercise 4 · write code

Sort strings by moving pointers

Write

void sort_strings(const char *a[], int n)

that sorts an array of n string pointers into ascending strcmp order. Do not copy or modify any characters: rearrange the pointers in the array. Any simple sort is fine (for example, insertion sort or bubble sort).

main should sort the command-line arguments (not including argv[0]) and print them one per line. Copy them into a local const char *words[100] array first (there are at most 100). With arguments pear apple fig it prints:

apple
fig
pear

With no arguments it prints nothing.

File: main.c

Starting point

#include <stdio.h>
#include <string.h>

void sort_strings(const char *a[], int n)
{
    /* your code here */
}

int main(int argc, char *argv[])
{
    const char *words[100];
    int n = 0;
    for (int i = 1; i < argc && n < 100; i++) {
        words[n++] = argv[i];
    }
    sort_strings(words, n);
    for (int i = 0; i < n; i++) {
        printf("%s\n", words[i]);
    }
    return 0;
}

Example with arguments pear apple fig

expected output

apple
fig
pear

Example with arguments beta Gamma alpha Beta

expected output

Beta
Gamma
alpha
beta
Hint 1

Each a[i] is a const char *. Swapping two elements means swapping two pointers: const char *tmp = a[i]; a[i] = a[j]; a[j] = tmp;.

Hint 2

Compare with strcmp(a[i], a[j]) > 0 ("a[i] sorts after a[j]"), never with > on the pointers.

Hint 3

Bubble sort: repeat n-1 passes; in each pass, swap neighbors a[j] and a[j+1] when they are out of order.

Reference solution
#include <stdio.h>
#include <string.h>

void sort_strings(const char *a[], int n)
{
    /* insertion sort: grow a sorted prefix one element at a time */
    for (int i = 1; i < n; i++) {
        const char *key = a[i];
        int j = i - 1;
        while (j >= 0 && strcmp(a[j], key) > 0) {
            a[j + 1] = a[j];
            j--;
        }
        a[j + 1] = key;
    }
}

int main(int argc, char *argv[])
{
    const char *words[100];
    int n = 0;
    for (int i = 1; i < argc && n < 100; i++) {
        words[n++] = argv[i];
    }
    sort_strings(words, n);
    for (int i = 0; i < n; i++) {
        printf("%s\n", words[i]);
    }
    return 0;
}