C / FUNCTIONS
Defining and calling functions
Define your own C functions with a matching return type and parameter list, then call them anywhere a value of that type belongs.
What you will learn
- Write a definition as return type, name, parameter list, then a braced body
- End a value-returning function with return expr; use bare return only in void functions
- Use a call as an expression: store it, nest it inside another call, or discard it
- Give a zero-parameter function the list (void) so wrong argument counts are caught
Understanding Defining and calling functions
A definition is four things in a fixed order: the type of the value the function hands back, its name, a parenthesized list of parameters where every parameter carries its own type, and a body in braces. That header line is the contract, and once the compiler has read it every call is checked against it, which is why int celsius_to_f_tenths(int c_tenths) can be called with 215 but not with two arguments or with a string. Nothing goes between the closing parenthesis and the opening brace; a semicolon there means something else entirely.
Calling is an expression, not a statement. When control reaches f(x), the argument expressions are evaluated, each parameter is created as a local variable holding a copy of the matching argument, the body runs, and the first return reached ends the function with its operand becoming the value of the whole call expression. Because that expression has a type, it can appear wherever a value of that type can: an initializer, one side of a subtraction, or an argument to another call. A void function produces no value, so the only thing you can do with such a call is write it as a statement of its own.
A function has one entrance and as many exits as it has return statements, plus the closing brace. For a void function that brace is a perfectly good exit and bare return; is an early one. For any other return type, reaching the brace without returning means the caller receives a value nobody supplied, so treat every path through the body as obliged to end in a return of the declared type. main is the exception the standard grants: falling off its end returns 0.
<stdio.h>
int celsius_to_f_tenths(int c_tenths)
{
return c_tenths * 9 / 5 + 320;
}
void print_reading(const char *label, int f_tenths)
{
printf("%s: %d.%d F\n", label, f_tenths / 10, f_tenths % 10);
}
int main(void)
{
int morning = celsius_to_f_tenths(215);
print_reading("morning", morning);
print_reading("noon", celsius_to_f_tenths(302));
printf("difference: %d tenths\n", celsius_to_f_tenths(302) - morning);
return 0;
}
A function definition is a contract the compiler enforces at every call site, and a call is an expression whose value is whatever the function returns.
Worked examples
A void function and an early return
Shows that a void call can only be a statement, and that bare return leaves the function without supplying a value.
<stdio.h>
void print_bar(const char *name, int width)
{
if (width <= 0) {
printf("%s: (empty)\n", name);
return;
}
printf("%s: ", name);
for (int i = 0; i < width; i++)
putchar('#');
putchar('\n');
}
int main(void)
{
print_bar("cpu", 5);
print_bar("net", 0);
print_bar("disk", 2);
return 0;
}
Example explained
Line 1The return type void says the call yields nothing, so print_bar("cpu", 5); can never be assigned or nested in an expression.
Line 2return; with no operand is legal only because the type is void, and it ends the function on the spot.
Line 3The width 0 call takes that early exit, which is why the loop and the trailing newline are skipped for the net line.
Line 4const char *name and int width are two parameters, each with its own type; a parameter list never shares one type keyword.
A call is an expression
Demonstrates the three fates of a return value: stored, discarded, or fed straight into another call.
<stdio.h>
int square(int n)
{
return n * n;
}
int main(void)
{
int written = printf("hello\n");
printf("printf returned %d\n", written);
square(9);
printf("%d\n", square(square(2)));
return 0;
}
Example explained
Line 1printf is itself an ordinary function with a return value, so int written = printf(...) records the 6 characters it wrote.
Line 2square(9); is a complete statement: the body runs and produces 81, but nothing consumes it and the value is dropped.
Line 3In square(square(2)) the inner call must finish first, because its result is the argument the outer call needs.
Line 4The definition of square never changes across these uses; the call site alone decides what happens to the returned value.
Important notes
Before C23, void f() and void f(void) differ: the empty list gives the compiler no parameter information to check against, so a bogus call like f(1, 2) can slip through. Write (void) when a function takes nothing.
The order in which a call's arguments are evaluated is unspecified, so never rely on two arguments of the same call running left to right when both have side effects.
Common mistakes
Putting a semicolon after the header line, as in int add(int a, int b); { return a + b; } — that is now a declaration followed by a stray block, and the error message points at the brace instead of the semicolon that caused it.
Letting one branch of a non-void function fall off the closing brace without returning: the caller then uses a value that was never supplied, so the program can look correct in one build and produce nonsense in the next.
Writing the function name without parentheses, as in int x = square; or if (validate) — no call happens at all, since you are copying or testing the function's address, and the always-true test can hide for a long time.
Try it yourself
Change, predict, then run
Write int clamp(int v, int low, int high) that returns v forced into the range low..high, and a void report(int v) that prints v followed by clamp(v, 0, 10). Call report(-3), report(7) and report(42) from main.
Open the C workspaceCheck your understanding
Given int bump(int n) { return n + 1; }, a line in main reads bump(3); and the result is not stored anywhere. What happens?
- A compile error, because the returned value is not used
- The body is skipped, since nothing needs the result
- The body runs, the value 4 is produced, and then it is discarded
- Undefined behavior, because a return value must always be consumed
Show answer
A call is an expression, and any expression followed by a semicolon is a valid statement, so ignoring a return value needs no cast and no variable. Option 1 is the tempting one, but a call's effects are not optional: the body executes, which matters as soon as the function prints or writes through a pointer, and an optimizer may erase the call only when it can prove nothing observable happens inside.