Headers, object files and the linker

One file is enough for a lesson and too little for a program. A real project has a header for every module, a source file for its definitions, and a linker that joins the object files into one executable. This lesson builds that shape from three files and then breaks it twice on purpose, so the errors are familiar before a real project prints them.

The rule behind all of it is short: in C++ a declaration can appear many times, a definition only once. Headers hold declarations, .cpp files hold definitions, and the linker enforces the rest.

A header and two source files

math.h declares what math.cpp defines: two functions, their parameter types and their return types. main.cpp includes the header, so the compiler knows how to call square before it sees the body; the body is in another translation unit and the linker joins them.

The quoted include, "math.h", searches the directory of the source file first, which is what a project header wants. The angle brackets of <iostream> search the system include directories instead.

Compiling both .cpp files in one command is the short form: g++ compiles each one and links the results. The program prints 25 and 27, and the header never becomes part of the binary.

math.h

#pragma once int square(int value); int cube(int value);

math.cpp

#include "math.h" int square(int value) { return value * value; } int cube(int value) { return value * value * value; }

main.cpp

#include <iostream> #include "math.h" int main() { std::cout << square(5) << ' ' << cube(3) << '\n'; }

g++ -std=c++23 -Wall -Wextra main.cpp math.cpp -o app
./app

25 27

Compile each file, then link

-c stops after the object file and leaves the linking for later. Each .cpp becomes a .o, and the last command joins the .o files into an executable; this is what the one-line command did internally.

The split is what makes a large build fast: edit main.cpp, recompile main.o only, and link again. The other object files do not change, so their compile time is not paid twice. Every build system in part 9 automates exactly this.

The object files are machine code with unresolved names: main.o calls square without knowing where it lives, and the linker fills in the address. Run the program and it behaves exactly like the single-command build.

math.h

#pragma once int square(int value); int cube(int value);

math.cpp

#include "math.h" int square(int value) { return value * value; } int cube(int value) { return value * value * value; }

main.cpp

#include <iostream> #include "math.h" int main() { std::cout << square(5) << ' ' << cube(3) << '\n'; }

g++ -std=c++23 -Wall -Wextra -c main.cpp -o main.o
g++ -std=c++23 -Wall -Wextra -c math.cpp -o math.o
g++ main.o math.o -o app_objects
./app_objects

25 27

Two definitions of one function

The one definition rule says that a function may be defined once in the whole program. Two source files that both define answer break it, and the compiler cannot see the problem: each file compiles on its own without a complaint.

The linker sees both object files and reports the duplicate, naming the symbol and the first object file that defined it. The message starts with the linker's own name, ld, or with collect2, which is the driver program g++ uses to call ld.

The usual cause is a function defined in a header without inline. Every file that includes the header gets its own copy, and the linker refuses to choose. The fix is to move the body to one .cpp file or to mark it inline.

a.cpp

int answer() { return 42; }

b.cpp

int answer() { return 43; }

main.cpp

#include <iostream> int answer(); int main() { std::cout << answer() << '\n'; }

g++ -std=c++23 -Wall -Wextra main.cpp a.cpp b.cpp -o duplicate

/usr/lib/gcc/x86_64-alpine-linux-musl/15.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: /tmp/ccLBLGga.o: in function `answer()':
b.cpp:(.text+0x0): multiple definition of `answer()'; /tmp/ccdAfhcP.o:a.cpp:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

A header without an include guard

A header that is included twice in one translation unit is pasted twice, and a struct defined twice in the same file is a redefinition. The compiler reports the second definition and points at the first one, which is usually in the same header.

The fix is one line at the top of the header. #pragma once tells the preprocessor to include the file at most once, and every compiler in this course understands it. The older portable form is #ifndef NAME / #define NAME / #endif around the whole file.

The same error appears when two headers include each other, and the guard breaks that cycle too. Put #pragma once in every header from the first line you write, before the file has a chance to be included twice.

no_guard.h

struct point { int x; int y; };

bad.cpp

