C from the ground up

Functions and prototypes

Name a piece of work and reuse it: how a function returns a value, why C wants to know about it in advance, and the prototype that says so.

Part 5 of 12

Intermediate14 min readBack to Courses

Why functions

A function gives a name to a piece of work. You write it once, call it wherever you need it, and the call site says what it does instead of how. That is the difference between a program you can reread and one you cannot.

Every function states what kind of value it hands back, then its name, then the values it takes in. int square(int n) returns a whole number and takes a whole number. The return line sends the answer back to the caller.

Telling C in advance

The compiler reads a file from top to bottom. If your int main calls a function written further down, the compiler meets the call before it meets the definition and does not yet know its shape.

A prototype fixes that. It is the function line ending in a semicolon, written above the places that call it, with no body. It promises that a function of this name and shape exists somewhere below.

The void cases

A function that hands nothing back is declared void. A function that takes nothing in writes void where the parameter list would be, as in int main(void). Both use the same keyword for two different kinds of nothing.

Arguments are passed by value: the function gets its own copy of each one. Changing a parameter inside the function does not touch the caller's variable. The next chapters show how pointers let a function reach the original.

What a call actually does

When you call a function, the program sets aside a stack frame holding its parameters and local variables. The frame lives only for that call; when the function returns, it is gone. That is why a local array cannot be handed back as a pointer.

The same machinery makes recursion work: each call gets its own frame, so a recursive function can hold many copies of its locals at once. Too many frames and the stack runs out — a stack overflow, which is not the same thing as running out of heap.

One return value, and the path out

A C function returns exactly one value, and return ends the call immediately. Any code after it in the same block never runs. A void function can still use a bare return; to leave early.

Reaching the closing brace of a non-void function without a return is undefined behaviour: the caller reads whatever happened to be in the return slot. Make every path return a value, and let the compiler warnings point out the branch you missed.

What the compiler checks at a call

With a prototype in view, the compiler knows how many arguments a function takes and what type each one should be. It converts a value that can be converted — an int passed where a double is expected — and warns about a pointer passed where a number belongs. Without a prototype, those checks weaken, and a wrong call can compile and fail at run time.

That is the practical reason to keep the prototype next to the definition and to include a header rather than retyping it. Two copies drift: one gains a parameter, the other does not, and the call site still compiles against the stale one.

Recursion needs a base case

A recursive function calls itself on a smaller piece of the problem and stops when the piece is small enough. factorial(n) returns 1 when n is 1 or less, and otherwise returns n times the factorial of n minus 1. The base case is what stops the descent; without it the calls never end and the stack fills.

Each level holds its own frame until the innermost returns, so the memory cost is the depth. A recursion a million levels deep will overflow the stack even though the loop version would run in constant space. When the recursive call is the last thing the function does, a loop is usually the simpler shape.

Three mistakes are common here: a prototype that does not match the definition, a branch that forgets to return a value, and a change to a by-value parameter that the caller never sees. Compare the prototype and definition character by character, return on every branch, and pass an address when the caller needs the change.

Try these next

  • Write int min(int a, int b) and call it with the arguments reversed; the answer should not change.
  • Add a void function that prints a separator line, then call it between two reports.
  • Write a recursive int factorial(int n) and print the result for 5.

Define and call

#include <stdio.h>

int doubled(int n) {
    return n * 2;
}

int main(void) {
    printf("Doubled: %d\n", doubled(21));
    return 0;
}

A function with a prototype

Run it, then move the definition of square above main and remove the prototype line. The program behaves the same either way.

Output
Click "Run" to execute your code

A helper, a report and a return

Run it, then move the largest-of-three call into a `max3` function that calls `max` twice. Add a fourth number and extend the chain.

Output
Click "Run" to execute your code

Move the definition of square below main and delete the prototype, then run. The compiler will tell you it does not know the function yet, which is the whole reason prototypes exist.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does the word before a function name describe?