Functions in C
Learn how to define, call, and compose functions in C - parameters, return values, scope, pass-by-pointer, recursion, and function pointers with Docker-ready examples
Functions are the unit of organization in C. As a procedural, imperative language, C builds programs by composing small, named blocks of code that each do one job. You have already met one function - main, the entry point of every C program. In this tutorial you will learn how to write your own.
C functions are deliberately simple: a function has a return type, a name, a parameter list, and a body. There are no methods, no classes, and no closures - just plain functions that take values and return values. Because C is a static, weakly typed language, the compiler checks the types of arguments against the function’s declaration, but it will also quietly convert between numeric types when it can.
Two ideas are central to using functions well in C. First, arguments are passed by value - a function receives a copy of each argument, so changing a parameter inside a function does not affect the caller. To let a function modify the caller’s data, you pass a pointer. Second, functions should usually be declared before they are used with a prototype, so the compiler knows their signature. We will cover both, along with recursion and function pointers - C’s way of treating functions as data.
Defining and Calling Functions
A function declaration (a prototype) tells the compiler the function’s signature; the definition supplies the body. Prototypes are commonly placed above main, with the definitions below it.
Create a file named functions_basic.c:
| |
Each function has a return type (int, double, or void for “no return value”), a name, and a typed parameter list. The return statement hands a value back to the caller. A void function performs an action - here, printing - and needs no return value.
Pass-by-Value and Pass-by-Pointer
C always passes arguments by value: the function works on a copy. If you want a function to change a variable in the caller, you pass its address and the function works through a pointer.
Create a file named scope.c:
| |
try_double receives a copy, so the caller’s value is untouched. really_double receives a pointer (int *), and *n reaches back through that address to modify the original. The & operator takes the address of a variable; * dereferences a pointer. This distinction is fundamental to how C handles scope and mutation.
Recursion
A function may call itself. Recursion expresses problems defined in terms of smaller versions of themselves, such as the factorial of a number.
Create a file named recursion.c:
| |
Every recursive function needs a base case that returns without recursing - here, n <= 1. Without it, the function would call itself forever and overflow the call stack. The %lu format specifier prints an unsigned long, which holds larger factorials than a plain int.
Function Pointers
In C, a function has an address, so you can store it in a function pointer and pass it to another function. This lets you write higher-order functions - functions that take other functions as parameters.
Create a file named function_pointers.c:
| |
The parameter int (*op)(int, int) is a pointer to a function. Passing add, subtract, or multiply lets apply call whichever operation you choose at runtime. Function pointers power callbacks, dispatch tables, and plug-in style designs throughout C codebases like the Linux kernel.
Running with Docker
| |
Expected Output
functions_basic.c:
7 + 5 = 12
Average of 7 and 5 = 6.0
Hello, Archaeologist!
scope.c:
Inside try_double: n = 20
After try_double: value = 10
After really_double: value = 20
recursion.c:
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
function_pointers.c:
apply(add, 6, 4) = 10
apply(subtract, 6, 4) = 2
apply(multiply, 6, 4) = 24
Key Concepts
- Prototypes come first - declare a function’s signature before it is used so the compiler can check calls; place definitions below
mainor in separate files. - Return type and parameters are typed - every function states what it returns (
int,double,void) and the type of each parameter. - Arguments are passed by value - a function gets a copy, so it cannot change the caller’s variables directly.
- Pointers enable mutation - pass an address with
&and dereference with*to let a function modify the caller’s data. voidmeans no return value - used for functions that act through side effects, like printing.- Recursion needs a base case - a self-calling function must have a condition that stops it, or it overflows the stack.
- Functions are addressable - function pointers let you store and pass functions, enabling callbacks and higher-order functions.
- Match format specifiers to types - use
%dforint,%luforunsigned long,%.1ffor adouble, and%sfor a string.
Comments
Loading comments...
Leave a Comment