Functions are the building blocks of any C program. They let you create a chunk of code, give it a name, and call it whenever you need that behaviour. Understanding how functions work is fundamental to writing C.
The Basic Structure of a Function
A function in C has four parts: a return type, a name, a parameter list, and a body.
int add(int a, int b) {
int result = a + b;
return result;
}The return type comes first. In this case it’s
int, meaning the function will produce an integer value that gets sent back to whoever called it. If a function doesn’t need to return anything, you usevoidas the return type.The name comes next. This is how you’ll refer to the function when you want to call it. Function names follow the same rules as variable names: they can contain letters, numbers, and underscores, but can’t start with a number.
The parameter list sits inside the parentheses. These are the inputs to the function. There are the values that get passed in when you call it. Each parameter has a type and a name. If a function takes no parameters, you can write
voidinside the parentheses (or leave them empty, thoughvoidis more explicit).The body is everything between the curly braces. This is the code that runs when the function is called. If the function has a non-void return type, the body must include a
returnstatement that provides a value of the correct type.
Calling a Function
Once you’ve defined a function, you call it by using its name followed by arguments in parentheses:
int sum = add(5, 3); // sum is now 8The arguments 5 and 3 get copied into the parameters a and b. The function runs, computes the result, and returns 8. That returned value gets assigned to sum.
This copying is important. C passes arguments by value, meaning the function receives copies of the values, not the original variables. If you modify a parameter inside the function, the original variable outside the function is unaffected:
void try_to_modify(int x) {
x = 100; // This only changes the local copy
}
int main(void) {
int num = 5;
try_to_modify(num);
// num is still 5 here, not 100
return 0;
}If you need a function to modify something outside itself, you pass a pointer to it. But that’s a topic for another post.
Return Types and void
The return type specifies what kind of value the function produces. It can be any type: int, float, char, a pointer, a struct, and so on.
// Returns an integer
int square(int x) {
return x * x;
}
// Returns a floating-point number
float divide(float a, float b) {
return a / b;
}
// Returns a pointer to a character (a string)
char *get_greeting(void) {
return "Hello, world!";
}When a function doesn’t need to return anything, you declare it with void:
void print_hello(void) {
printf("Hello!\n");
// No return statement needed, or you can write: return;
}You can include a bare return; statement in a void function to exit early, but you don’t have to. The function will return automatically when it reaches the closing brace.
One thing to watch out for: if you declare a function with a non-void return type, you must return a value on every code path. If you forget, the compiler might warn you, but the behaviour is undefined — the caller will receive garbage.
int risky(int x) {
if (x > 0) {
return x * 2;
}
// Oops! What happens if x <= 0? No return statement.
// This is undefined behaviour.
}Parameters: Inputs to Your Function
Parameters let you pass data into a function. You can have as many as you need, separated by commas:
void print_rectangle_info(int width, int height, const char *label) {
int area = width * height;
printf("%s: %d x %d = %d\n", label, width, height, area);
}Each parameter is essentially a local variable that gets initialised with the value you pass when calling the function.
If a function takes no parameters, you should write void in the parameter list:
int get_answer(void) {
return 42;
}There’s a subtle difference between int get_answer(void) and int get_answer(). The first explicitly says “this function takes no arguments.” The second, in C (unlike C++), means “this function takes an unspecified number of arguments” — it’s an old-style declaration that doesn’t prototype the parameters. For clarity and safety, always use void when you mean no parameters.
Declaration vs Definition
This is a distinction that trips up newcomers, but it’s important for understanding how C programs are structured.
A function definition is the complete function: return type, name, parameters, and body. It’s the actual implementation.
// This is a definition - it includes the body
int multiply(int a, int b) {
return a * b;
}A function declaration (also called a prototype) just tells the compiler that a function exists and what its signature looks like. It doesn’t include the body:
// This is a declaration - no body, ends with a semicolon
int multiply(int a, int b);Why would you need both? Because the C compiler processes your code from top to bottom. If you try to call a function before the compiler has seen it, it won’t know what parameters it takes or what it returns.
#include <stdio.h>
int main(void) {
int result = multiply(3, 4); // Error! Compiler hasn't seen multiply yet.
printf("%d\n", result);
return 0;
}
int multiply(int a, int b) {
return a * b;
}You have two options. Either move the function definition above main so the compiler sees it first, or provide a declaration before main and put the definition wherever you like:
#include <stdio.h>
// Declaration: tells the compiler what multiply looks like
int multiply(int a, int b);
int main(void) {
int result = multiply(3, 4); // Now this works
printf("%d\n", result);
return 0;
}
// Definition: the actual implementation
int multiply(int a, int b) {
return a * b;
}For small programs, you might just order your functions so each is defined before it’s called. For larger programs, the declaration/definition split becomes essential — you put declarations in header files and definitions in source files. That way, any source file that includes the header knows about the function and can call it, even though the actual code lives elsewhere.
The main Function: Where It All Begins
Every C program must have a function called main. This is the entry point — when you run your program, execution starts at the first line of main.
The main function is special in several ways. It’s called by the operating system, not by your code. Its return type is always int, and the value you return becomes the program’s exit status: 0 conventionally means success, and non-zero values indicate some kind of error.
There are two valid signatures for main:
// Version 1: takes no arguments
int main(void) {
// Your program here
return 0;
}
// Version 2: takes command-line arguments
int main(int argc, char *argv[]) {
// argc is the number of arguments
// argv is an array of strings containing the arguments
return 0;
}The second version gives you access to command-line arguments. If the user runs ./myprogram hello world, then argc would be 3 (the program name plus two arguments), and argv would contain {"./myprogram", "hello", "world"}.
You might occasionally see void main() in old code or tutorials. This is non-standard. The correct return type for main is int. Some compilers accept void main(), but it’s not portable and you shouldn’t use it.
One more quirk: main is the only function in C where you can omit the return statement. If you don’t explicitly return a value, main implicitly returns 0. This was added in the C99 standard. I’d still recommend including return 0; for clarity, but it’s not strictly required.
Why Use Functions?
Functions exist to solve several problems at once.
Code reuse is the obvious benefit. If you need to perform the same operation in multiple places, you write it once as a function and call it wherever needed. If you later need to fix a bug or change the behaviour, you update one place rather than hunting through your code for every copy.
// Without a function: repeated code
float area1 = 3.14159 * radius1 * radius1;
float area2 = 3.14159 * radius2 * radius2;
float area3 = 3.14159 * radius3 * radius3;
// With a function: write once, use anywhere
float circle_area(float radius) {
return 3.14159f * radius * radius;
}
float area1 = circle_area(radius1);
float area2 = circle_area(radius2);
float area3 = circle_area(radius3);Abstraction is equally important. Functions let you hide complexity behind a simple interface. When you call printf, you don’t need to know how it formats strings or talks to the terminal. You just pass it arguments and it does its job. You can apply the same principle in your own code: write a function that handles the messy details, give it a clear name, and then use it without thinking about what’s inside.
Organisation follows naturally. Functions let you break a large program into smaller, manageable pieces. Each function handles one task. This makes code easier to read, easier to test, and easier to maintain. Instead of one long main function with hundreds of lines, you have a collection of focused functions, each doing one thing well.
int main(void) {
initialise_system();
load_configuration();
while (running) {
process_input();
update_state();
render_output();
}
cleanup();
return 0;
}You can understand the structure of this program without reading the implementation of each function. The names tell you what happens at each step.
The Costs of Functions
Functions aren’t free, though the costs are usually small.
There’s a calling overhead. When you call a function, the CPU has to save some state, jump to the function’s code, pass the arguments, execute the function, and then return. For tiny operations called millions of times in a tight loop, this overhead can matter. In such cases, you might inline the code manually, use the inline keyword as a hint to the compiler, or (more commonly) trust the compiler to make sensible optimisations.
Arguments are copied. Because C passes by value, each argument is copied into the function’s parameter. For small types like int or float, this is trivial. For large structs, it can be expensive. That’s why you’ll often see functions take pointers to large structs rather than the structs themselves:
// Expensive: copies the entire struct
void process_data(struct LargeData data);
// Cheaper: copies only a pointer (8 bytes on 64-bit systems)
void process_data(const struct LargeData *data);Debugging can sometimes be slightly harder when code is spread across many functions, because you have to trace through multiple call sites. But this is usually outweighed by the clarity that good function decomposition provides.
In practice, you should almost always prefer using functions. The benefits of clarity, reuse, and maintainability far outweigh the minor performance costs. Modern compilers are very good at optimising function calls, and you should only avoid them when profiling shows a genuine bottleneck.
Putting It All Together
Here’s a complete example that demonstrates declarations, definitions, multiple functions, and main:
#include <stdio.h>
// Function declarations (prototypes)
// These tell the compiler what functions exist and their signatures
float celsius_to_fahrenheit(float celsius);
float fahrenheit_to_celsius(float fahrenheit);
void print_conversion(float temp, char from_unit);
int main(void) {
float temp = 100.0f;
// Convert boiling point of water both ways
print_conversion(temp, 'C');
print_conversion(212.0f, 'F');
return 0;
}
// Function definitions (implementations)
float celsius_to_fahrenheit(float celsius) {
return (celsius * 9.0f / 5.0f) + 32.0f;
}
float fahrenheit_to_celsius(float fahrenheit) {
return (fahrenheit - 32.0f) * 5.0f / 9.0f;
}
void print_conversion(float temp, char from_unit) {
if (from_unit == 'C') {
float result = celsius_to_fahrenheit(temp);
printf("%.1f°C = %.1f°F\n", temp, result);
} else if (from_unit == 'F') {
float result = fahrenheit_to_celsius(temp);
printf("%.1f°F = %.1f°C\n", temp, result);
} else {
printf("Unknown unit: %c\n", from_unit);
}
}The declarations at the top let us define main before the other functions. Each function has a single, clear purpose. The main function reads almost like prose — you can see what the program does without diving into implementation details. This is what good use of functions looks like.
Functions are simple in concept but foundational in practice. Master them, and you have the tools to write clear, organised, maintainable C code.
