CMPUT-201 · Lesson 4 of 12 · 29 min

Functions, scope, the stack and header files

Functions in C play the same role as in Python: they name a piece of work, take parameters, return a result and let you decompose a program into parts that can be understood and tested separately. C adds rules Python does not have. Every function has a declared return type and parameter types; a function must be declared before it is called; arguments are always passed by value, so a function receives copies; and every call creates a stack frame holding its parameters and local variables, which disappears when the function returns. This lesson covers defining and declaring functions, pass-by-value, scope and storage duration (local, static and global variables), how the call stack works, recursion, and how to design a module's interface in a header file. It ends with a first look at gdb, the debugger, showing the call stack of a running program.

Defining and declaring functions

A function definition gives the return type, the name, the parameter list with a type for each parameter, and the body. A function that returns nothing has return type void, and a function that takes no parameters should say so with (void); in C17 an empty pair of parentheses means "unspecified parameters", an old rule that turns off checking (C23 finally made () mean no parameters). The compiler reads a file from top to bottom, so it must see either the definition or a declaration of a function before the first call. A declaration, or prototype, is the first line of the definition followed by a semicolon. Small programs often define helper functions above main; larger ones put prototypes in headers, as lesson 2 showed.

Prototypes let main call functions defined later in the file
#include <stdio.h>

double celsius_to_fahrenheit(double c);     /* prototype: declared before use */
static void print_row(double c);

int main(void) {
    for (double c = -40.0; c <= 40.0; c += 20.0) {
        print_row(c);
    }
    return 0;
}

double celsius_to_fahrenheit(double c) {     /* definition */
    return c * 9.0 / 5.0 + 32.0;
}

static void print_row(double c) {
    printf("%6.1f C = %6.1f F\n", c, celsius_to_fahrenheit(c));
}
Output
 -40.0 C =  -40.0 F
 -20.0 C =   -4.0 F
   0.0 C =   32.0 F
  20.0 C =   68.0 F
  40.0 C =  104.0 F

The word static in front of print_row gives the function internal linkage: it is visible only inside this source file, and the linker will not let other files call it. Mark helper functions static unless they are part of a module's public interface; this keeps the global namespace clean and lets the compiler optimize more. celsius_to_fahrenheit has external linkage, the default, so another file could declare and call it.

Pass by value

When you call a function in C, each argument is evaluated and its value is copied into the corresponding parameter, which is a new local variable of the called function. Changing a parameter therefore changes only the copy. This is simpler than Python's rule of passing references to objects, but it means a function cannot change its caller's variables directly. The classic demonstration is a swap function.

Parameters are copies, so swapping needs pointers
#include <stdio.h>

static void swap_wrong(int a, int b) {
    int tmp = a;
    a = b;
    b = tmp;
    printf("  inside swap_wrong: a=%d b=%d\n", a, b);
}

