C++ 동적 초기화와 정적 초기화 순서 문제: Meyers 싱글톤·constinit으로 해결하기

이 글의 핵심

컴파일 타임에 끝나는 정적 초기화와 런타임에 실행되는 동적 초기화의 차이, 번역 단위가 다른 전역 객체끼리 초기화 순서가 보장되지 않아 생기는 문제를 정리합니다. 함수 내 static의 스레드 안전 초기화와 constinit·지연 초기화 같은 해결 패턴도 비교합니다.

동적 초기화란?

동적 초기화(dynamic initialization) 는 런타임에 함수 호출이나 표현식 평가를 통해 변수를 초기화하는 방법입니다. 컴파일 타임에 값을 알 수 없는 경우에 사용됩니다.

int getValue() { return 42; }

int x = getValue();  // 동적 초기화 (런타임)
constexpr int y = 42; // 상수 초기화 (컴파일 타임)

왜 필요한가?:

  • 유연성: 런타임 값(파일, 네트워크, 사용자 입력)으로 초기화
  • 복잡한 로직: 생성자, 함수 호출 등 복잡한 초기화 로직
  • 의존성: 다른 변수나 외부 상태에 의존
// 동적 초기화가 필요한 경우
int port = loadConfigFromFile();  // 파일에서 읽기
std::string name = getUserInput();  // 사용자 입력
Database db("localhost", port);  // 생성자 호출

초기화 비교:

초기화 방법시점예시
상수 초기화컴파일 타임constexpr int x = 10;
동적 초기화런타임int x = getValue();
0 초기화프로그램 로드static int x;

정적 저장 기간을 가진 변수(전역, 네임스페이스 범위, static 멤버, 함수 안 static)는 두 단계로 초기화됩니다. 먼저 정적 초기화 단계에서 모든 변수가 0으로 채워지거나(0 초기화), 상수 식으로 값이 정해지는 경우 그 값이 들어갑니다(상수 초기화). 이 단계는 실행 파일의 .data/.bss 섹션에 값이 이미 들어 있는 형태라 실행 시간이 전혀 들지 않습니다. 그다음 상수 식으로 정할 수 없는 변수만 동적 초기화 단계에서 실제 코드를 실행해 값을 채웁니다. 이 구분이 중요한 이유는 아래에서 볼 초기화 순서 문제가 동적 초기화끼리만 생기기 때문입니다. 상수 초기화된 변수는 어떤 동적 초기화보다도 먼저 값을 가지므로 순서 문제에서 안전합니다. 참고로 표준은 동적 초기화 대상이라도 결과가 같다면 컴파일러가 정적으로 미리 계산해 두는 것을 허용하므로, 위 int x = getValue();는 최적화 빌드에서 실제로는 상수 42로 채워질 수도 있습니다.

정적 변수 초기화

int compute() {
    return 42;
}

// 동적 초기화
int global = compute();

void func() {
    static int local = compute();  // 첫 호출 시
}

정적 변수 초기화 시점:

  1. 전역 변수: main() 실행 전에 초기화
  2. 정적 지역 변수: 첫 호출 시 초기화 (지연 초기화)
#include <iostream>

int initGlobal() {
    std::cout << "전역 변수 초기화\n";
    return 100;
}

int global = initGlobal();  // main() 전에 실행

void func() {
    static int local = []() {
        std::cout << "정적 지역 변수 초기화\n";
        return 200;
    }();
}

int main() {
    std::cout << "main 시작\n";
    func();  // "정적 지역 변수 초기화"
    func();  // 출력 없음 (이미 초기화됨)
}

// 출력:
// 전역 변수 초기화
// main 시작
// 정적 지역 변수 초기화

실무 권장:

  • 전역 변수: 가능하면 피하기 (초기화 순서 문제)
  • 정적 지역 변수: Singleton, 지연 초기화에 활용

“전역 변수는 main() 전에 초기화된다”는 설명은 대부분의 구현에서 맞지만, 표준은 조금 더 느슨합니다. 번역 단위의 동적 초기화를 그 번역 단위의 함수나 변수가 처음 사용되기 직전까지 미루는 것도 허용합니다. 주요 컴파일러는 실행 파일에서는 main 전에 초기화하지만, 이 여지 때문에 “다른 파일의 전역 생성자가 반드시 먼저 실행되었을 것”이라는 가정은 표준으로 보장되지 않습니다. 공유 라이브러리(.so, .dll)의 전역은 라이브러리가 로드될 때 초기화되므로, dlopen으로 늦게 로드되는 플러그인의 전역 생성자는 main 한참 뒤에 실행됩니다.

