← Back to list

The Complete Guide to C++ Libraries: Create, Compare, and Understand How Dynamic Linking Works

In the world of C++ development, understanding the distinction between static and dynamic libraries is critical — not just from a build…

Gaurav Singh · 2025-07-14 14:32 · 10 claps · 9.4 min read
#c-plus-plus-language #libraries #static-library #dynamic-library #linking
Open on Medium ↗
Wiki topics: 📚 · Books & Reading

The Complete Guide to C++ Libraries: Create, Compare, and Understand How Dynamic Linking Works

In the world of C++ development, understanding the distinction between static and dynamic libraries is critical — not just from a build process perspective but also in terms of runtime behavior, memory footprint, and binary composition. While most developers use libraries daily, few explore how they operate under the hood: how they are built, how linking differs between static and dynamic forms, and what implications these choices have at the binary and OS level.

In this article, we’ll dissect the creation, linking, and execution dynamics of static and dynamic libraries in C++. We’ll not only compare them but also dive deep into how dynamic linking actually works internally — covering concepts like the ELF format, PLT/GOT tables, and how the dynamic linker (ld.so) resolves symbols at runtime. Additionally, we'll examine the differences at the binary level using tools like nm andldd to inspect symbols and dependencies.

By the end, you’ll not only understand how to build and link both types of libraries but also gain visibility into what actually ends up in your executable and how the OS loads and links libraries dynamically under the hood.

What is a Library in C++?

At its core, a library in C++ is a collection of pre-compiled code — functions, classes, and objects — that can be reused across multiple programs. Instead of rewriting code for common utilities, algorithms, or system interactions, you can encapsulate them into libraries and link them to your application at build-time (static) or runtime (dynamic/shared).

There are two primary types of libraries:

  1. Static Libraries (.a or .lib files):
  • These are bundled into your executable during the linking phase of compilation.
  • Once linked, the executable is self-contained, carrying all the necessary code from the library.
  • Example in Unix-like systems: .a files.

2. Dynamic Libraries (.so or .dll files):

  • These are not bundled into the executable itself. Instead, the executable contains references to the dynamic libraries.
  • At runtime, the operating system loads these libraries into memory when the program starts or when required.
  • Example: .so (shared object) files on Linux, .dll (Dynamic Link Library) on Windows.

Understanding the differences between these two impacts:

  • Binary size: Static linking increases the size of the executable.
  • Memory usage: Dynamic libraries can be shared across processes.
  • Performance: Static libraries may yield faster execution since all code is loaded upfront, but dynamic libraries provide flexibility like on-demand loading.
  • Symbol resolution and visibility: This is especially relevant when using binary inspection tools.

In the next sections, we’ll build static and dynamic libraries from scratch, link them with applications, and inspect the resulting binaries to observe the differences at a deeper level.

🔧 Creating a Static Library in C++

Let’s walk through how to create, link, and use a static library in C++ with a practical example.

📂 Step 1: Create a Simple Library Source

Let’s create a header file and a corresponding source file that defines a simple math function.

**mathlib.h**

#ifndef MATHLIB_H
#define MATHLIB_H

int add(int a, int b);

#endif

**mathlib.cpp**

#include "mathlib.h"

int add(int a, int b) {
    return a + b;
}

⚙️ Step 2: Compile to an Object File

We first need to compile the source file into an object file (.o):

g++ -c mathlib.cpp -o mathlib.o

This produces mathlib.o, a compiled but unlinked object file.

📦 Step 3: Create the Static Library

We then archive the object file into a static library using the ar (archive) utility:

ar rcs libmathlib.a mathlib.o

Now we have a static library file named libmathlib.a.

In the context of C++ development, an archive is simply a collection of object files bundled together into a single file, typically with the .a extension on Unix-like systems. This is what we call a static library.

  • Think of it as a package of precompiled code — functions, classes, or data — that you can link into multiple programs without recompiling the source each time.
  • Under the hood, an archive is created using the ar (archive) tool, which combines multiple .o (object) files into one .a file.

For example:

mathlib.o  --->  libmathlib.a

The static library (archive) itself isn’t an executable — it’s a binary blob of reusable compiled code that gets embedded into your program at link time, making the final executable self-sufficient.

