Functions and Header Files
Declaring and defining functions, prototypes and pass by value, splitting a program into .c and .h files with include guards, and how static and extern control linkage and lifetime.
Why programs are split into functions at all, and what parameters and return values are, is explained on functions and abstraction. This page covers how C does it, and how a C program grows from one file into several.
Defining and calling functions
A function definition names a return type, the function name and a
list of parameters, each with its own type, followed by the body in
braces. return hands a value back to the caller. A
function that returns nothing has the return type void,
and a function without parameters is written with
(void), as main is here.
#include <stdio.h>
/* Price in ten-thousandths of a euro per kWh: 2700 = EUR 0.2700 */
long cost_cents(long kwh, long price_per_kwh)
{
long ten_thousandths = kwh * price_per_kwh;
return (ten_thousandths + 50) / 100; /* round to whole cents */
}
void print_euros(long cents)
{
printf("EUR %ld.%02ld\n", cents / 100, cents % 100);
}
int main(void)
{
print_euros(cost_cents(350, 2700));
print_euros(cost_cents(1, 2700));
return 0;
}
EUR 94.50 EUR 0.27
Money is kept in whole numbers (ten-thousandths of a euro, then cents) so that no floating-point rounding creeps in; why that matters is shown on variables, types and integers. Kestrel's fixed tariff of €0.2700 per kWh gives €94.50 for 350 kWh.
A function with a return type other than void must
return a value on every path. Forget one, and the caller receives
whatever happens to be in a register: undefined behavior. gcc's
-Wall catches the simple cases:
#include <stdio.h>
int tariff_band(int kwh)
{
if (kwh < 1000) {
return 1;
} else if (kwh < 3000) {
return 2;
}
}
int main(void)
{
printf("band %d\n", tariff_band(350));
return 0;
}
example.c:10:1: error: control reaches end of non-void function [-Werror=return-type]
Declare before use: prototypes
The compiler reads a file from top to bottom, once. When it meets a call, it must already know the function's parameter types and return type, so that it can convert the arguments and check them. A function prototype is a declaration that gives exactly that: the return type, the name and the parameter types, ended by a semicolon instead of a body. It can appear long before the definition, or in another file altogether.
#include <stdio.h>
long cost_cents(long kwh, long price_per_kwh); /* prototype */
int main(void)
{
printf("%ld cents\n", cost_cents(350, 2700));
return 0;
}
long cost_cents(long kwh, long price_per_kwh) /* definition */
{
return (kwh * price_per_kwh + 50) / 100;
}
9450 cents
Remove the prototype and the call comes before any declaration. Old
C then guessed that the function returns int and
accepts anything, an "implicit declaration". gcc 12 still only warns
without flags; with this site's flags the program does not compile,
and GCC 14 and later refuse it by default:
#include <stdio.h>
int main(void)
{
printf("%ld cents\n", cost_cents(350, 2700));
return 0;
}
long cost_cents(long kwh, long price_per_kwh)
{
return (kwh * price_per_kwh + 50) / 100;
}
example.c:5:27: error: implicit declaration of function ‘cost_cents’ [-Werror=implicit-function-declaration]
The same error appears when you call a library function without
including its header, such as sqrt without
<math.h>; gcc then even suggests the right
header. The headers of the standard library are nothing more than
files full of prototypes.
int f() is not int f(void)
Up to C17, empty parentheses in a declaration meant "parameters not
specified", not "no parameters". The compiler then checks nothing:
gcc 12 in C17 mode compiles a call such as
meter_count(2026) to a function declared
int meter_count() without a word, although passing the
wrong number of arguments that way is undefined behavior. C23
changed the meaning: () now means the same as
(void), and the same call is an error. Compiled as C23
with clang 21:
#include <stdio.h>
int meter_count()
{
return 400000;
}
int main(void)
{
printf("%d\n", meter_count(2026));
return 0;
}
example.c:10:32: error: too many arguments to function call, expected 0, have 1
Write (void) for a function without parameters. It
means the same thing in every version of C, and old code that relies
on () stops compiling on compilers that default to C23,
such as GCC 15 and later.
Arguments are copies
C passes every argument by value: the parameter is a new local variable that starts with a copy of the argument. Changing it inside the function does not change the caller's variable. The general idea is described under pass by value.
#include <stdio.h>
void add_bonus(int score)
{
score += 10; /* changes the copy only */
printf("inside: %d\n", score);
}
int main(void)
{
int score = 70;
add_bonus(score);
printf("after: %d\n", score);
return 0;
}
inside: 80 after: 70
To let a function change the caller's variable, pass its address and
let the function write through the pointer. That is how
scanf stores what it reads, and how a function returns
more than one result; the
pointer version of add_bonus
is shown on memory management, and the details are on
pointers.
Two things look like exceptions but follow the same rule. A struct argument is copied completely, however large it is, which is why large structs are usually passed by pointer (see structs, unions and enums). An array argument is not copied at all: what is passed, by value, is a pointer to its first element (see array decay), so the function can change the caller's elements.
Header files and source files
Once a program grows beyond one file, the functions it shares are
split in two parts. A
header file (.h)
contains the declarations other files need: prototypes, macros, type
definitions and extern declarations of shared
variables. The matching source file (.c) contains the
definitions, the actual code. Every file that wants to call the
functions includes the header with #include "tariff.h"
(quotes for your own headers, angle brackets for system headers).
This example builds a small billing program from three files and
compiles them in separate steps. Each .c file becomes
an object file (.o) with gcc -c, and a
final gcc call links the object files into the program
billing:
cat > tariff.h <<'EOF'
#ifndef TARIFF_H
#define TARIFF_H
/* Price in ten-thousandths of a euro per kWh: 2700 = EUR 0.2700 */
#define FIXED_PRICE 2700L
long cost_cents(long kwh, long price);
#endif
EOF
cat > tariff.c <<'EOF'
#include "tariff.h"
/* Rounds ten-thousandths of a euro to whole cents (half up). */
static long round_cents(long ten_thousandths)
{
return (ten_thousandths + 50) / 100;
}
long cost_cents(long kwh, long price)
{
return round_cents(kwh * price);
}
EOF
cat > main.c <<'EOF'
#include <stdio.h>
#include "tariff.h"
int main(void)
{
long cents = cost_cents(350, FIXED_PRICE);
printf("350 kWh costs EUR %ld.%02ld\n", cents / 100, cents % 100);
return 0;
}
EOF
gcc -std=c17 -Wall -Wextra -Werror -c tariff.c
gcc -std=c17 -Wall -Wextra -Werror -c main.c
gcc -o billing main.o tariff.o
./billing
350 kWh costs EUR 94.50
The #ifndef TARIFF_H … #endif lines are an
include guard: they make sure the header's contents are seen only
once per source file, even when it is included twice through other
headers. Include guards and the other preprocessor directives, and
building the same program with a Makefile, are explained on
the preprocessor and build tools. How the compiler and linker steps fit together is on
compiling and running C.
What belongs in a header
A header is copied into every source file that includes it. Anything it defines is therefore defined once per file, which the linker rejects for functions and variables. The rule of thumb:
| In the header (.h) | In one source file (.c) |
|---|---|
function prototypes:
long cost_cents(long kwh, long price);
|
function definitions with their bodies |
extern declarations:
extern int report_interval_s;
|
the variable's definition:
int report_interval_s = 60;
|
macros, struct, enum and
typedef declarations
|
static helper functions and variables that no
other file needs
|
Every header should also include the headers it needs itself, so
that it works whatever order the source files include things in.
Small helper functions are sometimes defined in a header as
static inline; that is the one common exception to the
rule.
Linkage: which names the linker sees
Linkage decides whether the same name in different places refers to the same function or variable. A name with external linkage is visible to the linker, so every file in the program can use it; functions and file-level variables have external linkage by default. A name with internal linkage is private to its own source file: another file can use the same name for something else without conflict. Local variables inside a function have no linkage at all.
The tool nm lists the names (symbols) in an object
file. In the billing example, cost_cents is shown with
a capital T (code with external linkage) and the
static helper round_cents with a lowercase
t (internal); U marks a name the file uses
but expects the linker to find elsewhere:
cat > tariff.c <<'EOF'
static long round_cents(long ten_thousandths)
{
return (ten_thousandths + 50) / 100;
}
long cost_cents(long kwh, long price)
{
return round_cents(kwh * price);
}
EOF
cat > main.c <<'EOF'
#include <stdio.h>
long cost_cents(long kwh, long price);
int main(void)
{
printf("%ld\n", cost_cents(350, 2700));
return 0;
}
EOF
gcc -std=c17 -Wall -Wextra -Werror -c tariff.c main.c
echo "--- tariff.o"
nm tariff.o
echo "--- main.o"
nm main.o
--- tariff.o
000000000000003c T cost_cents
0000000000000000 t round_cents
--- main.o
U cost_cents
0000000000000000 T main
U printf
static: private to the file
The keyword
static has two jobs
in C, depending on where it stands. At file level, in front of a
function or variable, it gives the name internal linkage. Inside a
function, in front of a local variable, it gives the variable static
storage duration (below). Make every
function and file-level variable static unless another
file really needs it: the smaller the set of shared names, the fewer
clashes and the easier the code is to change.
Calling a static function from another file compiles
fine, as long as there is a prototype, but fails when linking.
Recorded with gcc 12 (the temporary object file name changes on
every run and is shown here as /tmp/ccXXXXXX.o):
/usr/bin/ld: /tmp/ccXXXXXX.o: in function `main': main2.c:(.text+0x10): undefined reference to `round_cents' collect2: error: ld returned 1 exit status
Linker errors look different from compiler errors: they name no
source line, only an object file and an offset inside it, and they
appear only after every file compiled without errors. "undefined
reference" means a declared name was never defined in any object
file or library that was linked, often because a
.c file or a library such as -lm was left
off the command line.
extern: one definition, many declarations
A variable that several files share must be defined in exactly one
of them. The keyword
extern declares a
variable without defining it: it tells the compiler "this exists,
with this type, somewhere else", and the linker connects all uses to
the single definition. The declaration goes in the header, the
definition in one source file:
cat > settings.h <<'EOF'
#ifndef SETTINGS_H
#define SETTINGS_H
extern int report_interval_s; /* declaration: defined in settings.c */
#endif
EOF
cat > settings.c <<'EOF'
#include "settings.h"
int report_interval_s = 60; /* definition: the factory default */
EOF
cat > main.c <<'EOF'
#include <stdio.h>
#include "settings.h"
int main(void)
{
printf("factory default: every %d s\n", report_interval_s);
report_interval_s = 10;
printf("pilot setting: every %d s\n", report_interval_s);
return 0;
}
EOF
gcc -std=c17 -Wall -Wextra -Werror -o dongle main.c settings.c
./dongle
factory default: every 60 s pilot setting: every 10 s
The numbers echo the
Kestrel Home pilot's dongle, which averages power over 10 seconds instead of its
factory default of 60; the code is a teaching example, not the
manufacturer's firmware. Defining the variable in two files, for
instance by putting int report_interval_s = 60; in the
header, makes the link fail. Since GCC 10 (which defaults to
-fno-common) that is an error even when neither
definition has an initializer; older GCC versions silently merged
such definitions. Recorded:
/usr/bin/ld: /tmp/ccXXXXXX.o:(.data+0x0): multiple definition of `report_interval_s'; /tmp/ccYYYYYY.o:(.data+0x0): first defined here collect2: error: ld returned 1 exit status
Global variables are best kept few. Every file can change them, so a
value that is wrong at run time could have been set anywhere. A
static variable with a pair of small functions to read
and change it keeps control in one file.
Scope and storage duration
Two different questions are easy to mix up. Scope is where in the source text a name can be used. A name declared inside a block has block scope, from its declaration to the closing brace; a name declared outside every function has file scope. Storage duration is how long the object behind the name exists while the program runs:
| Storage duration | Which objects | Lives |
|---|---|---|
| automatic | local variables and parameters | from entering the block until leaving it, usually in the function's stack frame |
| static |
file-level variables, and local variables marked
static
|
the whole run of the program; initialized once, before
main starts, to zero if no value is given
|
| allocated | memory from malloc and friends |
until free (see
dynamic memory)
|
| thread | variables marked _Thread_local (C11) |
as long as their thread |
A static local variable combines block scope with
static storage duration: only its function can see it, but it keeps
its value from one call to the next. Its initializer runs once, not
on every call:
#include <stdio.h>
static int next_sequence(void)
{
static int last = 0; /* initialized once, kept between calls */
int step = 1; /* created anew on every call */
last += step;
return last;
}
int main(void)
{
for (int i = 0; i < 3; i++) {
printf("telegram %d\n", next_sequence());
}
return 0;
}
telegram 1 telegram 2 telegram 3
An automatic variable without an initializer is not zero: it holds
whatever was in that memory before. gcc refuses to compile an
obvious read of one (‘total’ is used uninitialized),
but it cannot see every case, so give every local variable a value
when you declare it.
Shadowing
A name declared in an inner block hides the same name from an outer block or from file scope for the rest of that block. That is legal, and a source of confusing bugs:
#include <stdio.h>
int total = 0; /* file scope */
int main(void)
{
int readings[3] = {120, 95, 130};
for (int i = 0; i < 3; i++) {
int total = readings[i] * 2; /* a new variable that hides the global */
printf("doubled: %d\n", total);
}
printf("global total: %d\n", total);
return 0;
}
doubled: 240 doubled: 190 doubled: 260 global total: 0
Neither -Wall nor -Wextra reports this;
the separate flag -Wshadow does.
main, recursion and variadic functions
main is the function the startup code calls. It has one
of two standard forms: int main(void), or
int main(int argc, char *argv[]) to receive the
command-line arguments as an array of strings (argv[0]
is the program name). Its return value becomes the program's
exit status:
0 means success; how to report failure is covered on
error handling.
A C function may call itself. Each call gets its own stack frame with its own copies of the parameters and locals, exactly as described on recursion. C does nothing to limit the depth: recursion that goes too deep runs out of stack and the program is killed with a segmentation fault. On a microcontroller with a few kilobytes of stack per task, firmware usually avoids recursion entirely.
Functions such as printf accept a variable number of
arguments. Writing your own uses <stdarg.h>; the
compiler cannot check those extra arguments, which is why gcc has
special checks for printf-style format strings (see
input, output and files).