Naming a Computation Once

Every program in this course has lived inside main. That works while a program does one thing once, but the moment a computation is needed twice, main starts repeating itself, and a formula written out three times is a formula that can be wrong in three different ways. A function is the fix: it gives a computation a name, a list of inputs and a result, so that you write it once and call it wherever you need it. This program has two.

#include <stdio.h>

void print_header(void)
{
    printf("floor plan\n");
    printf("----------\n");
}

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

int main(void)
{
    print_header();
    printf("kitchen: %d\n", area(4, 3));
    printf("total: %d\n", area(4, 3) + area(2, 5));

    return 0;
}
floor plan
----------
kitchen: 12
total: 22

The first line of area is its signature, and it makes three separate statements read left to right. int is the return type, the type of the value the function hands back. area is the name, spelled in snake_case like a variable. (int width, int height) is the parameter list, and it is not a list of names but a list of typed slots, each one a declaration in its own right, which is why int is written twice rather than shared between them. Then come the braces holding the body, an ordinary block of the statements you already know. The words parameter and argument are worth separating here, because the rest of this lesson turns on the difference: the parameters are width and height, the variables named in the definition, while the arguments are the 4 and the 3 written at the call. Parameters are ordinary local variables belonging to the function, created fresh on every call and initialized from the arguments, matched up by position and not by name, so the first argument initializes the first parameter whatever either of them happens to be called.

The Value Comes Back

return does two things at once. It ends the function immediately, and it hands one value back to the place the call was written. That is what lets area(4, 3) sit inside a printf argument list, and what makes area(4, 3) + area(2, 5) an ordinary addition of two int values: a call to a value-returning function is an expression of that function's return type, usable anywhere a value of that type is allowed. Three calls to area appear above and the formula width * height is written once. print_header is the other kind. Some functions are worth naming even though they produce no value, because what you want from them is their effect rather than their answer, and their return type is void, C's way of saying that nothing comes back. That is why print_header(); stands alone as a statement with no value to store or print, while area(4, 3) never could, and a bare return; carrying no value ends such a function early just as reaching the closing brace does. Now look at the other void in that signature, the one between the parentheses, because the two mean entirely different things. That one is the parameter list, and chapter 1 has already told you what it means. In C, empty parentheses do not mean "no parameters", they mean "unspecified parameters", which switches off argument checking rather than promising there is nothing to check. That was the reason for writing int main(void), and it was never a rule about main in particular: it holds for every function you write, whatever the return type, so a function taking nothing and handing back an int is int next_year(void). In C++ the two forms are identical, which is where the habit of writing empty parentheses comes from.

Declaring Before Defining

The compiler reads your file from top to bottom, once, and it will not call a function it has not heard of. What it needs before a call is not the whole function but its signature, and a signature stated on its own, ending in a semicolon where the body would go, is a declaration, or, when it describes a function, a prototype. The body is the definition, and the two can live apart:

#include <stdio.h>

int area(int width, int height);

int main(void)
{
    printf("kitchen: %d\n", area(4, 3));

    return 0;
}

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

That prints kitchen: 12, with main calling a function whose body it has not reached yet, because the prototype told the compiler everything a call needs checking against: two arguments, both int, result int. Delete that one line and the program does not merely warn, it fails to build, with error: implicit declaration of function 'area' [-Wimplicit-function-declaration]. A prototype always carries its full parameter list. int area(); also compiles, and it is a far weaker claim: the same "unspecified parameters" trapdoor as int main(), leaving the compiler nothing to check your arguments against. Parameter names in a prototype are optional and ignored, so int area(int, int); says the same thing, though naming them documents the order for whoever reads the line next. Prototypes earn their keep when a program is split across several files, which is chapter 6's subject. Inside one file you can usually do without them by defining every helper above main, as the first example did and as the rest of this course does, because a definition is also a declaration and it then arrives before any call.

Every Argument Arrives as a Copy

Here is the fact that decides how every function in C behaves, and the clearest way to see it is to break it on purpose. doubled assigns straight to its own parameter.

#include <stdio.h>

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

int main(void)
{
    int score = 21;

    printf("doubled is %d\n", doubled(score));
    printf("score is still %d\n", score);

    return 0;
}
doubled is 42
score is still 21

n = n * 2; certainly ran, and score did not move. C passes every argument by value: the call copies the argument into the parameter, and the parameter is a separate variable, at its own address, that merely starts life holding an equal value. Assigning to it overwrites the copy and leaves the original alone. There is no exception to this anywhere in the language, and nothing you can write in a parameter list changes it. That one fact also explains something you have been doing since chapter 1 without being given the reason. printf("%d\n", score) works perfectly on a copy, because printf only ever reads the value. scanf("%d", &score) needed that & because scanf has to change your variable, and handing over an address is the only way a function can reach memory that is not its own. Lesson 3 of this chapter makes that mechanism yours to write, and the next lesson looks at where the copy actually lives, because every call gets its own fresh patch of stack for its parameters and locals.

A Path With No Return

The next program is wrong. grade_points returns a value when the score is 50 or more, and simply runs off the end of the function when it is not, which gcc reports by pointing straight at its closing brace.

#include <stdio.h>

int grade_points(int score)
{
    if (score >= 50)
    {
        return score / 10;
    }
}

int main(void)
{
    printf("%d\n", grade_points(84));
    printf("%d\n", grade_points(12));

    return 0;
}
grade.c: In function 'grade_points':
grade.c:9:1: warning: control reaches end of non-void function [-Wreturn-type]
    9 | }
      | ^

It still compiles, still links, still runs, and on three consecutive runs here it printed 8 and then 0 every time. That 0 is the trap, because it is exactly the answer a reader would expect and it is not an answer at all. Falling off the end of a non-void function and then using the result is undefined behaviour, so the standard requires nothing of it: the 0 was whatever happened to be sitting where a return value is collected, and it is under no obligation to be 0 tomorrow, at a different optimization level, or after you edit some unrelated line. Note how precisely that rule is worded, because reaching the end only matters if the caller uses what comes back, and main is the one function allowed to do it, since the standard supplies its return 0; for you. Everywhere else, put a return on the path that lacked one:

#include <stdio.h>

int grade_points(int score)
{
    if (score >= 50)
    {
        return score / 10;
    }

    return 0;
}

int main(void)
{
    printf("%d\n", grade_points(84));
    printf("%d\n", grade_points(12));

    return 0;
}

The same 8 and 0 print, and now they are promises instead of coincidences. The habit worth building is to read back every non-void function you write while asking it one question: does every path out of here return something?

Key Takeaways

  • A function's signature is a return type, a name, and a parameter list of typed slots, as in int area(int width, int height), where every slot needs its own type. Parameters are the variables in the definition, arguments are the values at the call, and they are matched by position and never by name. A call to a value-returning function is an expression of that type, so it can sit inside any larger expression.
  • A void return type means nothing comes back, so the call is a statement with no value to store. (void) as a parameter list means the function takes nothing, while empty parentheses mean unspecified parameters and switch argument checking off. That is why chapter 1 insisted on int main(void), and the rule covers every function you write.
  • A declaration, or prototype, gives the signature and ends in a semicolon; the definition carries the body. Calling a function the compiler has not seen is an error: implicit declaration of function, and a prototype must always carry its full parameter list, because int area(); checks nothing. In a single file, define your helpers above main and you need no prototypes at all.
  • C passes every argument by value. A function that assigns to its parameter changes only its own copy, and the caller's variable is untouched. This is why printf takes values while scanf takes &score: reaching memory you do not own requires its address.
  • A non-void function must return on every path. Falling off the end and then using the result is undefined behaviour, reported by -Wall as control reaches end of non-void function [-Wreturn-type]. Only main gets its return 0; supplied for it.