The rcs are options that instruct ar on how to create or modify the archive. Here's what each letter means:

  1. **r (Replace or Insert): This tells ar to insert the specified object file into the archive. If a file with the same name already exists in the archive, it gets replaced**.
  2. **c (Create): This option tells ar to create the archive** if it doesn’t already exist. Without this, ar might display a warning if the archive is missing.
  3. **s (Index or Symbol Table): Generates a symbol table index** for the archive. This index is essential for the linker to quickly locate symbols during the linking phase, especially when multiple object files are present inside the static library.

📌 Pro Tip: Always include the s flag when creating static libraries to ensure the linker can resolve symbols without extra effort or warnings.

🚀 Step 4: Link the Static Library to an Application

Let’s create a simple main program that uses our library.

**main.cpp**

#include <iostream>
#include "mathlib.h"

int main() {
    std::cout << "3 + 4 = " << add(3, 4) << std::endl;
    return 0;
}

Now compile and link main.cpp with the static library:

g++ main.cpp -L. -lmathlib -o static_app
  • -L. tells the compiler to look for libraries in the current directory.
  • -lmathlib tells the linker to use libmathlib.a (lib prefix and .a suffix are implicit).

🔍 Inspecting the Static Library

To see what symbols (functions, variables) are defined in the static library:

nm libmathlib.a

You’ll see something like:

00000000 T _Z3addii

This indicates the **add(int, int)** function, with its mangled name due to C++ symbol mangling.

📌 Summary: A static library is linked directly into your executable at compile time. The resulting binary (static_app here) is self-contained and doesn’t need libmathlib.a to be present at runtime.

🔧 Creating a Dynamic (Shared) Library in C++

Now that we’ve seen how to create a static library, let’s build the same math library as a dynamic/shared library. On Unix-like systems, shared libraries have the .so (shared object) extension, and on Windows, they are .dll files.

📂 Step 1: Prepare the Same Source Files

We will reuse the **mathlib.h and `mathlib.cpp`** from the static library example. No changes are needed for these files to build a shared library, though in more advanced setups, you might add macros to handle symbol visibility across platforms.

⚙️ Step 2: Compile to a Shared Library

To compile a shared library, we use the g++ command with a couple of important flags:

g++ -fPIC -shared mathlib.cpp -o libmathlib.so

🧩 Explanation of the Flags

  • **-fPIC (Position-Independent Code): This flag tells the compiler to produce machine code that does not depend on being loaded at a specific memory address**. This is necessary because:
  • The shared library might be loaded into different memory addresses for different processes.
  • Without position independence, multiple programs using the same library could encounter conflicts or crashes.
  • **-shared: This instructs the compiler to generate a shared object (.so file) instead of an executable**. Specifically, it:
  • Prevents the inclusion of startup code (like main()).
  • Embeds metadata that allows the system’s dynamic linker to load and link the library at runtime.

🚀 Step 3: Link the Dynamic Library with Your Application

Now, let’s compile our main.cpp and link it with the newly created shared library:

g++ main.cpp -L. -lmathlib -o dynamic_app
  • -L.: Instructs the compiler to look for libraries in the current directory.
  • -lmathlib: This links with libmathlib.so (the lib prefix and .so suffix are implied).

⚡ Step 4: Running the Application with a Shared Library

When you try to run ./dynamic_app, the system needs to locate libmathlib.so. By default, it looks in standard paths like /lib or /usr/lib.

To help the system find your .so in the current directory, set the **LD_LIBRARY_PATH**:

export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./dynamic_app

🔍 Inspect Dynamic Dependencies

You can check which shared libraries an executable depends on using the ldd command:

ldd dynamic_app

Example output:

libmathlib.so => ./libmathlib.so (0x00007f...)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
...

This confirms that dynamic_app depends on libmathlib.so and the standard C library (libc.so.6).

📌 Summary: A dynamic (shared) library is compiled with -fPIC and -shared to generate a .so file that remains external to your executable. The resulting binary (dynamic_app here) relies on the shared library being present at runtime, enabling multiple programs to share the same code in memory and allowing for easier updates without recompiling the application.

⚖️ Static vs Dynamic Libraries: A Quick Comparison

Now that we’ve seen how to create and link both static (.a) and dynamic (.so) libraries, let’s compare them across key aspects:

1. Binary Size:

  • Static Library: The code from the library is copied into the executable, so the final binary is larger.
  • Dynamic Library: The executable stays smaller since the library code remains external and is loaded at runtime.

2. Memory Usage:

  • Static Library: Each program that uses the static library carries its own copy of the library code in memory.
  • Dynamic Library: Multiple running programs can share a single copy of the dynamic library in memory, saving overall system resources.