실전 예시

예시 1: 정적 지역 변수

class Database {
public:
    Database() {
        std::cout << "DB 연결" << std::endl;
    }
};

Database& getDB() {
    static Database db;  // 첫 호출 시 초기화
    return db;
}

int main() {
    getDB();  // "DB 연결"
    getDB();  // 출력 없음 (이미 초기화됨)
}

함수 지역 static은 처음 실행 흐름이 그 선언을 지나갈 때 초기화됩니다. 이 성질 덕분에 “필요해질 때까지 비싼 초기화를 미룬다”와 “사용하기 전에 반드시 초기화되어 있다”를 동시에 얻을 수 있습니다. 초기화 도중 예외가 발생하면 변수는 초기화되지 않은 상태로 남고, 다음 호출 때 다시 초기화를 시도합니다. DB 연결처럼 실패할 수 있는 초기화를 여기에 두면, 첫 시도가 실패해도 다음 요청에서 재시도되는 자연스러운 동작이 됩니다. 반대로 이 객체는 프로그램 종료 시 main이 끝난 뒤 소멸되므로, 소멸자에서 로그를 남기거나 다른 전역 객체를 쓰면 이미 파괴된 객체를 건드릴 수 있습니다.

예시 2: 싱글톤

class Singleton {
    Singleton() {
        std::cout << "생성" << std::endl;
    }
    
public:
    static Singleton& getInstance() {
        static Singleton instance;  // 스레드 안전 (C++11)
        return instance;
    }
};

int main() {
    auto& s1 = Singleton::getInstance();  // "생성"
    auto& s2 = Singleton::getInstance();  // 출력 없음
}

예시 3: 초기화 순서 문제

// file1.cpp
int x = compute1();

// file2.cpp
extern int x;
int y = x + 1;  // x가 초기화 안됐을 수 있음

// ✅ 함수 내 정적 변수로 해결
int& getX() {
    static int x = compute1();
    return x;
}

int y = getX() + 1;  // 순서 보장

이 해결책이 동작하는 이유는 전역 변수 x를 없애고 함수 호출로 바꿨기 때문입니다. 함수는 어느 파일에서 언제 호출되든 처음 호출되는 순간 내부 static을 초기화하므로, 다른 파일의 동적 초기화가 getX()를 부르면 그 자리에서 x가 만들어집니다. 흔히 이 관용구를 “Construct On First Use”라고 부릅니다. 단점은 모든 접근이 함수 호출이 되고, 매 호출마다 “초기화되었는지” 확인하는 가드 검사가 들어간다는 것입니다. 이 검사는 초기화 이후에는 원자적 읽기 한 번 수준이라 대부분 무시할 수 있지만, 아주 뜨거운 루프에서 반복 호출한다면 참조를 지역 변수에 받아 두고 쓰는 편이 좋습니다.

예시 4: 지연 초기화

class Resource {
public:
    Resource() {
        std::cout << "Resource 생성" << std::endl;
    }
};

Resource& getResource() {
    static Resource res;  // 첫 사용 시 생성
    return res;
}

int main() {
    const bool needResource = true;
    // Resource 사용 안하면 생성 안 됨
    if (needResource) {
        getResource();
    }
}

스레드 안전성

// C++11: 정적 지역 변수 초기화는 스레드 안전
void func() {
    static int x = compute();  // 한 스레드만 초기화
}

// C++03: 스레드 안전하지 않음

C++11 스레드 안전성 보장:

C++11부터 정적 지역 변수 초기화는 자동으로 스레드 안전합니다. 컴파일러가 내부적으로 뮤텍스를 사용하여 한 스레드만 초기화하도록 보장합니다.

#include <thread>
#include <iostream>

int expensiveInit() {
    std::cout << "초기화 시작 (스레드 " << std::this_thread::get_id() << ")\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "초기화 완료\n";
    return 42;
}

void func() {
    static int value = expensiveInit();  // 스레드 안전
    std::cout << "값: " << value << '\n';
}

int main() {
    std::thread t1(func);
    std::thread t2(func);
    std::thread t3(func);
    
    t1.join();
    t2.join();
    t3.join();
}

// 출력:
// 초기화 시작 (스레드 ...)
// 초기화 완료
// 값: 42
// 값: 42
// 값: 42
// (초기화는 한 번만 실행됨)

내부 동작 (개념적):

// 컴파일러가 생성하는 코드 (개념적)
void func() {
    static bool initialized = false;
    static std::mutex init_mutex;
    static int value;

    std::lock_guard<std::mutex> lock(init_mutex);
    if (!initialized) {
        value = expensiveInit();
        initialized = true;
    }
}

위 코드는 이해를 위한 단순화이고, 실제 구현은 매 호출마다 뮤텍스를 잡지 않습니다. GCC와 Clang(Itanium C++ ABI)은 변수마다 가드 변수를 두고, 먼저 가드를 원자적으로 읽어 이미 초기화되었으면 곧바로 넘어갑니다. 초기화되지 않은 경우에만 __cxa_guard_acquire를 호출해 한 스레드만 초기화하게 하고, 나머지 스레드는 초기화가 끝날 때까지 기다립니다. 그래서 초기화 이후의 비용은 원자적 읽기 한 번 수준입니다. 임베디드처럼 스레드가 없는 환경에서는 -fno-threadsafe-statics로 이 가드 코드를 빼서 코드 크기를 줄이기도 하지만, 스레드가 있는 프로그램에서 끄면 초기화가 두 번 일어날 수 있습니다.

주의사항:

  • 전역 변수: 초기화 자체는 보통 main 전 단일 스레드에서 끝나지만, 초기화 이후의 접근은 스레드 안전하지 않음
  • 정적 지역 변수: C++11 이상에서 초기화만 스레드 안전. 초기화된 객체를 여러 스레드가 수정하려면 별도 동기화가 필요

자주 발생하는 문제

문제 1: 초기화 순서

// ❌ 순서 보장 안 됨
// file1.cpp
int a = compute();  // 동적 초기화 (int a = 10;이었다면 상수 초기화라 안전)

// file2.cpp
extern int a;
int b = a + 1;  // a가 아직 0일 수 있음

// ✅ 함수 내 정적 변수
int& getA() {
    static int a = 10;
    return a;
}

int b = getA() + 1;  // 순서 보장

이 문제에서 흔히 헷갈리는 부분이 있습니다. 만약 file1이 int a = 10;이라면 a는 상수 초기화되어 어떤 동적 초기화보다도 먼저 10을 가지므로, b는 항상 11이 되어 문제가 없습니다. 순서가 문제가 되는 것은 a가 함수 호출, 생성자 실행처럼 동적으로 초기화될 때뿐입니다. 이때 b의 초기화가 먼저 실행되면 a는 정적 초기화 단계의 값인 0을 가지고 있어서 b는 1이 됩니다. 크래시가 아니라 조용히 잘못된 값이 들어가는 경우가 많다는 점이 이 버그를 찾기 어렵게 만듭니다. 링크 순서(g++ file1.o file2.o와 g++ file2.o file1.o)를 바꾸면 결과가 달라지는 것으로 이 문제를 확인할 수 있습니다.

문제 2: 순환 의존성

// ❌ 순환 의존 (서로 다른 파일)
// file1.cpp
extern int y;
int x = y + 1;
// file2.cpp
extern int x;
int y = x + 1;

// ✅ 의존 방향을 한쪽으로 정리
int x = 0;
int y = x + 1;

같은 파일 안에서 int x = y + 1; int y = x + 1;을 쓰면 x를 초기화하는 시점에 y가 아직 선언되지 않았으므로 컴파일 에러가 납니다. 순환 의존이 실제로 문제가 되는 것은 위처럼 다른 파일에 나뉜 경우입니다. 이때 두 변수는 정적 초기화 단계에서 이미 0이므로 결과는 미정의 동작이 아니라 어느 쪽이 먼저 초기화되느냐에 따라 달라지는 값입니다. file1이 먼저면 x = 1, y = 2, file2가 먼저면 y = 1, x = 2가 됩니다. 어느 쪽이든 설계 의도와 맞을 리 없으므로, 두 값 중 하나를 상수로 두거나 둘을 한 곳에서 계산하도록 의존 방향을 정리해야 합니다. 함수 지역 static끼리 서로를 호출하는 순환이라면 아래 “재진입” 문제로 이어집니다.

