Shipping libraries, debugging link errors and upgrading compilers safely all require understanding symbols and the ABI.
Each object file has a symbol table: the functions and variables it defines and those it needs. The linker matches them up. Linkage decides visibility: external (shared across translation units, the default for functions and globals), internal (static or an unnamed namespace, private to one file) or none (locals).
Overloading and namespaces mean C++ names can't be used as raw symbols, so compilers mangle them: geo::area(double) becomes _ZN3geo4areaEd under the Itanium C++ ABI used on Linux and macOS (MSVC has its own scheme). nm -C and c++filt demangle. extern "C" turns mangling off, giving a plain C symbol; use it for C interop and for stable plugin entry points.
Libraries come in two kinds. A static library (.a, .lib) is an archive of object files copied into the executable at link time. With GNU ld, order matters: list libraries after the objects that use them. A shared library (.so, .dylib, .dll) is loaded at run time and shared between processes. That makes its ABI, the binary interface, a contract.
The ABI covers calling conventions, name mangling, object layout, vtable layout and exception handling. Changing a public class's data members, adding a virtual function, or changing an inline function or template in a header breaks the ABI: existing binaries silently use the old layout. Even standard library types change, as with libstdc++'s std::string switch, controlled by _GLIBCXX_USE_CXX11_ABI. Mixing code built against different standard libraries or with incompatible flags causes link errors or worse.
To keep a stable ABI, hide data behind the pimpl idiom (class Widget { struct Impl; std::unique_ptr<Impl> p_; };), so the layout lives in the .cpp; or expose a C API. On ELF platforms, build with -fvisibility=hidden and export only the public symbols.
Example
# lib.cpp: namespace geo { double area(double r) { return 3.14159 * r * r; } }
# extern "C" double geo_area(double r) { return geo::area(r); }
g++ -std=c++20 -O2 -fPIC -c lib.cpp -o lib.o
nm lib.o
# T _ZN3geo4areaEd mangled C++ name (Itanium ABI)
# T geo_area extern "C": plain, unmangled
nm -C lib.o # demangled: geo::area(double)
echo _ZN3geo4areaEd | c++filt
# static library: archive of objects, copied into the executable
ar rcs libgeo.a lib.o
g++ main.o -L. -lgeo -o app # libraries AFTER the objects that need them
# shared library: loaded at run time, its ABI is now a contract
g++ -shared -o libgeo.so lib.o
g++ main.o -L. -lgeo -Wl,-rpath,'$ORIGIN' -o app
ldd app | grep geo
# undefined reference to `geo::area(double)' -> definition not linked, or wrong order
# multiple definition of `helper()' -> non-inline function defined in a headerRemember
- C++ names are mangled into symbols;
extern "C"gives plain C symbols - Static libraries are copied in at link time; shared libraries make the ABI a contract
- Pimpl or a C API keeps a library's ABI stable across changes
Watch out for
- Adding a data member or virtual function to a class exported from a shared library without rebuilding all its users.