C in 100 Seconds: Functions | Episode 9
Video: C in 100 Seconds: Functions — Define Once Call Anywhere | Episode 9 by Taught by Celeste AI - AI Coding Coach
C Functions: Define Once, Call Anywhere
int add(int a, int b) { return a + b; }. Return type, name, parameter list, body. The unit of code reuse in C — and one of the few abstractions C gives you.
C functions are the primary way to organise code. No classes, no methods — just functions and structs. Today: define one, call it.
The basic shape
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
void greet(char name[]) {
printf("Hello, %s!\n", name);
}
int main() {
int result = add(10, 25);
printf("10 + 25 = %d\n", result);
greet("Alice");
greet("Bob");
return 0;
}
Two functions — add returns an int; greet returns nothing.
Anatomy of a function
return_type name(parameter_list) {
body
return value;
}
- Return type —
int,double,void, etc.voidmeans "no return value." - Name — your identifier. Same rules as variable names.
- Parameter list — comma-separated
type namepairs. Empty()means no params. - Body — statements between
{and}. return— sends a value back. Required for non-void functions.
void functions
void greet(char name[]) {
printf("Hello, %s!\n", name);
}
void means "doesn't return anything." No return value; needed; you can write return; (with no value) to exit early.
The "in-order declaration" rule
C reads top to bottom. By the time main calls add, the compiler must already know add exists. So either:
- Define
addbeforemain(as in our example). - Declare
add's prototype beforemain, define it later.
A prototype (or forward declaration) is just the signature, semicolon-terminated:
int add(int a, int b); // prototype — declares the function exists
void greet(char name[]);
int main() {
int x = add(1, 2); // OK — compiler knows add's signature
return 0;
}
int add(int a, int b) { // definition — comes after main
return a + b;
}
For multi-file projects (episode 16, 41), prototypes go in header files; definitions in .c files.
Pass by value
C passes parameters by value — copies are made:
void increment(int x) {
x++;
printf("Inside: %d\n", x);
}
int main() {
int n = 5;
increment(n);
printf("Outside: %d\n", n);
return 0;
}
// Inside: 6
// Outside: 5
x inside increment is a copy. Modifying it doesn't affect n. To modify the caller's variable, pass a pointer (episode 19).
Returning multiple values
C functions return one value. To return multiple, you have options:
- Pass pointers for the others:
void divmod(int a, int b, int *quot, int *rem) {
*quot = a / b;
*rem = a % b;
}
int q, r;
divmod(17, 5, &q, &r);
// q = 3, r = 2
- Return a struct (episode 23):
typedef struct { int q, r; } DivMod;
DivMod divmod(int a, int b) {
return (DivMod){a / b, a % b};
}
Both are common. Pointers for "modify caller's vars"; structs for "compute a tuple."
Recursion
A function can call itself:
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
int main() {
printf("%d\n", factorial(5)); // 120
return 0;
}
Each recursive call adds a stack frame. For very deep recursion (thousands of frames), you risk a stack overflow. For factorial of huge numbers, iteration is safer.
Tail-call recursion (where the recursive call is the last action) can be optimized to a loop by some compilers — but C doesn't guarantee it.
Function-local variables
void demo() {
int x = 5; // local — destroyed when function returns
static int y = 0; // static local — persists across calls
y++;
printf("%d %d\n", x, y);
}
demo(); // 5 1
demo(); // 5 2
demo(); // 5 3
static inside a function makes the variable persist between calls — initialized once, kept until program exit. Useful for counters, caches.
Function calling conventions (briefly)
When you call a function, the CPU:
- Pushes the arguments and return address onto the stack.
- Jumps to the function's code.
- Function executes, leaves return value in a known register or stack slot.
- Function returns; CPU pops the stack back to where it was.
Every call has overhead — function-call setup, register saves, stack adjustment. For very small functions in hot loops, the overhead can dominate. Solutions:
inlinekeyword: hints the compiler to inline the function (paste body at call sites).static inlinefor header-defined helpers.- Manually inline.
For most code, function-call overhead is irrelevant.
Why prototypes matter
int add(); // K&R style: empty parens means "any number of args"
int add(void); // ANSI C: explicitly no args
int add(int, int); // two int args
The first form is dangerous — add(1, 2, 3, 4) would compile silently. Always use parameterized prototypes (or (void) for no-argument functions).
Common mistakes
Mismatched return type. int add(...) { return; } — returns nothing from a non-void function. Compiler warns; behavior is undefined.
Forgetting to return. int add(int a, int b) { a + b; } (no return). Returns garbage. Compiler warns with -Wall.
Wrong number of arguments. Pre-prototype declarations let this slide. Modern prototypes catch it.
Pass by value when you needed reference. increment(n) doesn't change n. Use increment(&n) and *x = ... (episode 19).
Returning a pointer to a local. int* foo() { int x = 5; return &x; } — x is destroyed when foo returns. Pointer dangles. Catastrophic.
Calling without prototype. Implicit-int return assumed; arg types unchecked. Use prototypes (in headers) for every function.
What's next
Episode 10: arrays. int nums[5]. Storage for many values; the foundation that pointers and dynamic allocation build on.
Recap
return_type name(params) { body; return value; }. void for "no return." Prototype int add(int, int); declares; definition implements. Functions called before their declaration cause errors (or implicit-int warnings). Pass by value — copies args. For "modify the caller's variable," pass pointers. Multiple returns: pointers or structs. static local variables persist across calls. Recursion costs stack frames — be careful with depth.
Next episode: arrays.