Pointers · about 50 min

Pointers as function parameters

Why this matters

C passes every argument by value: a function gets copies. That is safe and simple, but it means a function cannot change its caller's variables, and it can only return one value. Pointers solve both problems. Passing an address lets the function reach back into the caller's memory. This is how scanf stores what it reads, how library functions report several results at once, and how large data is shared without copying. It is the most common everyday use of pointers.

The classic: swap

#include <stdio.h>

void swap_broken(int a, int b)
{
    int tmp = a;
    a = b;
    b = tmp;
}

void swap(int *a, int *b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main(void)
{
    int x = 1, y = 2;

    swap_broken(x, y);
    printf("after swap_broken: x = %d, y = %d\n", x, y);
    swap(&x, &y);
    printf("after swap:        x = %d, y = %d\n", x, y);
    return 0;
}
after swap_broken: x = 1, y = 2
after swap:        x = 2, y = 1

Look at the call stack during each call (the stack was introduced in the functions module):

  swap_broken(x, y):                         swap(&x, &y):

  main's frame       swap_broken's frame     main's frame       swap's frame
 +-----------+      +-----------+           +-----------+      +-----------+
 | x |  1    |      | a |  1->2 |           | x |  1->2 |<-----+-* a       |
 | y |  2    |      | b |  2->1 |           | y |  2->1 |<-----+-* b       |
 +-----------+      | tmp | 1   |           +-----------+      | tmp | 1   |
                    +-----------+                              +-----------+
  copies swapped, discarded on return        writes land in main's variables

In swap, the parameters a and b are still copies, but copies of addresses. Both copies point at main's x and y, so *a = *b writes into main's frame. The function did not need to return anything.

The caller's side matters as much: swap(&x, &y) passes addresses. Writing swap(x, y) is an error in GCC 14 (passing argument 1 of 'swap' makes pointer from integer without a cast).

You have been doing this all along

scanf("%d", &n) is exactly this pattern. scanf returns how many items it converted (its status), and stores the values themselves through the addresses you give it. Now you know why forgetting the & is so bad: scanf would treat the value of n as an address and write the number to some random place.

Arrays, too: void fill(int a[], int n) is really void fill(int *a, int n), which is why such functions modify the caller's array. The next lesson connects arrays and pointers properly.

Output parameters: returning more than one value

A common C style: the return value reports success or failure, and the actual results come back through pointer parameters.

#include <stdio.h>

/* Returns 1 on success, 0 if b is zero (and then leaves *q and *r alone). */
int divide(int a, int b, int *q, int *r)
{
    if (b == 0) {
        return 0;
    }
    *q = a / b;
    *r = a % b;
    return 1;
}

int main(void)
{
    int quot, rem;

    if (divide(17, 5, &quot, &rem)) {
        printf("17 / 5 = %d remainder %d\n", quot, rem);
    }
    if (!divide(1, 0, &quot, &rem)) {
        printf("cannot divide by zero\n");
    }
    return 0;
}
17 / 5 = 3 remainder 2
cannot divide by zero

Parameters used this way are called output parameters (or "out parameters"). Good habits:

Pointers to pointers to... just pass them on

Inside a function whose parameter is int *a, a is already an address. To hand the same int to another function that wants an int *, pass a itself, not &a:

void sort2(int *a, int *b)
{
    if (*a > *b) {
        swap(a, b);    /* correct: a and b are already pointers */
    }
}

swap(&a, &b) would pass the addresses of the pointer variables (type int **), which gcc rejects as an incompatible pointer type. Pointers to pointers are a later lesson.

const: promising not to write

A pointer parameter can also be used just to avoid copying, with no intention of modifying. Mark it const:

double average(const int *values, int n);

const int *values means "a pointer to ints that I will not modify through this pointer". The compiler rejects any *values = ... or values[i] = ... in the function, and readers know the function is read-only. This is the same const you used on array parameters, now written in pointer form. The pointers-to-pointers lesson covers the other places const can go.

Never return the address of a local variable

A function's local variables live in its stack frame, which is released when the function returns. A pointer to one of them outlives the thing it points to; it is called a dangling pointer.

#include <stdio.h>

int *make_number(void)
{
    int n = 42;
    return &n;
}

int main(void)
{
    int *p = make_number();
    printf("%d\n", *p);
    return 0;
}
main.c: In function 'make_number':
main.c:6:12: warning: function returns address of local variable [-Wreturn-local-addr]
    6 |     return &n;
      |            ^~
$ ./prog
Segmentation fault (core dumped)

gcc knows this is always wrong, so it actually compiles return &n; as return NULL; to make the bug crash loudly rather than "work" by accident. In less obvious cases (storing &n in a global, for example) there is no warning, and the stale memory gets reused by the next function call.

  while make_number runs:            after it returns:
  +-----------------+               +-----------------+
  | make_number     |               |  (free stack,   |  <- p still points here
  |   n = 42        |               |   reused by the |
  +-----------------+               |   next call)    |
  | main  p = ?     |               +-----------------+
  +-----------------+               | main  p = *-----+--> ???
                                    +-----------------+

The fixes: return the value itself (int make_number(void)), let the caller supply the storage (an output parameter), or allocate memory that outlives the function with malloc, which is the next module.

Common mistakes

Recap

Going further: look at man 3 strtol. Its second parameter, char **endptr, is an output parameter through which strtol tells you where it stopped reading. You will understand the double star after the pointers-to-pointers lesson.

Exercises

Exercise 1 · quiz

Why the first swap fails

void swap_broken(int a, int b) { int t = a; a = b; b = t; } is called as swap_broken(x, y); with x = 1, y = 2. Afterwards x and y are still 1 and 2. Why?

Answer
  • a and b are copies of x and y living in swap_broken's own stack frame; swapping the copies leaves x and y alone

C always passes arguments by value: the parameters a and b are new variables initialized with copies of x and y. Swapping them swaps the copies, which vanish when the function returns. To let a function change the caller's variables, pass their addresses (&x, &y) and have the function write through them (*a, *b).

Exercise 2 · write code

A working swap

Write

void swap(int *a, int *b)

that exchanges the two integers a and b point to. After swap(&x, &y) with x = 1 and y = 2, x is 2 and y is 1. The check calls your function directly; the starter's main is for your own experiments.

File: main.c

Starting point

#include <stdio.h>

void swap(int *a, int *b)
{
    /* your code here */
}

int main(void)
{
    int x = 1, y = 2;
    swap(&x, &y);
    printf("x = %d, y = %d\n", x, y);
    return 0;
}

Example

expected output

x = 2, y = 1
Hint 1

You need a temporary int to hold one value while you overwrite it.

Hint 2

int tmp = *a; *a = *b; *b = tmp; (stars on every use: you are moving the ints, not the pointers).

Reference solution
#include <stdio.h>

void swap(int *a, int *b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main(void)
{
    int x = 1, y = 2;
    swap(&x, &y);
    printf("x = %d, y = %d\n", x, y);
    return 0;
}

Exercise 3 · write code

Two results from one function

Write

void min_max(const int a[], int n, int *min, int *max)

that finds the smallest and largest of a[0] to a[n-1] (with n >= 1) and stores them in *min and *max. A function can only return one value; output parameters are how C returns more.

Then make main read up to 100 integers until end of input (at least one is given), call min_max, and print:

min=<min> max=<max>

The check runs your program and also calls min_max directly.

File: main.c

Starting point

#include <stdio.h>

void min_max(const int a[], int n, int *min, int *max)
{
    /* your code here */
}

int main(void)
{
    int values[100];
    int n = 0;
    while (n < 100 && scanf("%d", &values[n]) == 1) {
        n++;
    }

    int lo = 0, hi = 0;
    /* call min_max here */
    printf("min=%d max=%d\n", lo, hi);
    return 0;
}

Example

input

4 8 -2 15 16

expected output

min=-2 max=16

Example

input

7

expected output

min=7 max=7
Hint 1

Inside min_max, track the smallest and largest in local variables starting from a[0], then store them with *min = ...; *max = ...; at the end.

Hint 2

Call it with the addresses of main's variables: min_max(values, n, &lo, &hi);

Reference solution
#include <stdio.h>

void min_max(const int a[], int n, int *min, int *max)
{
    int lo = a[0], hi = a[0];
    for (int i = 1; i < n; i++) {
        if (a[i] < lo) lo = a[i];
        if (a[i] > hi) hi = a[i];
    }
    *min = lo;
    *max = hi;
}

int main(void)
{
    int values[100];
    int n = 0;
    while (n < 100 && scanf("%d", &values[n]) == 1) {
        n++;
    }

    int lo = 0, hi = 0;
    min_max(values, n, &lo, &hi);
    printf("min=%d max=%d\n", lo, hi);
    return 0;
}

Exercise 4 · write code

Sort three variables

Write

void sort3(int *a, int *b, int *c)

that rearranges the three integers so that afterwards *a <= *b <= *c. Write your own swap (as in the earlier exercise) and use it; three compare-and-swap steps are enough.

main should read three integers, call sort3 on them, and print them separated by spaces: input 3 1 2 prints 1 2 3.

File: main.c

Starting point

#include <stdio.h>

void sort3(int *a, int *b, int *c)
{
    /* your code here */
}

int main(void)
{
    int x, y, z;
    if (scanf("%d %d %d", &x, &y, &z) != 3) {
        return 1;
    }
    sort3(&x, &y, &z);
    printf("%d %d %d\n", x, y, z);
    return 0;
}

Example

input

3 1 2

expected output

1 2 3
Hint 1

Compare the values (*a > *b), not the pointers (a > b compares addresses!).

Hint 2

a, b and c are already pointers, so pass them on to swap as they are: swap(a, b), not swap(&a, &b).

Hint 3

Order: fix a/b, then b/c (this moves the largest to c), then a/b once more.

Reference solution
#include <stdio.h>

void swap(int *a, int *b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

void sort3(int *a, int *b, int *c)
{
    if (*a > *b) swap(a, b);   /* now *a <= *b */
    if (*b > *c) swap(b, c);   /* now *c is the largest */
    if (*a > *b) swap(a, b);   /* fix the first two again */
}

int main(void)
{
    int x, y, z;
    if (scanf("%d %d %d", &x, &y, &z) != 3) {
        return 1;
    }
    sort3(&x, &y, &z);
    printf("%d %d %d\n", x, y, z);
    return 0;
}