#include "no_guard.h" #include "no_guard.h" #include <iostream> int main() { point p{1, 2}; std::cout << p.x + p.y << '\n'; }

with_guard.h

#pragma once struct point { int x; int y; };

good.cpp

#include "with_guard.h" #include "with_guard.h" #include <iostream> int main() { point p{1, 2}; std::cout << p.x + p.y << '\n'; }

g++ -std=c++23 -Wall -Wextra bad.cpp -o bad
g++ -std=c++23 -Wall -Wextra good.cpp -o good
./good

In file included from bad.cpp:2:
no_guard.h:1:8: error: redefinition of 'struct point'
1 | struct point
| ^~~~~
In file included from bad.cpp:1:
no_guard.h:1:8: note: previous definition of 'struct point'
1 | struct point
| ^~~~~
3

One definition, many translation units

A small function in a header is convenient and would break the one definition rule, so C++ gives it an exception: an inline function may be defined in several translation units, and the linker keeps one copy. Both source files below include util.h and call twice, and the program links.

inline is a request, and the compiler is free to ignore it for the call; what it really means is that the definition is allowed in every unit. Functions defined inside a class body are inline implicitly, which is why a header full of small classes just works.

A constexpr function is inline as well, and that is the modern way to put a small computation in a header. The next part of the course uses inline for such helpers and keeps the rest of the definitions in .cpp files.

util.h

#pragma once inline int twice(int value) { return value * 2; }

main.cpp

#include <iostream> #include "util.h" int twice_from_other(); int main() { std::cout << twice(4) << ' ' << twice_from_other() << '\n'; }

other.cpp

#include "util.h" int twice_from_other() { return twice(21); }

g++ -std=c++23 -Wall -Wextra main.cpp other.cpp -o inline_demo
./inline_demo

8 42

Exercises

  1. Add a function mean(int, int) to math.h and math.cpp and call it from main.cpp. Compile it in one command and then through object files.
  2. Move the body of twice into a .cpp file instead of the header and compile the last program. What changes in the link, and what would happen if two files included the header?
  3. Remove #pragma once from util.h and compile with -H. Which file is read twice, and what does the compiler report?

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. Why is a function declared in a header?

    Answer

    So every source file that includes it knows the signature

    A declaration is what a translation unit needs to call the function; the definition lives in one source file.

  2. What does compiling with -c produce?

    Answer

    An object file with code and unresolved symbols

    The object holds the compiled code of one translation unit, and linking is the step that resolves the rest.

  3. What does the linker report when two source files define the same function?

    Answer

    A multiple definition error for that function

    One function may be defined once across all the translation units, and the linker enforces it.

  4. What is an include guard for?

    Answer

    It keeps one header from being expanded twice in a translation unit

    The guard is a preprocessor test around the contents, so a second include in the same file expands to nothing.

  5. What does this program print? #include <iostream> int square(int value) { return value * value; } int cube(int value) { return value * value * value; } int main() { std::cout << square(5) << ' ' << cube(3); }

    Answer

    25 27

    square(5) is 25 and cube(3) is 27, and the two calls are printed with a space between them.

  6. What does the object-file build print?

    Answer

    25 27

    Compiling to objects and linking them produces the same program and the same output.

  7. What does the duplicate-definition link say?

    Answer

    multiple definition of answer()

    Both a.cpp and b.cpp define answer, and the linker names the symbol and the two objects.

  8. What does including a header without a guard twice produce?

    Answer

    redefinition of 'struct point'

    The struct is defined twice in one translation unit, which the compiler rejects.

  9. What does this program print? #include <iostream> inline int twice(int value) { return value * 2; } int main() { std::cout << twice(4) << ' ' << twice(21); }

    Answer

    8 42

    The inline function is defined in the header-like position and returns 8 and 42 for the two calls.

  10. Why may an inline function be defined in a header?

    Answer

    The standard allows one definition in every translation unit that uses it

    The inline keyword relaxes the one-definition rule for exactly this case.

The code on this page was run with g++ 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev