Advanced • Build & Linking
The C++ ABI, Name Mangling & Linking
By the end of this lesson you'll be able to read and fix the two linker errors that stop every real C++ project — "undefined reference" and "multiple definition" — explain the difference between a declaration and a definition, control whether a name is private to one file or shared across files, and understand why mixing compilers or STL versions corrupts a build.
What You'll Learn
- Separate the compile stage from the link stage and know which error is which
- Explain why C++ mangles names — and how extern "C" turns it off for C interop
- Tell a declaration apart from a definition and apply the One Definition Rule
- Give a name internal linkage (static / anonymous namespace) or external linkage (extern)
- Diagnose and fix "undefined reference" and "multiple definition" linker errors
- Explain ABI stability and why mixing compiler or STL versions breaks binaries
.h) and source (.cpp) files and including headers. This lesson explains what actually happens to those files when you build.💡 Real-World Analogy
Think of building a program like assembling a piece of flat-pack furniture from several boxes. Compiling is each box being made on its own production line — the factory only needs the instruction sheet (a declaration: "part B exists and bolts here") to make a box, not the actual part. Linking is opening every box at home and bolting the real parts together (the definitions). If a part the instructions promised is in no box, you get "undefined reference" — a missing piece. If two boxes each contain the same uniquely-numbered part, you get "multiple definition" — a duplicate the assembler refuses. And the ABI is the agreed bolt size and hole spacing: if one box was made to metric and another to imperial, nothing fits even though both look like furniture.
1. Compiling vs Linking — Two Jobs, Two Errors
Building a C++ program is a pipeline. The preprocessor expands every #include and #define into one big translation unit; the compiler turns each .cpp into an object file (.o or .obj) on its own; the linker stitches those object files (plus any libraries) into the final executable. The crucial insight: each .cpp is compiled alone, so the compiler only needs a declaration — a promise that a function exists — to accept a call to it. Finding the real definition is the linker's job, later. That split is why "compiler error" and "linker error" mean very different things.
Worked example: the three build stages
Read every comment, run it, and match the output.
#include <iostream>
using namespace std;
// === The C++ build pipeline (three stages) ===
// 1) PREPROCESSOR expands #include / #define -> one big "translation unit"
// 2) COMPILER turns each .cpp into an object file (.o / .obj)
// 3) LINKER stitches the .o files (+ libraries) into the executable
//
// Key idea: each .cpp is compiled ALONE. The compiler only needs a
// DECLARATION (a promise) to accept a call. The LINKER later finds the
// real DEFINITION (the body). Two different
...2. Name Mangling — Why Overloading Works
C allows only one function per name. C++ supports overloading — several functions sharing a name but differing in their parameters. The linker, though, only sees a flat list of symbol names with no notion of types. So the compiler mangles each name, encoding the parameter types into the symbol, which is how process(int) and process(double) become two distinct symbols the linker can tell apart. You'll meet these mangled names whenever you read a linker error.
Pro Tip: Demangle any symbol with c++filt: c++filt _Z7processi prints process(int). Run nm -C ./app to list every symbol in a binary already demangled — invaluable when an error mentions a name like _ZN4math3addEii.
Worked example: overloads get unique mangled names
See how the encoded parameter types make overloading resolvable.
#include <iostream>
using namespace std;
// C allows ONE function per name. C++ allows OVERLOADING — same name,
// different parameters. The linker only sees flat symbol names, so the
// compiler MANGLES each name to bake the parameter types in.
void process(int x) { cout << "int: " << x << endl; }
void process(double x) { cout << "double: " << x << endl; }
// GCC/Clang mangle the two functions to DIFFERENT symbols:
// process(int) -> _Z7processi (i = one int)
// process(double
...When you need C code (or an OS API, or dlopen) to call your function, mangling gets in the way — C has no mangling, so the names must match exactly. extern "C" tells the compiler to emit a plain, unmangled symbol. The trade-off: because the type info is gone, an extern "C" function cannot be overloaded.
Worked example: extern "C" disables mangling
Produce a plain C-compatible symbol name.
#include <iostream>
using namespace std;
// extern "C" turns OFF mangling, so the symbol is the plain name "c_add".
// That is how C code (and dlopen, and most OS APIs) can find the function:
// C has no mangling, so the names must match exactly.
extern "C" int c_add(int a, int b) { // symbol is literally "c_add"
return a + b;
}
// The classic dual-language header guard looks like this:
// #ifdef __cplusplus
// extern "C" {
// #endif
// int my_init(void); // declarations
...Your turn. The program below promises a function exists (a declaration) and calls it, but its body is missing — the exact setup that triggers "undefined reference". Fill in the two blanks marked ___ to call it and to supply the definition.
🎯 Your turn: fix an undefined reference
Add the missing call and the missing definition, then run it.
#include <iostream>
using namespace std;
int main() {
// 🎯 YOUR TURN — fix the "undefined reference" by adding a DEFINITION.
// The forward DECLARATION below promises greet() exists, and main()
// calls it — but the body is missing, so the LINKER fails.
// 1) A declaration already promises greet exists:
// (this line is correct, leave it)
// -> see the line just below main's closing brace
cout << "Message: ";
cout << ___ << endl; // 👉 call greet() (r
...3. Internal vs External Linkage & the One Definition Rule
Linkage decides who can see a name across files. A name with internal linkage is private to one .cpp file — give it that with an anonymous namespace (the modern way) or file-scope static, and two files can each have their own same-named helper with no clash. A name with external linkage is visible to every file; you reach a variable defined elsewhere by declaring it extern. Tying it together is the One Definition Rule (ODR): you may declare a name as often as you like, but you must define it exactly once across the whole program (unless it is inline or a template, which are allowed to repeat and get merged).
Worked example: internal, external, inline & the ODR
See anonymous namespaces, static, extern, and inline in action.
#include <iostream>
using namespace std;
// === LINKAGE: who can see a name across files ===
//
// INTERNAL linkage = private to THIS file. Two files can each have their
// own "secret" with the same name and never collide.
namespace { // anonymous namespace -> internal linkage (modern)
int secret = 7;
void bump() { secret++; }
}
static int fileCounter = 0; // file-scope 'static' -> also internal linkage
// EXTERNAL linkage = visible to OTHER files. There must be exactl
...Now you try. The helper below would collide with a same-named helper in another file because it has external linkage. Wrap it so it becomes private to this file:
🎯 Your turn: make a helper file-private
Give the helper internal linkage with an anonymous namespace.
#include <iostream>
using namespace std;
// Imagine TWO .cpp files both define a helper called "log". With external
// linkage that is a "multiple definition" link error. Give YOUR copy
// INTERNAL linkage so it can never clash with the other file's "log".
// 🎯 YOUR TURN — wrap the helper so it is private to this file.
___ { // 👉 start an anonymous namespace: namespace {
void logMsg(const string& m) {
cout << "[log] " << m << endl;
}
}
...4. The Linker Errors You'll Actually Hit
Almost every real linker error is one of three things: an undefined reference (declared and used, but never defined or never linked), a multiple definition (the same symbol defined in two translation units — usually a non-inline definition placed in a header that two files included), or a declaration/definition mismatch (the signatures disagree, so the mangled names differ and the call resolves to nothing). The worked example shows each, with the comment spelling out the message and the fix.
Worked example: three linker errors and their fixes
Undefined reference, multiple definition, and signature mismatch — each fixed.
#include <iostream>
using namespace std;
// === Worked example: the THREE classic linker errors, and the fix ===
// (A) UNDEFINED REFERENCE — declared, used, never defined.
// int compute(); // declaration only
// int main(){ compute(); } // link error:
// undefined reference to 'compute()'
// FIX: provide a definition AND compile/link its .cpp:
int compute() { return 42; } // <- the missing body
// (B) MULTIPLE DEFINITION — same symbol defined in two TUs.
// A pl
...5. ABI Stability — Why Mixing Compilers Breaks
The ABI (Application Binary Interface) is the binary contract two object files must agree on: how names are mangled, how arguments are passed in registers or on the stack, and how a class or a standard-library type like std::string is laid out in memory. Source-level (API) compatibility is not enough — if one binary thinks std::string is 32 bytes and another thinks it is 24, a string passed across that boundary reads the wrong memory and crashes. That is why everything in a program, including every library, must be built with one compiler and one standard-library version.
Worked example: memory layout is the ABI
Inspect struct size and offsets, and see why ABI mismatches corrupt data.
#include <iostream>
using namespace std;
// === ABI = Application Binary Interface ===
// The ABI is the BINARY contract two object files must agree on:
// * how names are mangled
// * how function arguments are passed (registers / stack)
// * how a class / struct is laid out (size, member offsets, vtable)
// * how std::string, std::vector, etc. are structured internally
//
// Compile-time API compatibility is NOT enough — the BINARY layout must
// match too. Two pieces of code only lin
...🔎 Deep Dive: where to put definitions so the ODR is happy
Headers carry promises, source files carry bodies. Put declarations in the .h (function prototypes, extern variable declarations, class definitions, type aliases) and the single definition in one .cpp. Break that and the same symbol lands in every file that includes the header — the classic "multiple definition".
The escape hatch is inline: an inline function or variable is allowed to be defined in multiple translation units, and the linker merges the copies into one. That is exactly why header-only libraries mark their definitions inline, and why constexpr (which is implicitly inline) and templates may live entirely in headers.
// util.h — DECLARATIONS only (safe to include everywhere)
int add(int a, int b); // promise: defined in some .cpp
extern int callCount; // promise: storage lives in some .cpp
inline int twice(int x){return x*2;} // OK in a header: inline -> merged
// util.cpp — DEFINITIONS exactly once
int add(int a, int b){ return a + b; } // the one real body
int callCount = 0; // the one real storagePro Tips
- 💡 Compiler error vs linker error: if the message names a line and a syntax/type problem it's the compiler; if it says "undefined reference" or "multiple definition" with a mangled symbol, it's the linker.
- 💡 Prefer anonymous namespaces to
staticfor file-private code — they also apply to types, not just functions and variables. - 💡 Keep ABI boundaries in C: across a stable library boundary, an
extern "C"interface is far more robust than passing C++ types, because C's ABI rarely changes. - 💡 Build everything the same way: one compiler, one standard library, matching flags (especially the
_GLIBCXX_USE_CXX11_ABIsetting on GCC) for every object file and dependency.
Common Errors (and the fix)
- "undefined reference to 'foo()'": you declared and called
foobut the linker found no body. Either you never wrote the definition, or you forgot to compile/link the.cppthat defines it. Provide the definition and add its file to the build (g++ main.cpp foo.cpp). - "multiple definition of 'bar'; first defined here": a non-
inlinefunction body or a plain global variable lives in a header that two files included. Move the definition into one.cpp(leave only a declaration in the header), or mark itinlineso duplicates merge. - Declaration / definition mismatch: the header says
void save(long);but the source definesvoid save(int). Different types mangle to different symbols, so you getundefined reference to 'save(long)'. Make the signatures match exactly. - "undefined reference to 'vtable for Shape'": a class has a virtual function declared but its first non-inline virtual (or destructor) is never defined. Define that out-of-line virtual function in one
.cpp. - Mysterious crashes after upgrading a library: usually an ABI mismatch — the library and your code were built with different compilers or STL versions. Rebuild everything with one toolchain and matching ABI flags.
📋 Quick Reference
| Concept | What it means | Example |
|---|---|---|
| Declaration | Promise a name exists (no body) | int add(int, int); |
| Definition | The body / storage (once only) | int add(int a,int b){...} |
| Internal linkage | Private to one file | namespace { ... } |
| External linkage | Shared across files | extern int count; |
| Allow duplicates | Merge copies across files | inline int twice(int); |
| Disable mangling | Plain C symbol name | extern "C" int f(); |
| Demangle | Read a mangled symbol | c++filt _Z7processi |
| Inspect ABI | Layout / runtime libs | sizeof / ldd ./app |
Frequently Asked Questions
Q: What does "undefined reference to ..." actually mean?
It is a linker error, not a compiler error. The compiler found a declaration (a promise the function or variable exists), so the .cpp file compiled fine — but the linker could not find the matching definition (the body) in any object file or library. Either you forgot to compile/link the .cpp that defines it, or you only ever wrote the declaration. Provide the definition and link it in.
Q: Why do I get "multiple definition of ..." when I include a header in two files?
Your header almost certainly defines something (a non-inline function body or a plain global variable), and you included it in two .cpp files. Each file gets its own copy, so the linker sees two definitions of the same symbol — a One Definition Rule violation. Put only declarations in headers; move the definition to one .cpp file, or mark it inline so duplicate copies are allowed and merged.
Q: What is the difference between a declaration and a definition?
A declaration introduces a name and its type so the compiler will accept uses of it (e.g. int add(int, int); or extern int count;). A definition actually creates the thing — the function body, or the storage for the variable. You may declare a name as many times as you like, but you may define it only once across the whole program.
Q: Why does mixing GCC and Clang (or two STL versions) break my build at link or run time?
Because they can disagree on the ABI — the binary contract for how names are mangled, how classes are laid out, and how the standard library types like std::string are structured. If one object file thinks std::string is 32 bytes and another thinks it is 24, calls and member access read the wrong memory. The fix is to build everything, including every library, with one compiler and one standard-library version.
Q: When should I use static, an anonymous namespace, or extern?
Use an anonymous namespace (the modern choice) or file-scope static to give a function or variable internal linkage — private to one .cpp file, so it can never clash with a same-named symbol elsewhere. Use extern to declare that a variable is defined in another file, giving it external linkage so the linker connects the two. Anonymous namespaces are preferred over static for this in modern C++ because they also work for types.
Mini-Challenge: Declaration, Definition & Linkage
No blanks this time — just a brief and a blank canvas (with an outline to keep you on track). Apply the three rules from this lesson in a single program: a declaration that promises a function, a file-private helper with internal linkage, and exactly one definition. Run it and check your output against the example in the comments.
🎯 Mini-Challenge: put the rules together
Use a declaration, an anonymous namespace, and one definition.
#include <iostream>
using namespace std;
// 🎯 MINI-CHALLENGE: a clean two-"file" mental model in one program
// You cannot make two real files here, so SIMULATE the rules:
//
// 1. Declare a function: int total(int a, int b); (a promise / header)
// 2. Give a file-private helper INTERNAL linkage by wrapping it in an
// anonymous namespace: namespace { int doubleIt(int x){ return x*2; } }
// 3. DEFINE total() exactly once; have it call doubleIt and add the two.
// 4. In main, print total
...🎉 Lesson Complete
- ✅ Build pipeline: preprocess → compile (object files) → link (executable)
- ✅ C++ mangles names to make overloading work;
extern "C"turns mangling off for C interop - ✅ A declaration is a promise; a definition is the body — define each name once (the ODR)
- ✅ Internal linkage (anonymous namespace /
static) is file-private; external linkage (extern) is shared - ✅ "undefined reference" = missing definition/link; "multiple definition" = same symbol in two files
- ✅ The ABI is the binary layout contract — mixing compilers or STL versions corrupts it
- ✅ Next lesson: C Interoperability — calling C libraries from C++ and exposing C++ to C cleanly
Sign up for free to track which lessons you've completed and get learning reminders.