static void swap(int *a, int *b) {      /* receives the addresses */
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main(void) {
    int x = 1, y = 2;
    swap_wrong(x, y);
    printf("after swap_wrong: x=%d y=%d\n", x, y);
    swap(&x, &y);
    printf("after swap:       x=%d y=%d\n", x, y);
    return 0;
}
Output
  inside swap_wrong: a=2 b=1
after swap_wrong: x=1 y=2
after swap:       x=2 y=1

swap_wrong exchanges its own copies and then discards them. The working version receives the addresses of x and y, written &x and &y, and uses the * operator to reach the caller's variables through those addresses. That is a pointer, the subject of lesson 5. For now the point is that pass-by-value is universal in C: even the pointer is passed by value, but a copy of an address still leads to the same variable. The same mechanism is how a function returns more than one result, and how standard functions such as scanf store values into your variables.

Arrays are the one apparent exception. When you pass an array to a function, C passes the address of its first element rather than copying the whole array, so the function can change the caller's elements. Lesson 6 explains why. Mark array and pointer parameters const when the function only reads them, as mean did in lesson 2, so that the compiler stops accidental changes and readers know the function is safe to call.

Scope and storage duration

Scope is the part of the program where a name can be used. A variable declared inside a block, between braces, has block scope: it exists from its declaration to the closing brace, and a variable declared in an inner block can hide one with the same name in an outer block (-Wshadow warns about this). A variable declared outside all functions has file scope and can be used anywhere below its declaration in the file.

Storage duration is how long the variable's memory exists. Local variables normally have automatic storage duration: they are created when execution enters their block and destroyed when it leaves, and they start with indeterminate values, so reading one before assigning it is a bug that -Wall often catches. Variables at file scope, and local variables declared static, have static storage duration: they exist for the whole run of the program, are initialized once before main starts, and are set to zero if you give no initializer. A static local variable therefore keeps its value between calls.

A static local variable keeps its value between calls
#include <stdio.h>

static int calls_total = 0;          /* file scope, static duration, internal linkage */

static int next_ticket(void) {
    static int ticket = 100;         /* initialized once, keeps its value */
    calls_total++;
    return ticket++;
}

int main(void) {
    for (int i = 0; i < 3; i++) {
        int local = i * 10;          /* automatic: new each time round the loop */
        printf("ticket %d, local %d\n", next_ticket(), local);
    }
    printf("next_ticket was called %d times\n", calls_total);
    return 0;
}
Output
ticket 100, local 0
ticket 101, local 10
ticket 102, local 20
next_ticket was called 3 times

Global variables, file-scope variables with external linkage, can be shared between files: one file defines int verbose = 0; and a header declares extern int verbose; so other files can use it. Use them sparingly. Any function anywhere can change a global, which makes programs hard to reason about and test; passing values as parameters is almost always clearer. The static keyword at file scope keeps a variable private to its file, a good compromise for state that one module needs to remember.

The call stack

Each time a function is called, the program reserves a block of memory called a stack frame or activation record. It holds the function's parameters, its local variables, the address to return to when the function finishes, and some saved processor state. The frames are kept on the call stack, a region of memory that grows when functions are called and shrinks when they return, in strict last-in, first-out order. When a function returns, its frame is popped and its local variables cease to exist; that is why returning the address of a local variable is a serious bug, because the address points into memory that will soon be reused.

A vertical stack of frames during the call factorial(3) from main. At the bottom, the frame for main with its local variables. Above it, a frame for factorial with n equal to 3 and a return address into main. Above that, factorial with n equal to 2, and at the top, factorial with n equal to 1, the base case. An arrow shows the stack growing toward lower addresses as calls are made, and returning values flowing back down: 1, then 2, then 6.
Stack frames during factorial(3): each call has its own copy of n

Recursion works because every call has its own frame with its own copies of the parameters. The factorial function below calls itself with a smaller n until it reaches the base case; then the frames return one by one, multiplying as they go. Recursion is elegant for problems that are naturally self-similar, such as tree traversal and divide-and-conquer algorithms, which CMPUT-204 studies. But the stack is limited, typically 8 MB for the main thread on Linux, so very deep recursion causes a stack overflow and the program crashes with a segmentation fault. C compilers may turn some tail calls into loops at -O2, but the standard does not guarantee it, so for a loop of a million steps write a loop.

A recursive factorial
#include <stdio.h>

static long factorial(int n) {
    if (n <= 1) {
        return 1;          /* base case */
    }
    return n * factorial(n - 1);
}

int main(void) {
    printf("5! = %ld\n", factorial(5));
    return 0;
}
Output
5! = 120

The debugger gdb can show the call stack of a running program. In the session below, gdb stops the program each time factorial is entered with n equal to 1, and the backtrace command, bt, lists every frame from the innermost outward, with the value of each parameter. Lesson 11 covers gdb fully; here it is a window into the stack. The gdb commands are given in batch mode so the whole session runs without typing.

gdb shows one stack frame per active call
gcc -std=c17 -Wall -Wextra -g -O0 fact.c -o fact
gdb -q -batch -ex "break factorial if n == 1" -ex run -ex bt ./fact
Output
Breakpoint 1 at 0x1159: file fact.c, line 4.

Breakpoint 1, factorial (n=1) at fact.c:4
4	    if (n <= 1) {
#0  factorial (n=1) at fact.c:4
#1  0x0000555555555179 in factorial (n=2) at fact.c:7
#2  0x0000555555555179 in factorial (n=3) at fact.c:7
#3  0x0000555555555179 in factorial (n=4) at fact.c:7
#4  0x0000555555555179 in factorial (n=5) at fact.c:7
#5  0x0000555555555195 in main () at fact.c:11

Frame #0 is the innermost call, factorial(n=1), and frame #5 is main. Each frame line gives the function, its arguments and the source line where it is currently executing, which is exactly the information you need when a program crashes deep inside a chain of calls.

Designing interfaces with headers

A header file is a contract between a module and its users. It should declare only what users need: the public functions with clear names and parameter types, any types and constants they need to call those functions, and comments describing what each function does, what it requires of its arguments and what it returns. Everything else, helper functions and private state, stays in the .c file and is marked static. This separation, an interface hiding an implementation, is the same abstraction CMPUT-174 described, enforced by the compiler and linker instead of by convention.

A small, documented header that hides its implementation
/* temperature.h - conversions between temperature scales */
#ifndef TEMPERATURE_H
#define TEMPERATURE_H

#include <stdbool.h>

/* Absolute zero in degrees Celsius. */
#define ABSOLUTE_ZERO_C (-273.15)

/* Convert Celsius to Fahrenheit. */
double c_to_f(double celsius);

/* Convert Fahrenheit to Celsius. */
double f_to_c(double fahrenheit);

/* Return true if the Celsius temperature is physically possible. */
bool is_valid_celsius(double celsius);

#endif

A good header compiles on its own: it includes the headers it needs, such as stdbool.h for bool, so that users can include it in any order. It never defines variables or function bodies (except static inline functions, an advanced case), because that would cause multiple definitions when two files include it. Finally, the module's own .c file should include its header first, which checks that the header is self-contained and that the definitions match the declarations.

Worked problem

Tracing a recursive function

Trace power(2, 5) for: long power(long b, int e) { if (e == 0) return 1; long half = power(b, e / 2); return (e % 2 == 0) ? half * half : half * half * b; } How many calls are made, and what is the result?

Show solution
  1. power(2, 5) calls power(2, 2), since 5 / 2 is 2 in integer division.
  2. power(2, 2) calls power(2, 1), which calls power(2, 0), the base case returning 1.
  3. Returning: power(2, 1) has e odd, so it returns 1 x 1 x 2 = 2. power(2, 2) has e even, so it returns 2 x 2 = 4. power(2, 5) has e odd, so it returns 4 x 4 x 2 = 32.
  4. There are four calls in total (e = 5, 2, 1, 0), and at most four frames for power are on the stack at once. Computing 2 to the power 5 by repeated multiplication would take five multiplications; the number of calls grows with the number of bits in e, a fact CMPUT-204 builds on.

Answer: 32, using four calls of power.

Worked problem

Why a function sees the wrong value

A function int *make_counter(void) { int count = 0; return &count; } compiles with a warning, and the caller later finds its counter holds strange values. Explain the problem and give a correct design.

Show solution
  1. count is an automatic local variable; its storage is part of make_counter's stack frame.
  2. When make_counter returns, the frame is released, so the returned address points to memory that the next function call will reuse. Using it is undefined behaviour. GCC warns: function returns address of local variable.
  3. Correct designs: return the value itself (return 0;) if the caller just needs a number; let the caller own the variable and pass its address in (void init_counter(int *c) { *c = 0; }); or allocate memory on the heap with malloc, covered in lesson 7.

Answer: It returns the address of a local variable that no longer exists; return a value, take a pointer from the caller, or allocate on the heap.

Common mistakes

  • MistakeCalling a function before it is declared and relying on the compiler to guess.

    InsteadDeclare every function before use, with a prototype or a header.

  • MistakeExpecting a function to change the caller's variable by assigning to its parameter.

    InsteadParameters are copies; pass a pointer to the variable, or return the new value.

  • MistakeReturning a pointer to a local variable.

    InsteadLocals vanish when the function returns; return a value, use caller-provided storage or the heap.

  • MistakeUsing uninitialized local variables.

    InsteadAlways initialize locals when declaring them; compile with -Wall to catch many cases.

  • MistakeUsing global variables to pass information between functions.

    InsteadPass parameters and return values; keep any necessary module state static in one file.

  • MistakeWriting () for a function with no parameters.

    InsteadWrite (void), so that the compiler checks calls; in C17 () means unspecified parameters.

Summary and key terms

  • Functions have declared parameter and return types and must be declared before they are called.
  • All arguments are passed by value; to let a function change a caller's variable, pass its address.
  • static on a function or file-scope variable limits it to one file; static on a local variable gives it static storage duration.
  • Automatic locals start with indeterminate values; static and global variables start at zero.
  • Each call creates a stack frame for parameters, locals and the return address; frames are popped in last-in, first-out order.
  • Recursion needs a base case and uses one frame per call, so very deep recursion can overflow the stack.
  • Headers declare a module's public interface and hide its implementation; gdb's bt shows the call stack.
Prototype
A function declaration giving its return type, name and parameter types.
Pass by value
Calling convention in which each parameter receives a copy of the argument.
Scope
The region of source code in which a name can be used.
Storage duration
How long a variable's memory exists: automatic, static or allocated.
Linkage
Whether a name refers to the same entity in other files: external, internal or none.
Stack frame
The block of stack memory for one function call.
Call stack
The last-in, first-out sequence of frames for active function calls.
Recursion
A function calling itself, directly or indirectly.
Stack overflow
A crash caused by using more stack memory than is available.
Backtrace
A list of the active stack frames, shown by gdb's bt command.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. Write a prototype for a function average that takes a const array of int and its length and returns a double.

    Show answer

    double average(const int values[], size_t n);

  2. Why does a function void reset(int x) { x = 0; } not reset the caller's variable?

    Show answer

    x is a copy of the argument; changing it does not affect the caller.

  3. Rewrite reset so that it works.

    Show answer

    void reset(int *x) { *x = 0; } called as reset(&value);

  4. What does static mean on a local variable? On a function?

    Show answer

    On a local: static storage duration, keeping its value between calls. On a function: internal linkage, visible only in its file.

  5. What value does an uninitialized global int have? An uninitialized local int?

    Show answer

    The global is zero; the local is indeterminate.

  6. What is stored in a stack frame?

    Show answer

    Parameters, local variables, the return address and saved registers.

  7. Why can deep recursion crash a C program?

    Show answer

    Each call uses stack memory, and the stack has a fixed limit, often 8 MB.

  8. Write a recursive function that returns the sum of the digits of a non-negative int.

    Show answer

    int digit_sum(int n) { return n < 10 ? n : n % 10 + digit_sum(n / 10); }

  9. What gdb command shows the call stack?

    Show answer

    bt (backtrace).

  10. List two things a good header should not contain.

    Show answer

    Variable definitions and function bodies (other than static inline), because they cause multiple definitions.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz