A C function has a return type, name, parameter list, and body. A declaration lets the compiler check calls before it sees the definition. Strong function design goes beyond syntax: the caller and function need a clear agreement about valid inputs, returned status, modified memory, and ownership.
After this lesson, you can declare and call a function with type checking, explain C’s pass-by-value behavior, use pointers for output or mutation, keep array bounds explicit, and separate a public declaration from a private implementation.
A function is a reusable block of code that performs a specific task. Functions help break a large program into smaller, manageable pieces. Every C program has at least one function - main().
// Declaration (prototype)
int add(int a, int b);
// Definition
int add(int a, int b) {
return a + b;
}
// Call
int result = add(5, 3); // result = 8
#include <stdio.h>
// Function prototypes (declarations)
int add(int a, int b);
float average(int a, int b, int c);
void greet(char name[]);
void countCalls();
int main() {
printf("Sum: %d\n", add(10, 5));
printf("Average: %.2f\n", average(10, 20, 30));
greet("Alice");
// Static variable demo
countCalls();
countCalls();
countCalls();
return 0;
}
int add(int a, int b) {
return a + b;
}
float average(int a, int b, int c) {
return (a + b + c) / 3.0f;
}
void greet(char name[]) {
printf("Hello, %s!\n", name);
// no return statement needed for void
}
void countCalls() {
static int count = 0; // retains value between calls
count++;
printf("Function called %d time(s)\n", count);
}
/*
Output:
Sum: 15
Average: 20.00
Hello, Alice!
Function called 1 time(s)
Function called 2 time(s)
Function called 3 time(s)
*/
#include <stdio.h>
// Recursive factorial: n! = n * (n-1)!
long long factorial(int n) {
if (n == 0 || n == 1) return 1; // base case
return n * factorial(n - 1); // recursive call
}
// Recursive Fibonacci: fib(n) = fib(n-1) + fib(n-2)
int fibonacci(int n) {
if (n <= 1) return n; // base cases: fib(0)=0, fib(1)=1
return fibonacci(n - 1) + fibonacci(n - 2);
}
int main() {
// Factorial
for (int i = 0; i <= 10; i++) {
printf("%d! = %lld\n", i, factorial(i));
}
// Fibonacci series
printf("\nFibonacci (first 10): ");
for (int i = 0; i < 10; i++) {
printf("%d ", fibonacci(i));
}
printf("\n");
return 0;
}
/*
0! = 1
1! = 1
...
10! = 3628800
Fibonacci (first 10): 0 1 1 2 3 5 8 13 21 34
*/
#include <stdio.h>
// Call by value - a copy is passed; original is NOT modified
void doubleByValue(int x) {
x = x * 2;
printf("Inside doubleByValue: %d\n", x);
}
// Call by reference - pointer is passed; original IS modified
void doubleByRef(int *x) {
*x = *x * 2;
printf("Inside doubleByRef: %d\n", *x);
}
// Swap using pointers
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int num = 10;
doubleByValue(num);
printf("After doubleByValue: %d\n\n", num); // still 10
doubleByRef(&num);
printf("After doubleByRef: %d\n\n", num); // now 20
int a = 5, b = 8;
printf("Before swap: a=%d, b=%d\n", a, b);
swap(&a, &b);
printf("After swap: a=%d, b=%d\n", a, b);
return 0;
}
/*
Inside doubleByValue: 20
After doubleByValue: 10
Inside doubleByRef: 20
After doubleByRef: 20
Before swap: a=5, b=8
After swap: a=8, b=5
*/
Place declarations shared by several source files in a guarded header and put definitions in one .c file. Use parameter names in declarations when they improve meaning. Write f(void), not f(), when a function intentionally accepts no arguments: in C, an empty parameter list declaration does not provide the same prototype information.
The declaration and definition must agree exactly on return and parameter types. Include the header in the implementation file so the compiler checks that agreement. Do not rely on implicit function declarations; modern C requires a visible declaration before a call.
C passes every argument by value. Passing an int copies the integer; assigning to the parameter does not change the caller’s object. Passing a pointer also copies a value, but that pointer value identifies the caller’s memory, so dereferencing it can modify the pointed object. Check whether NULL is allowed and document that part of the contract.
An array parameter is adjusted to a pointer parameter and does not carry its element count. Pass the length separately and keep the units clear. sizeof on an array parameter returns the size of a pointer, not the caller’s complete array. Use const for pointed data the function reads but must not modify.
A function can return a computed value directly when every value in the type is a valid result. When failure must be represented separately, return a status code and write the result through an output pointer, or return a pointer with NULL as a documented failure. Do not return the address of an automatic local variable because its lifetime ends when the function returns.
Keep one function focused enough that its name and contract remain accurate. A short helper is useful when it isolates a decision, invariant, or resource operation; splitting every expression into a function only makes control flow harder to follow.
Try this next
0 of 2 completed
Explore 500+ free tutorials across 20+ languages and frameworks.