Chapter 07 of 24
7. Functions and program structure
Give a task a name
A function is a named piece of work. Instead of repeating a calculation in several places, define it once and call it whenever that task is needed. A function can receive inputs called parameters and can send one result back to its caller.
- Describe one small task, such as converting minutes to seconds.
- Choose the input and result types, then write a function declaration (prototype) that tells callers its contract.
- Write the function body, call it with an input, and use the returned value.
Keeping each function focused makes a program easier to test and explain. The sections that follow build from ordinary functions to error reporting, recursion, and callbacks.
Functions split work into named, reusable units.
int square(int value)
{
return value * value;
}
Declare a function before it is used, often with a prototype:
int square(int value);
C passes arguments by value: a function receives copies of argument values.
To let a function modify a caller's object, pass its address using a pointer.
Every non-void function must return a value of its declared return type on
every path that reaches the end.
Functions should have clear responsibilities and validate assumptions at
boundaries. Avoid overly long functions and hidden reliance on global state.
Parameters, return values, and prototypes
A function declaration (prototype) tells the compiler the function's name,
return type, and parameter types. A definition supplies the body. Put a
prototype before the first call or include a header that declares it. In a
prototype, int calculate(void) means no parameters; int calculate() in
older C styles does not provide that same explicit guarantee.
Each argument is evaluated and its value is passed to the function. Changes
to a parameter variable do not change the caller's variable. A pointer
parameter still passes a pointer value, but dereferencing that pointer lets
the function access the caller's object:
#include <stddef.h>
void set_to_zero(int *value)
{
if (value != NULL) {
*value = 0;
}
}
The caller can use set_to_zero(&number);. This illustrates both the
address-of operator and a useful pointer precondition check.
Design a function
Give a function one clear job, choose names that describe that job, and state
what its return value means. A function that calculates a value should usually
return that value rather than print it; separating calculation from display
makes the function reusable and testable. Decide how failure is represented:
a status return, an output parameter, or a documented valid range are
common approaches.
Variables declared inside a function are local to it. When a function returns,
its automatic local objects cease to exist. Returning a pointer to a local
automatic variable is therefore invalid; return a value, let the caller
provide storage, or allocate storage with an explicit ownership rule.
#include <stdbool.h>
#include <limits.h>
bool rectangle_area(int width, int height, int *area)
{
if (width < 0 || height < 0 || area == NULL) {
return false;
}
if (height != 0 && width > INT_MAX / height) {
return false;
}
*area = width * height;
return true;
}
This function calculates and returns a status rather than printing. Its
output parameter receives the area only when dimensions are nonnegative and
the product fits in int.
Checkpoint: Write a prototype and definition for a function that converts
minutes to seconds. Decide whether it should print or return its answer, and
explain why.
Function results and failure
A function's return type is part of its interface. Use a meaningful result
type for successful calculations. If the operation can fail, make that
failure visible instead of returning a value that could be mistaken for
success. A common pattern is a status return with the actual result written
through an output parameter:
#include <stdbool.h>
#include <stddef.h>
bool divide(double numerator, double denominator, double *result)
{
if (result == NULL || denominator == 0.0) {
return false;
}
*result = numerator / denominator;
return true;
}
The caller checks the status before using the output:
double quotient;
if (divide(10.0, 4.0, "ient)) {
printf("%.2f\n", quotient);
} else {
puts("Division could not be performed.");
}
This interface has a clear contract: result must point to a writable
double, and the function writes it only on success. A different design
could return an error code and use an output parameter, or return a
sentinel such as NULL when that cannot be confused with a valid result.
Choose one convention and document it.
#include <stdbool.h>
#include <stdio.h>
bool divide(double numerator, double denominator, double *result)
{
if (result == NULL || denominator == 0.0) {
return false;
}
*result = numerator / denominator;
return true;
}
int main(void)
{
double quotient;
if (!divide(10.0, 4.0, "ient)) {
return 1;
}
printf("%.2f\n", quotient);
return 0;
}
Input parameters and output parameters
Use ordinary values for small inputs such as integers and floating-point
numbers. Use a pointer to const for a large read-only object, or for an
array whose elements must not be changed. Use a non-const pointer when the
function intentionally writes through it. Pointer parameters should state
their requirements: may the pointer be NULL? How many elements are
available? Can the function retain the pointer after returning? Is ownership
transferred?
For arrays, pass both the pointer and the valid element count. A pointer
alone does not carry the allocation length. When a function writes into a
caller-provided array, also pass its capacity and return how many elements
were produced or whether the operation succeeded.
#include <stdbool.h>
#include <stddef.h>
bool fill_indices(size_t output[], size_t capacity, size_t count)
{
if (output == NULL || count > capacity) {
return false;
}
for (size_t i = 0; i < count; ++i) {
output[i] = i;
}
return true;
}
The caller supplies the storage and its capacity. The function neither
allocates nor retains the pointer, and it rejects a request that would write
beyond that storage.
Recursion
A recursive function calls itself. Every recursive design needs:
- A base case that returns without another recursive call.
- A progress step that makes the next input closer to a base case.
- A proof or convincing argument that progress eventually reaches the base.
For example, summing from 1 to n can be defined as n + sum_to(n - 1),
with zero as the base case. Recursion can make tree traversal natural, but
deep recursion can exhaust the call stack. For simple counting or large
inputs, an iterative solution may use less stack space.
unsigned long long factorial(unsigned int n)
{
if (n < 2u) {
return 1u;
}
return n * factorial(n - 1u);
}
This illustrates a base case and progress toward it, but the result can
overflow for sufficiently large n; a real interface should define and
check its supported input range. The recursive calls also consume stack
space.
Function pointers and callbacks
A function pointer stores the address of a function with a particular
signature. Its type must match the function that will be called:
int compare_ints(int left, int right)
{
return (left > right) - (left < right);
}
int main(void)
{
int (*comparison)(int, int) = compare_ints;
int order = comparison(3, 5);
return order < 0 ? 0 : 1;
}
Function pointers allow a caller to supply behavior, such as a comparison
rule or event callback. They are different from object pointers: standard C
does not promise that a function pointer can be converted to void *.
Callbacks must obey the documented signature and lifetime rules.
Function design checklist
Before implementing a function, write down:
- Its one responsibility and its name.
- The meaning and valid range of each parameter.
- Whether each pointer may be null and how many elements it addresses.
- The return value on success and the way failure is reported.
- Whether it changes caller-owned data or takes ownership of memory.
- What happens at boundary values such as zero, empty input, and maximum size.
Prefer a function that is easy to explain in a few sentences. If it has many
unrelated responsibilities or many parameters, consider splitting it into
smaller operations or grouping related data in a structure.
#include <stdbool.h>
#include <stddef.h>
// Contract: return true and store the maximum for non-empty input.
// Return false for a null pointer, empty input, or null output.
bool maximum(const int values[], size_t length, int *result)
{
if (values == NULL || length == 0 || result == NULL) {
return false;
}
int current = values[0];
for (size_t i = 1; i < length; ++i) {
if (values[i] > current) {
current = values[i];
}
}
*result = current;
return true;
}
Exercises: write functions for minimum, maximum, and average; split a
calculator into one function per operation; make a function that reports
whether an integer is prime.