Part of C in 100s

C in 100 Seconds: Functions | Episode 9

Celest KimCelest Kim

Video: C in 100 Seconds: Functions — Define Once Call Anywhere | Episode 9 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

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 typeint, double, void, etc. void means "no return value."
  • Name — your identifier. Same rules as variable names.
  • Parameter list — comma-separated type name pairs. 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 add before main (as in our example).
  • Declare add's prototype before main, 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:

  1. 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
  1. 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:

  1. Pushes the arguments and return address onto the stack.
  2. Jumps to the function's code.
  3. Function executes, leaves return value in a known register or stack slot.
  4. 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:

  • inline keyword: hints the compiler to inline the function (paste body at call sites).
  • static inline for 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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.