Multi-file programs
One source file becomes hard to navigate long before it becomes hard to compile. Splitting a program by responsibility gives each file one job and puts the shared declarations in a header. The functions themselves are the subject of lesson 17; here the question is how separate files see each other.
One file becomes three
The header declares what the rest of the program may use, and the source file defines it. The main file includes the header and calls the functions without knowing their bodies. Both source files are listed on one compiler command, and the compiler driver passes them through the linker together.
vi stats.h
#ifndef STATS_H
#define STATS_H
int sum(int a, int b);
int max(int a, int b);
#endif
vi stats.c
#include "stats.h"
int sum(int a, int b)
{
return a + b;
}
int max(int a, int b)
{
return (a > b) ? a : b;
}
vi main.c
#include <stdio.h>
#include "stats.h"
int main(void)
{
printf("sum: %d\n", sum(4, 7));
printf("max: %d\n", max(4, 7));
return 0;
}
gcc -Wall -Wextra stats.c main.c -o app
./app
sum: 11
max: 7
Quoted includes search the directory of the including file first, then the system paths; angle brackets search only the system paths. That is why a project header uses quotes and stdio.h uses angle brackets.
The header and its guard
The three lines around the declarations are an include guard. The first include defines the macro, and a second include of the same file finds the name defined, skips to #endif, and contributes nothing. Declarations may legally repeat, but a type or an object definition in a header may not, and the guard makes the header safe either way.
vi twice_include.c
#include <stdio.h>
#include "stats.h"
#include "stats.h"
int main(void)
{
printf("%d\n", sum(1, 2));
return 0;
}
gcc -Wall -Wextra twice_include.c stats.c -o twice_include
./twice_include
3
The guard also lets headers include each other without an endless expansion. Every project header in this course starts with the same three lines; the part on the preprocessor explains what it does with them.
Objects and the linker
A single file shows the same thing in miniature. Its object holds the machine code of main and one defined symbol; the linker turns that object into an executable by adding the startup code and the C library.
#include <stdio.h>
int main(void)
{
printf("object files\n");
return 0;
}
gcc -Wall -Wextra -c hello.c -o hello.o
nm hello.o | grep " T "
gcc hello.o -o hello
./hello
0000000000000000 T main
object files
Back in the project, -c stops after assembling and writes an object file per source file. The object holds machine code and a symbol table: the names it defines and the names it still needs. The linker joins the objects and resolves those names. nm prints the table, and an upper-case T marks a definition in the text section.
gcc -Wall -Wextra -c stats.c -o stats.o
gcc -Wall -Wextra -c main.c -o main.o
nm stats.o | grep " T "
gcc stats.o main.o -o app
./app
0000000000000014 T max
0000000000000000 T sum
sum: 11
max: 7
The addresses belong to the object file and change with the compiler and the code; the names and the letter are the useful part. A lower-case t would mean a static function, visible only inside its own object, and a U would mean a name still undefined there.
static across files
A static function is local to its translation unit, so two files may define the same helper name without a conflict. This is how a file keeps its internals private: only the functions declared in the header are part of its interface. The two twice functions below are different functions with the same name.
vi helpers.c
static int twice(int value)
{
return value * 2;
}
int scaled(int value)
{
return twice(value) + 1;
}
vi use.c
#include <stdio.h>
static int twice(int value)
{
return value + value;
}
int scaled(int value);
int main(void)
{
printf("local: %d\n", twice(5));
printf("from helpers: %d\n", scaled(5));
return 0;
}
gcc -Wall -Wextra helpers.c use.c -o use
./use
local: 10
from helpers: 11
The local helper adds 5 + 5 and the one in helpers.c multiplies by two before adding one. Without static the linker would see two global definitions of twice and stop, as the next section shows for other names.
Undefined and duplicate symbols
The first mistake is a declared function that no file defines. The compiler accepts the call because the declaration promises it, and the linker reports that nobody supplied it. Forgetting a source file on the command line produces exactly this message for every function in it:
#include <stdio.h>
int missing(int value);
int main(void)
{
printf("%d\n", missing(1));
return 0;
}
gcc -Wall -Wextra broken.c -o broken
/usr/lib/gcc/x86_64-alpine-linux-musl/15.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: /tmp/ccGkgECP.o: in function `main':
broken.c:(.text+0xa): undefined reference to `missing'
collect2: error: ld returned 1 exit status
The temporary object name changes on every run; the two undefined names are the message. The second mistake is the opposite: the same source file twice on the command line, which defines every function twice.
gcc -Wall -Wextra stats.c stats.c main.c -o double
/usr/lib/gcc/x86_64-alpine-linux-musl/15.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: /tmp/cciIiheI.o: in function `sum':
stats.c:(.text+0x0): multiple definition of `sum'; /tmp/ccBcdeOH.o:stats.c:(.text+0x0): first defined here
/usr/lib/gcc/x86_64-alpine-linux-musl/15.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: /tmp/cciIiheI.o: in function `max':
stats.c:(.text+0x14): multiple definition of `max'; /tmp/ccBcdeOH.o:stats.c:(.text+0x14): first defined here
collect2: error: ld returned 1 exit status
The linker names both objects and the function, so the duplicate is easy to find. Typing every file name by hand is what makes this mistake common, and lesson 20 replaces the command with a makefile.
Exercises
- Add a third function, min, to the header and the source file, call it from main, and build without naming the source files twice.
- Put the two twice functions in one file and describe the error. Then make both static and check that the file builds.
- Compile only main.c with -c and list the undefined symbols with nm main.o | grep " U ". Which names appear and why?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What belongs in a project header?
Answer
Declarations shared by the source files
A header declares what other files may use, while the definitions stay in the source files.
-
What does an include guard prevent?
Answer
A header being read twice in one translation unit
The first include defines the guard macro, and later includes skip the file, so repeated declarations and definitions are avoided.
-
How does
#include "stats.h"differ from#include <stats.h>?Answer
Quotes search the including file directory first
Quoted includes look in the directory of the including file before the system paths; angle brackets use the system paths only.
-
Why can two source files define a
static int twice(int)function with the same name?Answer
Static functions are local to their translation unit
Internal linkage keeps each
staticfunction private to its own object, so the names do not collide at link time. -
What does this program print?
#include <stdio.h> static int sum(int a, int b) { return a + b; } static int max(int a, int b) { return a > b ? a : b; } int main(void) { printf("sum: %d\n", sum(4, 7)); printf("max: %d\n", max(4, 7)); return 0; }Answer
sum: 11 / max: 7
The addition gives 11 and the larger argument is 7, so the two lines print those values.
-
A header with an include guard is included twice in one source file. How many times do its declarations reach the compiler?
Answer
Once
The first include defines the guard macro, and the second include is skipped, so the declarations appear once.
-
What does this program print?
#include <stdio.h> static int twice(int value) { return value + value; } static int scaled(int value) { return twice(value) + 1; } int main(void) { printf("local: %d\n", twice(5)); printf("from helpers: %d\n", scaled(5)); return 0; }Answer
local: 10 / from helpers: 11
The local helper adds 5 and 5, while
scaleddoubles 5 and adds one. -
In
nmoutput, what does an upper-caseTnext to a name mean?Answer
The symbol is defined in the text section
Tmarks a global definition in the text section; a lower-casetwould be a static function andUan undefined name. -
This program is built with
gcc -std=c17 -Wall -Wextra broken.c -o broken. What does the linker report?#include <stdio.h> int missing(int value); int main(void) { printf("%d\n", missing(1)); return 0; }Answer
undefined reference to `missing'
The declaration satisfies the compiler, but no object defines
missing, so the linker reports the undefined reference. -
A source file that defines
mainand a globalsumis listed twice on onegcccommand line. What does the linker report?Answer
multiple definition of 'main'
The same source is listed twice, so both objects define the same global symbols and the linker stops.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev