본문으로 건너뛰기
Previous
Next
C++ Linkage and Storage Duration: extern, static,

C++ Linkage and Storage Duration: extern, static,

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 — static at 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/ContextLinkageStorage Duration
int x; at namespace scopeExternalStatic (program)
static int x; at namespace scopeInternal (file-only)Static (program)
Anonymous namespace variableInternal (file-only)Static (program)
int x; inside a functionNoneAutomatic (block)
static int x; inside a functionNoneStatic (program)
thread_local int x;Depends on scopeThread lifetime
extern int x;ExternalDefined elsewhere
Class static memberClass 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 .cpp files — the default for non-static namespace-scope names. One definition, many extern declarations
  • Internal linkage: file-private — use static at file scope or anonymous namespaces (prefer anonymous namespace in modern C++)
  • Static storage: global lifetime — globals, static locals, 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
  • const at namespace scope has internal linkage by default — use extern const or inline constexpr to 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 등으로 검색하시면 이 글이 도움이 됩니다.