C / Basic: Read and Construct Programs / Lesson 5
Functions and Contracts
Use functions to give calculations clear interfaces, and state the conditions under which callers may rely on a result.
On this page
Lesson goals
- Separate declarations, definitions, and calls.
- Explain value parameters, return values, and local scope.
- Design a checked interface with explicit failure reporting.
A declaration is a promise
A function declaration states the return type and parameter types available at a call site. A definition also provides the body. In C17, int f(void) declares no parameters; int f() does not provide that same prototype information. Prefer explicit prototypes and include the relevant header before calling a function.
A good function has one coherent responsibility and a name that describes the result or action. Its contract includes preconditions, effects, a return convention, and any resource ownership. A comment saying “calculate value” adds little; explaining that a count may be zero and that the input remains unchanged prevents real ambiguity.
Parameters receive values
C passes arguments by value. A parameter is a separate local object initialized from the argument value, so changing an int parameter does not update the caller’s int. The first example returns the transformed value, leaving the caller free to store or ignore it. Pointer parameters also pass a value, but that value can identify another object that the function is permitted to modify.
Local block scope controls where a name is visible. Automatic storage duration controls how long an ordinary local object exists. These are distinct concepts: a name can stop being visible while an object still exists in an enclosing scope. Returning an ordinary arithmetic result avoids exposing local storage.
#include <stdio.h>
static int increased(int value)
{
return value + 1;
}
int main(void)
{
int original = 4;
int result = increased(original);
printf("%d %d\n", original, result);
return 0;
}
4 5
For this small input, increased returns five; original remains four because its value was copied.
Report failure without a fake value
A numeric return alone can be ambiguous when every numeric value is valid data. A checked interface can return a success flag and store the result through an output pointer. State that the output object remains unchanged on failure, then place the assignment only after every validation succeeds.
The next helper accepts only nonnegative operands and a nonnull output pointer. It guards subtraction and addition in a sequence that remains within int range. The caller must still supply a pointer to a live writable int; checking against null cannot prove all aspects of pointer validity. This example introduces pointer syntax for a narrow, documented purpose.
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
static bool add_nonnegative(int a, int b, int *out)
{
if (out == NULL || a < 0 || b < 0) return false;
if (a > INT_MAX - b) return false;
*out = a + b;
return true;
}
int main(void)
{
int result = -1;
if (!add_nonnegative(12, 8, &result)) return 1;
printf("result=%d\n", result);
return 0;
}
result=20
&result identifies the caller’s live int. The checked sum is stored only on success.
Keep dependencies visible
Passing needed data as parameters usually makes a calculation easier to test than reading hidden global state. File-scope static functions have internal linkage, so their names are private to that translation unit. This is a useful tool for implementation helpers, not a substitute for a well-defined contract.
Avoid functions whose result depends unexpectedly on a previous call. A static local object persists across calls, which can be appropriate for a deliberate state machine but surprising inside a seemingly pure calculation. Separate computation from input and output so tests can supply known values without simulating an entire terminal conversation.
Reason about every return path
Every path through a non-void function should provide the promised result or a documented failure outcome. Review exceptional inputs before the normal calculation. Early returns often make rejected cases easier to see, provided resource cleanup remains correct. A function contract should describe which arguments may alias when several pointers are involved.
Recursion needs a base case and progress toward it. C does not promise unlimited recursion depth or tail-call optimization. For large inputs, an iterative algorithm with explicit bounds may be easier to justify. Choose decomposition to clarify invariants and responsibilities, rather than maximizing the number of short functions.
Common errors and fixes
Changing a parameter does not change the caller.
- Why it happens
- The parameter contains a copied value.
- How to fix it
- Return the new value or use a documented output pointer.
- Prevent it next time
- Draw caller objects and parameter objects separately.
Failure destroys the previous result.
- Why it happens
- The output was assigned before validation completed.
- How to fix it
- Delay output mutation until the success path.
- Prevent it next time
- Test that failure preserves an initialized sentinel result.
A call has missing type checking.
- Why it happens
- An old-style empty parameter list was used as a declaration.
- How to fix it
- Declare explicit parameter types and use void for no parameters in C17.
- Prevent it next time
- Include the public header in callers and the defining source.
Check your understanding
1. Call add_nonnegative with a negative operand and result initially 99. What should happen, and why?
Show worked solution
The initial guard returns false before evaluating the sum or assigning through out. Therefore result remains 99. This follows from the explicit unchanged-on-failure contract, not from a convention imposed on all C functions. A test should check both the false return and the preserved result, because checking only the return would miss an accidental early write.
2. Design a contract for a function that returns the larger of two int values without modifying either caller object.
Show worked solution
Declare int max_int(int a, int b), require no additional precondition, and return a >= b ? a : b. Comparison does not overflow, and either input is already representable as int. The postcondition is that the result equals one operand and is not less than either. Test equal values, opposite signs, and INT_MIN with INT_MAX.
Key takeaways
- A function interface includes types, preconditions, effects, and failure policy.
- Arguments are passed by value, including pointer values.
- Test failure paths and preserve the state promised by the contract.