C++ Linkage and Storage Duration: extern, static,
이 글의 핵심
External vs internal linkage, no linkage, and automatic/static/thread/dynamic storage. How static locals, anonymous namespaces, and thread_local interact with lifetime. Complete guide with practical examples.
What is Linkage?
When you build a C++ program, it is compiled as multiple translation units (roughly, one .cpp file each). Linkage determines which names are visible across these translation units.
There are three kinds of linkage:
- External linkage: visible to other translation units — the default for non-static functions and variables at namespace scope
- Internal linkage: visible only within the current translation unit —
staticat file scope, or anonymous namespaces - No linkage: local variables inside functions
External Linkage with extern
Variables and functions with external linkage are shared across translation units. Declare them in a header, define them in exactly one .cpp:
// globals.h — declarations (no definition, no storage allocated)
extern int requestCount; // declared, not defined
void processRequest(int id);
// globals.cpp — definition (storage allocated here)
int requestCount = 0;
void processRequest(int id) {
++requestCount;
// ...
}
// main.cpp — uses the extern declaration
#include "globals.h"
#include <iostream>
int main() {
processRequest(1);
processRequest(2);
std::cout << "Requests: " << requestCount << '\n'; // 2
}
Rule: one definition, many declarations. If you define the same variable in two .cpp files, you get a linker error: “multiple definition of requestCount”.
extern for const
const variables at namespace scope have internal linkage by default (unlike non-const). To share a const across translation units, you need extern explicitly:
// constants.h
extern const double PI; // declare
// constants.cpp
extern const double PI = 3.14159265358979; // define (extern required for const)
// any.cpp
#include "constants.h"
double circumference(double r) { return 2.0 * PI * r; }
Without extern const, each .cpp that const double PI = ... defines gets its own copy — technically OK (internal linkage), but wastes space and requires separate definitions.
In modern C++, use inline constexpr in headers instead:
// constants.h — C++17
inline constexpr double PI = 3.14159265358979; // one definition in each TU, same value
Internal Linkage
Use static at file scope (or an anonymous namespace) to give a name internal linkage — it’s only visible within the current .cpp file:
// helpers.cpp
// These are invisible to other .cpp files — no linker conflicts
static int helperCounter = 0;
static void resetCounter() {
helperCounter = 0;
}
// Modern C++ prefers anonymous namespace
namespace {
double scaleFactor = 1.5; // file-local — internal linkage
bool validate(int x) { // file-local function
return x > 0 && x < 1000;
}
}
// Public API — external linkage
void processData(int x) {
if (!validate(x)) return; // can use file-local validate
resetCounter();
// ...
}
Anonymous namespaces are preferred over static in modern C++ because they work for types and classes too:
namespace {
struct ParseState { // file-local type
int pos;
bool error;
};
class InternalParser { // file-local class
// ...
};
}
Storage Duration
Linkage is about visibility. Storage duration is about lifetime.
Automatic Storage (the most common)
Local variables live on the stack. They’re created when the block is entered and destroyed when it exits:
void compute() {
int local = 0; // created here
double buffer[1024]; // allocated on stack
// ...
} // local and buffer destroyed here
Static Storage
Globals, static locals, and static class members have static storage — they live for the entire program duration:
int globalCounter = 0; // static storage — lives until main() returns
void increment() {
static int callCount = 0; // static local — initialized once, persists between calls
++callCount;
std::cout << "Called " << callCount << " times\n";
}
int main() {
increment(); // Called 1 times
increment(); // Called 2 times
increment(); // Called 3 times
}
Static local initialization is thread-safe since C++11 — the first thread to reach the initialization point initializes it; other threads wait.
Thread-Local Storage
thread_local gives each thread its own instance:
#include <thread>
#include <iostream>
thread_local int threadId = 0; // each thread has its own copy
thread_local std::mt19937 rng; // each thread has its own RNG
void worker(int id) {
threadId = id; // sets this thread's copy
std::cout << "Thread " << threadId << " started\n";
}
int main() {
threadId = 0; // main thread's copy
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join();
t2.join();
std::cout << "Main thread id: " << threadId << '\n'; // still 0
}
thread_local is initialized once per thread on first use. It’s ideal for:
- Per-thread random number generators (avoids locking shared
mt19937) - Thread-local caches (avoids cache line ping-pong between threads)
- Per-thread buffers (formatting, string building)
Dynamic Storage
Objects allocated with new (or smart pointers) have dynamic storage. Their lifetime is manual — they live until delete (or the last shared_ptr is destroyed):
// Dynamic — lifetime controlled explicitly
auto p = std::make_unique<Widget>(); // created now
// ...
// destroyed when p goes out of scope (unique_ptr)
The Static Initialization Order Fiasco
When two global variables depend on each other across .cpp files, their initialization order is unspecified:
// logger.cpp
Logger globalLogger("app.log"); // may initialize before or after...
// config.cpp
Config globalConfig("config.ini"); // ... this
// If Logger reads from Config in its constructor — Config might not be ready
Fix: use function-local static for lazy initialization:
// logger.h
Logger& getLogger() {
static Logger logger("app.log"); // initialized on first call
return logger;
}
// config.h
Config& getConfig() {
static Config config("config.ini"); // initialized on first call
return config;
}
// logger.cpp — if Logger needs Config
Logger& getLogger() {
static Logger logger(getConfig().logPath()); // getConfig() initializes first
return logger;
}
Function-local statics initialize on first call — after all dependencies are ready.
Summary Table
| Keyword/Context | Linkage | Storage Duration |
|---|---|---|
int x; at namespace scope | External | Static (program) |
static int x; at namespace scope | Internal (file-only) | Static (program) |
| Anonymous namespace variable | Internal (file-only) | Static (program) |
int x; inside a function | None | Automatic (block) |
static int x; inside a function | None | Static (program) |
thread_local int x; | Depends on scope | Thread lifetime |
extern int x; | External | Defined elsewhere |
Class static member | Class scope (external) | Static (program) |
Practical: Per-Thread RNG
#include <random>
#include <thread>
#include <iostream>
#include <vector>
// Each thread has its own engine — no contention, no locks
thread_local std::mt19937 tl_rng{std::random_device{}()};
int randomInt(int lo, int hi) {
return std::uniform_int_distribution<int>{lo, hi}(tl_rng);
}
void worker(int threadNum, int count) {
int sum = 0;
for (int i = 0; i < count; ++i) {
sum += randomInt(1, 100);
}
std::cout << "Thread " << threadNum << " sum: " << sum << '\n';
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back(worker, i, 1000);
}
for (auto& t : threads) t.join();
}
Each thread initializes tl_rng independently when it first calls randomInt. No mutex, no contention — each thread gets its own seeded Mersenne Twister.
Key Takeaways
- External linkage: visible to all
.cppfiles — the default for non-static namespace-scope names. One definition, manyexterndeclarations - Internal linkage: file-private — use
staticat file scope or anonymous namespaces (prefer anonymous namespace in modern C++) - Static storage: global lifetime — globals,
staticlocals, class static members. Static locals are thread-safe since C++11 thread_local: each thread has its own instance — ideal for RNGs, caches, thread-specific buffers- Static initialization order fiasco: cross-TU global initialization order is unspecified — use function-local statics for lazy init to avoid it
constat namespace scope has internal linkage by default — useextern constorinline constexprto share across TUs
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. External vs internal linkage, no linkage, and automatic/static/thread/dynamic storage. How static locals, anonymous name… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [C++ static Members: Static Data, Static Functions, and](/en/blog/cpp-static-members/
- [C++ Namespaces: Complete Guide to Name Boundaries](/en/blog/cpp-namespace-complete/
- [C++ Functions: Parameters, Return Values, Overloading, and](/en/blog/cpp-function-basics/
이 글에서 다루는 키워드 (관련 검색어)
C++, linkage, storage, extern, static 등으로 검색하시면 이 글이 도움이 됩니다.