문제 3: 예외

int compute() {
    throw std::runtime_error("에러");
}

// 초기화 실패
int global = compute();  // 예외 발생

int main() {
    // 프로그램 종료
}

전역 변수의 동적 초기화에서 예외가 빠져나오면 이를 잡을 try 블록이 없으므로 std::terminate가 호출되어 프로그램이 즉시 종료됩니다. 보통 terminate called after throwing an instance of 'std::runtime_error' 같은 메시지만 남고, main의 로그 설정이 아직 실행되지 않았으므로 로그 파일에는 아무것도 기록되지 않습니다. 설정 파일 누락처럼 운영 환경에서만 발생하는 초기화 실패가 “프로그램이 아무 로그 없이 바로 죽는다”는 증상으로 나타나는 이유입니다. 실패할 수 있는 초기화는 main 안으로 옮기거나 함수 지역 static으로 바꿔, 호출하는 쪽에서 예외를 처리할 수 있게 만드는 것이 원칙입니다.

문제 4: 소멸 순서

class Resource {
public:
    ~Resource() {
        std::cout << "소멸" << std::endl;
    }
};

Resource r1;
Resource r2;

// 소멸 순서: r2 -> r1 (생성 역순)

소멸은 생성의 역순이라는 규칙은 함수 지역 static까지 포함해 적용됩니다. 즉 먼저 생성된 것이 나중에 소멸됩니다. 이 때문에 생긴 유명한 함정이 로거 싱글턴입니다. 전역 객체 A가 생성될 때는 로거를 쓰지 않다가 소멸자에서 처음 로거를 쓰면, 로거는 A보다 나중에 생성되었으므로 A보다 먼저 소멸되어 A의 소멸자가 파괴된 로거를 사용하게 됩니다. 해결책은 A의 생성자에서 로거를 한 번 호출해 생성 순서를 강제하거나, 로거를 static Logger* logger = new Logger;처럼 힙에 만들고 일부러 해제하지 않는 것입니다. 후자는 누수 검사 도구에 “still reachable”로 잡히지만, 종료 순서 문제를 원천적으로 피하는 실용적인 관용구로 널리 쓰입니다.

최적화

// ❌ 동적 초기화
int getValue() { return 42; }
int x = getValue();

// ✅ 상수 초기화
constexpr int getValue() { return 42; }
constexpr int x = getValue();

최적화 전략:

// 1. constexpr로 전환 (가능한 경우)
// Before
int square(int x) { return x * x; }
int result = square(10);  // 런타임 계산

// After
constexpr int square(int x) { return x * x; }
constexpr int result = square(10);  // 컴파일 타임 계산

// 2. 정적 지역 변수로 지연 초기화
// Before
Database globalDB("localhost");  // 프로그램 시작 시 연결

// After
Database& getDB() {
    static Database db("localhost");  // 첫 사용 시 연결
    return db;
}

// 3. constinit로 초기화 보장 (C++20)
// Before
int config = 100;  // 동적 초기화일 수 있음

// After
constinit int config = 100;  // 상수 초기화 강제

성능 영향:

#include <chrono>
#include <iostream>

// 동적 초기화: 프로그램 시작 시 비용
int expensiveGlobal = []() {
    int sum = 0;
    for (int i = 0; i < 1000000; ++i) {
        sum += i;
    }
    return sum;
}();

int main() {
    auto start = std::chrono::steady_clock::now();
    // expensiveGlobal은 이미 초기화됨 (main 전)
    auto elapsed = std::chrono::steady_clock::now() - start;
    std::cout << "main 시작 시간: " << elapsed.count() << "ns\n";
}
// 프로그램 시작 시간이 느려짐

이 코드의 측정값은 전역 초기화 비용을 보여 주지 않는다는 점에 주의하세요. start와 elapsed는 모두 main 안에서 재므로, 이미 끝난 main 이전의 작업은 측정에 포함되지 않습니다. 실제 비용을 보려면 셸에서 time ./app으로 프로세스 전체 실행 시간을 재거나, perf record로 main 이전 구간의 샘플을 확인해야 합니다. 또 이 람다의 계산은 결과가 상수이므로 최적화 컴파일러가 컴파일 시점에 계산해 버려 실제로는 비용이 0일 수도 있습니다. 대규모 C++ 애플리케이션에서 시작 시간이 느린 원인을 찾아보면, 수많은 번역 단위의 전역 std::string, std::map 생성자와 정규식 컴파일 같은 작업이 main 전에 쌓여 있는 경우가 흔합니다.

