← Fundamentals
Guided lesson 6 questions · 6 languages

Function calls

Arguments in, a result out, and back to the code that made the call.

A call pauses the code that made it. The function runs with its own new variables, which receive the call’s values, and then the program continues at the place of the call with the function’s result.

Before you begin

Why this matters

Almost every line of a C program calls a function. If you can follow a call step by step, you can predict what a program prints and say why a function did, or did not, change your variable.

What this helps you reason about

  • Predict what a call computes first, and know where C leaves the order open.
  • Tell whether a change made inside a function reaches the code that called it, for an int, a struct, and an array.
  • Pass a function to another function through a function pointer.

In C, every argument is copied into the function, and arrays are passed as pointers. This lesson follows one call from start to finish.

Already know this? The short version
  • A call computes the function and all its arguments, in an order the standard leaves unspecified, then runs the body in a new frame; the caller waits until the call returns and continues at the call with the result.
  • Every argument is copied: an int and a struct give independent copies. An array argument becomes a pointer to its first element, so the callee writes the caller’s elements and does not know the length.
  • To let a function change a caller’s variable, pass its address and write through the pointer; assigning to the pointer parameter changes only the parameter.
  • return ends the call; never return the address of a local. A function pointer such as int (*step)(int) passes a function to another function.

Go to the final check· Go to the practice tickets

Words in this lesson (6)
argument
A value written in a call, such as 3 in area(3, 4). Each argument is computed before the function’s body starts.
parameter
A name in a function’s definition, such as width in int area(int width, int height). Each call creates its own parameters and gives them copies of the argument values, matched by position.
frame
The record of one function call that has not finished: its parameters and local variables. It is created when the call starts and removed when the call returns. The C standard does not mention frames; it calls these variables automatic.
pointer
A value that holds the address of a variable or of a block of memory, declared as in int *alias. *alias means what is stored at that address. Copying a pointer copies the address, not the memory it points to.
array parameter
A parameter written with brackets, such as int scores[]. C treats it as a pointer to the first element, so the function reaches the caller’s array and does not know its length.
unspecified order
A choice the C standard leaves to the compiler, such as which argument of a call is computed first. A program must not depend on it.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / The call

What happens when a function is called?

A function is a named piece of code. Calling it, as in area(3, 4), runs that code. The values in the parentheses, 3 and 4, are the arguments. The names in the function’s definition, width and height, are its parameters: variables that get the argument values when the call starts.

While area runs, the code that called it waits at the call. When area finishes, the program continues at the place of the call, and the call’s result, 12, is stored in size.

The stepper shows this with a frame list. A frame is the record of one call that has not finished: its parameters and local variables. Calling area adds a frame on top of main’s frame, and returning removes it.

main.c
int area(int width, int height) {
    int result = width * height;
    return result;
}

int main(void) {
    int size = area(3, 4);
    printf("after %d\n", size); /* after 12 */
    return 0;
}

A picture of one call: which code is running and which names exist. It is a model, not a memory layout.

A conceptual model, not a physical memory layout or live code execution.

1 / 3

Before the call

caller
running
area
not called yet

The caller is running. size does not have a value yet.

A call pauses the caller, runs in its own frame, and comes back to the place of the call.

Precisely: C does not mention a stack

The standard says each call’s parameters and local variables have automatic storage duration: they exist until that call returns. Implementations almost always use a call stack for this. At higher optimization levels the compiler may copy a small function’s body into its caller; the stepper’s -O0 build keeps every call.

Experiment

Follow a call there and back

Calling area pauses the code that called it. area gets a new frame with its own width and height, set from the arguments 3 and 4. When area returns, its frame is removed, and the caller continues at the place of the call with the result, 12.

Why it matters A call always comes back to the place it was made. Two calls in one line run one after the other, each in its own frame.

Predict which frames the frame list shows while area runs, and whether the caller already has size. Then change the call line to int size = area(3, 4) + area(5, 2); and predict: are both area frames ever in the list at the same time?

#include <stdio.h>

int area(int width, int height) {
    int result = width * height;
    return result;
}

int main(void) {
    setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
    printf("before\n");
    int size = area(3, 4);
    printf("after %d\n", size);
    return 0;
}
What to look for

