C++ 정적 초기화 순서 | "전역 변수 크래시" Static Initialization Fiasco 해결
이 글의 핵심
C++ 정적 초기화 순서에 대한 실전 가이드입니다.
들어가며: “전역 변수를 사용했더니 프로그램이 크래시해요"
"초기화되지 않은 변수를 사용하고 있어요”
C++에서 서로 다른 파일의 전역 변수를 사용하면, 초기화 순서가 정해지지 않아 초기화되지 않은 변수를 사용하는 Static Initialization Order Fiasco가 발생할 수 있습니다.
// ❌ file1.cpp
std::vector<int> globalVec = {1, 2, 3};
// ❌ file2.cpp
extern std::vector<int> globalVec;
int globalSize = globalVec.size(); // ❌ globalVec이 초기화 안 됐을 수 있음!
int main() {
std::cout << globalSize << '\n'; // 0 또는 쓰레기 값
}
이 글에서 다루는 것:
- Static Initialization Order Fiasco란?
- 초기화 순서 규칙
- 함수 내 정적 지역 변수 해결책
- Singleton 패턴
목차
1. Static Initialization Order Fiasco란?
문제 발생
// config.cpp
#include <string>
std::string configPath = "/etc/config.txt";
// logger.cpp
#include <fstream>
extern std::string configPath;
std::ofstream logFile(configPath); // ❌ configPath가 초기화 안 됐을 수 있음!
// main.cpp
int main() {
logFile << "Hello\n"; // ❌ 크래시 또는 잘못된 파일 경로
}
문제:
configPath와logFile의 초기화 순서가 정해지지 않음logFile이 먼저 초기화되면 빈 문자열로 파일을 열려고 함- 크래시 또는 잘못된 동작
2. 초기화 순서 규칙
규칙 1: 같은 번역 단위 내
// file.cpp
int a = 10;
int b = a + 5; // ✅ a가 먼저 초기화됨 (선언 순서)
int main() {
std::cout << b << '\n'; // 15
}
규칙: 같은 파일 내에서는 선언 순서대로 초기화.
규칙 2: 다른 번역 단위 간
// file1.cpp
int x = 10;
// file2.cpp
extern int x;
int y = x + 5; // ❌ x가 초기화 안 됐을 수 있음!
규칙: 다른 파일 간에는 순서가 정해지지 않음.
3. 해결책: 함수 내 정적 지역 변수
해결책 1: 함수로 감싸기
// config.cpp
#include <string>
std::string& getConfigPath() {
static std::string configPath = "/etc/config.txt";
return configPath;
}
// logger.cpp
#include <fstream>
std::ofstream& getLogFile() {
static std::ofstream logFile(getConfigPath()); // ✅ 첫 호출 시 초기화
return logFile;
}
// main.cpp
int main() {
getLogFile() << "Hello\n"; // ✅ 안전
}
장점:
- 첫 호출 시 초기화 (Lazy Initialization)
- C++11부터 스레드 안전 (Magic Statics)
- 초기화 순서 문제 해결
해결책 2: constexpr (C++11)
// config.cpp
constexpr const char* configPath = "/etc/config.txt";
// logger.cpp
extern constexpr const char* configPath;
std::ofstream logFile(configPath); // ✅ constexpr은 컴파일 타임 초기화
장점:
- 컴파일 타임 초기화
- 초기화 순서 문제 없음
단점:
- 리터럴 타입만 가능
4. Singleton 패턴
Meyer의 Singleton (권장)
class Logger {
private:
Logger() {
file_.open("/var/log/app.log");
}
std::ofstream file_;
public:
// 복사·이동 금지
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
static Logger& getInstance() {
static Logger instance; // ✅ 첫 호출 시 초기화 (스레드 안전)
return instance;
}
void log(const std::string& msg) {
file_ << msg << '\n';
}
};
int main() {
Logger::getInstance().log("Hello"); // ✅ 안전
}
장점:
- 스레드 안전 (C++11 이후)
- 초기화 순서 문제 없음
- Lazy Initialization
잘못된 Singleton (권장 안 함)
// ❌ 전역 변수 Singleton
class Logger {
// ...
};
Logger& getLogger() {
static Logger* instance = new Logger(); // ❌ 메모리 누수
return *instance;
}
문제:
- 메모리 누수 (delete 안 됨)
- 소멸자 호출 안 됨
정리
초기화 순서 규칙
| 범위 | 초기화 순서 |
|---|---|
| 같은 파일 내 | 선언 순서 |
| 다른 파일 간 | 정해지지 않음 |
| 함수 내 static | 첫 호출 시 |
| constexpr | 컴파일 타임 |
핵심 규칙
- 전역 변수 피하기 (함수 내 static 사용)
- Meyer의 Singleton (스레드 안전)
- constexpr (컴파일 타임 초기화)
- 다른 파일의 전역 변수 의존 금지
체크리스트
- 전역 변수가 다른 파일의 전역 변수를 사용하는가?
- 함수 내 정적 지역 변수로 바꿀 수 있는가?
- Singleton이 스레드 안전한가?
- constexpr을 사용할 수 있는가?
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Singleton 패턴 | 스레드 안전 구현
- C++ 전역 변수 | 사용 주의사항
- C++ static 멤버 | 정적 멤버 가이드
- C++ constexpr | 컴파일 타임 상수
마치며
Static Initialization Order Fiasco는 전역 변수의 초기화 순서가 정해지지 않아 발생하는 문제입니다.
핵심 원칙:
- 전역 변수 피하기
- 함수 내 static 사용
- Meyer의 Singleton
다른 파일의 전역 변수에 의존하지 마세요. 함수 내 정적 지역 변수로 안전하게 초기화하세요.
다음 단계: 정적 초기화를 이해했다면, C++ Singleton 패턴에서 더 깊이 배워보세요.
관련 글
- C++ 시리즈 전체 보기
- C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
- C++ ADL |
- C++ Aggregate Initialization |