Question

Static vs Dynamic Libraries in C++: When to Use Each

cppdllshared-librariesstatic-linkingdynamic-linking

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.
Take Quiz

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

  1. greeting.cpp is compiled into machine code in an object file.
  2. The object file is placed in libgreeting.a or a static .lib file.
  3. main.cpp is compiled.
  4. At link time, the linker finds the required makeGreeting code in the static library.
  5. The linker places that code in the final executable.
  6. At runtime, the executable runs without needing the static library archive.

With a dynamic library

  1. greeting.cpp is compiled into a shared library such as libgreeting.so or greeting.dll.
  2. main.cpp is compiled.
  3. At link time, the linker records that makeGreeting comes from the dynamic library. On Windows, it usually links through an import .lib file.
  4. The executable is produced without copying the DLL/shared-library implementation into it.
  5. At runtime, the operating system loader locates and loads the required .so or .dll.
  6. 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.

Take Quiz

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 .so by 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 .so is usually a runtime loading issue.

Comparisons

AspectStatic libraryDynamic library
Typical file extension.a on Linux/macOS; .lib on Windows.so on Linux, .dylib on macOS, .dll on Windows
When implementation is includedDuring linkingAt program startup or runtime loading
Runtime dependencyUsually no separate library fileThe matching dynamic library must be available
Executable sizeOften largerOften smaller
Sharing across applicationsEach executable commonly has its own copyCompatible processes can share loaded code pages
Updating library codeRebuild and redistribute each executableReplace/update the library if ABI compatibility is maintained
DeploymentOften simpler: fewer filesRequires dependency packaging and search-path handling
ABI concernsLower across the library boundary because code is linked into the appImportant: 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.

Take Quiz

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.
  • Dynamic library: separate compiled code loaded by the operating system.

    • Windows: .dll
    • Linux: .so
    • macOS: .dylib
    • Must be available at runtime.
  • Windows warning: a .lib may 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.

Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more