Step until the stop inside area. The frame list shows area on top of main, the code that called it. area’s frame has width 3 and height 4. Select main: it does not list size yet, because size gets its value only when area returns. After the return, area’s frame is gone and size shows 12. After the edit, area’s frame appears twice, one call after the other: first with width 3, then with width 5. The two frames are never in the list together, and the run prints after 22. The stepper lists a local only once it has a value; gdb on its own would show size before the call too, holding whatever bytes were already there.

Expected output

before
after 12

02 / Arguments

When are the arguments computed?

Before a function’s body starts, every argument is computed. In add(left(), right()), both left() and right() run before add starts with a = 1 and b = 2. An argument is computed even if the body never uses it.

The C standard does not say which argument is computed first: the order is unspecified, a choice left to the compiler. The stepper’s build happens to compute left() first; another compiler may compute right() first. When the order matters, compute the values in separate statements.

main.c
int add(int a, int b) {
    return a + b;
}

/* Order of left() and right() is not promised. */
int total = add(left(), right());

/* Here it is: one statement after another. */
int a = left();
int b = right();
total = add(a, b);

All arguments are computed before the body; C does not promise their order.

Coming from Java: Java promises left-to-right evaluation of arguments; C does not, so f(next(), next()) can pass the two values in either order.

Precisely: sequence points

C11 6.5.2.2 places a sequence point after the function and its arguments are evaluated and before the call. Between the arguments there is none, so add(i++, i++), which changes i twice without one, is undefined behavior, not only an unknown order.

Experiment

Watch the arguments run first

Before add can start, the call needs a value for each parameter. So both arguments are computed first, and add starts with a = 1 and b = 2. The order between left() and right() is chosen by the compiler.

Why it matters An argument is computed even when the function never uses it. When the order of two arguments’ effects matters, compute them in separate statements first: int a = left(); int b = right();

Predict the order of the three lines printed before 3. Then change return a + b to return a, so add no longer uses b, and predict whether evaluate right is still printed.

#include <stdio.h>

int left(void) {
    printf("evaluate left\n");
    return 1;
}

int right(void) {
    printf("evaluate right\n");
    return 2;
}

int add(int a, int b) {
    printf("add runs\n");
    return a + b;
}

int main(void) {
    setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
    printf("%d\n", add(left(), right()));
    return 0;
}
What to look for

Watch the output and the frame list together. left gets a frame, prints, and returns; then right does the same. Only then does add’s frame appear, already holding a 1 and b 2. After the edit, evaluate right is still printed and the run ends with 1: every argument is computed before the body starts, whether the body uses it or not. This build computes left before right. The C standard does not promise that order, and another compiler may compute right first; it promises only that both are computed before add starts.

Expected output

evaluate left
evaluate right
add runs
3

03 / Parameters

What does the function receive?

Arguments are matched to parameters by position: the first argument goes to the first parameter, whatever the variables are called.

Each parameter is a new variable that holds a copy of the argument. The whole struct is copied, so add_bonus changes its own Card. An array is different: it is never copied into a parameter. The argument scores becomes the address of its first element, and the parameter int scores[] is a pointer. Writing scores[0] writes the caller’s array. The function does not know the array’s length, so pass the length as another argument when it is needed.

To let a function change a caller’s int or struct, pass its address, as in Variables: add_bonus(&card), with a parameter struct Card *card.

main.c
void add_bonus(struct Card card, int scores[]) {
    card.total = card.total + 10;
    scores[0] = scores[0] + 10;
}

struct Card card = {5};
int scores[1] = {5};
add_bonus(card, scores);
printf("%d %d\n", card.total, scores[0]); /* 5 15 */

A struct is copied; an array parameter is a pointer to the caller’s elements.

Coming from Python: Passing a struct copies all of its members, unlike passing a Python object, which shares it.

Coming from Java: An array argument carries no length: the function sees only a pointer, so it cannot ask for scores.length.

Precisely: sizeof inside the function

Inside add_bonus, sizeof scores is the size of a pointer, not of the array; gcc warns about it by default (-Wsizeof-array-argument). A pointer to a const element, const int scores[], lets the function read the caller’s array but not write it.

