Skip to content

Functions and the call stack

Each call gets its own frame on the stack, holding copies of its arguments. The frame disappears when the function returns.

Helps to know firstVariables live in memory

Helps to know first
This topic

First, the picture

Does a function get your variables, or copies of them? Read the program, guess what line 15 prints, then watch.

Why this swap changes nothing

main’s x and y, and swap’s copies of them

#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 / 13.00 s
Describe this picture

The 17 lines of swap.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, two rows: b at 0x7fd0 and a at 0x7fd4 in the upper row, and t at 0x7fcc in the right half of the lower 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 13 seconds, 1.3 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 with a holding 3 and b holding 8, and t ”?”. Then t becomes 3, a becomes 8 and b becomes 3. With the mark on line 8, swap’s frame holds a 8, b 3 and t 3 while main’s x and y still hold 3 and 8; the clip rests there for 2.6 seconds. Then swap’s card fades out, and x = 3, y = 8 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

The program prints x = 3, y = 8. If you guessed x = 8, y = 3, so do many people. The picture shows where that guess goes wrong.

Before the call, main’s frame holds x = 3 and y = 8. Line 14, swap(x, y);, makes a new box of memory for swap, below main’s. In it are swap’s own boxes: a, b and t. a gets a copy of the value in x, 3, and b a copy of the value in y, 8.

Lines 5 to 7 then do a correct swap, with t as the spare box: t takes 3, a takes 8, b takes 3. Step to the moment the mark is on line 8. swap’s a and b hold 8 and 3: the swap worked. But it worked on swap’s own boxes. main’s x and y still hold 3 and 8.

Then swap returns, and its boxes disappear, copies and all. Nothing is handed back to x and y, and line 15 prints the values they always had.

Key idea

A function gets copies of its arguments, in boxes of its own. Whatever it does to them, the caller’s variables do not change.

The code and its cost

Declaring and calling a function

A function definition starts with a line such as void swap(int a, int b). It says three things: what the function hands back, its name and its parameters. void means it hands back nothing. int a, int b means it takes two ints, which it calls a and b.

A call such as swap(x, y); gives the function its arguments, x and y. Each argument’s value is copied into the matching parameter, in order: x’s into a, y’s into b.

A function that hands back a value has that value’s type in front of its name, and ends with return:

int twice(int n)
{
    n = n * 2;
    return n;
}

return n; ends the function and hands back the value in n. The call is then an expression with that value, so int m = twice(5); stores 10 in m.

Prototypes

C reads a file from the top, and a function must be declared before a call to it. In swap.c, swap is written above main, so that is already the case. To write main first, put a prototype above it: the function’s first line, ending in a semicolon.

void swap(int a, int b);

int main(void)
{
    ...
}

The prototype gives the return type, the name and the parameters’ types; the full function can then come anywhere in the file.

Frames and the stack

Each call gets a frame: a block of memory for the function’s parameters and its own variables. main has one too, since main is a function like any other. The frames live in an area of memory called the stack.

main’s frame starts at the top, just below 0x7ff0: x at 0x7fec and y at 0x7fe8. swap’s frame starts 16 bytes lower, at 0x7fd8, and its boxes go down from there: a at 0x7fd4, b at 0x7fd0, t at 0x7fcc.

The 16 bytes between, 0x7fd8 to 0x7fe7, are not drawn in the picture. On this machine, built without optimisation, they hold where to return: what the machine needs to come back into main, at the right place, when swap ends. The call writes them; the return reads them and goes back.

Each new call takes the next space below the last frame, so the stack grows downward, towards lower addresses. A return removes the frame on the bottom, the newest one. The addresses in the picture are stand-ins, by the rule this course uses on every page; a real compiler lays a frame out in its own way.

Pass by value

Copying each argument into a fresh parameter is called pass by value, and it is the only way C passes arguments. A function never receives the caller’s variable itself, only the value that was in it.

So how could a function ever change the caller’s x? It would need to know where x lives: its address. Passing pointers to functions (2.2) gives swap the addresses of x and y, and then the swap works.

Scope and lifetime

A function’s parameters and variables exist only while its frame does. t comes into being when swap is called and is gone when swap returns; the next call to swap gets a fresh t, with no memory of the last one.

