Foundations · Module 1 · Lesson 3 of 7
Reusing Code: Functions and Parameters
Learn how to define functions, call them with arguments, return values, and avoid repeating code by organizing programs into reusable pieces.
By the end you can
- Define a function with a return type, name, and parameters
- Call a function and pass arguments to it
- Return a value from a function using the return statement
- Trace a function call through parameters and return value
Builds on Repeating Yourself: for and while Loops.
01 · A question first
You need to calculate the area of a circle in three different places in your program. Do you copy the formula `3.14159 * radius * radius` each time? What if you want to change it to a more precise pi later? How do you write the formula once and use it everywhere?
Make a guess, then step through the trace below to check it.
02 · Watch it run
int square(int x) { return x * x;} int main(void) { int result = square(5); return 0;}- slot 0main: result = uninitialized(looked at in this step)
- slot 1square: x = 5(looked at in this step)
- slot 2return value: none
main calls square(5). The value 5 is copied into parameter x.
Machine work: call square(5); x = 5
03 · The idea
Start with the problem
Suppose your program needs to calculate the area of a circle or check whether a user's input lies within a valid range in four different places. If you copy and paste the same calculation four times across main, three problems appear immediately:
- Maintenance bugs: When you later realize
3.14is not accurate enough and switch to3.141592653589793, you must hunt down every copy. Miss just one copy and two parts of your program will disagree. - Unreadable
main: A 1,000-linemainfunction where every low-level step is written inline becomes a wall of text where you cannot see the high-level story of the program. - No isolated testing: You cannot easily test one piece of logic on edge cases (
0, negative numbers, large inputs) when it is tangled inside the middle of a giant loop.
Programmers solve all three problems with functions: named, self-contained blocks of code that accept inputs (parameters), perform a well-defined task, and optionally send back a result (return value).
How the idea works
You have already been writing one function—main—and calling two library functions (printf and scanf). Defining your own function follows the exact same structure:
int square(int x) {
return x * x;
}Every function definition has four parts:
- Return type (
int): The type of value the function hands back to its caller when it finishes. If a function performs an action (such as printing a banner) without producing a value, its return type isvoid. - Function name (
square): A descriptive identifier that says what the function computes or does. - Parameter list (
(int x)): A comma-separated list of typed variables that receive the input values passed by the caller. If a function takes no inputs, write(void). - Function body (
\{ ... \}): The statements that execute when the function is called. Areturn expr;statement evaluatesexpr, stops the function immediately, and sends that value back to the caller.
Tracing a function call step by step
Look at the trace above to see what the CPU and memory do when main calls square(5):
- Before the call: Execution is inside
mainon line 6 (int result = square(5);).mainhas its own local variableresultwaiting for a value. - Entering the function (Step 1): Calling
square(5)allocates a fresh workspace in memory—called a stack frame—forsquare. Its parameterxis created inside that frame and initialized with a copy of the argument5. - Computing the result (Step 2): The CPU jumps to line 2 inside
square, multipliesx * x(5 * 5 = 25), and executesreturn 25;. - Returning to the caller (Step 3):
square's stack frame and its local parameterxare destroyed (popped from the stack). Execution resumes on line 6 ofmain, wheresquare(5)is replaced by the returned value25, storing25intoresult.
Pass-by-value: parameters are independent copies
A critical rule in C is that all arguments are passed by value. When you call a function and pass a variable as an argument, C copies the variable's number into the function's parameter slot. Modifying the parameter inside the function changes only the local copy, leaving the caller's original variable completely untouched:
#include <stdio.h>
int add_ten(int n) {
n = n + 10; /* modifies only the local copy n */
return n;
}
int main(void) {
int original = 5;
int updated = add_ten(original);
printf("original = %d, updated = %d", original, updated);
/* Prints: original = 5, updated = 15 */
return 0;
}Function prototypes and top-to-bottom visibility
The C compiler reads your source file once from top to bottom. If main calls square(5) on line 6, the compiler must already know square's return type and parameter types before line 6. You can satisfy the compiler in two ways:
- Define helper functions above
main(as shown in the trace above), or - Place a function prototype (the function header ending with a semicolon, such as
int square(int x);) near the top of the file, and put the full function body belowmain.
Why it is correct
Giving each function its own private local variables (lexical scope) and passing arguments by value provides isolation. When main has a loop counter named i and calls a helper function is_prime(n) that also uses a loop counter named i, the two i variables live in separate stack frames at different memory addresses. Neither function can accidentally overwrite the other's local variables.
Furthermore, a return statement acts as a clean contract boundary: once a path through a function hits return value;, no code below that line in the function runs, guaranteeing a single well-defined exit value for that path.
What it costs
Calling a function has a cost for entering the function, passing arguments, and returning, but the exact cost depends on the target ABI, optimization level, and whether the compiler inlines the call. When you compile with optimization enabled (gcc -O2), the compiler may inline small functions like square(x), preserving the source-level organization without a separate call at runtime.
Pitfalls and variations
| Mistake | What goes wrong | Fix |
|---|---|---|
| Calling a function before declaring or defining it | The compiler rejects the call with an implicit declaration error | Define the function above main or add a prototype int fn(int x); at the top |
Expecting void increment(int x) \{ x++; \} to change caller's variable | x is a local copy; the caller's variable stays unchanged | Return the new value (x = increment(x);) or pass a pointer (covered in Lesson 6) |
Including types when calling (int res = square(int 5);) | Syntax error: parameter types belong only in declarations/definitions | Pass only expressions when calling: int res = square(5); |
Where it is used
Every C program is a tree of function calls starting at main. Standard library headers like <stdio.h>, <math.h>, and <string.h> are simply collections of function prototypes (printf, sqrt, strlen) whose compiled bodies are linked into your program. Breaking your own code into small, single-purpose functions is the primary way professional engineers keep 100,000-line systems understandable and testable.
In the next lesson, you will learn how to store and process sequences of values in contiguous memory using arrays.
04 · Prove it
Question 1 of 3 · answer from memory
What does square(7) return?
05 · Practice
Asked in interviews
Something wrong with this lesson? Report a problem