Experiment

Pass a struct and an array

C copies every argument. A struct is copied as a whole. An array is not passed at all: a parameter written int scores[] or int scores[1] is a pointer, and the argument scores becomes the address of main’s first element. Writing scores[0] writes main’s array.

Why it matters Passing a struct gives the function its own copy; passing an array lets the function change the caller’s elements. A size written in an array parameter is not checked and does not make a copy.

Predict both printed numbers. Then change the parameter int scores[] to int scores[1] and predict: does giving a size make the array a copy?

#include <stdio.h>

struct Card {
    int total;
};

void add_bonus(struct Card card, int scores[]) {
    card.total = card.total + 10;
    scores[0] = scores[0] + 10;
}

int main(void) {
    struct Card card = {5};
    int scores[1] = {5};
    add_bonus(card, scores);
    printf("%d %d\n", card.total, scores[0]);
    return 0;
}
What to look for

Step into add_bonus and select each frame in turn. add_bonus’s card shows {total: 15} after its first line, while main’s card still shows {total: 5}: the struct was copied. add_bonus’s scores shows an address followed by → and a number, while main’s shows {5}: the parameter is a pointer to main’s array. After scores[0] changes, main’s scores shows {15}. The run prints 5 15. After the edit, scores is still shown as an address, and the run still prints 5 15.

Expected output

5 15

04 / Return

How does the result get back to the code that called the function?

return ends the call at once and gives its value back. The value takes the place of the call in the caller’s code: entry_fee(4) + entry_fee(30) becomes 0 + 3. A function declared void returns no value.

When the call ends, its parameters and local variables stop existing. Only the returned value reaches the caller. Returning the address of a local variable gives the caller a pointer to something that no longer exists; using it is undefined behavior. Why, and what to do instead, is the subject of Stack and heap in the Runtimes module.

main.c
int entry_fee(int age) {
    if (age < 5) {
        return 0;
    }
    return 3;
}

int total = entry_fee(4) + entry_fee(30); /* 3 */

return ends the call; its value replaces the call in the caller’s expression.

Precisely: two results

A C function returns one value. For two, return a struct, or take a pointer parameter for each extra result and write through it, as scanf does with &value.

05 / Functions as values

Can a function be passed to another function?

A function can be passed to another function. The parameter int (*step)(int) is a function pointer: a pointer to a function that takes an int and returns an int. In apply_to(4, triple), triple is written without parentheses, so it is not called; it becomes the function’s address. apply_to calls it as step(value).

C has no functions without names and no closures: a function cannot carry variables from the code around it. When it needs extra data, pass that data as another argument.

main.c
int apply_to(int value, int (*step)(int)) {
    return step(value);
}

int triple(int n) {
    return n * 3;
}

printf("%d\n", apply_to(4, triple)); /* 12 */

Without parentheses a function is passed as a pointer, not called.

Precisely: callbacks with context

Library functions such as qsort take a function pointer. When a callback needs extra data, C interfaces usually take a void * context argument as well and pass it back to the callback.

06 / Try it

Can you follow a fresh call?

Follow the int and the array separately, and then the returned value. The next lesson, Recursion, uses these two facts: each call gets its own parameters and variables, and a call returns to the code that called it. Its link is at the end of the page.

main.c
#include <stdio.h>

int shift(int value, int list[]) {
    value = value + 1;
    list[0] = list[0] + 1;
    return value;
}

int main(void) {
    int value = 1;
    int list[1] = {1};
    int result = shift(value, list);
    printf("%d %d %d\n", value, list[0], result);
    return 0;
}

What is printed?

Reveal the reasoning

1 2 2. shift’s value is a copy of the caller’s 1, so the caller’s value stays 1. list is a pointer to the caller’s array, so list[0] becomes 2 for both. The returned value, 2, is stored in result.

Practice tickets

  1. CALL-101 · Charge both rides to the bus pass → Free
  2. CALL-102 · Grow the plant for two weeks → Sign in · Pro
  3. CALL-103 · Number the orders at each coffee cart → Sign in · Pro

Keep exploring

Recursion →

Primary references