Writing Functions
Declare, define, and call your own functions: prototypes, parameters, return values, void, and the fact that every argument arrives as a copy.
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
voidreturn 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 onint 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, becauseint area();checks nothing. In a single file, define your helpers abovemainand 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
printftakes values whilescanftakes&score: reaching memory you do not own requires its address. - A non-
voidfunction must return on every path. Falling off the end and then using the result is undefined behaviour, reported by-Wallascontrol reaches end of non-void function [-Wreturn-type]. Onlymaingets itsreturn 0;supplied for it.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Writing Functions - Quiz
Test your understanding of the lesson.
Practice Exercises
Filling In Two Function Bodies
This exercise is the other way round from every one before it. main is already written for you, and it will not change: it calls two functions that do not work yet, and your job is to make them work. Do not edit main, and do not move the two functions below it, because a definition placed after its call is a function the compiler has not heard of yet. print_header takes nothing and returns nothing, which is what the void before its name and the (void) between its parentheses each say, and it must print two lines, "clamp report" and then twelve dashes. clamp takes three int values and returns one: low when value sits below low, high when value sits above high, and value itself when it already lies between the two. That is the whole of clamping, and the shape is already in the template as two ifs and a final return, each of them currently returning the wrong thing. Leave every one of those returns in place and change what it hands back, because a non-void function has to return a value on every path through it, and falling off the end and then using the result is undefined behaviour rather than a zero. Watch the boundaries: a value exactly equal to low or to high is already inside the range and must be returned untouched, so the comparisons are < and >, not <= and >=. main reads the three numbers with one scanf and checks its conversion count first, exactly as chapter 2 taught, and prints an error line when three numbers did not arrive. Note that the header prints before that check, so it appears on every run including the failing one. Every path out of main returns 0, the error path included, because the checker compares what you print and reports a nonzero exit status as a failure however right the output looks.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!