Skip to content

Passing pointers to functions

A function given an address can change the caller’s variables. It gets a copy of the address, and every write through it lands in the caller’s box.

Helps to know first
This topic

First, the picture

On the call stack page, swap changed only its own copies. This time it gets the addresses of x and y. Watch where the writes go.

Making swap actually swap

swap receives the addresses of x and y

#include <stdio.h> void swap(int *a, int *b){    int t = *a;    *a = *b;    *b = t;} int main(void){    int x = 3;    int y = 8;    swap(&x, &y);    printf("x = %d, y = %d\n", x, y);    return 0;}

Output

main has started. Its frame holds x and y, with no values yet.

0.00 / 14.00 s
Describe this picture

The 17 lines of swap-pointers.c with the line about to run marked, beside a picture of memory under the label “Stack”. At the top is the card for main’s frame, one row of 8 byte cells: y, an int at 0x7fe8, in the left half and x, an int at 0x7fec, in the right half. Below it, while swap runs, is the card for swap’s frame, three rows: a, an int * of 8 bytes at 0x7fd0, fills the top row; b, an int * at 0x7fc8, fills the middle row; t, an int at 0x7fc4, sits in the right half of the bottom row. Each box shows its type and name at the top left, its address at the top right, and its value in the middle, ”?” until a line writes one. Under the picture the program’s output appears once it prints, and under everything one sentence on the current step. The clip plays the program once, 10 steps in 14 seconds, 1.4 seconds a line, and rests on the end. x takes 3 and y takes 8. At the call on line 14, swap’s card fades in; the numbers 0x7fec and 0x7fe8 fly from the corners of x and y into a and b and grow into arrows from a up to x and from b up to y; t shows ”?”. Line 5 makes t 3. Line 6 sends a dot along a’s arrow and x becomes 8. Line 7 sends a dot along b’s arrow and y becomes 3. The arrows never move. With the mark on line 8 the clip rests for 2.8 seconds: x holds 8 and y holds 3 while a and b still hold 0x7fec and 0x7fe8. Then swap’s card fades out, and x = 8, y = 3 appears under the picture. The box the last line wrote is outlined. The transport plays and pauses, steps one line back or forward, and scrubs; a paused step is kept in the page address as mem.step.

What swap gets this time

The program prints x = 8, y = 3. The swap worked, and the picture shows why.

The call on line 14 is swap(&x, &y). From Pointers and addresses (2.1): &x is x’s address, a number, 0x7fec here. So swap’s frame still gets copies, exactly as on Functions and the call stack (1.3). But what is copied is now two addresses. a holds 0x7fec and b holds 0x7fe8, and each number is drawn as an arrow into main’s frame.

Line 5, int t = *a;, follows a and reads 3 out of x. Line 6, *a = *b;, reads 8 through b and writes it through a: the write leaves swap’s frame and lands in x. Line 7 writes 3 through b, into y. Watch a and b during all of this. They hold the same two numbers from the call to the return. Only the boxes at those addresses change.

When swap returns, its frame goes, a and b with it. Nothing needed handing back: the new values were written into x and y while swap ran.

Key idea

A function given an address still gets a copy, but a copy of an address points at the same box. Every write through it changes the caller’s variable.

The code and its cost

Pointer parameters

A parameter declared int *a holds the address of an int. The caller passes one with &:

void swap(int *a, int *b);

swap(&x, &y);

C still passes by value: the values are 0x7fec and 0x7fe8, and swap gets them in boxes of its own. That is why the call gives swap the power to change x and y: it knows where they live.

Here is a prediction to try. Suppose line 7 were b = &t; instead of *b = t;. What would the program print? That line changes b, swap’s own copy of an address, and writes nothing into y. Line 6 has already put 8 into x, and y keeps its 8, so the program prints x = 8, y = 8. Only the copy b moved.

Two results from one call

A function returns one value with return. To hand back more, the caller passes an address for each extra result, and the function writes there. A pointer parameter used this way is called an out-parameter.

void divide(int n, int d, int *q, int *r)
{
    *q = n / d;
    *r = n % d;
}

main calls it as divide(17, 5, &quot, &rem);. n and d get copies of 17 and 5; q and r get the addresses of quot and rem. n / d divides two ints and keeps the whole part, 3, and n % d is the remainder, 2. The replay shows each write reaching main.

Two results through two pointers

divide writes the quotient and the remainder into main’s boxes

#include <stdio.h> void divide(int n, int d, int *q, int *r){    *q = n / d;    *r = n % d;} int main(void){    int quot, rem;    divide(17, 5, &quot, &rem);    printf("17 = %d * 5 + %d\n", quot, rem);    return 0;}

Output

main has started. quot and rem have no values yet.

0.00 / 11.40 s
Describe this picture

