ABI & Linking
Reviewed & published by Brayan K
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.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
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
💡 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.
#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 jobs, two different errors.
int add(int a, int b); // DECLARATION — "this exists somewhere", no body
int add(int a, int b) { // DEFINITION — the actual body (lives once)
return a + b;
}
int main() {
cout << "Compile turns source -> object files (.o)" << endl;
cout << "Link turns object files -> executable" << endl;
cout << "add(2, 3) = " << add(2, 3) << endl; // add(2, 3) = 5
// Try it on a real machine:
// g++ -c main.cpp # COMPILE only -> main.o
// g++ main.o -o app # LINK -> ./app
return 0;
}
// ✅ Expected output:
// Compile turns source -> object files (.o)
// Link turns object files -> executable
// add(2, 3) = 52. 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.
#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) -> _Z7processd (d = one double)
// That difference is exactly what makes overloading resolvable.
namespace math {
int add(int a, int b) { return a + b; }
// Mangled: _ZN4math3addEii
// N4math = namespace "math" (4 letters)
// 3add = function "add" (3 letters)
// ii = two int parameters
}
int main() {
process(42); // int: 42
process(3.14); // double: 3.14
cout << math::add(5, 3) << endl; // 8
// On a real machine, read a mangled name back:
// c++filt _Z7processi -> process(int)
// nm -C ./app -> lists demangled symbols
return 0;
}
// ✅ Expected output:
// int: 42
// double: 3.14
// 8When 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.
#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 only
// #ifdef __cplusplus
// }
// #endif
int main() {
cout << "c_add(3, 4) = " << c_add(3, 4) << endl; // c_add(3, 4) = 7
// WATCH OUT: extern "C" cannot mangle, so it cannot overload.
// extern "C" void f(int);
// extern "C" void f(double); // ERROR: conflicting declaration
return 0;
}
// ✅ Expected output:
// c_add(3, 4) = 7Your 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.
#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() (replace ___ with: greet())
// ✅ Expected output: Message: Hello from a real definition!
return 0;
}
// 2) Provide the DEFINITION so the linker can resolve the call:
string greet() {
return ___; // 👉 return "Hello from a real definition!";
}
// Declaration the compiler needs BEFORE main uses greet:
// (already provided above main in your head — here is the rule of thumb)
// Declaration = promise (in a header). Definition = body (in one .cpp).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).
#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 exactly ONE
// definition across the whole program (the One Definition Rule, ODR).
int sharedValue = 100; // define ONCE; other files say: extern int sharedValue;
// inline lets the SAME definition appear in many files (headers!) and be
// merged into one — the linker is told "duplicates are fine, dedupe them".
inline double square(double x) { return x * x; }
int main() {
bump(); bump();
fileCounter += 5;
cout << "secret (internal): " << secret << endl; // 9
cout << "fileCounter: " << fileCounter << endl; // 5
cout << "sharedValue (ext): " << sharedValue << endl; // 100
cout << "square(6.0): " << square(6.0) << endl; // 36
// ODR in one line: declare a name as often as you like,
// but DEFINE it exactly once (unless it is inline / a template).
return 0;
}
// ✅ Expected output:
// secret (internal): 9
// fileCounter: 5
// sharedValue (ext): 100
// square(6.0): 36Now 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:
#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;
}
} // (closing brace already here)
int main() {
logMsg("internal linkage means no clash");
// ✅ Expected output: [log] internal linkage means no clash
return 0;
}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.
#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 plain global in a header, #included by a.cpp and b.cpp:
// "multiple definition of 'config'; b.o: first defined here"
// FIX: header has extern int config; define it ONCE in one .cpp,
// OR mark it inline int config = 0; so duplicates merge.
inline int config = 0; // inline -> safe to appear in many files
// (C) DECLARATION/DEFINITION MISMATCH — signatures disagree, so the
// mangled names differ and the call never resolves:
// header: void save(long id);
// source: void save(int id) { ... } // different type!
// -> undefined reference to 'save(long)'
// FIX: make the declaration and definition match EXACTLY.
void save(int id) { cout << "saved " << id << endl; } // matches its decl
int main() {
cout << "compute() = " << compute() << endl; // compute() = 42
cout << "config = " << config << endl; // config = 0
save(7); // saved 7
return 0;
}
// ✅ Expected output:
// compute() = 42
// config = 0
// saved 75. 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.
#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 link & run together if their ABI
// agrees.
struct Widget {
int id; // offset 0
double price; // offset 8 (after padding)
}; // sizeof(Widget) is fixed by the ABI
int main() {
cout << "sizeof(Widget) = " << sizeof(Widget) << " bytes" << endl; // 16
cout << "id offset = " << offsetof(Widget, id) << endl; // 0
cout << "price offset = " << offsetof(Widget, price) << endl; // 8
// WHY MIXING COMPILERS/STL VERSIONS BREAKS:
// If lib A was built where std::string is 32 bytes and your code thinks
// it is 24, passing a string across that boundary reads the wrong
// memory -> crashes or garbage. GCC's std::string ABI change (the
// "_GLIBCXX_USE_CXX11_ABI" split) is the famous real-world example.
//
// RULE: build EVERY object file and library with ONE compiler and ONE
// standard-library version. Across a stable boundary, prefer a plain-C
// (extern "C") interface — C has a far more stable ABI.
return 0;
}
// ✅ Expected output:
// sizeof(Widget) = 16 bytes
// id offset = 0
// price offset = 8🔎 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 static for 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_ABI setting on GCC) for every object file and dependency.
Common Errors (and the fix)
- "undefined reference to 'foo()'": you declared and called foo but the linker found no body. Either you never wrote the definition, or you forgot to compile/link the .cpp that 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-inline function 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 it inline so duplicates merge.
- Declaration / definition mismatch: the header says void save(long); but the source defines void save(int). Different types mangle to different symbols, so you get undefined 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 |
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.
#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(3, 4).
//
// Remember the rules you just learned:
// * declaration = promise, definition = body (define exactly once = ODR)
// * anonymous namespace = internal linkage = no "multiple definition"
//
// ✅ Expected output: total(3, 4) = 14 (because (3+4)*2 = 14)
// your code here🎉 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
Practice quiz
During a C++ build, whose job is it to find the actual definition (body) of a function?
- The preprocessor
- The compiler
- The linker
- The loader at runtime
Answer: The linker. Each .cpp compiles alone needing only a declaration; the linker later matches calls to their definitions across object files.
You see 'undefined reference to foo()'. What kind of error is this?
- A linker error
- A compiler (syntax) error
- A preprocessor error
- A runtime exception
Answer: A linker error. The declaration let it compile, but the linker found no matching definition to link against.
Why does C++ mangle function names like process(int) into symbols such as _Z7processi?
- To encrypt the binary
- To make names shorter
- To support runtime reflection
- To encode parameter types so overloads become distinct symbols
Answer: To encode parameter types so overloads become distinct symbols. The linker only sees flat names, so the parameter types are baked into the symbol to keep overloads apart.
What does extern "C" do to a C++ function?
- Makes it run faster
- Gives it C linkage with a plain, unmangled symbol name
- Forces it to be inline
- Allows it to be overloaded
Answer: Gives it C linkage with a plain, unmangled symbol name. extern "C" emits the plain name (like c_add) so C code and OS APIs can find it; the trade-off is no overloading.
What is the difference between a declaration and a definition?
- A declaration introduces a name/type; a definition creates the actual body or storage
- They are the same thing
- A definition is only for variables
- A declaration allocates memory
Answer: A declaration introduces a name/type; a definition creates the actual body or storage. You may declare a name many times, but define it exactly once across the whole program.
What is the One Definition Rule (ODR)?
- Every file needs exactly one function
- Headers may only hold one declaration
- A name may be defined only once across the program (unless inline/template)
- Each .cpp may include a header only once
Answer: A name may be defined only once across the program (unless inline/template). Declare freely, but define once — inline functions and templates are the allowed exceptions and get merged.
Which gives a name INTERNAL linkage (private to one .cpp file)?
- extern
- An anonymous namespace or file-scope static
- inline
- A forward declaration
Answer: An anonymous namespace or file-scope static. An anonymous namespace (preferred) or file-scope static makes a name private so it can never clash across files.
You get 'multiple definition of config' linking two files that include the same header. The likely fix is:
- Add #pragma once only
- Compile with -O2
- Rename the header
- Mark the variable inline, or declare it extern and define it in one .cpp
Answer: Mark the variable inline, or declare it extern and define it in one .cpp. A non-inline definition in a header lands in every including TU; inline merges duplicates, or move the single definition to one .cpp.
Why can mixing GCC and Clang builds, or two STL versions, break a program at link or run time?
- They use different keywords
- They can disagree on the ABI: name mangling and how types like std::string are laid out
- Clang cannot link at all
- Only one compiler can be installed
Answer: They can disagree on the ABI: name mangling and how types like std::string are laid out. If one side thinks std::string is 32 bytes and the other 24, passing it across the boundary reads the wrong memory.
Which keyword lets the SAME function definition appear in many translation units and be merged into one?
- static
- extern
- inline
- constexpr only
Answer: inline. inline tells the linker duplicate copies are allowed and should be deduped — which is why header-only libraries mark definitions inline.
Continue this course
- Previous: Building Modular Applications with Header/Implementation Architecture
- Next: Working with C Libraries: Interoperability & extern "C" — Call C libraries from C++, use extern C, and manage ABI differences
- Quick reference: C++ cheat sheet
Frequently asked questions
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.
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.
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.
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.
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.