A name means a box only inside its own function. swap’s a and main’s x are different boxes, and they would still be different boxes if swap had called its parameter x. The worked example shows exactly that.

What a call costs

A call does a fixed amount of work: it writes where to return, 16 bytes on this machine without optimisation, copies each argument into its parameter, and runs the function; the return removes the frame. For swap(x, y) the copying is two ints, 8 bytes. The cost of copying grows with the arguments: Structs (1.4) shows a struct copied whole into a call.

Here is one more prediction. Suppose swap ended with printf("%d %d\n", a, b); after line 7. The program would print 8 3 from inside swap, then x = 3, y = 8 from main.

Worked example

This program uses the name n twice: once in main, once in twice. Before you step the replay, write down what it prints.

int twice(int n)
{
    n = n * 2;
    return n;
}

int main(void)
{
    int n = 5;
    int m = twice(n);
    printf("n = %d, m = %d\n", n, m);
    return 0;
}

The call twice(n) copies the value of main’s n, 5, into twice’s n, a different box in twice’s frame. twice doubles its own n to 10 and hands back 10, which goes into m. main’s n was never touched. The program prints n = 5, m = 10.

The replay runs twice.c, which is these lines with #include <stdio.h> on top. Watch the two boxes named n.

Two boxes named n

main’s n, and the copy twice gets

#include <stdio.h> int twice(int n){    n = n * 2;    return n;} int main(void){    int n = 5;    int m = twice(n);    printf("n = %d, m = %d\n", n, m);    return 0;}

Output

main has started. Its frame holds n and m, with no values yet.

0.00 / 13.30 s
Describe this picture

The 15 lines of twice.c with the line about to run marked, beside the stack: main’s card, one row with m, an int at 0x7fe8, in the left half and n, an int at 0x7fec, in the right half; below it, while twice runs, twice’s card with its own n, an int at 0x7fd4. Each box shows its type and name, its address and its value, ”?” until written. The replay runs one line every 1.9 seconds, 8 steps in about 13 seconds, and rests on the end. main’s n takes 5. At the call, twice’s card fades in with its n holding 5; then that n becomes 10 while main’s n stays 5. After return n; twice’s card fades out and m takes 10. The output, n = 5, m = 10, appears under the picture when line 13 has run. The transport plays and pauses, steps one line back or forward, and scrubs; a paused step is kept in the page address as twice.step.

Three frames by hand

Now a call inside a call. main calls area, and area calls square:

int square(int s)
{
    return s * s;
}

int area(int w)
{
    int a = square(w);
    return a;
}

and in main, int r = area(4);. Draw the frames on paper at the deepest point, while square runs:

main     r = ?
area     w = 4, a = ?
square   s = 4

Three frames are on the stack, each below the one that called it. square returns 4 × 4 = 16, and its frame goes. a takes 16, area returns 16, and its frame goes. r takes 16, and main is alone again. Each return removes exactly one frame, the newest.

Where it’s used

Every program. Every C program you will meet runs this way: main calls functions, which call others, and the stack grows and shrinks with every call and return.

Stack traces. When a program crashes, a debugger can print the frames that were on the stack at that moment, newest first. That list, called a stack trace or backtrace, is this picture written as text, and it says which calls led to the crash.

A limit on depth. The stack has a fixed size, 8 MB by default on the Linux systems this course uses. A chain of calls that goes too deep runs out of it, which is called a stack overflow. Chapter 4, on recursion, meets it.

Reference card

Code or ideaMeaning
int twice(int n) { … return n; }a function: return type, name, parameters; return hands back one value
void swap(int a, int b)void: the function hands back nothing
void swap(int a, int b);a prototype: declares the function before its first call
swap(x, y);a call: x’s value is copied into a, y’s into b
framea call’s own memory on the stack: its parameters and variables
where to returnbetween a caller’s frame and the called one’s; 16 bytes here (x86-64, unoptimised)
the stackgrows downward with each call; a return removes the newest frame
pass by valuethe only way C passes arguments; the caller’s variables never change
lifetimea function’s variables exist only while its frame does

Platform: C11, gcc 13 on x86-64 Linux. 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