C++ Libraries
Reviewed & published by Brayan K
By the end of this lesson you'll know the difference between the standard library and third-party code, when to reach for static vs dynamic vs header-only libraries, what the -I/-L/-l flags actually do, and how a package manager like vcpkg or Conan saves you from wiring all of it up by hand.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Tell the standard library apart from third-party libraries
- Choose between static (.a/.lib) and dynamic (.so/.dll/.dylib) libraries
- Use header-only libraries with nothing but #include
- Link libraries correctly with -I, -L and -l
- Install dependencies with package managers (vcpkg, Conan)
- Consume any library: include its header, then link its code
💡 Real-World Analogy
Think of building a program like assembling flat-pack furniture. The header is the instruction sheet — it promises "a function called area() exists and takes a radius". The library is the box of actual parts — the compiled code that does the work. The compiler reads the instructions; the linker bolts in the parts. A static library is parts you glue permanently inside the finished piece (heavier, but nothing can fall off). A dynamic library is parts you keep in a shared drawer that several pieces of furniture borrow at use-time (lighter, but the drawer had better still be there). When you "#include the header but forget to link the library", you've read the instructions but never opened the box — hence the famous undefined reference error.
📊 The Four Kinds of Library
| Kind | Where the code lives | Files | Link step? |
|---|---|---|---|
| standard | Ships with the compiler | <iostream>… | Automatic |
| header-only | All in the header | .hpp / .h | None |
| static | Copied into your exe | .a / .lib | At build time |
| dynamic | Separate file, loaded at run time | .so / .dll / .dylib | Build + run time |
A library is just reusable compiled code plus a header that describes it. The header tells the compiler what exists; the library provides the actual instructions the linker bolts in.
1. Standard Library vs Third-Party
Every header you've used so far — <iostream>, <vector>, <string>, <algorithm> — is part of the standard library. It ships with the compiler, so you only #include it and it just works. A third-party library is code someone else wrote and published; you have to obtain it, tell the compiler where its headers are, and (usually) link its compiled code. Read this worked example, run it, and notice that everything here is "free" — no extra build flags needed.
#include <iostream> // standard library: input/output
#include <vector> // standard library: a growable array
#include <string> // standard library: text
#include <algorithm> // standard library: sort, max, min...
using namespace std;
// The STANDARD library ships with every compiler. You only
// #include it — no download, no extra link flags.
//
// A THIRD-PARTY library would look like:
// #include <nlohmann/json.hpp> // you must install it first
// and might need a link flag at build time (see later sections).
int main() {
vector<int> scores = {42, 17, 99, 3}; // from <vector>
sort(scores.begin(), scores.end()); // from <algorithm>
cout << "Sorted: ";
for (int s : scores) cout << s << " "; // 3 17 42 99
cout << endl;
string name = "ada"; // from <string>
cout << "Highest score: " << scores.back() << endl; // 99
cout << "Name length: " << name.size() << endl; // 3
return 0;
}
// ✅ Expected output:
// Sorted: 3 17 42 99
// Highest score: 99
// Name length: 32. Header-Only Libraries
The simplest kind of third-party library is header-only: all of its code lives in the header file, so there is nothing to compile or link separately — you #include it and build. This is why templates and inline functions live in headers, and why libraries like nlohmann/json, Catch2, and fmt are so easy to adopt. The one rule: mark non-template functions inline so including the header in several .cpp files doesn't cause "multiple definition" errors.
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
// HEADER-ONLY library: all the code lives in the header, so you
// just #include it. No separate compile step, no link flag.
// Real examples: nlohmann/json, Catch2, fmt (header mode).
//
// Imagine this block is the file math_utils.hpp that you
// would normally pull in with #include "math_utils.hpp"
namespace mathlib {
// 'inline' lets a function live in a header without causing a
// "multiple definition" error when several .cpp files include it.
inline int clampInt(int value, int low, int high) {
return max(low, min(value, high)); // keep value in [low, high]
}
inline double degToRad(double deg) {
return deg * 3.14159265 / 180.0; // degrees -> radians
}
}
int main() {
cout << "clampInt(15, 0, 10) = " << mathlib::clampInt(15, 0, 10) << endl; // 10
cout << "clampInt(-4, 0, 10) = " << mathlib::clampInt(-4, 0, 10) << endl; // 0
cout << "degToRad(90) = " << mathlib::degToRad(90) << endl; // 1.5708
return 0;
}
// ✅ Expected output:
// clampInt(15, 0, 10) = 10
// clampInt(-4, 0, 10) = 0
// degToRad(90) = 1.5708Your turn. The namespace below is pretending to be a header-only file. Fill in the two blanks marked ___ using the hints in the comments, then run it.
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// Pretend this namespace is your header-only file text_utils.hpp
namespace textlib {
// 1) Mark this helper 'inline' so it is safe in a header
___ string shout(string s) { // 👉 the keyword is inline
transform(s.begin(), s.end(), s.begin(), ::toupper);
return s + "!";
}
}
int main() {
// 2) Call your header-only helper using the namespace + ::
string loud = ___; // 👉 textlib::shout("hello")
cout << loud << endl;
// ✅ Expected output:
// HELLO!
return 0;
}3. Static Libraries (.a / .lib)
A static library is an archive of pre-compiled object files — .a on Linux/macOS, .lib on Windows. At link time the linker copies the bits you actually use straight into your executable. The upside: the program is self-contained, with no runtime dependency to ship. The downside: a bigger binary, and you must rebuild to pick up a library update. The worked example below runs real geometry code and shows, in comments, the exact g++ and CMake commands you'd use to build and link it.
#include <iostream>
#include <cmath>
using namespace std;
// STATIC library (.a on Linux/macOS, .lib on Windows).
// The linker copies the library's code straight INTO your .exe,
// so the program is self-contained but larger.
//
// If this namespace lived in geometry.cpp, you would build it like:
// 1) compile to an object file: g++ -c geometry.cpp -o geometry.o
// 2) archive into a static lib: ar rcs libgeometry.a geometry.o
// 3) link it into your program: g++ main.cpp -L. -lgeometry -o app
// -L. adds the current folder to the LIBRARY search path
// -lgeometry links libgeometry.a (drop the 'lib' and the '.a')
namespace geometry {
const double PI = 3.14159265358979;
struct Circle {
double radius;
double area() const { return PI * radius * radius; }
};
struct Rectangle {
double w, h;
double area() const { return w * h; }
double diagonal() const { return sqrt(w*w + h*h); }
};
}
int main() {
geometry::Circle c{5.0};
cout << "Circle area (r=5): " << c.area() << endl; // 78.5398
geometry::Rectangle r{3.0, 4.0};
cout << "Rect area (3x4): " << r.area() << endl; // 12
cout << "Rect diagonal: " << r.diagonal() << endl; // 5
// The same build, expressed in CMake:
// add_library(geometry STATIC geometry.cpp)
// target_link_libraries(app PRIVATE geometry)
return 0;
}
// ✅ Expected output:
// Circle area (r=5): 78.5398
// Rect area (3x4): 12
// Rect diagonal: 54. Dynamic Libraries & the Linking Flags
A dynamic (or shared) library — .so on Linux, .dll on Windows, .dylib on macOS — stays a separate file that is loaded when your program runs. Your executable stays small and many programs can share one copy, but that file must be found at run time or the program won't start. Linking any non-header library comes down to three flags: -I<dir> says where the headers are, -L<dir> says where the library files are, and -l<name> says which library to link (-lcurl links libcurl.so — drop the lib prefix and the extension).
#include <iostream>
#include <string>
using namespace std;
// DYNAMIC / SHARED library (.so Linux, .dll Windows, .dylib macOS).
// It stays a SEPARATE file, loaded when the program runs. The .exe
// is small, and many programs can share one copy — but the file
// must be found at run time or you get a "cannot open shared object".
//
// Build a shared lib and link against it:
// g++ -shared -fPIC logger.cpp -o liblogger.so // -fPIC = position-independent code
// g++ main.cpp -L. -llogger -o app // -L. find it, -llogger link it
// ./app (Linux may need: LD_LIBRARY_PATH=. ./app)
// Pretend this is the code inside liblogger.so:
namespace logger {
void info(const string& msg) { cout << "[INFO] " << msg << endl; }
void error(const string& msg) { cout << "[ERROR] " << msg << endl; }
}
int main() {
logger::info("App started");
logger::error("Disk almost full");
logger::info("Shutting down");
// The three linking flags you will use constantly:
// -I<dir> where to find HEADERS (the #include search path)
// -L<dir> where to find LIBRARIES (the link search path)
// -l<name> which library to LINK (libNAME.so / libNAME.a)
return 0;
}
// ✅ Expected output:
// [INFO] App started
// [ERROR] Disk almost full
// [INFO] Shutting downNow you try. The blanks below aren't C++ — they're the build flags. Fill in the right flag for the library folder and the library name so the recipe would actually find and link libcurl:
#include <iostream>
#include <string>
using namespace std;
// 🎯 YOUR TURN — these are the flags, not C++. Fill the ___ in the
// comment so the build command would actually find and link a
// third-party library called libcurl installed under /opt/curl.
// Headers live in /opt/curl/include, the .so in /opt/curl/lib.
//
// g++ main.cpp \
// -I/opt/curl/include \ // 👉 -I = where to find HEADERS
// ___/opt/curl/lib \ // 👉 the flag for the LIBRARY folder is -L
// ___curl \ // 👉 the flag that LINKS libcurl is -l
// -o app
int main() {
// Nothing to change in main — read the comment above and fill the
// two blanks with the correct flags, then run to confirm it builds.
cout << "Link recipe: g++ main.cpp -I... -L... -lcurl -o app" << endl;
// ✅ Expected output:
// Link recipe: g++ main.cpp -I... -L... -lcurl -o app
return 0;
}5. Package Managers & Consuming a Library
Hand-writing -I/-L/-l for one library is fine; doing it for a dozen libraries that each have their own dependencies is miserable. A package manager — vcpkg (Microsoft) or Conan — downloads a library, builds it for your platform, and wires it into your build (typically via CMake's find_package). Whatever the tooling, consuming a library is always the same two steps: include its header, then link its compiled code. The example shows the vcpkg and Conan recipes in comments around runnable code.
#include <iostream>
#include <string>
using namespace std;
// PACKAGE MANAGERS install third-party libraries for you and wire
// them into your build, so you stop hand-writing -I/-L/-l flags.
//
// vcpkg:
// vcpkg install fmt
// (CMake) find_package(fmt CONFIG REQUIRED)
// target_link_libraries(app PRIVATE fmt::fmt)
//
// Conan:
// conanfile.txt -> [requires] fmt/10.2.1
// conan install .
//
// To CONSUME any library you always do the same two things:
// 1) INCLUDE its header: #include <fmt/core.h>
// 2) LINK its compiled code (a package manager does this for you).
namespace fakefmt { // pretend this came from a package
string format(const string& who) { return "Hello, " + who + "!"; }
}
int main() {
// 1) include (done above) 2) link (the package manager handles it)
cout << fakefmt::format("vcpkg") << endl; // Hello, vcpkg!
cout << fakefmt::format("Conan") << endl; // Hello, Conan!
return 0;
}
// ✅ Expected output:
// Hello, vcpkg!
// Hello, Conan!🔎 Deep Dive: static vs dynamic — which do I pick?
Reach for static when you want a single self-contained binary you can copy anywhere — command-line tools and containers love this. Reach for dynamic when several programs share one library, when the library is large, or when you want to patch the library (a security fix) without rebuilding every program that uses it.
Header-only wins when the library is small or template-heavy and you value "just drop it in". The trade-off is slower compiles, because the code is recompiled in every translation unit that includes it.
# static -> baked into the exe, no runtime file needed
g++ main.cpp -L. -lgeometry -o app
# dynamic -> exe stays small, liblogger.so must exist at run time
g++ main.cpp -L. -llogger -o app
LD_LIBRARY_PATH=. ./app # so the loader can find liblogger.soPro Tips
- 💡 Link order matters with g++: put -l flags after the files that use them — g++ main.cpp -lfoo, not g++ -lfoo main.cpp.
- 💡 -l drops the lib and the extension: -lcurl links libcurl.so or libcurl.a.
- 💡 Header-only? mark functions inline: it prevents "multiple definition" errors when several files include the header.
- 💡 Prefer a package manager once you have 2+ deps: vcpkg/Conan handle transitive dependencies and versions you'd otherwise track by hand.
Common Errors (and the fix)
- "undefined reference to `foo'" (a LINK error): the header was found and compiled, but the function's actual code was never linked in. Add the library to the link step: g++ main.cpp -L. -lfoo -o app.
- "fatal error: foo.h: No such file or directory": the compiler can't find the header. Point it at the include folder with -I/path/to/include.
- "cannot find -lfoo": the linker can't find the library file. Add its folder with -L/path/to/lib, and check the file is named libfoo.a / libfoo.so.
- "error while loading shared libraries: libfoo.so: cannot open shared object file": the dynamic library isn't on the run-time search path. Set LD_LIBRARY_PATH (Linux) or place the .dll next to the exe (Windows).
- ABI / version mismatch (crashes or garbage at run time): your code was built against one version of a library's headers but linked or loaded a different version (different compiler, flags, or release). Rebuild everything with the same compiler and the matching library version.
📋 Quick Reference: Static vs Dynamic
| Aspect | Static | Dynamic / Shared |
|---|---|---|
| Extension | .a / .lib | .so / .dll / .dylib |
| Code goes | Into the executable | Stays a separate file |
| Binary size | Larger | Smaller |
| Needed at run time | No | Yes (file must be found) |
| Update the library | Rebuild the program | Swap the file, no rebuild |
| Build flag | -L. -lfoo | -L. -lfoo |
| Build it | ar rcs libfoo.a foo.o | g++ -shared -fPIC |
Mini-Challenge: Your Own Header-Only Toolkit
No blanks this time — just a brief and a blank canvas (with an outline to keep you on track). Build a tiny header-only string library, run it, and check your output against the example in the comments. This is exactly how real header-only libraries start.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 🎯 MINI-CHALLENGE: your own header-only string toolkit
// 1. Above main, make a namespace strkit with TWO inline helpers:
// - inline string repeat(const string& s, int n) -> s joined n times
// - inline bool isBlank(const string& s) -> true if s is empty
// 2. In main(), call both and print the results.
// 3. BONUS: add an inline helper surround(s, edge) that returns
// edge + s + edge (e.g. surround("hi", "**") -> "**hi**").
//
// ✅ Example output:
// repeat("ab", 3) = ababab
// isBlank("") = 1
// surround = **hi**
// namespace strkit { ... your inline helpers here ... }
int main() {
// your code here — call your helpers and print the results
return 0;
}🎉 Lesson Complete
- ✅ The standard library ships with the compiler; third-party code you obtain and link yourself
- ✅ Header-only libraries need only #include (mark functions inline)
- ✅ Static (.a/.lib) is copied into the exe; dynamic (.so/.dll/.dylib) loads at run time
- ✅ Link with -I (headers), -L (library folder), -l (which library)
- ✅ undefined reference means "found the header, didn't link the code"
- ✅ vcpkg and Conan install and wire up dependencies for you
Practice quiz
What is the difference between the standard library and a third-party library?
- There is none
- Third-party libraries are always faster
- The standard library ships with every compiler; a third-party library you must obtain, point the compiler at, and usually link
- The standard library cannot be #included
Answer: The standard library ships with every compiler; a third-party library you must obtain, point the compiler at, and usually link. The standard library (iostream, vector, string...) ships with the compiler — just #include it. Third-party code you download, find, and link yourself.
Where does a STATIC library's code end up?
- Copied INTO your executable at link time
- In a separate file loaded at run time
- On the heap
- It is never linked
Answer: Copied INTO your executable at link time. A static library (.a / .lib) is copied into your executable at link time, so the program is self-contained but larger.
What is true of a DYNAMIC (shared) library?
- It is baked into the exe
- It needs no header
- It cannot be shared between programs
- It stays a separate file (.so/.dll/.dylib) loaded at run time, so the exe is smaller
Answer: It stays a separate file (.so/.dll/.dylib) loaded at run time, so the exe is smaller. A dynamic/shared library stays separate and is loaded when the program runs. The exe is small and many programs share one copy, but the file must be present at run time.
What does a header-only library require to use it?
- A separate compile and link step
- Nothing but #include — all its code lives in the header
- A package manager
- A static .a file
Answer: Nothing but #include — all its code lives in the header. A header-only library (like nlohmann/json or Catch2) puts all code in headers, so you just #include it — nothing to compile or link separately.
Why must non-template functions in a header be marked 'inline'?
- To avoid 'multiple definition' errors when several .cpp files include the header
- To make them faster
- To make them private
- It is never necessary
Answer: To avoid 'multiple definition' errors when several .cpp files include the header. inline lets a definition live in a header without breaking the one-definition rule when multiple translation units include it.
What does the 'undefined reference' linker error usually mean?
- A syntax error in your code
- Out of memory
- The header was found and compiled, but the function's actual code was never linked in
- A missing semicolon
Answer: The header was found and compiled, but the function's actual code was never linked in. Including a header only declares that a function exists. 'undefined reference' is a LINKER error — add the library to the link step with -lname and -Lpath.
What does the -l flag do, and how do you name the library?
- Sets the language; use the full filename
- Links a library; drop the 'lib' prefix and extension, so -lcurl links libcurl.so
- Lists files; use the path
- Loads at run time only
Answer: Links a library; drop the 'lib' prefix and extension, so -lcurl links libcurl.so. -l<name> links a library. -lcurl links libcurl.so or libcurl.a — you drop the 'lib' prefix and the extension.
Which three flags do you use to link a non-header library?
- -c, -o, -g
- -O2, -O3, -Os
- -std, -Wall, -Werror
- -I (header dir), -L (library dir), -l (which library)
Answer: -I (header dir), -L (library dir), -l (which library). -I says where the headers are, -L says where the library files are, and -l says which library to link.
What do package managers like vcpkg and Conan do for you?
- Compile your main.cpp
- Download, build, and wire libraries (and their dependencies) into your build
- Replace the compiler
- Write your code
Answer: Download, build, and wire libraries (and their dependencies) into your build. They install libraries and wire them into your build (often via CMake's find_package), sparing you manual -I/-L/-l bookkeeping across many dependencies.
To CONSUME any library, what two steps are always required?
- Compile and run
- Download and delete
- Include its header, then link its compiled code
- Reserve and free
Answer: Include its header, then link its compiled code. Whatever the tooling, consuming a library is always: include its header, then link its compiled code (a package manager handles the linking for you).
Continue this course
- Previous: Game Development Essentials in C++ (Architecture, Components, Timing)
- Next: Final Project — Guided project ideas to consolidate your C++ skills in a real codebase
- Quick reference: C++ cheat sheet
Frequently asked questions
What is the difference between the standard library and a third-party library?
The standard library (the STL plus things like <iostream>, <vector>, <string>) ships with every C++ compiler, so you only need to #include it. A third-party library is written by someone else — you have to download it, tell the compiler where its headers are, and usually link against its compiled code with -l.
What is the difference between a static and a dynamic (shared) library?
A static library (.a on Linux/macOS, .lib on Windows) is copied INTO your executable at link time, so the program is self-contained but bigger. A dynamic/shared library (.so, .dll, .dylib) stays a separate file that is loaded when the program runs, so the executable is smaller and many programs can share one copy — but that file must be present at run time.
Why do I get 'undefined reference' when my header includes compile fine?
Including a header only tells the compiler the function exists; it does not provide the function's body. 'undefined reference' is a LINKER error meaning the implementation was never linked in. Add the library to the link step with -lname and point the linker at its folder with -Lpath.
What is a header-only library and why is it so popular?
A header-only library puts all of its code (usually templates and inline functions) inside header files, so there is nothing to compile or link separately — you just #include it and build. nlohmann/json, Catch2, and fmt (in header mode) work this way, which makes them very easy to drop into a project.
Do I really need a package manager like vcpkg or Conan?
Not for a single header-only library — you can just copy the header in. But once you depend on several libraries with their own dependencies and version requirements, a package manager (vcpkg, Conan) downloads, builds, and wires them into your build for you, which saves a lot of manual -I/-L/-l bookkeeping.