C Part I · Module 5 · Functions and Program Decomposition
Defining and Calling a Function
The previous chapter showed what a function buys you and deliberately left the syntax vague. This one takes it apart. There are four pieces to a definition and each of them is a decision, including one that looks like pure punctuation and is not — the difference between empty parentheses and the word void inside them changes what the compiler is willing to let you do, and the C23 standard changed the rules around it in a way worth seeing demonstrated rather than described. The second half of the chapter is about the call itself: what is handed over, in what order it happens, what comes back, and which parts of that sequence C guarantees and which it deliberately leaves open.
Foundation17 min readCPart IFundamentalsFunctions
Part I · Module 5 · Chapter 5.2 · Defining and Calling a Function
1. The Four Parts
int is_even(int value)
{
return value % 2 == 0;
}int is_even (int value) { ... }
↑ ↑ ↑ ↑
return type function name parameter list bodyThe return type says what type of value a call to this function produces. Here, int. This is the same int you have been declaring objects with since Module 2 — a call to is_even is an expression of type int, and everything Chapter 3.12 said about conversions applies to it.
The name is how you call it. The naming rules are the same as for variables.
The parameter list declares the objects the function needs to be given. int value is a declaration: it creates an object of type int named value, which exists while the function runs. If a function needs two things, the parameters are separated by commas and each needs its own type:
int larger_of(int a, int b) /* correct */
int larger_of(int a, b) /* not C — b has no type */The body is a block, exactly the kind Chapter 4.1 introduced. Statements run in it top to bottom, and return ends the function.
Together, the return type, the name and the parameter types are the function's signature — the part a caller has to agree with.
2. Functions That Give Nothing Back
Some functions exist entirely for their effect. report_level in Chapter 5.1 printed something; there is no result to hand back. The return type for that is void:
void report_sample(unsigned int code, unsigned int millivolts)
{
printf("code %4u -> %4u mV\n", code, millivolts);
}void is not a type you can make objects of. It is how C says there is no value here. A call to a void function is still an expression, but one with no value, so there is nothing to use:
int x = show(5); /* show returns void */error: initializing 'int' with an expression of incompatible type 'void'A void call therefore appears as a statement on its own:
report_sample(code, mv);3. The Parameter List Is Not Optional Punctuation
A function that takes nothing is written (void):
int get_limit(void)
{
return 10;
}It is tempting to write () instead, and it is tempting to assume the two mean the same thing. On this curriculum's C17 baseline they do not, and the difference is not cosmetic.
int get_limit(void) declares a function taking no parameters. int get_limit() declares a function whose parameters are unspecified — it is not saying no arguments, it is saying I am not telling you. And a compiler cannot check calls against information it was not given.
Here is the same file, compiled twice. The function takes nothing, and the call passes three arguments:
#include <stdio.h>
int get_limit() { return 10; }
int main(void)
{
printf("%d\n", get_limit(1, 2, 3));
return 0;
}Built as C17:
warning: a function declaration without a prototype is deprecated in all
versions of C [-Wstrict-prototypes]
warning: too many arguments in call to 'get_limit'
warning: passing arguments to 'get_limit' without a prototype is deprecated in
all versions of C and is not supported in C23 [-Wdeprecated-non-prototype]Warnings only. The program builds, runs, and prints 10.
The identical file, built as C23:
error: too many arguments to function call, expected 0, have 3An error. The program does not build.
And with (void) instead of (), C17 rejects it too, immediately:
error: too many arguments to function call, expected 0, have 34. What a Call Transfers
int answer = is_even(7);Five things happen, in this order:
- The argument expressions are evaluated.
7here; in general any expression. Each produces a value, with a type. - Those values are given to the function's parameters. The parameter
valueis an object belonging to this call, and it starts out holding the argument's value. - Control leaves the call site and the function body runs, to completion.
returnends the function and names the value to hand back.- Control resumes immediately after the call, and the call expression now is that value, with the function's return type.
Step 2 is the one with consequences, and Chapter 5.3 is entirely about it. For now, note the precise wording: the parameter starts out holding the argument's value. It is an object in its own right, not the caller's object under a new name.
Step 6 is why a call can go wherever a value of that type can go:
printf("%d\n", is_even(4)); /* as an argument */
int answer = is_even(4); /* as an initialiser */
if (is_even(count)) { ... } /* as a condition */
int n = is_even(4) + is_even(6); /* inside a larger expression */Nested calls follow from the same rule — the inner call has to produce its value before the outer one can be given it:
#include <stdio.h>
int double_it(int n)
{
printf(" double_it(%d) running\n", n);
return n * 2;
}
int add_one(int n)
{
printf(" add_one(%d) running\n", n);
return n + 1;
}
int main(void)
{
printf("start\n");
int result = add_one(double_it(5));
printf("result %d\n", result);
return 0;
}Output:
start
double_it(5) running
add_one(10) running
result 11double_it(5) is an argument of add_one, so by step 1 it must be evaluated before add_one can run. Its value, 10, is what add_one's parameter starts out holding.
5. What C Does Not Promise About the Order
Step 1 says the arguments are evaluated. It does not say in which order, and neither does C.
When a call has several arguments, the order in which they are evaluated is unspecified. Nothing requires left to right, and nothing requires a compiler to be consistent about it.
Chapter 3.11 established this for operands generally; function arguments are the place you are most likely to trip over it, because it is natural to read a call as happening left to right.
#include <stdio.h>
int announce(int which)
{
printf(" evaluating argument %d\n", which);
return which;
}
int main(void)
{
printf("before the call\n");
printf("result %d and %d\n", announce(1), announce(2));
return 0;
}On clang 21, this run produced:
before the call
evaluating argument 1
evaluating argument 2
result 1 and 2That is an observation, not a rule. A different compiler, or the same compiler with different settings, may evaluate announce(2) first and print those two lines in the other order — and it would be equally correct. What is guaranteed is that both arguments are fully evaluated before printf runs, which is why result 1 and 2 always appears last and always pairs the right values.
The engineering consequence is a short one:
Do not write argument expressions whose effects depend on each other. If the order matters, evaluate them into named objects on separate lines first, where the order is written down.
Chapter 3.11 also covered the harsher case — two arguments that modify the same object are not merely unspecified but undefined, and the standard imposes no requirements on what happens at all.
6. Where a Function Must Appear
A function must be declared before it is called. In a single file, the simplest way to satisfy that is to define it above the first call:
int helper(int n) { return n + 1; } /* defined first */
int main(void)
{
return helper(3); /* then called */
}Reverse the order and C99 and later reject it:
error: call to undeclared function 'helper'; ISO C99 and later do not support
implicit function declarations [-Wimplicit-function-declaration]The message is worth reading carefully, because it describes a rule that used to be different. Very old C would assume an undeclared function returned int and accept whatever you passed it. That assumption caused enough damage that C99 removed it, and this is now an error rather than the silent guess it once was.
Defining above the first call is a real solution and it is what every example in this chapter does. It is not the general one — it fails as soon as two functions need to call each other, and it forces you to order a file by the compiler's needs rather than by what a reader wants to meet first. Chapter 5.5 introduces the mechanism that removes the constraint.
7. A Realistic Set of Functions
#include <stdio.h>
unsigned int clamp_code(unsigned int code)
{
if (code > 4095u) {
return 4095u;
}
return code;
}
unsigned int code_to_millivolts(unsigned int code)
{
return (code * 3300u) / 4095u;
}
void report_sample(unsigned int code, unsigned int millivolts)
{
printf("code %4u -> %4u mV\n", code, millivolts);
}
int main(void)
{
unsigned int raw = 2048u;
unsigned int code = clamp_code(raw);
unsigned int mv = code_to_millivolts(code);
report_sample(code, mv);
report_sample(clamp_code(5000u), code_to_millivolts(clamp_code(5000u)));
return 0;
}Output:
code 2048 -> 1650 mV
code 4095 -> 3300 mVSeveral decisions in that program are worth naming.
clamp_code and code_to_millivolts return a value and have no effect; report_sample has an effect and returns nothing. Keeping those two kinds of function apart is one of the most useful habits in the module, and Chapter 5.9 makes the case for it properly.
The parameter types are unsigned int throughout, matching the values being passed, so no conversion happens at the call. If clamp_code took an int and you passed an unsigned int, Chapter 3.12's conversions would apply at step 2 — silently. Argument passing is one more place where types matter.
The arithmetic in code_to_millivolts multiplies before dividing, which Chapter 3.3 explains: integer division truncates, so dividing first would throw away the precision the multiplication needed.
And the last line is deliberately written badly. clamp_code(5000u) appears twice, doing the same work twice, and the two arguments are evaluated in an order §5 says you cannot predict. It happens to be correct here because neither call has any effect — but the first version, which names code and mv on separate lines, is the one to copy.
8. A Misconception About Names
9. What You Should Be Able to Say Now
- A definition has four parts: return type, name, parameter list, body. The first three are its signature.
- Each parameter needs its own type;
(int a, b)is not C. voidas a return type means the call produces no value — using one is a compile error.(void)means no parameters.()in C17 means unspecified, so calls are not checked.- C23 redefined
()to mean(void)— the same file warns under C17 and is an error under C23. - A call evaluates the arguments, gives their values to the parameters, runs the body, and resumes after the call.
- A call is an expression with the function's return type, so it goes anywhere that type is allowed.
- The order in which arguments are evaluated is unspecified — but all of them are evaluated before the function runs.
- A function must be declared before it is called; implicit declarations were removed in C99.
- Parameter names are internal. Only the signature crosses the boundary.
10. Exercises
Exercise 1 — predict the output. Do not compile yet.
#include <stdio.h>
int half(int n)
{
return n / 2;
}
int main(void)
{
printf("%d\n", half(10));
printf("%d\n", half(7));
printf("%d\n", half(half(20)));
return 0;
}The second line needs Chapter 3.3. Say which rule gives the answer.
Exercise 2 — fix the signatures. Each of these is rejected or unsafe. Say what is wrong and write the corrected line.
int add(int a, b)
void report(int n) { return n; }
int get_count() { return 7; }
int scale(int n) { return n * 2 }Exercise 3 — explain the two builds. §3's program warns under C17 and fails under C23. Explain what () means in each standard, and say why the C17 version was allowed to run despite being obviously wrong.
Exercise 4 — trace the nesting. Give the exact output, in order, and explain which rule determines it.
#include <stdio.h>
int f(int n) { printf("f(%d)\n", n); return n + 1; }
int g(int n) { printf("g(%d)\n", n); return n * 10; }
int main(void)
{
printf("answer %d\n", f(g(2)));
return 0;
}Exercise 5 — classify the behaviour. For each, say whether the result is guaranteed, unspecified, or undefined, and why.
printf("%d %d\n", f(1), f(2));wherefprints and returns its argumentprintf("%d %d\n", i++, i++);int x = f(1) + f(2);wherefhas no effects at allint y = f(g(3));
Exercise 6 — repair the call site. §7's last line calls clamp_code(5000u) twice. Rewrite those two lines so the clamping happens once, and give two reasons the rewrite is better — one about duplicated work and one about §5.
Exercise 7 — find the error. This does not compile. Quote what the compiler objects to, explain the rule, and give two different repairs — one that moves code and one that does not.
int main(void)
{
printf("%d\n", square(4));
return 0;
}
int square(int n)
{
return n * n;
}Exercise 8 — write two functions. Write int clamp(int value, int low, int high) returning the value limited to the range, and void describe(int value, int clamped) printing both. Call them from main for values below, inside and above the range. State what each function returns for value exactly equal to low and to high, before you compile.
11. Summary
- A function definition is return type, name, parameter list, body; the first three form the signature.
voidmeans no value here — as a return type it makes the call unusable as a value.(void)is not decoration. In C17()means unspecified parameters and disables argument checking; C23 redefined it to mean no parameters.- A call evaluates the arguments, hands their values to the parameters, runs the body, and resumes after the call.
- A call is an expression of the function's return type.
- Arguments are all evaluated before the body runs, but their relative order is unspecified.
- A function must be declared before it is called — implicit declarations have been gone since C99.
- Parameter names are private to the function. Only the signature is shared.
Step 2 of the call sequence was stated carefully and then left alone: the parameter starts out holding the argument's value. That phrasing was chosen over the more natural-sounding alternative — that the function is given your variable — because the difference between the two is the single most consequential rule about calls in C, and getting it wrong produces a class of bug that looks like the language is broken. The next chapter is about nothing else.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
