Functions and the call stack · about 45 min

Prototypes, declarations and pass-by-value

Why this matters

Two questions come up the moment programs have more than a couple of functions. First, how does the compiler check a call to a function it has not reached yet, or one that lives in another file or a library? The answer, the prototype, is also what every header file is made of. Second, what exactly does a function receive when you pass it a variable? C's answer, a copy, is simple and strict, and it explains why scanf needs that & you have been typing, and why a whole module is devoted to pointers.

The compiler reads top to bottom

The compiler processes a file in one pass from the top. When it meets a call, it must already know the function's return type and parameter types so it can check the arguments and generate the right machine code. So far you have satisfied this by defining every function above its first use. That stops working as soon as two functions call each other, when you prefer main at the top, or when the function is in another file entirely (like printf).

Declarations and prototypes

A declaration introduces a name and its type without providing the body. For a function, a declaration with full parameter types is called a prototype:

int area(int w, int h);       /* prototype: a declaration, note the semicolon */

int area(int w, int h)        /* definition: provides the body */
{
    return w * h;
}
q.c:4:5: error: conflicting types for 'half'; have 'int(int)'
q.c:2:8: note: previous declaration of 'half' with type 'double(double)'

This is exactly what the headers you include contain. <stdio.h> contains a line like int printf(const char *format, ...); and <math.h> contains double sqrt(double x);. The definitions are already compiled, in the C library, and the linker connects your calls to them, as you saw in the module 1 pipeline. Later in the track you will write your own headers in exactly this way.

A common layout for a single-file program:

#include <stdio.h>

/* prototypes: the table of contents */
int read_number(void);
int is_prime(int n);
void print_report(int n, int prime);

int main(void)
{
    /* the outline of the program */
}

/* definitions, in any order */

What a prototype buys you

With a prototype in scope, the compiler checks every call:

The trap of empty parentheses

In C17 and earlier, empty parentheses in a declaration do not mean "no parameters". They mean "parameters not specified", an old-style declaration from the 1970s that turns off checking:

int old_style();              /* no information about parameters */

int main(void)
{
    printf("%d\n", old_style(1, 2, 3));   /* compiles silently! */
}

int old_style(int a) { return a; }

With -Wall -Wextra this compiles with no diagnostic at all and, here, prints 1. Calling a function with the wrong arguments is undefined behavior; it only happened to work. Always write (void) for a function without parameters: int new_style(void);. (C23 changes () to mean (void), but much existing code and this track use C17.)

Pass-by-value

When you call a function, each argument expression is evaluated, and the value is copied into the parameter, which is a brand new local variable of the called function. The function works on its copy. Nothing it does to the copy affects the caller.

#include <stdio.h>

void bump(int n)
{
    n = n + 1;
    printf("inside bump: n = %d\n", n);
}

int main(void)
{
    int n = 5;
    bump(n);
    printf("in main:     n = %d\n", n);
    return 0;
}
inside bump: n = 6
in main:     n = 5

In memory, there are two separate boxes that happen to share a name:

  main's variables          bump's variables
  +-------+                 +-------+
  | n = 5 |  -- copy 5 -->  | n = 5 |   then n = n + 1
  +-------+                 | n = 6 |
                            +-------+
  still 5                   gone when bump returns

Consequences worth internalizing:

That is also why scanf("%d", &n) has the &. scanf must store a number in your variable, and a copy of n's current value would be useless to it. &n passes the variable's address instead: a value that tells scanf where n lives. Passing addresses is still pass-by-value (the address itself is copied), and it is the whole subject of the pointers module.

Order of evaluation

C does not specify the order in which the arguments of a call are evaluated. Usually it does not matter, but it does when the arguments have side effects:

int counter = 0;
int next(void) { return ++counter; }

printf("%d %d\n", next(), next());

With gcc on x86-64 this prints 2 1: the second argument was evaluated first. Another compiler, or another optimization level, may print 1 2. Both are correct. If order matters, evaluate into variables first:

int first = next();
int second = next();
printf("%d %d\n", first, second);    /* always 1 2 */

Common mistakes

Recap

Going further: run grep -n 'sqrt' /usr/include/x86_64-linux-gnu/bits/mathcalls.h | head in the sandbox to see how the C library declares math functions (through a few macros). Look up "C23 function declarations without prototypes" to see why the committee finally removed () declarations.

