🌱 C Basics
Functions: name it once, use it forever
▶ Open the interactive lesson — free, no signupCopy-paste the same five lines into three places and one day you'll fix a bug in two of them and forget the third. Functions let you write logic once, name it, and reuse it forever — they're the reason million-line programs like games and browsers don't collapse under their own weight. You've been calling printf since day one; today you build your own.
You've been using functions since your first printf — now you get to make them. A function packages a piece of logic behind a name: define it once, call it from anywhere, trust it to do its job. It is the single most important tool for taming complexity in C.
Anatomy of a function
#include <stdio.h>
int square(int n) { /* definition: the real thing */
return n * n;
}
void greet(void) { /* returns nothing, takes nothing */
printf("Hello from a function!\n");
}
int main(void) {
greet();
int a = square(6); /* call: argument 6 -> parameter n */
printf("square(6) = %d\n", a);
printf("square(a) = %d\n", square(a)); /* results compose */
return 0;
}$ gcc square.c -o square && ./square Hello from a function! square(6) = 36 square(a) = 1296
- Return type (
int) — the type of value handed back. Use the keywordvoidfor "returns nothing". - Name (
square) — pick verbs for actions, nouns for computations. - Parameters (
int n) — fresh local variables, filled in with the caller's arguments at each call. A function taking nothing declares(void). return— stops the function immediately and delivers the value. Avoidfunction may use barereturn;or just fall off the end.
🧠 Checkpoint: What does the keyword void mean in void greet(void)?
- The function is empty
- Returns nothing, and takes no parameters
- The function can’t be called twice
- The function is private
Show answer
Returns nothing, and takes no parameters — First void: no return value (a bare return; or falling off the end is fine). Second void: no parameters. In C, empty parens () historically meant "unspecified parameters" — C23 finally fixed that, but (void) remains the bulletproof spelling.
Declaration vs definition: promises vs delivery
C compilers read top to bottom, and a function must be known before it's called. Two ways to arrange that:
- Define the function above its callers (fine for small files), or
- Put a prototype — the header line ending in a semicolon — near the top, and define the body anywhere:
#include <stdio.h>
double celsius_to_f(double c); /* PROTOTYPE: a promise */
int main(void) {
printf("100 C = %.1f F\n", celsius_to_f(100.0));
printf("37 C = %.1f F\n", celsius_to_f(37.0));
return 0;
}
/* DEFINITION: the promise, delivered (below its caller!) */
double celsius_to_f(double c) {
return c * 9.0 / 5.0 + 32.0;
}A prototype is a declaration: "a function with this name, these parameter types, this return type exists — trust me." The definition with the body is the delivery on that promise. Headers like stdio.h are, at heart, just bundles of prototypes — that's the missing piece of the #include story from lesson one. Promise a function and never define it, and you'll meet our old friend from Part 0: the linker's undefined reference.
🧠 Checkpoint: A prototype exists but the definition was never written. When does the build fail?
- At the linker stage — undefined reference
- At the compile stage — syntax error
- At runtime — crash on call
- It works; C invents an empty body
Show answer
At the linker stage — undefined reference — The compiler is satisfied by the promise (prototype) and generates a call to a name. The LINKER must then find the actual code — and can’t. Compiler checks grammar and types; linker resolves names. Same split you saw in the pipeline lesson.
The big rule: arguments are passed BY VALUE
When you call doubler(x), the parameter does not become x — it receives a copy of x's value, in a brand-new variable inside the function's own stack frame (remember Part 0's stack?). Modify the copy all you like; the original never notices:
#include <stdio.h>
void doubler(int n) {
n = n * 2; /* modifies the COPY */
printf(" inside : n = %d\n", n);
}
int main(void) {
int x = 10;
printf("before : x = %d\n", x);
doubler(x);
printf("after : x = %d <- unchanged!\n", x);
return 0;
}$ gcc byvalue.c -o byvalue && ./byvalue before : x = 10 inside : n = 20 after : x = 10 <- unchanged!
Here's the memory picture at the moment doubler sets n = 20 — two separate cells, two separate lives:
▶ This spot has an interactive memgrid widget — open the interactive lesson to play with it.
"But I want the function to change my variable!" — everyone, week two. The C answer is to pass the variable's address so the function can reach back and modify the original. That is exactly what pointers are for, and it's the opening act of Part 2. For now: return the new value and assign it, like x = doubler(x);.
🧠 Checkpoint: After void f(int a) { a = 99; } is called as f(x) with x = 5, x is…
- 99
- undefined
- 5
- 0
Show answer
5 — Pass by value: a is a fresh variable initialized with a copy of 5. Assigning to it touches only the copy, which dies when f returns. Every argument in C works this way — even (spoiler) pointers, which copy the address.
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
Functions create little private worlds for their variables — the precise rules of who can see what, and for how long, are our final Part 1 topic: scope and lifetime.
▶ Practice this lesson interactively (with live gcc)