3. Performance:

  • Static Library: Slightly faster at runtime because all code is resolved and laid out in memory at compile time. No runtime linking overhead.
  • Dynamic Library: May have a slight overhead during program startup due to runtime linking, but this is negligible for most applications.

4. Updates & Maintenance:

  • Static Library: To apply a bug fix or update, you need to recompile and redistribute every executable that used the static library.
  • Dynamic Library: You can simply update the .so file, and all applications that depend on it will automatically use the updated library on their next run.

5. Symbol Visibility:

  • Static Library: All symbols included are part of the final executable, and symbol conflicts can be handled at compile/link time.
  • Dynamic Library: You must manage symbol visibility carefully (e.g., with __attribute__((visibility("hidden")))) to avoid symbol clashes across libraries.

🔍 How Dynamic Libraries Work Internally in Linux

Now that we’ve compared static and dynamic libraries, let’s peek under the hood to understand how Linux actually loads and resolves symbols from shared libraries at runtime.

When you execute a program that depends on shared libraries (.so), a sequence of steps occurs to make those libraries available to your application:

🗂️ ELF (Executable and Linkable Format): The Structure

Every compiled executable and shared library in Linux follows the ELF (Executable and Linkable Format). This format organizes the binary into different sections:

  • .text: Contains compiled machine instructions (your code).
  • .data: Holds initialized global/static variables.
  • .bss: For uninitialized global/static variables (allocated at runtime).
  • .rodata: Stores read-only data like string constants.
  • .plt (.plt): Procedure Linkage Table, used to resolve function calls to dynamic libraries.
  • .got (.got): Global Offset Table, stores actual memory addresses of external functions or variables after they’re resolved.

These specialized sections are essential for the dynamic linking process.

🔗 PLT & GOT: The Heart of Dynamic Linking

When your application calls a function from a shared library, it doesn’t call the function directly by its absolute address. Instead, it goes through two key mechanisms:

  1. Procedure Linkage Table (PLT):
  • Think of this as a jump table that redirects function calls.
  • On the first call, the PLT entry triggers a lookup to find where the actual function is located in memory.

2. Global Offset Table (GOT):

  • The GOT stores the resolved addresses of external symbols (functions, variables).
  • After the first function call resolves the symbol, the GOT is updated, and subsequent calls go directly to the correct address.

This process is called lazy binding — the system resolves symbols only when they’re needed, which speeds up program start time.

🛠️ Role of ld.so: The Dynamic Linker

The dynamic linker/loader (ld.so or ld-linux.so) is a special program that kicks in when you start an executable relying on shared libraries. It performs several crucial tasks:

  1. Library Discovery:
  • Searches for .so files in paths defined by:
  • LD_LIBRARY_PATH environment variable
  • /etc/ld.so.cache (maintained by ldconfig)
  • Default directories like /lib, /usr/lib

2. Memory Mapping:

  • Loads the shared libraries into the program’s memory space without duplicating them across processes.

3. Symbol Resolution:

  • Patches the GOT with the actual addresses from the loaded .so files.

4. Running Initialization Code:

  • Runs any static constructors or initialization logic defined in the library (.init section).

📌 Summary: When using dynamic libraries, the ELF structure, PLT, GOT, and the dynamic linker (ld.so) work together to resolve and link symbols at runtime. This mechanism enables flexible, memory-efficient programs that can benefit from updated libraries without recompilation.

Thank you for reading! 🙌 If you found this deep dive into C++ static and dynamic libraries helpful — from building them to understanding how dynamic linking works internally — don’t forget to leave a few claps 👏 to show your support. It motivates me to write more in-depth content like this!

In the next article, we’ll go even further into the lifecycle of a compiled binary — covering program loading, runtime symbol resolution, and how to inspect binaries using powerful tools like nm, objdump, and readelf. If you’re curious about what actually happens between executing ./app and your code running on the CPU, you won’t want to miss it. See you there! 🚀


메타데이터
post_id
a403d947dc27
slug
the-complete-guide-to-c-libraries-create-compare-and-understand-how-dynamic-linking-works-a403d947dc27
url
https://medium.com/@gs8763076/the-complete-guide-to-c-libraries-create-compare-and-understand-how-dynamic-linking-works-a403d947dc27
canonical_url
https://medium.com/@gs8763076/the-complete-guide-to-c-libraries-create-compare-and-understand-how-dynamic-linking-works-a403d947dc27
author_url
https://medium.com/@gs8763076
status
ok
fetched_at
2026-08-06 20:16:35