The 15 lines of divide.c with the line about to run marked, beside the stack. main’s card has one row: rem, an int at 0x7fe8, in the left half and quot, an int at 0x7fec, in the right half, both ”?” at first. While divide runs, its card appears below with three rows: d at 0x7fd0 and n at 0x7fd4 in the top row, q, an int * at 0x7fc8, in the middle row, and r, an int * at 0x7fc0, in the bottom row. Each box shows its type and name, its address and its value. The replay runs one line every 1.9 seconds, 7 steps in about 11 seconds, and rests on the end. At the call, divide’s card fades in with n 17 and d 5, and arrows run from q to quot and from r to rem. Line 5 sends a dot along q’s arrow and quot becomes 3; line 6 sends a dot along r’s arrow and rem becomes 2. divide’s card fades out, and 17 = 3 * 5 + 2 appears under the picture. The transport plays and pauses, steps one line back or forward, and scrubs; a paused step is kept in the page address as two.step.

You have met an out-parameter before. In scanf("%d", &n), scanf reads a number typed by the user and must store it in n, so it needs n’s address. Written scanf("%d", n), it gets n’s value instead, which says nothing about where n is; gcc with -Wall warns that %d expects an int *.

Read-only access: const

Sometimes a function needs to read through a pointer and should never write. const says so:

int show(const int *p);

const int *p is a pointer to an int that may not be changed through p. Reading *p is allowed. Writing *p = 0; does not compile. p itself is an ordinary box, though, and p can be given another address.

The compiler checks it for you, and a reader of the function’s first line knows at once that their variable is safe.

Structs by pointer, and ->

From Structs (1.4): passing a struct copies every byte of it, 24 bytes for a reading. Passing its address copies 8 bytes, whatever the struct’s size. Inside the function, the fields are reached through the pointer:

double twice(const struct reading *r)
{
    return r->value * 2;
}

r->value means (*r).value: follow r to the struct, then take its field value. The arrow form is shorter and is what C programmers write. The parentheses in (*r).value are needed, because . binds more tightly than *. Called as twice(&r) on the reading of 1.4, it returns 7.5.

With const, the function gets the struct’s address without the right to change it: the speed of a pointer and the safety of a copy.

C++ has references, which do this job with a different syntax; this course uses C only, where an address is always written with & and *.

What it costs

A pointer parameter is 8 bytes to copy, the same for an int or a struct of thousands of bytes. Each use costs one extra step: the machine reads the address out of the parameter, then the box at that address, as on 2.1.

Worked example

Dividing by hand. For divide(17, 5, &quot, &rem): 17 / 5 keeps the whole part, 3, and 17 % 5 is what is left, 2. Check: 3 × 5 + 2 = 17.

A negative dividend. Now divide(-17, 5, &quot, &rem). C rounds the quotient toward zero, so -17 / 5 is -3, not -4. The remainder makes the check work: -3 × 5 = -15, and -17 - (-15) = -2. So q is -3 and r is -2, and -3 × 5 + (-2) = -17. The remainder takes the sign of the number divided.

What a call copies. A function double twice(const struct reading *r) receives 8 bytes. The same function taking struct reading r would receive 24. For a struct of 4,000 bytes, it is 8 against 4,000 on every call.

A write through const. This function does not compile:

void clear(const int *p)
{
    *p = 0;
}

gcc stops with error: assignment of read-only location '*p', pointing at line 3. Remove the const and it compiles; then clear(&n) sets n to 0.

Try it yourself. In swap(&x, &y), what exactly is copied into swap’s frame? Two addresses, 0x7fec and 0x7fe8, 8 bytes each. Nothing of x or y is copied at all.

Where it’s used

Library functions that fill in results. scanf stores what it reads at the addresses it is given. Functions that read a file take the address of a buffer and fill it.

Handing over large records. A program passes a large struct by its address, often with const, so that no call copies it.

Linked structures. From Singly linked lists (7.1) on, the functions that insert and remove nodes take pointers and change the structure through them, and a stack (8.1) is handed to its push and pop functions by its address.

Reference card

Code or ideaMeaning
void swap(int *a, int *b)parameters that hold addresses of ints
swap(&x, &y);copies the addresses of x and y into a and b
*a = *b;reads through b, writes through a: the caller’s boxes change
out-parametera pointer parameter the function writes a result through
scanf("%d", &n)scanf needs n’s address to store into n
const int *preading *p is allowed, writing it does not compile; p itself may change
r->value(*r).value: the field of the struct r points to
costa pointer parameter is 8 bytes, whatever it points to
-17 / 5, -17 % 5-3 and -2: the quotient rounds toward zero

Platform: C11, gcc 13 on x86-64 Linux: int 4 bytes, pointers 8. The addresses in the pictures are short stand-ins, by one rule for the whole course.

End of the lesson

Where to go next.

New lessons by email

Are you 13 or over?

Accounts and email updates are for people aged 13 or over. We keep only your answer, never your date of birth.

Phasorium

Account

Sign inWith Google, GitHub or your email

Subjects

Site

Look