실무 패턴

패턴 1: Singleton (Meyer’s Singleton)

class Logger {
    Logger() { /* 초기화 */ }
    
public:
    static Logger& instance() {
        static Logger logger;  // 스레드 안전, 지연 초기화
        return logger;
    }
    
    void log(const std::string& msg) {
        // 로깅 로직
    }
};

// 사용
Logger::instance().log("Hello");

패턴 2: 지연 초기화 (Lazy Initialization)

class ResourceManager {
    static std::unique_ptr<Database> db_;
    
public:
    static Database& getDB() {
        if (!db_) {
            db_ = std::make_unique<Database>("localhost");
        }
        return *db_;
    }
};
// 클래스 밖 정의 필요: std::unique_ptr<Database> ResourceManager::db_;

// 사용하지 않으면 초기화 안 됨

이 방식은 함수 지역 static과 달리 스레드 안전하지 않습니다. 두 스레드가 동시에 getDB()를 처음 호출하면 둘 다 !db_를 참으로 보고 각자 객체를 만들 수 있고, 한쪽이 만든 객체는 다른 쪽의 대입으로 덮어써져 파괴됩니다. 이미 그 객체의 참조를 받은 스레드는 파괴된 객체를 쓰게 됩니다. 이 패턴이 필요한 경우(나중에 reset()으로 다시 만들 수 있어야 하는 등)에는 std::call_once와 std::once_flag로 보호하거나 뮤텍스를 써야 하고, 그런 요구가 없다면 함수 지역 static이 더 짧고 안전합니다.

패턴 3: 초기화 순서 보장

// config.cpp
int& getPort() {
    static int port = loadPortFromFile();
    return port;
}

// server.cpp
Server& getServer() {
    static Server server(getPort());  // getPort() 먼저 초기화됨
    return server;
}

다른 초기화 방식과 비교 (실무용)

종류예언제 쓰나
상수 초기화constexpr int x = 42;값이 컴파일 타임에 확정
0/값 초기화정적 int a; 후 동적 단계 전바탕이 되는 영(零) 채움
동적 초기화int x = load();파일·환경·랜덤 등 런타임 입력

동적 초기화가 필요 없는데 습관적으로 전역에서 호출하면 시작 지연과 순서 버그만 얻는 경우가 많습니다. 먼저 constexpr/constinit 가능 여부를 보며, 안 되면 지역 static으로 늦추는 순서가 안전합니다.

실전 활용 사례 (보강)

  • 플러그인·동적 라이브러리: 로드 순서와 전역 생성자 순서가 플랫폼마다 달라, 전역 동적 초기화에 의존하면 디버깅이 어렵습니다. 가능하면 명시적 init API로 모읍니다.
  • 테스트: 전역 부작용을 줄이기 위해 동적 전역 대신 테스트 픽스처나 main 직후 초기화로 옮깁니다.
  • 임베디드: 부팅 직후 제한된 시간 안에 끝나야 하면, 무거운 동적 전역 초기화를 단계적으로 나누거나 constexpr로 줄입니다.

성능 영향 (정리)

  • 전역 동적 초기화: 프로세스/스레드 시작 전에 모두 실행되므로, 개수·비용이 크면 TTI(time-to-interactive) 가 나빠집니다.
  • 정적 지역 변수: 첫 진입 시 한 번만 비용이 들고, 이후는 포인터 역참조 수준입니다. 대신 초기화에 락/원자 연산이 끼면(구현 의존) 마이크로벤치에서 드물게 보입니다.
  • 최적화: 컴파일러가 동적 전역을 “한 번만 실행되는 함수”로 두는 것이 일반적이며, 불필요한 중복 제거는 링크 타임과 전역 제거 설정에도 영향받습니다.

