Functions and the call stack · about 45 min

Defining and calling functions

Why this matters

So far every program has been a single main. That works for twenty lines and collapses at two hundred: the same logic gets copied in three places, a fix is applied to two of them, and nobody can tell what any block is for. Functions are the main tool for managing complexity. A function gives a piece of logic a name, lets you use it from anywhere, and lets you think about it in isolation: once is_prime works and is tested, you can call it without remembering how it works. Every large C program, including the Linux kernel's tens of millions of lines, is built from functions that each do one understandable job.

Anatomy of a function

int square(int x)
{
    return x * x;
}
  return type   name    parameter list
      |          |         |
      int      square   (int x)
      {
          return x * x;     <- body: statements that run on each call
      }

Calling it:

#include <stdio.h>

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

void print_line(int width)
{
    for (int i = 0; i < width; i++)
        putchar('-');
    putchar('\n');
}

int main(void)
{
    int a = square(7);
    printf("square(7) = %d\n", a);
    printf("square(3) + square(4) = %d\n", square(3) + square(4));
    printf("square(square(2)) = %d\n", square(square(2)));
    print_line(20);
    return 0;
}
square(7) = 49
square(3) + square(4) = 25
square(square(2)) = 16
--------------------

A call is an expression: square(7) is the value 49 wherever it appears, so it can be stored, printed, added, or passed straight into another call. A call to a void function such as print_line(20) has no value and is used as a statement on its own.

Parameters and arguments

The values in a call are arguments; the variables in the definition that receive them are parameters. On each call, each parameter is initialized with the corresponding argument:

  call:        square( 7 )            print_line( 20 )
                       |                          |
  definition:  int square(int x)      void print_line(int width)
               x starts as 7          width starts as 20

Arguments can be any expressions; they are fully evaluated before the function starts. In square(square(2)), the inner call runs first and returns 4, and then the outer call runs with x = 4. The number and types of the arguments must match the parameters. Passing the wrong number is an error:

o.c:6:13: error: too many arguments to function 'twice'
    6 |     int r = twice(3, 4);
      |             ^~~~~
o.c:2:5: note: declared here

If an argument has a different but convertible type, it is converted, just as in an assignment: passing 7.9 to an int parameter gives it the value 7.

What happens during a call

When main executes square(7):

  1. The argument 7 is evaluated.
  2. Control jumps to the start of square, remembering where to come back to (the return address). The machine instruction is literally call square.
  3. square gets its own fresh parameter x = 7 and runs its body.
  4. return x * x; computes 49, places it where the caller expects results (on x86-64, the rax register), and jumps back to the return address.
  5. In main, the call expression evaluates to 49.
   main                          square
   ----                          ------
   a = square(7) ---call-------> x = 7
                                 return 49
   a = 49        <---return-----
   printf(...)

Each call is independent: square(3) and square(4) in the same expression each get their own x. The lesson on the call stack shows exactly where those variables live.

return

return does two things: it chooses the result and it ends the function right away. A function can have several returns, which is handy for handling special cases first:

int abs_value(int n)
{
    if (n < 0)
        return -n;
    return n;
}

A void function may use a bare return; to finish early, or simply run to its closing brace.

Every path through a non-void function must return a value. If execution can reach the closing brace of an int function, the caller receives garbage (undefined behavior), and gcc warns:

o.c:3:44: warning: control reaches end of non-void function [-Wreturn-type]

(main is the one exception, as you saw in module 1: falling off its end returns 0.)

Designing functions

Good functions have a few properties in common:

Breaking a problem into functions is called decomposition. A typical main ends up reading like an outline: read input, compute, print, each step a call.

Common mistakes

Calling a function above its definition without a declaration. The compiler reads top to bottom; at the call it does not yet know the function exists:

p.c:6:20: error: implicit declaration of function 'area' [-Wimplicit-function-declaration]

Define functions above main, or declare them first (next lesson).

Using the value of a void function:

p.c:5:13: error: void value not ignored as it ought to be

Returning a value from a void function:

p.c:2:43: error: 'return' with a value, in function returning void [-Wreturn-mismatch]

Ignoring a return value. square(5); on its own line compiles and computes 25, then throws it away. Functions do not change their arguments; to keep the result you must store or use it: n = square(n);.

Printing instead of returning. A function that should compute a value but prints it instead cannot be reused in a larger expression. Return the value; let the caller decide what to print.

Recap

