Pointers · about 55 min

Addresses and pointers

Why this matters

Pointers are the idea that makes C C. Every array you passed to a function, every string, every scanf("%d", &x) already used them behind the scenes. They are how a function changes its caller's variables, how strings and arrays are handed around without copying, how memory from malloc is reached, and how linked lists, trees and every other dynamic data structure are built. They are also the source of C's most feared bugs. The good news is that the core idea is small: a pointer is a variable that holds an address. This module takes that idea slowly and draws a lot of pictures.

Every variable has an address

Recall from the first module that memory is a long row of bytes, each with a numeric address. When you declare int x = 42;, the compiler reserves 4 bytes somewhere (on the stack, for a local variable) and from then on "x" means "the int stored at that address".

The address-of operator & gives you that address. You have used it with scanf since the start: scanf("%d", &x) hands scanf the address of x so it can store the number there.

#include <stdio.h>

int main(void)
{
    int x = 42;
    int *p = &x;          /* p holds the address of x */

    printf("x  = %d\n", x);
    printf("&x = %p\n", (void *)&x);
    printf("p  = %p\n", (void *)p);
    printf("*p = %d\n", *p);

    *p = 100;             /* write through the pointer */
    printf("x is now %d\n", x);

    printf("sizeof x = %zu, sizeof p = %zu\n", sizeof x, sizeof p);
    return 0;
}

One run in the sandbox printed:

x  = 42
&x = 0x7ffe50d43544
p  = 0x7ffe50d43544
*p = 42
x is now 100
sizeof x = 4, sizeof p = 8

The addresses are different on every run (Linux deliberately randomizes where the stack goes, a security feature called ASLR), so never write code or tests that depend on their exact value. %p prints an address in hexadecimal; it expects a void *, hence the (void *) cast, which the last lesson of this module explains.

Declaring, assigning, dereferencing

int *p;

declares p as a pointer to int: a variable whose value is the address of an int. Read the declaration as "*p is an int". The * here is part of the declaration syntax.

p = &x;

stores the address of x in p. We say "p points to x".

*p

in an expression is the dereference (or indirection) operator: "the thing p points to". *p can be read (int y = *p;) or assigned (*p = 100;), exactly as if you had written x.

So the same symbol * has two jobs, which confuses everyone at first:

wheremeaning
in a declaration: int *p;p is a pointer to int
in an expression: *p = 5;the int that p points to
between two values: a * bmultiplication (unrelated)

& and * are opposites: *&x is x, and &*p is p.

The picture

Here is the program above as boxes in memory. A pointer is just another box; its contents happen to be an address.

          address          contents          name
        +------------+  +--------------+
        | 0x...3544  |  |      42      |     x       (int, 4 bytes)
        +------------+  +--------------+
                               ^
                               |  p points to x
        +------------+  +------+-------+
        | 0x...3548  |  |  0x...3544   |     p       (int *, 8 bytes)
        +------------+  +--------------+

Most of the time we leave the numbers out and draw an arrow:

   p              x
  +----+        +-----+
  |  *-+------->|  42 |
  +----+        +-----+

After *p = 100; the arrow is unchanged and the box it leads to holds 100. After p = &y; the arrow would move to y and x would not change at all. Assigning to p moves the arrow; assigning to *p changes what is at the end of it.

#include <stdio.h>

int main(void)
{
    int a = 1, b = 2;
    int *p = &a;

    *p += 10;      /* a is now 11 */
    p = &b;        /* p now points at b; a is untouched */
    *p *= 3;       /* b is now 6 */
    printf("a = %d, b = %d, *p = %d\n", a, b, *p);
    return 0;
}
a = 11, b = 6, *p = 6

Pointers have types

A pointer's type says what kind of object is at the address, which tells * how many bytes to read and how to interpret them. Dereferencing an int * reads 4 bytes as an integer; a double * reads 8 bytes as a floating-point number. Every pointer on the sandbox is 8 bytes (64-bit addresses), whatever it points to, but the types are not interchangeable:

double d = 1.5;
int *p = &d;
main.c:5:14: error: initialization of 'int *' from incompatible pointer type 'double *' [-Wincompatible-pointer-types]

Likewise, an address is not an integer:

int y = &x;
main.c:7:13: error: initialization of 'int' from 'int *' makes integer from pointer without a cast [-Wint-conversion]

Older compilers only warned about these; GCC 14 makes them errors, which is a good thing.

NULL: pointing at nothing

NULL (defined in <stdio.h>, <stdlib.h> and others) is a special pointer value meaning "no object". Use it to initialize pointers that do not point anywhere yet, and to signal "not found" or "failed": you have already seen fgets return NULL at end of input.

Dereferencing NULL is undefined behavior. On Linux the lowest pages of memory are never mapped, so the CPU faults and the kernel kills the process:

#include <stdio.h>

int main(void)
{
    int *p = NULL;
    printf("about to dereference NULL\n");
    *p = 5;
    printf("never printed\n");
    return 0;
}
$ ./prog
about to dereference NULL
Segmentation fault (core dumped)
$ echo $?
139

A segmentation fault means the program touched memory it is not allowed to touch. The exit status 139 is 128 + 11, where 11 is the number of the signal SIGSEGV. (If the output goes to a pipe or file instead of a terminal, even the first line can be lost, because it was still waiting in an output buffer when the process died. Output buffering is covered in the I/O module.)

