77 / 163 · C++11 · 13 min
From Source Files to a Program: Compilation and Linking
A compiler seeing a declaration is not the same as a linker finding a definition. Locate errors by preprocessing, compilation, assembly, and linking, then use a local three-file project to understand static libraries, dynamic libraries, and CMake target dependencies; these build styles are toolchain conventions, not language-mandated file formats.
In this lesson
Four Stages, and the Question Each Answers
In a traditional header-file build, a source file becomes a translation unit after header expansion, macro replacement, and conditional compilation. The compiler performs syntax checking, type checking, and code generation; the assembler produces an object file; the linker merges object files and resolves symbols. Tools may fuse stages, and link-time optimization can also optimize across files; do not mistake the teaching pipeline for a requirement to launch four processes.
Save the program below as main.cpp. On a machine with GCC installed, run in order g++ -std=c++20 -E main.cpp -o main.ii, g++ -std=c++20 -S main.cpp -o main.s, g++ -std=c++20 -c main.cpp -o main.o, g++ main.o -o app, then run ./app. A missing header fails in the front end; a missing function definition usually fails at link time.
Split the Same Program into Three Local Files
Create math.hpp containing namespace demo { unsigned add(unsigned, unsigned); }. Create math.cpp, write #include "math.hpp" first, then place the function definition from the end of the example. Keep the standard headers and main in main.cpp, replace the original demo declaration block with an include of math.hpp, and delete the trailing definition. This header holds only a repeatable declaration; a real project should still use include guards.
Run g++ -std=c++20 -c main.cpp math.cpp, then g++ main.o math.o -o app. main.cpp needs only the signature to check the call; math.cpp supplies the unique implementation. Changing the implementation requires recompiling only math.cpp and relinking; changing a public declaration affects every translation unit that includes it. The implementation file should include its own header so signature mismatches are caught promptly.
Static and Dynamic Libraries: A Successful Link Is Not Enough
The following is a Linux/GNU toolchain example. ar rcs libmath.a math.o archives the object file; then g++ main.o ./libmath.a -o app-static statically links that library. That does not mean the system runtime libraries are also fully statically linked. A traditional static linker extracts members on demand; library order can affect symbol resolution.
Run g++ -std=c++20 -fPIC -shared math.cpp -o libmath.so, then g++ main.o -L. -lmath -Wl,-rpath,'$ORIGIN' -o app-shared. Place the program and the shared library in the same directory to run it. A shared library must be findable at run time and remain ABI-compatible; class layout, the exception runtime, and compiler options all affect compatibility. File extensions, load paths, and name mangling are not a uniform promise of ISO C++.
Express Build Relationships with Targets
Create CMakeLists.txt for the three files above, with each of the following seven items on its own line: cmake_minimum_required(VERSION 3.16), project(local_demo LANGUAGES CXX), add_library(math STATIC math.cpp), target_include_directories(math PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}"), target_compile_features(math PUBLIC cxx_std_11), add_executable(app main.cpp), target_link_libraries(app PRIVATE math).
Run cmake -S . -B build, then cmake --build build; a single-config generator typically runs ./build/app. PUBLIC means conditions needed by this target and by its consumers, PRIVATE applies only to the current target, and INTERFACE is propagated only to consumers. Attaching dependencies to targets prevents unrelated libraries from polluting one another more effectively than stacking global search paths.
Pitfalls
- #include brings a declaration or definition into the current translation unit; it does not automatically add another .cpp to the link. Do not include a .cpp file to paper over a missing build dependency.
- A successful link does not guarantee that a dynamic library can be loaded on a deployment machine; -L is a link-time search path, not a runtime load path.
Run an example
Minimum C++11 · complete program · Download .cpp
#include <cassert>
#include <iostream>
namespace demo {
unsigned add(unsigned a, unsigned b);
}
int main() {
const auto result = demo::add(2u, 3u);
assert(result == 5u);
assert(demo::add(0u, 7u) == 7u);
std::cout << result << '\n';
}
namespace demo {
unsigned add(unsigned a, unsigned b) {
return a + b;
}
}
Compile locally
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread tooling-compilation.cpp -o example && ./exampleExpected result
5
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
In the three-file version, why does executing only g++ main.o -o app fail? If you copy the definition of add into main.cpp and still link math.o, what happens? Give the correct commands.
Show a reference answer
The first case has a call to add but does not provide a definition, and you usually get an undefined reference. The second case gives the same non-inline external function two definitions, which violates the ODR and usually produces a multiple definition error; you cannot rely on the linker always diagnosing it. Keep the declaration in math.hpp and the unique definition in math.cpp, run g++ -std=c++20 -c main.cpp math.cpp and g++ main.o math.o -o app, then run ./app; the expected output is 5.
Check the sources
- GCC:Overall Options(-E、-S、-c)
- GCC:Link Options(库搜索与链接顺序)
- CMake:Adding a Library(4.1 教程)
- CMake:target_compile_features 的传播范围
Drafts and official chapters change. The version mark is only the example’s minimum.