Question
Static vs Dynamic Libraries in C++: When to Use Each
Question
When creating a C++ class library, you can choose between a dynamic library (.dll on Windows or .so on Linux) and a static library (.lib on Windows or .a on Linux). What are the differences between static and dynamic libraries, and when is each option appropriate?
Short Answer
You will learn how C++ static and dynamic libraries are linked into applications, how they affect deployment and updates, and the trade-offs that help you choose between them.
A key detail: on Windows, the .lib extension can mean either a static library or an import library used to link against a DLL. The library type depends on how it was built and used, not only on its file extension.
Concept
A library is compiled, reusable code that another program can use. Instead of copying source files into every application, developers compile shared functionality once and link applications to it.
There are two common ways to distribute native C++ libraries:
- A static library is copied into the executable during the link step.
- A dynamic library remains a separate file and is loaded when the program starts or while it is running.
For example, if an executable uses a static library containing formatName, the linker copies the required compiled code into the executable. The final executable is more self-contained.
If it uses a dynamic library, the executable contains references to the library's functions instead. At runtime, the operating system loader must find and load the DLL or shared object.
This choice matters because it changes:
- Deployment: whether an extra runtime library file must be shipped.
- Updates: whether library code can be replaced without rebuilding the application.
- File size and memory: whether code is duplicated across executables or shared in memory.
- Compatibility: whether separately built binaries continue to agree on the library's interface and binary details.
A dynamic library exposes a binary interface, often called an ABI (Application Binary Interface). Changing exported C++ classes, compiler settings, runtime-library settings, or data layouts can break programs built against an older version. A stable C-style exported API is often easier to maintain across DLL boundaries than exporting complex C++ classes directly.
Mental Model
Think of a static library as a recipe printed inside every cookbook you sell. Each cookbook works by itself, but the same recipe appears in every copy.
A dynamic library is like a separate recipe booklet shared by several cookbooks. The cookbooks are smaller, and replacing the booklet updates the recipe for all of them. However, the booklet must be available when someone wants to use a cookbook.
In programming terms:
- Static linking: package the needed library code into the application.
- Dynamic linking: keep the library code outside the application and connect to it at runtime.
Syntax and Examples
A library's build commands vary by compiler and build system, but the workflow is the same: compile source files into object files, then package or link them.
Example library source
// greeting.h
#pragma once
#include <string>
std::string makeGreeting(const std::string& name);
// greeting.cpp
#include "greeting.h"
std::string makeGreeting(const std::string& name) {
return "Hello, " + name + "!";
}
// main.cpp
#include "greeting.h"
#include <iostream>
int main() {
std::cout << makeGreeting("Ada") << '\n';
}
Build a static library with GCC or Clang
# Compile the implementation into an object file.
g++ -c greeting.cpp -o greeting.o
# Create an archive: libgreeting.a
ar rcs libgreeting.a greeting.o
# Link the application with the static library.
g++ main.cpp -L. -lgreeting -o app
The linker copies the necessary machine code from libgreeting.a into app. After building, app does not need libgreeting.a to run.
Build a shared library with GCC or Clang
# -fPIC creates position-independent code, commonly required for shared libraries.
g++ -fPIC -c greeting.cpp -o greeting.o
# Create a Linux shared library: libgreeting.so
g++ -shared greeting.o -o libgreeting.so
# Link the application against the shared library.
g++ main.cpp -L. -lgreeting -o app
On Linux, the resulting app needs libgreeting.so to be discoverable at runtime, such as through an installed system location, a configured loader path, or an application packaging strategy.
Windows DLL export example
Windows DLLs must explicitly export functions unless a build configuration handles this for you.
// greeting_api.h
#pragma once
#ifdef _WIN32
#ifdef GREETING_BUILDING_DLL
#define GREETING_API __declspec(dllexport)
#else
#define GREETING_API __declspec(dllimport)
#endif
#else
#define GREETING_API
#endif
extern "C" GREETING_API const char* greetingVersion();
extern "C" avoids C++ name mangling for this function, which can make a small public DLL API easier to consume from different compilers or languages.
Step by Step Execution
Consider a program that calls a function from greeting.cpp.
// main.cpp
#include "greeting.h"
#include <iostream>
int main() {
std::cout << makeGreeting("Lin") << '\n';
}
With a static library
greeting.cppis compiled into machine code in an object file.- The object file is placed in
libgreeting.aor a static.libfile. main.cppis compiled.- At link time, the linker finds the required
makeGreetingcode in the static library. - The linker places that code in the final executable.
- At runtime, the executable runs without needing the static library archive.
With a dynamic library
greeting.cppis compiled into a shared library such aslibgreeting.soorgreeting.dll.main.cppis compiled.- At link time, the linker records that
makeGreetingcomes from the dynamic library. On Windows, it usually links through an import.libfile. - The executable is produced without copying the DLL/shared-library implementation into it.
- At runtime, the operating system loader locates and loads the required
.soor.dll. - If the loader cannot find a compatible library, the program fails to start or fails when it tries to load that library.
Real World Use Cases
Good fits for static libraries
- Command-line tools: Ship one executable with fewer runtime dependencies.
- Embedded and appliance software: Control exactly which code is included in a fixed firmware image.
- Simple internal utilities: Avoid installing and locating separate library files.
- Libraries with difficult ABI requirements: Link code into the same program build to reduce cross-binary compatibility concerns.
- Security-sensitive deployment: Reduce accidental loading of the wrong external library, while still keeping normal update practices in mind.
Good fits for dynamic libraries
- Plugin systems: Load optional modules, such as image format handlers or editor extensions.
- Shared platform components: Several applications can use one installed library.
- Applications needing independent library updates: Fix a library defect without relinking every consumer, provided ABI compatibility is preserved.
- Operating-system and third-party APIs: System libraries are commonly dynamic libraries.
- Large suites of applications: Avoid embedding the same large library implementation in every executable.
Neither choice is universally better. Deployment requirements, update policy, supported platforms, licensing, ABI stability, and build tooling should drive the decision.
Real Codebase Usage
In real C++ projects, teams often use both library types.
Common patterns
- Static linking for private implementation libraries: A project may statically link small internal modules that are not intended to be installed separately.
- Dynamic linking for public components: A reusable SDK or plugin host may publish a shared library with a carefully versioned API.
- Dynamic loading for plugins: Instead of linking a plugin at build time, an application can load a DLL or
.soby filename at runtime. The plugin exposes a small known entry function. - C-compatible boundary for shared libraries: Export opaque handles and functions rather than STL containers or C++ class objects. This reduces ABI and memory-ownership problems.
- Build-system targets: Modern CMake projects usually describe dependencies as targets rather than manually adding filenames and include paths.
For example, a CMake project can state whether a target is static or shared:
add_library(greeting STATIC greeting.cpp)
# Or:
# add_library(greeting SHARED greeting.cpp)
target_include_directories(greeting PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
add_executable(app main.cpp)
target_link_libraries(app PRIVATE greeting)
Consumers link to the target name, while CMake carries the include paths, compile settings, and linker settings associated with that dependency.
Design rule for dynamic-library APIs
Be explicit about ownership. If a DLL allocates memory, avoid requiring the application to free it using a potentially different allocator or C++ runtime. Prefer an API that provides a matching library-owned destroy/free function, or pass caller-owned buffers into the library.
Common Mistakes
Assuming every .lib file is a static library
On Windows, a DLL build often produces two files:
greeting.dll: the runtime implementation.greeting.lib: an import library used while linking the application.
The import .lib is not the same as a static library. Linking it still requires greeting.dll at runtime.
Shipping a dynamic executable without its dependency
A program can link successfully and still fail on another machine because its DLL or .so cannot be found.
Error: library not found at runtime
Avoid this by packaging the required dynamic libraries correctly, installing them through the platform's normal mechanism, and testing in a clean environment.
Replacing a DLL with an incompatible version
Changing an exported C++ class can change its binary layout or symbol names. An older application may then crash or fail to load.
Avoid this by maintaining ABI compatibility, versioning shared libraries, and keeping exported interfaces small and stable.
Mixing allocation and deallocation across a DLL boundary
This risky design returns dynamically allocated memory from a library:
// Risky if the caller uses a different runtime/allocator.
char* createMessage();
A safer pattern makes the ownership rule explicit:
extern "C" char* createMessage();
extern "C" void freeMessage(char* message);
The library allocates and frees its own memory.
Believing static linking always makes a smaller program
A single static executable can be larger because it includes library code. Dynamic libraries can save total disk and memory usage when several processes share the same compatible library. Actual results depend on the platform, linker, optimization settings, and which code is used.
Confusing compile-time, link-time, and runtime errors
- A missing header is usually a compile-time issue.
- An unresolved function is usually a link-time issue.
- A missing DLL or
.sois usually a runtime loading issue.
Comparisons
| Aspect | Static library | Dynamic library |
|---|---|---|
| Typical file extension | .a on Linux/macOS; .lib on Windows | .so on Linux, .dylib on macOS, .dll on Windows |
| When implementation is included | During linking | At program startup or runtime loading |
| Runtime dependency | Usually no separate library file | The matching dynamic library must be available |
| Executable size | Often larger | Often smaller |
| Sharing across applications | Each executable commonly has its own copy | Compatible processes can share loaded code pages |
| Updating library code | Rebuild and redistribute each executable | Replace/update the library if ABI compatibility is maintained |
| Deployment | Often simpler: fewer files | Requires dependency packaging and search-path handling |
| ABI concerns | Lower across the library boundary because code is linked into the app | Important: independently built binaries must remain compatible |
Static linking vs dynamic loading
A dynamically linked library is often selected when the program starts. Dynamic loading is more explicit: the application chooses a library file at runtime, commonly for plugins. Dynamic loading offers flexibility but requires careful error handling and a stable plugin interface.
Cheat Sheet
-
Static library: compiled code copied into the executable during linking.
- Windows: often
.lib - Unix-like systems: often
.a - Runtime library file is usually not needed.
- Windows: often
-
Dynamic library: separate compiled code loaded by the operating system.
- Windows:
.dll - Linux:
.so - macOS:
.dylib - Must be available at runtime.
- Windows:
-
Windows warning: a
.libmay be:- a static library, or
- an import library for a DLL.
-
Choose static when self-contained deployment and simple distribution matter most.
-
Choose dynamic when multiple programs share a component, plugins are needed, or compatible library updates should be delivered separately.
-
For dynamic library APIs:
- keep interfaces small and versioned;
- document ownership of memory;
- avoid exposing unstable C++ implementation details across the boundary;
- test installation and runtime loading on a clean machine.
-
CMake:
add_library(name STATIC source.cpp)
add_library(name SHARED source.cpp)
target_link_libraries(app PRIVATE name)
FAQ
Is a static library faster than a dynamic library?
Usually, the difference is not the deciding factor. Dynamic calls can add indirection in some cases, while compiler and linker optimizations can change results. Measure a real workload before choosing based on performance.
Does static linking mean no dependencies at all?
Not necessarily. Your executable may still rely on the operating system, drivers, or other dynamically linked system components. It means the selected static library's code is included in the executable.
Why does my Windows DLL project create both .dll and .lib files?
The DLL contains runtime code. The .lib commonly acts as an import library that helps the linker create an executable that refers to the DLL.
Can I update a DLL without recompiling the application?
Yes, only if the replacement DLL is compatible with what the application expects. Breaking exported symbols, function signatures, class layouts, or runtime assumptions can require rebuilding the application.
Should I export C++ classes from a DLL?
It can work when compiler, compiler options, and runtime libraries are controlled, but it creates ABI and ownership risks. For broadly distributed libraries, a small C-compatible API or opaque-handle design is often more robust.
Can several applications use the same dynamic library?
Yes. This is a major reason to use shared libraries. The operating system can load a compatible library for multiple processes, subject to platform behavior and versioning.
Which library type should a beginner choose?
Start with a static library for a small project when simple deployment is the priority. Use a dynamic library when you have a concrete need for plugins, shared installation, or independent updates.
Mini Project
Description
Build a small greeting library and application in CMake. By changing one CMake keyword, you can build the same code as either a static or shared library and observe the deployment difference.
Goal
Create and run an application that calls a function from a C++ library target.
Requirements
Create a library function that returns a greeting string. Build an executable that uses the library function. Configure the library as a static library and run the executable. Change the library to a shared library and rebuild it. Ensure the shared library is available beside the executable when running on a platform that requires it.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.