Exercises

Exercise 1 · quiz

Does the caller see the change?

What does this print?

void reset(int count)
{
    count = 0;
}

int main(void)
{
    int count = 42;
    reset(count);
    printf("%d\n", count);
    return 0;
}
Answer
  • 42

C passes arguments by value: reset receives a copy of 42 in its own variable that happens to also be called count. Setting the copy to 0 has no effect on main's count. Two functions may each have a local variable with the same name; they are completely separate. To let a function change a caller's variable you need its address, which is what pointers (module 7) are for.

Exercise 2 · write code

Put main first

Many programmers like main at the top of the file so a reader sees the outline first. The starter does that, but it does not compile: main calls area and perimeter before the compiler has seen them.

Fix it by adding prototypes above main, without moving any function. The program reads a width and a height and must print exactly (for input 3 4):

area: 12
perimeter: 14

The check also verifies that main still comes before the other two definitions.

File: main.c

Starting point

#include <stdio.h>

int main(void)
{
    int w, h;
    if (scanf("%d %d", &w, &h) != 2)
        return 1;
    printf("area: %d\n", area(w, h));
    printf("perimeter: %d\n", perimeter(w, h));
    return 0;
}

int area(int w, int h)
{
    return w * h;
}

int perimeter(int w, int h)
{
    return 2 * (w + h);
}

Example

input

3 4

expected output

area: 12
perimeter: 14

Example

input

10 1

expected output

area: 10
perimeter: 22
Hint 1

A prototype is the first line of the definition with a semicolon instead of the body.

Hint 2

Add int area(int w, int h); and int perimeter(int w, int h); after the #include.

Reference solution
#include <stdio.h>

int area(int w, int h);
int perimeter(int w, int h);

int main(void)
{
    int w, h;
    if (scanf("%d %d", &w, &h) != 2)
        return 1;
    printf("area: %d\n", area(w, h));
    printf("perimeter: %d\n", perimeter(w, h));
    return 0;
}

int area(int w, int h)
{
    return w * h;
}

int perimeter(int w, int h)
{
    return 2 * (w + h);
}

Exercise 3 · quiz

f() versus f(void)

In C17, a file declares int count_items(); (empty parentheses) and later calls count_items(1, 2, 3). What happens at compile time with -Wall -Wextra?

Answer
  • It compiles without any diagnostic, because empty parentheses in a declaration mean "arguments not specified", so the compiler cannot check them

Before C23, int f(); is an old-style declaration that says nothing about the parameters, so calls are not checked at all, and a wrong call is undefined behavior at run time. int f(void); is a real prototype that says "exactly zero arguments", and then f(1) is a compile error. Always write (void) for functions without parameters. (C23 finally makes () mean the same as (void), but you are compiling as C17.)

Exercise 4 · write code

Return the new value

The starter's apply_discount tries to change the caller's price by modifying its parameter, which cannot work in C. Replace it with a function

int discounted(int price, int percent)

that returns the price reduced by percent percent, using integer arithmetic and rounding down (compute price * (100 - percent) / 100). Update main to use the return value. For input 250 20 the program must print exactly:

before: 250
after: 200

The check also tests discounted directly.

File: main.c

Starting point

#include <stdio.h>

void apply_discount(int price, int percent)
{
    price = price * (100 - percent) / 100;
}

int main(void)
{
    int price, percent;
    if (scanf("%d %d", &price, &percent) != 2)
        return 1;
    printf("before: %d\n", price);
    apply_discount(price, percent);
    printf("after: %d\n", price);
    return 0;
}

Example

input

250 20

expected output

before: 250
after: 200

Example

input

99 50

expected output

before: 99
after: 49
Hint 1

Change the return type from void to int and return the computed value.

Hint 2

In main, store the result: price = discounted(price, percent);

Reference solution
#include <stdio.h>

int discounted(int price, int percent)
{
    return price * (100 - percent) / 100;
}

int main(void)
{
    int price, percent;
    if (scanf("%d %d", &price, &percent) != 2)
        return 1;
    printf("before: %d\n", price);
    price = discounted(price, percent);
    printf("after: %d\n", price);
    return 0;
}