Static and Shared Libraries
The difference between static and dynamic libraries, how to create them, and the trade-offs between them.
In our last lesson, we saw how the linker takes our scattered collection of object files (.o
or .obj
) and stitches them together into an executable. We learned that the linker's main job is resolving symbols - connecting function calls to their definitions across different files.
We briefly touched on the idea of linking against a library, an archive of pre-compiled object files. This is how we use code from external sources like the C++ Standard Library without having to compile it ourselves every time.
Now, we'll introduce the two fundamental types of libraries in the C++ world: static libraries and dynamic (or shared) libraries. By the end of this lesson, you'll know what .a
, .lib
, .so
, and .dll
files are, how they're created and used, and the trade-offs that will guide your choice between them.
Creating and Using Static Libraries
Let's start with the simpler of the two: the static library. As we learned previously, static linking involves copying all the required code from object files and libraries directly into your final executable at build time. The result is a single, self-contained program.
A static library is essentially just a package - an archive - of object files. On Unix-like systems (Linux, macOS), they have a .a
(for "archive") extension. On Windows, they typically use .lib
.
Why Bother with Static Libraries?
You might wonder, if a static library is just a bag of object files, why not just give all the object files to the linker directly? For a small project, you could. But imagine a library with hundreds of source files. Your link command would become enormous and unmanageable:
g++ main.o app.o ui.o network.o logger.o math.o ... (100 more .o files) ... -o my_app
A static library cleans this up. You bundle all those object files into a single library file, say libutils.a
, and your link command becomes much simpler. The command might look something like the following, which we'll break down in the next section:
g++ main.o -L. -lutils -o my_app
How to Create a Static Library
Creating a static library is a two-step process:
- Compile: Compile all your library's source files into object files.
- Archive: Use a special tool called an archiver to bundle the object files into a single library file.
Let's create a small utility library with a logger and a math function.
Files
Compiling to Object Files
We use the -c
flag to tell the compiler to stop after producing object files and not to proceed with linking.
g++ -c logger.cpp -o logger.o
g++ -c math_utils.cpp -o math_utils.o
Now we have logger.o
and math_utils.o
.
Archive the Object Files
To create the library, we use the ar
(archiver) command. The rcs
flags mean:
r
- replace older files in the archivec
- create the archive if it doesn't exists
- write an object-file index into the archive. This index just makes it faster for the linker to find symbols that our code is trying to use.
Putting this together, our command would look like this:
ar rcs libutils.a logger.o math_utils.o
We've now created libutils.a
. If you're using the Visual Studio toolchain, you'd use lib.exe instead of ar
.

How to Use a Static Library
Now let's write a main.cpp
that uses our new library and link it. Alongside our precompiled library, we'd also share the header files describing what is in the library.
Then, developers using our library can do so in much the same way they'd use code they wrote themselves. They'd just #include
the relevant headers and use the symbols they declare:
main.cpp
#include "logger.h"
#include "math_utils.h"
#include <iostream>
int main() {
log_message("Starting program.");
int result = add(5, 7);
std::cout << "5 + 7 = " << result;
return 0;
}
First, let's compile main.cpp
to an object file:
g++ -c main.cpp -o main.o
Now, we'll link main.o
with our static library libutils.a
:
g++ main.o -L. -lutils -o my_app
Let's break down those linker flags:
L.
tells the linker to look for library files in the current directory (.
). You can provide any path here, like-L/path/to/libs
.lutils
tells the linker to find and link a library namedutils
. The linker automatically prependslib
and appends.a
(or.so
, as we'll see) to the name. So-lutils
becomes a search forlibutils.a
.
The linker now resolves the undefined symbols log_message
and add
from main.o
by looking inside libutils.a
. It finds the definitions in logger.o
and math_utils.o
within the archive, copies their machine code into my_app
, and produces the final executable.
The resulting my_app
is completely self-contained. You can delete libutils.a
, main.o
, logger.o
, and math_utils.o
, and my_app
will still run perfectly. All the necessary code is baked in:
./my_app.exe
[LOG]: Starting program.
5 + 7 = 12
Dynamic Libraries and Runtime Linking
Static libraries are simple and robust, but they have a few major drawbacks. Every program that uses a static library gets its own copy of the code, which can waste disk space and memory.

And if you need to update the library (e.g., to fix a bug), you have to re-link and redistribute every single application that uses it.
Dynamic libraries solve these problems. They are also known as shared libraries because their main benefit is that they can be shared by multiple running programs. On Linux/macOS, they are .so
(Shared Object) files. On Windows, they are the famous .dll
(Dynamic Link Library) files.
How Dynamic Linking Works
Unlike static linking, dynamic linking defers most of the work until runtime.
At Link Time: When you link your program against a dynamic library, the linker does not copy the library's code. Instead, it places a small placeholder (a stub) in your executable. This stub essentially says, "When this program runs, it will need the function add()
from the library libutils.so
". The executable is now much smaller, but it has an external dependency.
At Run Time: When you execute your program, the operating system's dynamic linker (or loader) kicks in. It reads the list of dependencies from your executable, finds the required .so
or .dll
files on your system, and loads them into memory. It then performs the final symbol resolution and relocation on the fly, patching your program's function calls to point to the correct addresses in the newly loaded library code.
If multiple programs using libutils.so
are running, the OS is smart enough to load only one copy of libutils.so
into physical memory and share it among all of them, saving a significant amount of RAM.

Creating a Dynamic Library
The process is similar to creating a static library, but with two key differences in the compilation and linking commands.
Compile with Position-Independent Code (PIC)
Because a shared library can be loaded at any address in memory by the OS, its code cannot rely on absolute memory addresses. It must be written in a way that it can run regardless of where it's placed. This is called Position-Independent Code, or
PIC. We tell the compiler to generate this with the -fPIC
flag:
g++ -c -fPIC logger.cpp -o logger.o
g++ -c -fPIC math_utils.cpp -o math_utils.o
Link into a Shared Library
Instead of using ar
, we use the compiler/linker again, but this time with the -shared
flag to tell it to produce a shared library instead of an executable.
g++ -shared logger.o math_utils.o -o libutils.so
For Windows, we'd use a .dll
extension instead of .so
:
g++ -shared logger.o math_utils.o -o libutils.dll
We now have our shared library ready to use - libutils.so
or libutils.dll
.
Using a Dynamic Library
Linking against it looks identical to the static library case:
g++ main.o -L. -lutils -o my_app
Most linkers, when given the choice between libutils.a
and libutils.so
in the same directory, will prefer the shared version by default.
We've now created our my_app
executable, and it should be smaller than our previous version that included the contents of the static library. We can try to run it:
./my_app
On Windows, this is likely to work. Windows checks the directory containing the executable for any dll
files it requires. So, as long as our dll
is in the same directory, Windows will find it, and our program will run successfully:
[LOG]: Starting program.
5 + 7 = 12
However, on Unix-like systems, we may have gotten an error similar to this:
./my_app: error while loading shared libraries: libutils.so: cannot open shared object file: No such file or directory
What happened? The program started, the OS loader saw it needed libutils.so
, but it didn't know where to find it. The OS only looks in a few standard system directories by default (like /usr/lib
on Linux).
We'll see more robust ways to solve this later but, for now, you can tell the loader where to look using the LD_LIBRARY_PATH
environment variable. We'll set it to .
, indicating the current directory, thereby replicating Windows' behaviour:
export LD_LIBRARY_PATH=.
Our program should now work:
./my_app
[LOG]: Starting program.
5 + 7 = 12
Managing these paths and cross-platform differences for deployment is a major challenge that build systems like CMake help solve, often by embedding the library search path directly into the executable using a mechanism called rpath
. We'll cover this in detail later.
Static vs. Dynamic Linking - The Trade-offs
Choosing between static and dynamic linking involves a series of trade-offs. There is no single "best" answer; the right choice depends entirely on your project's needs.
Feature | Static Linking | Dynamic Linking |
---|---|---|
Executable Size | Larger. All library code is copied into the final executable. | Smaller. The executable only contains stubs and references to the library. |
Deployment | Simpler. Distribute a single file. No dependency issues. | More Complex. Must distribute the executable and all required .dll/.so files. |
Memory Usage | Higher. Each running program has its own copy of the library in memory. | Lower. The OS loads one copy of the library and shares it among all programs. |
Updates/Maintenance | Harder. To update a library, every application using it must be re-linked. | Easier. Update a bug-fixed library by just replacing the .so/.dll file. |
Startup Performance | Slightly Faster. No runtime work needed to find and load libraries. | Slightly Slower. The OS loader must find, load, and link libraries on startup. |
Link Time | Slower. The linker has more work to do, copying code and resolving symbols. | Faster. The linker just creates stubs, deferring most work to runtime. |
Summary
We've covered the core concepts of C++ libraries, a foundational topic for building any non-trivial application.
- Static Libraries (
.a
,.lib
files): These are archives of object files. When you link against them, their code is copied into your executable at build time. This creates a large, self-contained program that's easy to deploy but inefficient in memory and difficult to update. - Dynamic/Shared Libraries (
.so
,.dll
files): These are standalone files that are loaded at runtime. Your executable is smaller and only contains references. This model saves memory and makes updates easy, but complicates deployment as you must ship the library files alongside your executable. - The Process: We saw the command-line tools (
ar
,g++ -shared
,-fPIC
) for creating both types of libraries and the linker flags (-L
,-l
) for using them.
Ultimately, the choice between static and dynamic linking is a design decision. Do you want a simple, standalone executable? Use static linking. Are you building a large system with shared components or need to be able to patch libraries without a full redeployment? Dynamic linking is your friend.
In the upcoming chapters, we will see how CMake abstracts away the platform-specific commands and makes creating, finding, and linking both static and dynamic libraries a simple, portable, and elegant process.