C — Functions
Functions are the fundamental unit of code organization in C. Every C program is a collection of functions, with main() as the entry point. Functions enable code reuse, modularity, abstraction, and testability. Unlike some languages, C functions can only return a single value — to return multiple values, you use pointers or structs.
C passes all arguments by value — the function receives a copy. To modify the caller's variable, you pass a pointer to it. This is the core mechanism for "pass by reference" in C. Understanding this distinction, along with scope rules, storage classes, and recursion, is essential for writing correct C programs.
This page covers function declarations, definitions, parameter passing, return values, scope, storage classes, recursion, function pointers, inline functions, and variadic functions.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | // Function declaration (prototype) |
| 4 | int add(int a, int b); |
| 5 | void greet(const char *name); |
| 6 | |
| 7 | // Function definition |
| 8 | int add(int a, int b) { |
| 9 | return a + b; |
| 10 | } |
| 11 | |
| 12 | void greet(const char *name) { |
| 13 | printf("Hello, %s!\n", name); |
| 14 | } |
| 15 | |
| 16 | int main(void) { |
| 17 | int sum = add(3, 4); |
| 18 | greet("World"); |
| 19 | printf("Sum: %d\n", sum); |
| 20 | return 0; |
| 21 | } |
A declaration (prototype) tells the compiler the function's signature — return type, name, and parameter types. A definitionprovides the actual body. Declarations allow you to call functions before they're defined in the file, and are essential for multi-file programs.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | // Declarations (prototypes) — no function body |
| 4 | int multiply(int x, int y); |
| 5 | double circle_area(double radius); |
| 6 | void swap(int *a, int *b); |
| 7 | |
| 8 | // Definition of multiply |
| 9 | int multiply(int x, int y) { |
| 10 | return x * y; |
| 11 | } |
| 12 | |
| 13 | // Definition of circle_area |
| 14 | double circle_area(double radius) { |
| 15 | return 3.14159265358979 * radius * radius; |
| 16 | } |
| 17 | |
| 18 | // Definition of swap |
| 19 | void swap(int *a, int *b) { |
| 20 | int temp = *a; |
| 21 | *a = *b; |
| 22 | *b = temp; |
| 23 | } |
| 24 | |
| 25 | int main(void) { |
| 26 | printf("3 * 4 = %d\n", multiply(3, 4)); |
| 27 | printf("Area(r=5) = %.2f\n", circle_area(5.0)); |
| 28 | |
| 29 | int x = 10, y = 20; |
| 30 | printf("Before swap: x=%d, y=%d\n", x, y); |
| 31 | swap(&x, &y); |
| 32 | printf("After swap: x=%d, y=%d\n", x, y); |
| 33 | |
| 34 | return 0; |
| 35 | } |
info
All arguments in C are passed by value — the function receives a copy of the argument. Modifying the parameter inside the function does not affect the caller's variable. To modify the caller's data, you must pass a pointer.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | // This does NOT modify the caller's variable |
| 4 | void try_to_double(int x) { |
| 5 | x = x * 2; // modifies the local copy only |
| 6 | printf("Inside try_to_double: x = %d\n", x); |
| 7 | } |
| 8 | |
| 9 | // This DOES modify the caller's variable (pass by pointer) |
| 10 | void actually_double(int *x) { |
| 11 | *x = *x * 2; // dereferences the pointer to modify original |
| 12 | printf("Inside actually_double: *x = %d\n", *x); |
| 13 | } |
| 14 | |
| 15 | // Common mistake: swapping with pass by value (doesn't work) |
| 16 | void bad_swap(int a, int b) { |
| 17 | int temp = a; |
| 18 | a = b; |
| 19 | b = temp; |
| 20 | // a and b are copies — caller is unaffected |
| 21 | } |
| 22 | |
| 23 | // Correct swap: pass pointers |
| 24 | void good_swap(int *a, int *b) { |
| 25 | int temp = *a; |
| 26 | *a = *b; |
| 27 | *b = temp; |
| 28 | } |
| 29 | |
| 30 | int main(void) { |
| 31 | int val = 10; |
| 32 | |
| 33 | try_to_double(val); |
| 34 | printf("After try_to_double: val = %d\n", val); // still 10 |
| 35 | |
| 36 | actually_double(&val); |
| 37 | printf("After actually_double: val = %d\n", val); // 20 |
| 38 | |
| 39 | int x = 1, y = 2; |
| 40 | bad_swap(x, y); |
| 41 | printf("After bad_swap: x=%d, y=%d\n", x, y); // 1, 2 |
| 42 | |
| 43 | good_swap(&x, &y); |
| 44 | printf("After good_swap: x=%d, y=%d\n", x, y); // 2, 1 |
| 45 | |
| 46 | return 0; |
| 47 | } |
warning
A function can return a value via the return statement. You can return any type except arrays and functions. Returning a pointer to a local variable is undefined behavior — the memory is freed when the function exits.
| 1 | #include <stdio.h> |
| 2 | #include <stdlib.h> |
| 3 | #include <string.h> |
| 4 | |
| 5 | // Returning a computed value |
| 6 | int factorial(int n) { |
| 7 | int result = 1; |
| 8 | for (int i = 2; i <= n; i++) { |
| 9 | result *= i; |
| 10 | } |
| 11 | return result; |
| 12 | } |
| 13 | |
| 14 | // Returning a pointer — must point to heap or static data |
| 15 | char *create_greeting(const char *name) { |
| 16 | size_t len = strlen("Hello, ") + strlen(name) + 2; |
| 17 | char *greeting = malloc(len); |
| 18 | if (greeting) { |
| 19 | snprintf(greeting, len, "Hello, %s!", name); |
| 20 | } |
| 21 | return greeting; // caller must free() |
| 22 | } |
| 23 | |
| 24 | // WARNING: returning address of local variable |
| 25 | int *dangerous(void) { |
| 26 | int local = 42; |
| 27 | return &local; // UNDEFINED BEHAVIOR — local is destroyed |
| 28 | } |
| 29 | |
| 30 | // Safe: static variable persists after function returns |
| 31 | int counter(void) { |
| 32 | static int count = 0; // initialized only once |
| 33 | return ++count; |
| 34 | } |
| 35 | |
| 36 | int main(void) { |
| 37 | printf("5! = %d\n", factorial(5)); |
| 38 | |
| 39 | char *g = create_greeting("World"); |
| 40 | if (g) { |
| 41 | printf("%s\n", g); |
| 42 | free(g); |
| 43 | } |
| 44 | |
| 45 | printf("Counter: %d\n", counter()); |
| 46 | printf("Counter: %d\n", counter()); |
| 47 | printf("Counter: %d\n", counter()); |
| 48 | |
| 49 | return 0; |
| 50 | } |
Scope determines where a variable can be accessed. C has three scope levels: file scope (global), function scope, and block scope (local).
| 1 | #include <stdio.h> |
| 2 | |
| 3 | // File scope — visible from declaration to end of file |
| 4 | int global_var = 100; |
| 5 | static int file_local = 200; // internal linkage |
| 6 | |
| 7 | void function_a(void) { |
| 8 | // Function scope — label names |
| 9 | // Block scope — local variables |
| 10 | int local = 1; // block scope of function_a |
| 11 | |
| 12 | if (local > 0) { |
| 13 | int inner = 2; // block scope of if-block |
| 14 | printf("global: %d, local: %d, inner: %d\n", |
| 15 | global_var, local, inner); |
| 16 | } |
| 17 | // inner is no longer accessible here |
| 18 | |
| 19 | for (int i = 0; i < 3; i++) { |
| 20 | // C99: i is scoped to the for block |
| 21 | } |
| 22 | // i is no longer accessible here |
| 23 | } |
| 24 | |
| 25 | void function_b(void) { |
| 26 | // Different function, different scope — no conflict |
| 27 | int local = 99; |
| 28 | printf("function_b local: %d\n", local); |
| 29 | } |
| 30 | |
| 31 | int main(void) { |
| 32 | // Block scope — only accessible within main |
| 33 | int main_only = 42; |
| 34 | |
| 35 | function_a(); |
| 36 | function_b(); |
| 37 | printf("global: %d\n", global_var); |
| 38 | |
| 39 | // Shadowing — inner scope hides outer |
| 40 | int x = 10; |
| 41 | { |
| 42 | int x = 20; // shadows outer x |
| 43 | printf("inner x: %d\n", x); |
| 44 | } |
| 45 | printf("outer x: %d\n", x); // 10 |
| 46 | |
| 47 | return 0; |
| 48 | } |
The static keyword on a function limits its visibility to the current translation unit (internal linkage). The extern keyword declares a variable or function defined in another file (external linkage).
| 1 | // math_utils.c |
| 2 | #include <stdio.h> |
| 3 | |
| 4 | // static — only visible within this file |
| 5 | static int internal_helper(int x) { |
| 6 | return x * x + 1; |
| 7 | } |
| 8 | |
| 9 | // public function — visible to other files |
| 10 | int compute(int x) { |
| 11 | return internal_helper(x) + x; |
| 12 | } |
| 13 | |
| 14 | // config.c |
| 15 | #include <stdio.h> |
| 16 | |
| 17 | extern int shared_counter; // defined in main.c |
| 18 | |
| 19 | void increment(void) { |
| 20 | shared_counter++; |
| 21 | } |
| 22 | |
| 23 | // main.c |
| 24 | #include <stdio.h> |
| 25 | |
| 26 | int shared_counter = 0; // external linkage |
| 27 | |
| 28 | // static function — cannot be called from other files |
| 29 | static void print_status(void) { |
| 30 | printf("Counter: %d\n", shared_counter); |
| 31 | } |
| 32 | |
| 33 | int compute(int x); // declared in math_utils.c |
| 34 | |
| 35 | int main(void) { |
| 36 | printf("compute(5) = %d\n", compute(5)); |
| 37 | shared_counter = 10; |
| 38 | print_status(); |
| 39 | return 0; |
| 40 | } |
A recursive function calls itself. Every recursion needs a base case to terminate and a recursive case that moves toward the base. Recursion uses stack space — deep recursion can cause stack overflow.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | // Factorial: n! = n * (n-1)!, base case: 0! = 1 |
| 4 | long long factorial(int n) { |
| 5 | if (n <= 1) return 1; // base case |
| 6 | return n * factorial(n - 1); // recursive case |
| 7 | } |
| 8 | |
| 9 | // Fibonacci: fib(n) = fib(n-1) + fib(n-2), base: 0,1 |
| 10 | // WARNING: O(2^n) time — exponential! Use iterative for large n. |
| 11 | long long fibonacci(int n) { |
| 12 | if (n <= 0) return 0; |
| 13 | if (n == 1) return 1; |
| 14 | return fibonacci(n - 1) + fibonacci(n - 2); |
| 15 | } |
| 16 | |
| 17 | // Tail-recursive factorial — O(1) stack space with optimization |
| 18 | long long factorial_tail(int n, long long acc) { |
| 19 | if (n <= 1) return acc; |
| 20 | return factorial_tail(n - 1, n * acc); |
| 21 | } |
| 22 | |
| 23 | // Towers of Hanoi |
| 24 | void hanoi(int n, char from, char to, char aux) { |
| 25 | if (n == 1) { |
| 26 | printf("Move disk 1 from %c to %c\n", from, to); |
| 27 | return; |
| 28 | } |
| 29 | hanoi(n - 1, from, aux, to); |
| 30 | printf("Move disk %d from %c to %c\n", n, from, to); |
| 31 | hanoi(n - 1, aux, to, from); |
| 32 | } |
| 33 | |
| 34 | // Binary search (recursive) |
| 35 | int bsearch(int arr[], int lo, int hi, int target) { |
| 36 | if (lo > hi) return -1; |
| 37 | int mid = lo + (hi - lo) / 2; |
| 38 | if (arr[mid] == target) return mid; |
| 39 | if (arr[mid] < target) return bsearch(arr, mid + 1, hi, target); |
| 40 | return bsearch(arr, lo, mid - 1, target); |
| 41 | } |
| 42 | |
| 43 | int main(void) { |
| 44 | printf("10! = %lld\n", factorial(10)); |
| 45 | printf("tail(10) = %lld\n", factorial_tail(10, 1)); |
| 46 | printf("fib(10) = %lld\n", fibonacci(10)); |
| 47 | |
| 48 | printf("\nTowers of Hanoi (3 disks):\n"); |
| 49 | hanoi(3, 'A', 'C', 'B'); |
| 50 | |
| 51 | int arr[] = {1, 3, 5, 7, 9, 11, 13}; |
| 52 | int idx = bsearch(arr, 0, 6, 7); |
| 53 | printf("\nSearch for 7: index %d\n", idx); |
| 54 | |
| 55 | return 0; |
| 56 | } |
| Approach | Time | Space | When to Use |
|---|---|---|---|
| Recursive factorial | O(n) | O(n) stack | Educational, small n |
| Iterative factorial | O(n) | O(1) | Production code |
| Recursive fibonacci | O(2^n) | O(n) stack | Never (use iterative) |
| Tail-recursive | O(n) | O(1)* | With compiler optimization |
note
Function pointers store the address of a function and allow you to call functions indirectly. They enable callbacks, strategy patterns, and dynamic dispatch.
| 1 | #include <stdio.h> |
| 2 | |
| 3 | int add(int a, int b) { return a + b; } |
| 4 | int sub(int a, int b) { return a - b; } |
| 5 | int mul(int a, int b) { return a * b; } |
| 6 | |
| 7 | // Function that takes a function pointer as parameter |
| 8 | int apply(int a, int b, int (*operation)(int, int)) { |
| 9 | return operation(a, b); |
| 10 | } |
| 11 | |
| 12 | // Sorting with function pointer (qsort pattern) |
| 13 | int compare_ints(const void *a, const void *b) { |
| 14 | return (*(int *)a - *(int *)b); |
| 15 | } |
| 16 | |
| 17 | int main(void) { |
| 18 | // Declare and assign a function pointer |
| 19 | int (*op)(int, int) = add; |
| 20 | printf("add: %d\n", op(3, 4)); |
| 21 | |
| 22 | // Pass function pointer as argument |
| 23 | printf("apply(add): %d\n", apply(10, 5, add)); |
| 24 | printf("apply(sub): %d\n", apply(10, 5, sub)); |
| 25 | printf("apply(mul): %d\n", apply(10, 5, mul)); |
| 26 | |
| 27 | // Array of function pointers |
| 28 | int (*operations[])(int, int) = {add, sub, mul}; |
| 29 | const char *names[] = {"add", "sub", "mul"}; |
| 30 | |
| 31 | for (int i = 0; i < 3; i++) { |
| 32 | printf("%s(10,5) = %d\n", names[i], operations[i](10, 5)); |
| 33 | } |
| 34 | |
| 35 | // qsort — standard library uses function pointers |
| 36 | int arr[] = {5, 2, 8, 1, 9, 3}; |
| 37 | int n = sizeof(arr) / sizeof(arr[0]); |
| 38 | qsort(arr, n, sizeof(int), compare_ints); |
| 39 | printf("Sorted: "); |
| 40 | for (int i = 0; i < n; i++) printf("%d ", arr[i]); |
| 41 | printf("\n"); |
| 42 | |
| 43 | return 0; |
| 44 | } |
The inline keyword (C99) suggests the compiler embed the function body at the call site, avoiding function call overhead. The _Noreturn keyword (C11) tells the compiler the function never returns.
| 1 | #include <stdio.h> |
| 2 | #include <stdlib.h> |
| 3 | |
| 4 | // inline — compiler may inline this function |
| 5 | static inline int square(int x) { |
| 6 | return x * x; |
| 7 | } |
| 8 | |
| 9 | static inline int max(int a, int b) { |
| 10 | return (a > b) ? a : b; |
| 11 | } |
| 12 | |
| 13 | // _Noreturn — function never returns (C11) |
| 14 | _Noreturn void fatal_error(const char *msg) { |
| 15 | fprintf(stderr, "FATAL: %s\n", msg); |
| 16 | exit(1); |
| 17 | } |
| 18 | |
| 19 | // Common pattern: _Noreturn for assertion failure |
| 20 | _Noreturn void assert_failed(const char *file, int line) { |
| 21 | fprintf(stderr, "Assertion failed at %s:%d\n", file, line); |
| 22 | abort(); |
| 23 | } |
| 24 | |
| 25 | int main(void) { |
| 26 | printf("square(7) = %d\n", square(7)); |
| 27 | printf("max(3,9) = %d\n", max(3, 9)); |
| 28 | |
| 29 | // fatal_error("something went wrong"); // would not return |
| 30 | |
| 31 | return 0; |
| 32 | } |
Variadic functions accept a variable number of arguments using stdarg.h. The most famous example is printf. You must have at least one fixed parameter before the variadic part.
| 1 | #include <stdio.h> |
| 2 | #include <stdarg.h> |
| 3 | |
| 4 | // Sum of variable number of integers |
| 5 | int sum(int count, ...) { |
| 6 | va_list args; |
| 7 | va_start(args, count); |
| 8 | |
| 9 | int total = 0; |
| 10 | for (int i = 0; i < count; i++) { |
| 11 | total += va_arg(args, int); |
| 12 | } |
| 13 | |
| 14 | va_end(args); |
| 15 | return total; |
| 16 | } |
| 17 | |
| 18 | // Custom printf-like function |
| 19 | void debug_log(const char *prefix, const char *fmt, ...) { |
| 20 | va_list args; |
| 21 | va_start(args, fmt); |
| 22 | |
| 23 | printf("[%s] ", prefix); |
| 24 | vprintf(fmt, args); // vprintf handles va_list |
| 25 | printf("\n"); |
| 26 | |
| 27 | va_end(args); |
| 28 | } |
| 29 | |
| 30 | // Maximum of variable number of doubles (last arg is 0.0 sentinel) |
| 31 | double find_max(double first, ...) { |
| 32 | va_list args; |
| 33 | va_start(args, first); |
| 34 | |
| 35 | double max_val = first; |
| 36 | double val; |
| 37 | while ((val = va_arg(args, double)) != 0.0) { |
| 38 | if (val > max_val) max_val = val; |
| 39 | } |
| 40 | |
| 41 | va_end(args); |
| 42 | return max_val; |
| 43 | } |
| 44 | |
| 45 | int main(void) { |
| 46 | printf("sum(3, 10,20,30) = %d\n", sum(3, 10, 20, 30)); |
| 47 | printf("sum(5, 1,2,3,4,5) = %d\n", sum(5, 1, 2, 3, 4, 5)); |
| 48 | |
| 49 | debug_log("INFO", "Processing %d items", 42); |
| 50 | debug_log("WARN", "Value %s is too low", "high"); |
| 51 | |
| 52 | printf("max: %.1f\n", find_max(3.0, 7.0, 2.0, 9.0, 1.0, 0.0)); |
| 53 | |
| 54 | return 0; |
| 55 | } |
warning