Functions
Function declarations, definitions, calls, parameters, return values, pass-by-value, scope, storage duration, decomposition, and side effects.
Functions let a program replace a sequence of details with a meaningful operation. A good function is not merely code moved elsewhere: it has a clear contract, one coherent responsibility, explicit inputs and outputs, and limited dependence on hidden state.
Function Anatomy
#include <stdbool.h>
bool is_leap_year(int year) {
return year % 400 == 0 ||
(year % 4 == 0 && year % 100 != 0);
}
This definition contains:
- the return type
bool; - the function name
is_leap_year; - one parameter declaration,
int year; - a compound-statement body;
- a
returnstatement whose expression supplies the result.
The function’s informal contract is: given a year in the intended calendar, report whether it satisfies the Gregorian leap-year rule. If the application restricts supported years, that precondition must be stated or checked.
Function Prototypes
A prototype declares a function’s interface before use:
double area_rectangle(double width, double height);
It lets the compiler check argument count and types and understand the return type. Parameter names are optional in a prototype, but meaningful names document roles:
double area_rectangle(double, double); /* valid */
double area_rectangle(double width, double height); /* clearer */
Use void to state explicitly that a function accepts no arguments:
void print_banner(void);
An empty parameter list in a C declaration, void print_banner();, means the parameter information is unspecified, not necessarily no parameters.
Project interfaces normally belong in headers; Chapter 14 develops that design. A static helper used only within one source file can be declared near the top of that file.
Function Definitions
A definition provides the body:
double area_rectangle(double width, double height) {
return width * height;
}
The definition must be compatible with every visible declaration. A mismatch is a constraint violation and should be diagnosed.
Write the contract before the mechanics:
/* Returns true exactly when value is within the inclusive range.
Precondition: minimum <= maximum. */
bool is_within(int value, int minimum, int maximum) {
return minimum <= value && value <= maximum;
}
Contracts may state units, valid ranges, ownership, mutation, failure signalling, and relationships between parameters. Comments need not restate facts already explicit in types and names.
Function Calls
A call evaluates argument expressions, initializes parameter objects with the converted values, executes the function body, and yields the returned result:
double room_area = area_rectangle(4.5, 3.2);
Do not depend on argument evaluation order:
display(next_value(), next_value()); /* either call may happen first */
Sequence stateful operations first:
int first = next_value();
int second = next_value();
display(first, second);
Calling a function adds conceptual overhead even when a compiler later inlines machine instructions. The gain should be a useful abstraction, reuse, test boundary, or separation of responsibility.
Parameters
Parameters are local objects initialized from arguments:
int clamp(int value, int minimum, int maximum) {
if (value < minimum) {
return minimum;
}
if (value > maximum) {
return maximum;
}
return value;
}
Parameter order should be predictable and documented. Group related values, avoid long lists, and use distinct types or a structure when several same-typed values can be confused.
Validate a precondition at the correct layer. A general-purpose public function may reject minimum > maximum; a private helper called only after validation may state that relationship as a requirement.
Pass-by-Value
C passes every argument by value. The parameter receives a copy of the argument’s value:
#include <stdio.h>
void add_one(int value) {
value++;
printf("inside: %d\n", value);
}
int main(void) {
int number = 10;
add_one(number);
printf("outside: %d\n", number);
return 0;
}
Output:
inside: 11
outside: 10
Trace:
| Moment | number in main | value in add_one |
|---|---|---|
| before call | 10 | does not exist |
| function entry | 10 | 10 |
after value++ | 10 | 11 |
| after return | 10 | lifetime ended |
Later, pointers allow a copied pointer value to designate a caller’s object. That is still pass by value; the pointer itself is copied.
Return Values
A non-void function returns a value compatible with its declared return type:
double celsius_to_fahrenheit(double celsius) {
return celsius * 9.0 / 5.0 + 32.0;
}
Reaching the end of a value-returning function other than main without returning a value produces undefined behaviour if the caller uses the result. Enable return-type warnings.
void functions perform an action without producing a value:
void print_rule(void) {
puts("--------------------");
}
They may use return; for an early exit.
Failure Signalling
A function must distinguish valid results from failure. Common C designs include:
- return a status and write the result through a pointer;
- return a sentinel value that cannot be a valid result;
- return an enumeration describing outcomes;
- return a structure containing status and value.
Do not use an ambiguous sentinel. If every int is valid data, returning -1 cannot also mean failure without another signal.
Local Variables
A variable declared inside a block has block scope from its declaration to the block’s end:
/* Precondition: values designates count elements and count > 0. */
double mean(const double values[], size_t count) {
double total = 0.0;
for (size_t i = 0; i < count; i++) {
total += values[i];
}
return total / (double)count;
}
total is visible through the remainder of the function block. The loop’s i is visible only within the for statement. Narrow scope prevents unrelated code from modifying state.
An inner declaration can hide an outer name:
int count = 10;
{
int count = 2; /* different object; hides outer count */
}
Legal shadowing often confuses readers. Use distinct names unless a narrow, conventional shadow is unquestionably clear.
Global Variables
An object declared outside every function has file scope:
int request_count = 0; /* externally linked unless constrained */
Any function with access can change it, so behaviour depends on hidden shared state. This complicates testing, reuse, concurrency, and reasoning.
Prefer passing input and returning results. When persistent file-private state is justified, constrain visibility:
static unsigned long request_count;
Then expose purposeful functions rather than the object itself:
unsigned long current_request_count(void) {
return request_count;
}
Global constants can also be overused. A value that varies by call is a parameter, not configuration hidden at file scope.
Scope
Scope is where a name can be used to denote its entity.
Block Scope
Parameters and names declared inside blocks have block scope. A parameter is visible throughout its function definition. A declaration in an inner block exists in a narrower textual region.
File Scope
A name declared outside every function has file scope from its declaration to the end of the translation unit. File scope concerns name visibility, not automatically program-wide accessibility; linkage determines whether declarations in different translation units denote the same entity.
Function names normally have file scope. static can give a file-scope function internal linkage:
static bool is_separator(int ch) {
return ch == ',' || ch == ';';
}
Chapter 14 separates scope, linkage, and storage duration fully.
Storage Duration
Storage duration answers when an object’s storage exists.
Automatic Duration
Ordinary block-local objects have automatic storage duration. Storage begins when execution enters the block and ends when it leaves:
void record_sample(void) {
int sample_count = 0;
sample_count++;
printf("%d\n", sample_count); /* always 1 per call */
}
Each call creates a fresh sample_count. Its initial value must be supplied each time.
Static Duration
File-scope objects and block objects declared static exist for the program’s entire execution. They are zero-initialized before program startup if no explicit initializer is supplied:
unsigned long next_ticket(void) {
static unsigned long ticket = 0;
ticket++;
return ticket;
}
The name ticket has block scope, but the object has static storage duration. Repeated calls observe shared persistent state. This function is harder to reset and is not safely independent across concurrent callers; persistence is a design consequence, not a free convenience.
Function Decomposition
Decomposition should follow responsibilities, not arbitrary line counts. Consider processing one input line:
read line → parse fields → validate record → compute result → print result
Each arrow suggests a contract boundary. A high-level function can read like a plan:
int process_order(const char *line) {
struct Order order;
if (!parse_order(line, &order)) {
return STATUS_INVALID_FORMAT;
}
if (!is_valid_order(&order)) {
return STATUS_INVALID_VALUE;
}
print_invoice(&order);
return STATUS_OK;
}
The types used here are developed later; the design lesson is that parsing, validation, and presentation are different reasons to change.
Cohesion
A cohesive function performs one conceptual task. “Read a number and sort an array and write a report” is three responsibilities. A function named process_data often hides an opportunity for better names and boundaries.
Interface Size
Minimise the information a function needs. Passing six unrelated flags makes call sites cryptic. Group genuinely related data in a record or introduce several focused functions.
Extraction Limits
Do not extract every two-line calculation. A helper adds value when it names a concept, eliminates meaningful duplication, creates a test seam, or isolates an implementation detail.
Side Effects
A side effect changes observable state: modifying an object beyond the function’s locals, writing output, reading input, changing a file, or updating static state.
Compare:
double area(double width, double height) {
return width * height;
}
with:
void print_area(double width, double height) {
printf("%.2f\n", width * height);
}
The first separates computation from presentation and is easier to reuse and test. The second may be appropriate at the UI boundary, but its effect is output rather than a reusable value.
Side effects are necessary—useful programs interact with the world. Control them by:
- making effects visible in names and contracts;
- keeping computation separate from I/O;
- avoiding mutable global state;
- not depending on unspecified argument evaluation order;
- returning statuses for effects that can fail.
Decomposition Case
Initial design:
#include <stdio.h>
int main(void) {
double width;
double height;
if (scanf("%lf%lf", &width, &height) != 2) {
fputs("invalid input\n", stderr);
return 1;
}
if (width > 0 && height > 0) {
printf("%.2f\n", width * height);
} else {
puts("invalid");
}
return 0;
}
Responsibilities are entangled. A clearer design separates computation:
static bool has_positive_dimensions(double width, double height) {
return width > 0.0 && height > 0.0;
}
static double rectangle_area(double width, double height) {
return width * height;
}
Now input code can validate acquisition, the predicate can be tested at zero and negative boundaries, and the computation can be reused by other interfaces. The functions do not make the tiny program shorter; they make responsibilities explicit.
Calendar Case
Calendar rules are a good decomposition exercise because each function can express one question with no input/output side effects. This complete program validates a date by building small contracts from smaller ones.
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
static bool is_leap_year(int year) {
return year % 400 == 0 ||
(year % 4 == 0 && year % 100 != 0);
}
static int days_in_month(int month, int year) {
switch (month) {
case 2:
return is_leap_year(year) ? 29 : 28;
case 4:
case 6:
case 9:
case 11:
return 30;
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
return 31;
default:
return 0;
}
}
static bool is_valid_date(int day, int month, int year) {
int limit = days_in_month(month, year);
return year >= 1 && limit != 0 && day >= 1 && day <= limit;
}
static void print_date_result(int day, int month, int year) {
printf("%04d-%02d-%02d: %s\n",
year, month, day,
is_valid_date(day, month, year) ? "valid" : "invalid");
}
int main(void) {
print_date_result(29, 2, 2000);
print_date_result(29, 2, 1900);
print_date_result(31, 4, 2028);
print_date_result(30, 4, 2028);
return EXIT_SUCCESS;
}
The functions form a dependency direction:
main
|
v
print_date_result performs output
|
v
is_valid_date combines domain rules
|
v
days_in_month selects a month length
|
v
is_leap_year answers one arithmetic predicate
Lower functions do not call upward into presentation. That one-way dependency keeps computation reusable. A graphical interface, file importer, and unit test can all call is_valid_date without capturing printed text.
Contract Stack
is_leap_year accepts every int mechanically, but the calendar model later rejects years below one. Its return value is true exactly for years divisible by 400 or divisible by 4 but not 100.
days_in_month uses 0 as an invalid-month result because no valid month has zero days. Its postcondition can be stated precisely:
result is 28, 29, 30, or 31 when 1 <= month <= 12
result is 0 otherwise
is_valid_date first obtains that result, then validates year and day. The order in its Boolean expression is safe because day <= limit is harmless even when limit is zero; no dangerous operation depends on short-circuiting. The local name limit also prevents two calls to days_in_month and gives the intermediate concept a visible identity.
The output function receives a complete date as three scalar copies. Assigning to day inside it would not change any caller object. Its side effect is explicit in the verb print; the three computational functions are pure for the same input values.
Call Trace
For print_date_result(29, 2, 1900), the active calls evolve as follows:
| Event | Active call | Important local or result |
|---|---|---|
| enter | print_date_result(29,2,1900) | parameters are local copies |
| call | is_valid_date(29,2,1900) | needs month limit |
| call | days_in_month(2,1900) | February branch |
| call | is_leap_year(1900) | divisible by 100, not 400: false |
| return | days_in_month | returns 28 |
| evaluate | is_valid_date | 29 <= 28 is false |
| return | print_date_result | prints invalid |
Each invocation owns its parameters and automatic locals until it returns. A debugger’s call stack displays this same nesting. “The function returns to its caller” means control resumes at the call expression that was waiting for the result.
Boundary Matrix
Examples are stronger when chosen from rules rather than convenience. For the calendar functions, a compact test matrix is:
| Case | Expected reason |
|---|---|
2000-02-29 | century divisible by 400 |
1900-02-29 | century not divisible by 400 |
2024-02-29 | ordinary leap year |
2023-02-29 | ordinary non-leap year |
2028-04-30 | final valid day of 30-day month |
2028-04-31 | one above month limit |
month 0 or 13 | invalid selection |
day 0 | below valid lower bound |
year 0 | outside chosen calendar model |
This matrix reveals a design decision: the code uses a proleptic Gregorian rule for all positive years. If a historical application needs calendar transitions by country, the same function name would hide a much more complex contract. Decomposition does not remove domain ambiguity; it gives that ambiguity a place to be specified.
Interface Review
Before accepting a function, read it from the caller’s side:
- Can every argument combination be represented by the parameter types?
- Which combinations are meaningful, and how are invalid ones reported?
- Is every result value distinguishable from failure?
- Which external state can the call read or change?
- Does the caller retain any pointer or resource whose lifetime the function affects?
- Is the function’s cost surprising relative to its name?
Then read it from the implementation side: are all paths covered, are preconditions checked before dependent operations, and does failure leave externally visible state well defined? This two-sided review turns a prototype from syntax into an agreement.
Result Channels
A function can communicate several kinds of outcome, and the interface should keep them distinguishable.
Direct Value
Return the computed value when every call has a natural result or when a reserved result can represent failure without ambiguity:
double celsius_to_fahrenheit(double celsius) {
return celsius * 9.0 / 5.0 + 32.0;
}
This function has no ordinary failure for finite input under a contract that accepts floating overflow behaviour. The return expression is the result.
Status Plus Output
When all values of the result type are valid, return success separately and write the result only on success:
bool checked_divide(int numerator, int denominator, int *quotient) {
if (quotient == NULL || denominator == 0 ||
(numerator == INT_MIN && denominator == -1)) {
return false;
}
int candidate = numerator / denominator;
*quotient = candidate;
return true;
}
The local candidate is not strictly necessary for this one calculation, but it reinforces a transaction: validate, compute, then publish. The caller can retain its former output value after false.
Status Enumeration
A Boolean loses the reason for failure. A parser may need to distinguish empty input, invalid syntax, and out-of-range data. An enumeration or named integer statuses can provide those outcomes. The layer with user context turns status into one diagnostic; low-level functions need not print.
Returning a structure can combine status and value once structures have been introduced. Allocating a result transfers ownership questions to the interface. Choose the simplest channel that represents every valid result and required failure distinctly.
Parameter Boundary
Arguments are evaluated in the caller, converted to parameter types, and copied into a new invocation. The order among separate argument evaluations is generally unspecified:
report(next_value(), next_value());
If the calls change shared state, split them into named statements before calling report. The function cannot control which argument was evaluated first.
Parameter declarations also describe permission:
size_t text_length(const char text[]);
void uppercase(char text[]);
Both array parameters adjust to pointers. The first should use const char text[] if it does not modify characters; the second advertises mutation. Neither declaration communicates array capacity, so another parameter or termination contract remains necessary.
Pass small scalar values by value. Passing const int * merely to avoid copying one int adds null/lifetime/alias questions without meaningful savings. Pass larger records by pointer when copying cost or mutation semantics justify it, and measure rather than guessing when performance matters.
Persistent State
A static local can retain state while hiding its name from other functions:
unsigned long issue_ticket(void) {
static unsigned long next = 1;
return next++;
}
This interface has hidden consequences:
- tests cannot easily reset the sequence;
- independent consumers share one counter;
- overflow policy is absent;
- concurrent calls may race without synchronization;
- reproducing a result depends on prior call history.
An explicit state object makes the dependency visible:
typedef struct {
unsigned long next;
} TicketSource;
Records appear later, but the design principle already applies: hidden persistent state behaves like an implicit parameter and result. Use it for genuine process-wide identity or caching under a documented policy, not to avoid passing one value.
Call-Site Readability
Function names and argument order should let a call explain itself:
copy_range(destination, destination_capacity, source, source_length);
Four adjacent integer-like quantities are still easy to swap. A record can group related values later; today, choose consistent ordering and avoid Boolean arguments whose meaning is invisible:
print_report(data, true, false); /* what do the flags mean? */
Separate functions, a named mode, or a configuration record gives each choice vocabulary. Narrow interfaces help experienced programmers move quickly because fewer hidden combinations require reconstruction.
Contract Notes
A useful function comment records facts the type cannot:
/* Computes the mean of values[0..count).
Requires count > 0 and values to designate count doubles.
On success writes result and returns true.
On failure leaves *result unchanged. */
bool mean(const double values[], size_t count, double *result);
Do not repeat “count is a size_t” or narrate the loop. State valid ranges, ownership, units, alias restrictions, failure effects, and complexity when surprising.
Comments can drift. Assertions and tests encode parts of the contract, while header declarations keep call types synchronized. None replaces the others: prose explains domain meaning, code enforces available checks, and tests preserve examples and boundaries.
When a contract becomes difficult to state in a few precise sentences, the interface may combine too many responsibilities or lack a type that represents its states.
Interface Failures
- Calling before a valid declaration: put shared prototypes in headers and private prototypes before use.
- Writing
f()when no arguments are intended: usef(void)in C. - Expecting a scalar parameter modification to affect the caller: parameters receive copied values.
- Falling off a value-returning function: return a value on every reachable path.
- Returning an ambiguous failure sentinel: design a separate status channel.
- Using mutable global objects as implicit parameters: pass dependencies explicitly.
- Confusing scope with lifetime: a static local name is narrow even though its object persists.
- Extracting functions by line count: decompose by contracts and responsibilities.
- Hiding I/O inside computation: separate effects when reuse or testing benefits.
Function Reasoning
- A prototype declares a function contract; a definition supplies its body; a call transfers control with converted argument values.
- C passes all arguments by value.
- Return values are the clearest result channel when one value suffices.
- Scope controls where a name is visible; storage duration controls how long an object exists.
- Automatic locals are fresh per block execution; static objects persist for the program’s lifetime.
- Mutable global state creates hidden coupling and should be constrained or eliminated.
- Good decomposition creates cohesive functions with narrow interfaces and visible effects.
Function Design
Read the Interface
- Distinguish a declaration, prototype, definition, and call.
- Explain why pointer parameters do not make C a pass-by-reference language.
- Distinguish block scope, file scope, automatic duration, and static duration.
Follow the Calls
- Trace two calls to a function with a static local counter, then two calls to an equivalent function with an automatic local counter.
- Predict the caller’s value after passing an
intto a function that assigns and increments its parameter.
Repair the Contract
- Correct a non-
voidfunction that returns only from one branch. - Find a prototype/definition mismatch involving
floatanddoubleparameters. - Refactor a function that returns
-1both for failure and as a valid computed result.
Extract Functions
- Write and test
bool is_within(double value, double minimum, double maximum)with a documented precondition. - Write
int days_in_month(int month, int year), using a separate leap-year function and an explicit error policy for invalid months. - Create a statistics program with separate functions for input, minimum, maximum, mean, and output. Keep computation functions free of I/O.
Control the Effects
- Take a monolithic program of at least 40 lines and identify responsibilities before extracting functions. For each new function, write inputs, output, preconditions, and side effects.
- Compare a function that reads a value internally with one that accepts the value as a parameter. Discuss testing, reuse, and failure reporting.
- Give one justified and one unjustified use of a static local object.