Before dereferencing a pointer that might be NULL, check: if (p != NULL) ..., or simply if (p) ..., since a null pointer counts as false.

Uninitialized pointers

A local pointer that you never assign holds whatever bytes were there before, which is some random address:

int *p;
*p = 1;

gcc catches the simple case: warning: 'p' is used uninitialized [-Wuninitialized]. In general it cannot, and writing through a garbage pointer may crash, or may silently overwrite some other variable, which is far worse. Rule: every pointer is initialized when declared, to a real address or to NULL.

Common mistakes

Recap

Going further: in the sandbox terminal, compile the first example with -g and run it in gdb: break 9, run, then print x, print &x, print p, print *p. gdb understands pointers and shows the same relationships you drew on paper. (The debugging module covers gdb properly.)

Exercises

Exercise 1 · quiz

Predict the output

What does this print?

int x = 5;
int *p = &x;
*p = *p * 2;
x = x + 1;
printf("%d %d\n", x, *p);
Answer
  • 11 11

p holds the address of x, so *p and x are two names for the same int. *p = *p * 2 makes x 10, x = x + 1 makes it 11, and *p reads that same memory: 11 11. There is only ever one integer here; the pointer is a second way to reach it.

Exercise 2 · quiz

Pointing somewhere else

Draw the boxes and arrows, then say what this prints.

int a = 1, b = 2;
int *p = &a;
int *q = &b;
*p = *q;
p = q;
*p = 7;
printf("%d %d\n", a, b);
Answer
  • 2 7

*p = *q copies the int that q points to (b, 2) into the int that p points to (a): a becomes 2. p = q copies the pointer: now p also points at b; a is not affected. *p = 7 writes into b. So a is 2 and b is 7. The difference between *p = *q (copy the values) and p = q (copy the addresses) is the single most important distinction in this module.

Exercise 3 · quiz

Reading a declaration

After int *p, q;, what are the types of p and q?

Answer
  • p is int *, q is int

The * belongs to the name, not the type: the declaration says "p is an int, and q is an int". To declare two pointers write int *p, *q;. Many programmers avoid the trap by declaring one pointer per line, and by writing the star next to the name (int *p) rather than the type (`int p`), so the code looks like what it means.

Exercise 4 · write code

Point at the largest

Read three integers into the variables a, b and c. Then make the pointer biggest point at whichever variable holds the largest value (the first one if there is a tie), and use it to set that variable to 0 with *biggest = 0;. Finally print the three variables as:

a=<a> b=<b> c=<c>

For input 4 9 2 the output is a=4 b=0 c=2.

The point is to choose which variable to change by choosing an address; the starter already contains the *biggest = 0; line.

File: main.c

Starting point

#include <stdio.h>

int main(void)
{
    int a, b, c;
    if (scanf("%d %d %d", &a, &b, &c) != 3) {
        return 1;
    }

    int *biggest = &a;
    /* make biggest point at b or c instead, if one of them is larger */

    *biggest = 0;
    printf("a=%d b=%d c=%d\n", a, b, c);
    return 0;
}

Example

input

4 9 2

expected output

a=4 b=0 c=2

Example

input

1 2 3

expected output

a=1 b=2 c=0
Hint 1

Compare each variable with the value biggest currently points to: if (b > *biggest).

Hint 2

To make it point at b, assign an address: biggest = &b; (no star: you are changing the pointer, not the int).

Reference solution
#include <stdio.h>

int main(void)
{
    int a, b, c;
    if (scanf("%d %d %d", &a, &b, &c) != 3) {
        return 1;
    }

    int *biggest = &a;
    if (b > *biggest) {
        biggest = &b;
    }
    if (c > *biggest) {
        biggest = &c;
    }

    *biggest = 0;
    printf("a=%d b=%d c=%d\n", a, b, c);
    return 0;
}

Exercise 5 · write code

Fix the crash

The starter crashes with Segmentation fault because it writes through a pointer that points nowhere. Fix it so that counter is a valid pointer to a real int that starts at 0 (declare one), keeping the rest of the logic. The program reads integers until end of input and counts the positive ones through the pointer, then prints:

positive: <count>

For input 3 -1 4 0 5 the output is positive: 3.

File: main.c

Starting point

#include <stdio.h>

int main(void)
{
    int *counter = NULL;     /* points nowhere! */
    int x;

    *counter = 0;
    while (scanf("%d", &x) == 1) {
        if (x > 0) {
            *counter = *counter + 1;
        }
    }
    printf("positive: %d\n", *counter);
    return 0;
}

Example

input

3 -1 4 0 5

expected output

positive: 3

Example

input

-1 -2 0

expected output

positive: 0
Hint 1

A pointer must hold the address of something that exists before you use *.

Hint 2

Declare int count = 0; and initialize the pointer with int *counter = &count;.

Reference solution
#include <stdio.h>

int main(void)
{
    int count = 0;
    int *counter = &count;   /* now points at a real int */
    int x;

    *counter = 0;
    while (scanf("%d", &x) == 1) {
        if (x > 0) {
            *counter = *counter + 1;
        }
    }
    printf("positive: %d\n", *counter);
    return 0;
}