Going further: take the leap-year or prime program from the control-flow module and turn the test into a function int is_leap(int year) or int is_prime(int n) that returns 1 or 0, so that main only reads, calls and prints.

Exercises

Exercise 1 · write code

The largest of three

Write a function

int max3(int a, int b, int c)

that returns the largest of its three arguments. Keep a main in your file (anything you like: it is used for the Run button); the check calls max3 directly from its own test program and reports which inputs gave a wrong answer.

File: main.c

Starting point

#include <stdio.h>

int max3(int a, int b, int c)
{
    return a;
}

int main(void)
{
    printf("%d\n", max3(4, 9, 2));
    return 0;
}
Hint 1

Keep a variable m holding the largest value seen so far, starting with a.

Hint 2

Compare b and then c against m, replacing m when one is larger. Return m.

Reference solution
#include <stdio.h>

int max3(int a, int b, int c)
{
    int m = a;
    if (b > m)
        m = b;
    if (c > m)
        m = c;
    return m;
}

int main(void)
{
    printf("%d\n", max3(4, 9, 2));
    return 0;
}

Exercise 2 · quiz

Follow the calls

What does this program print?

#include <stdio.h>

int triple(int x)
{
    printf("triple(%d) ", x);
    return 3 * x;
}

int main(void)
{
    int r = triple(triple(2));
    printf("-> %d\n", r);
    return 0;
}
Answer
  • triple(2) triple(6) -> 18

Arguments are evaluated before the function is called. To call the outer triple, C first needs its argument, the value of triple(2): that call prints "triple(2) " and returns 6. Then the outer call runs with x = 6, prints "triple(6) " and returns 18.

Exercise 3 · write code

A function that prints

Write a function

void print_row(int n, char ch)

that prints the character ch exactly n times followed by a newline (just the newline when n is 0 or negative). Then write main so that it reads an integer h and uses print_row to draw a box of # characters h rows high and 2*h columns wide. For h = 2:

####
####

The check runs your program and also calls print_row directly.

File: main.c

Starting point

#include <stdio.h>

void print_row(int n, char ch)
{
}

int main(void)
{
    int h;
    if (scanf("%d", &h) != 1)
        return 1;
    return 0;
}

Example

input

2

expected output

####
####

Example

input

3

expected output

######
######
######
Hint 1

print_row is a for loop calling putchar(ch) n times, then putchar('\n').

Hint 2

In main, loop h times and call print_row(2 * h, '#').

Reference solution
#include <stdio.h>

void print_row(int n, char ch)
{
    for (int i = 0; i < n; i++)
        putchar(ch);
    putchar('\n');
}

int main(void)
{
    int h;
    if (scanf("%d", &h) != 1)
        return 1;
    for (int r = 0; r < h; r++)
        print_row(2 * h, '#');
    return 0;
}

Exercise 4 · write code

Greatest common divisor

Write int gcd(int a, int b) returning the greatest common divisor of two non-negative integers, using Euclid's algorithm: while b is not zero, replace the pair (a, b) with (b, a % b); when b reaches zero, a is the answer. (So gcd(48, 18): (48,18) -> (18,12) -> (12,6) -> (6,0) -> 6. By this rule gcd(n, 0) is n.)

Then write main to read two integers and print, for example:

gcd(48, 18) = 6

Careful: replacing two variables "at the same time" needs a temporary variable.

File: main.c

Starting point

#include <stdio.h>

int gcd(int a, int b)
{
    while (b != 0) {
        a = b;
        b = a % b;
    }
    return a;
}

int main(void)
{
    int a, b;
    if (scanf("%d %d", &a, &b) != 2)
        return 1;
    printf("gcd(%d, %d) = %d\n", a, b, gcd(a, b));
    return 0;
}

Example

input

48 18

expected output

gcd(48, 18) = 6
Hint 1

In the starter, after a = b; the old value of a is gone, so a % b computes b % b, which is 0.

Hint 2

Compute the remainder first into a temporary: int t = a % b; a = b; b = t;

Reference solution
#include <stdio.h>

int gcd(int a, int b)
{
    while (b != 0) {
        int t = a % b;
        a = b;
        b = t;
    }
    return a;
}

int main(void)
{
    int a, b;
    if (scanf("%d %d", &a, &b) != 2)
        return 1;
    printf("gcd(%d, %d) = %d\n", a, b, gcd(a, b));
    return 0;
}