컴파일러·링크 관점

  • 초기화 우선순위: 표준은 TU 간 동적 순서를 보장하지 않습니다. GCC의 __attribute__((init_priority(N)))처럼 순서를 지정하는 비표준 확장이 있지만, 이식성이 없고 MSVC에서는 #pragma init_seg처럼 전혀 다른 방식이라 의존하지 않는 편이 좋습니다.
  • 초기화 섹션: 구현은 종종 .init_array 등에 함수 포인터를 등록합니다. 동적 전역이 많을수록 시작 시 호출 테이블이 커집니다.
  • LTO/LTCG: 사용되지 않는 TU의 전역이 제거되면 동적 초기화도 함께 사라질 수 있습니다. 반대로, “한 번도 안 쓰는 전역”이 링크에 남으면 비용만 남습니다.

흔한 실수 (보강)

  1. 다른 .cpp의 전역을 참조하는 동적 초기화: SIOF의 정석입니다. extern만으로 순서를 기대하지 마세요.
  2. 정적 지역 초기화 재진입: 초기화가 끝나기 전에 같은 스레드가 그 선언에 다시 도달하면(초기화 코드가 돌고 돌아 같은 함수를 다시 호출하는 경우) 표준상 미정의 동작입니다. 실제로는 구현에 따라 데드락이 되거나, libstdc++처럼 __gnu_cxx::recursive_init_error 예외를 던집니다. 싱글턴 A의 생성자가 싱글턴 B를 부르고, B의 생성자가 다시 A를 부르는 구조에서 흔히 생깁니다.
  3. 전역 예외: main 전 예외는 잡기 어렵습니다. 실패 가능한 초기화는 정적 지역 + 명시적 오류 처리나 main 안으로 옮기세요.

FAQ

Q1: 동적 초기화는 무엇인가요?

A: 런타임에 함수 호출이나 표현식 평가를 통해 변수를 초기화하는 방법입니다. 컴파일 타임에 값을 알 수 없는 경우에 사용됩니다.

Q2: 초기화 순서는 어떻게 되나요?

A:

  • 파일 내: 선언 순서대로 초기화
  • 파일 간: 순서 보장 안 됨 (Static Initialization Order Fiasco)

Q3: 정적 지역 변수는 스레드 안전한가요?

A: C++11부터 초기화는 스레드 안전합니다. 컴파일러가 가드 변수와 내부 동기화로 한 스레드만 초기화하게 하고, 다른 스레드는 초기화가 끝날 때까지 기다립니다. 초기화된 객체를 여러 스레드에서 읽고 쓰는 것은 별개의 문제라 직접 동기화해야 합니다.

Q4: 성능 영향은?

A: 런타임 비용이 있습니다. 전역 변수는 프로그램 시작 시간을 늦추고, 정적 지역 변수는 첫 호출 시 약간의 오버헤드가 있습니다.

Q5: 초기화 순서 문제를 어떻게 해결하나요?

A:

  • 방법 1: 함수 내 정적 변수 사용 (지연 초기화)
  • 방법 2: constexpr 사용 (가능한 경우)
  • 방법 3: Singleton 패턴

Q6: 전역 변수 초기화 중 예외가 발생하면?

A: 프로그램이 종료됩니다. main() 전에 발생한 예외는 try-catch로 잡을 수 없습니다.

int initGlobal() {
    throw std::runtime_error("초기화 실패");
}

int global = initGlobal();  // 프로그램 종료

int main() {
    // 실행 안 됨
}

Q7: 정적 지역 변수는 언제 소멸되나요?

A: 프로그램 종료 시 소멸됩니다. 소멸 순서는 생성 역순입니다.

Q8: constinit과 constexpr은 무엇이 다른가요?

A: constexpr 변수는 컴파일 타임에 초기화될 뿐 아니라 값을 바꿀 수 없는 상수입니다. C++20의 constinit은 “이 변수는 반드시 상수 초기화되어야 한다”는 것만 강제하고, 이후에 값을 바꾸는 것은 허용합니다. 그래서 순서 문제에서 안전한 변경 가능한 전역 변수가 필요할 때 constinit을 씁니다. 초기화 식이 상수 식이 아니면 컴파일 에러가 나므로, 누군가 초기화 식을 함수 호출로 바꿔 동적 초기화가 되는 것을 막는 안전장치 역할도 합니다. 함수 지역 변수에는 쓸 수 없고, 정적·스레드 저장 기간 변수에만 붙일 수 있습니다.

관련 글: Constant Initialization, Value Initialization, Zero Initialization.

동적 초기화는 런타임에 변수를 초기화하며, 정적 지역 변수를 사용하면 초기화 순서 문제를 해결할 수 있습니다.


같이 보면 좋은 글