C++ Zero Initialization | "0 초기화" 가이드

C++ Zero Initialization | "0 초기화" 가이드

이 글의 핵심

0 초기화는 비트를 0에 맞추는 단계로, 정적·전역·thread_local에서 자동으로 이어지기도 합니다. 지역 자동 변수는 기다리지 않고, 값 초기화·기본 초기화와의 관계·초기화 순서를 알면 디버깅이 쉬워집니다.

0 초기화란?

0 초기화(zero initialization) 는 변수를 0(또는 nullptr, false 등)으로 채우는 초기화입니다. 정적·전역 저장 기간 객체는 프로그램 로드 시 자동으로 0 초기화되며, 값 초기화 {}로 지역 변수도 0으로 맞출 수 있습니다. 기본 초기화상수 초기화·집합체 초기화와 구분해 두면 좋습니다.

int global;        // 0 (전역)
static int s;      // 0 (정적)

void func() {
    static int x;  // 0 (정적 지역)
    int y;         // 쓰레기 값 (지역)
}

왜 필요한가?:

  • 안전성: 정적/전역 변수가 쓰레기 값을 갖지 않도록 보장
  • 예측 가능성: 프로그램 시작 시 일관된 상태
  • 자동화: 명시적 초기화 없이도 안전한 기본값

0 초기화 규칙:

타입0 초기화 결과
int, long, short0
float, double0.0
boolfalse
포인터nullptr
배열모든 요소 0
클래스모든 멤버 0 초기화
struct Data {
    int a;
    double b;
    int* ptr;
};

static Data d;  // {0, 0.0, nullptr}

적용 대상

// 0 초기화됨
int global;
static int s;
static double d;
static int* ptr;

// 0 초기화 안됨
void func() {
    int local;  // 쓰레기 값
}

0 초기화 적용 조건:

  1. 정적 저장 기간(static storage duration):

    • 전역 변수
    • static 변수 (전역, 지역, 클래스 멤버)
    • thread_local 변수
  2. 프로그램 로드 시 자동 적용:

    • 프로그래머가 명시적으로 초기화하지 않아도 자동으로 0으로 설정
    • 다른 초기화(상수 초기화, 동적 초기화) 전에 수행
#include <iostream>

int g1;              // 0 초기화
static int g2;       // 0 초기화
thread_local int g3; // 0 초기화

void func() {
    static int s1;   // 0 초기화
    int local;       // ❌ 0 초기화 안됨
    
    std::cout << "g1: " << g1 << '\n';      // 0
    std::cout << "g2: " << g2 << '\n';      // 0
    std::cout << "s1: " << s1 << '\n';      // 0
    // std::cout << "local: " << local << '\n';  // 정의되지 않은 동작
}

실무 권장:

  • 전역/정적 변수: 0 초기화에 의존해도 안전
  • 지역 변수: 항상 명시적으로 초기화 (int x{};)

다른 초기화와의 관계: 0 초기화는 “한 단계”

정적·전역 객체의 초기화는 표준에서 여러 단계로 설명됩니다. 0 초기화는 그중 첫 단계로, 비트를 0으로 맞추는 쪽에 가깝습니다. 이후 상수 초기화·동적 초기화가 덧붙을 수 있습니다.

구분예시0 초기화와의 관계
값 초기화 int x{}지역 int를 0으로0 초기화가 포함될 수 있는 복합 규칙(타입에 따라)
기본 초기화 int x;지역 int0 초기화 아님 → 쓰레기
집합체 S{}멤버를 0으로 채우는 부분0 초기화 + 나머지 규칙

“지역 변수도 0으로 두고 싶다”면 0 초기화만 기다리면 안 되고, int x{} 같은 값 초기화를 써야 합니다.

static·전역 vs 지역: 같은 int x;가 다른 이유

저장 기간이 다르면 같은 문법이라도 결과가 달라집니다.

int g;                    // 정적 저장 기간 → 먼저 0 초기화

void f() {
    static int s;         // 정적 지역 → 0 초기화
    int a;                // 자동(지역) → 기본 타입은 미정의 값
    thread_local int t;   // 스레드 지역 정적 → 0 초기화
}
  • 전역·정적·thread_local: 프로그램/스레드 수명에 걸쳐 한 번만 존재하므로, 구현은 보통 BSS 등에 올려 0으로 채우는 방식으로 맞춥니다.
  • 일반 지역 변수: 스택(또는 레지스터)에 매 호출마다 새로 잡히므로, 비용을 줄이기 위해 자동으로 0을 쓰지 않는 것이 일반적입니다. 필요하면 값 초기화로 명시하세요.

흔한 오해: “전역은 0이니까 지역도 0에 가깝지 않을까?” → 아닙니다. 지역 int읽기 전에 반드시 대입하거나 int x{}로 초기화하세요.

클래스 vs 기본 타입

  • 기본 타입의 정적/전역: 0 초기화로 0, nullptr, false 등으로 시작합니다.
  • 클래스 타입의 정적/전역: 0 초기화 단계에서 멤버가 기본 타입이면 0에 가깝게 맞고, 이어서 생성자·필요 시 동적 초기화가 올 수 있습니다. POD가 아닌 타입은 “전부 비트 0이 항상 유효한 상태”는 아니므로, 실무에서는 동적 초기화에서 제대로 된 생성자를 두는 편이 안전합니다.
struct Count {
    int n;      // 정적 정의 시 0 초기화로 n은 0부터
};
Count global_count;  // 전역 Count — 집합체 규칙과 조합 시 멤버 0

인스턴스 멤버 int n;는 객체가 자동 저장 기간이면 기본 초기화 경로로 가기 쉬워 쓰레기가 될 수 있으므로, int n{} 또는 생성자에서 초기화하세요.

실전 예시

예시 1: 전역 변수

int counter;  // 0

int main() {
    std::cout << counter << std::endl;  // 0
    counter++;
    std::cout << counter << std::endl;  // 1
}

예시 2: 정적 지역 변수

void func() {
    static int callCount;  // 0 (첫 호출 시)
    callCount++;
    std::cout << "호출 횟수: " << callCount << std::endl;
}

int main() {
    func();  // 1
    func();  // 2
    func();  // 3
}

예시 3: 클래스 정적 멤버

class Counter {
    static int count;  // 선언
    
public:
    Counter() {
        count++;
    }
    
    static int getCount() {
        return count;
    }
};

// 정의 (0 초기화)
int Counter::count;

int main() {
    Counter c1, c2, c3;
    std::cout << Counter::getCount() << std::endl;  // 3
}

예시 4: 배열

static int arr[5];  // {0, 0, 0, 0, 0}

int main() {
    for (int x : arr) {
        std::cout << x << " ";  // 0 0 0 0 0
    }
}

초기화 순서

// 1. 0 초기화 (정적/전역)
int global1;  // 0

// 2. 상수 초기화 (constexpr)
constexpr int global2 = 42;

// 3. 동적 초기화 (런타임)
int global3 = compute();

초기화 단계 상세:

정적/전역 변수는 다음 순서로 초기화됩니다:

  1. Zero Initialization (0 초기화):

    • 프로그램 로드 시 모든 정적/전역 변수를 0으로 설정
    • 바이너리의 .bss 세그먼트에 배치 (디스크 공간 절약)
  2. Constant Initialization (상수 초기화):

    • 컴파일 타임에 값이 결정된 변수 초기화
    • 바이너리의 .data 세그먼트에 값 내장
  3. Dynamic Initialization (동적 초기화):

    • main() 전에 런타임 함수 호출로 초기화
    • 파일 내 순서는 보장, 파일 간 순서는 미정의
#include <iostream>

int compute() {
    std::cout << "compute() 호출\n";
    return 100;
}

int g1;                  // 1단계: 0
constexpr int g2 = 50;   // 2단계: 50 (바이너리에 내장)
int g3 = compute();      // 3단계: main() 전 호출

int main() {
    std::cout << "main 시작\n";
    std::cout << "g1: " << g1 << ", g2: " << g2 << ", g3: " << g3 << '\n';
}

// 출력:
// compute() 호출
// main 시작
// g1: 0, g2: 50, g3: 100

실무 팁:

  • 0 초기화: 명시적 초기화 없이도 안전
  • 상수 초기화: 성능 최적화
  • 동적 초기화: 초기화 순서 문제 주의

자주 발생하는 문제

문제 1: 지역 vs 전역

int global;  // 0

void func() {
    int local;  // 쓰레기 값
    
    std::cout << global << std::endl;  // 0
    // std::cout << local << std::endl;  // 정의되지 않은 동작
}

문제 2: 정적 초기화 순서

// file1.cpp
int x = 10;

// file2.cpp
extern int x;
int y = x + 1;  // 순서 보장 안됨

문제 3: 클래스 멤버

class Widget {
    int value;  // 기본 초기화 (쓰레기 값)
    
public:
    Widget() {}
};

// ✅ 멤버 초기화
class Widget {
    int value = 0;  // 0 초기화
};

문제 4: const 변수

// ❌ const는 초기화 필수
// const int x;  // 에러

// ✅ 초기화
const int x = 0;
const int y{};

명시적 0 초기화

// 모두 동일
int x = 0;
int y{};
int z = int();
int w{0};

명시적 0 초기화 방법 비교:

방법예시특징
직접 대입int x = 0;명확, 전통적
중괄호int x{};값 초기화, 좁히기 방지
함수 스타일int x = int();임시 객체 생성
중괄호 + 값int x{0};명시적, 좁히기 방지

실무 권장:

// ✅ 지역 변수: 중괄호 사용 (안전)
void func() {
    int counter{};
    double sum{};
    int* ptr{};
}

// ✅ 전역/정적 변수: 명시적 초기화 (가독성)
int g_counter = 0;
static int s_total = 0;

// ✅ 배열: 중괄호
int arr[5]{};  // {0, 0, 0, 0, 0}

// ✅ 구조체: 중괄호
struct Point { int x, y; };
Point p{};  // {0, 0}

좁히기 변환 방지:

// ❌ 좁히기 변환 허용
int x = 3.14;  // OK: 3 (소수점 버려짐)

// ✅ 좁히기 변환 방지
int y{3.14};  // 에러: narrowing conversion

// 실무 활용
double getValue();
int result{getValue()};  // 컴파일 에러 (의도하지 않은 변환 방지)

실무 패턴

패턴 1: 카운터

class RequestCounter {
    static int count_;  // 0 초기화
    
public:
    RequestCounter() { ++count_; }
    static int getCount() { return count_; }
};

int RequestCounter::count_;  // 정의 (0 초기화)

패턴 2: 플래그

static bool g_initialized;  // false (0 초기화)

void initialize() {
    if (!g_initialized) {
        // 초기화 로직
        g_initialized = true;
    }
}

패턴 3: 버퍼

class Buffer {
    static char data_[1024];  // 모두 '\0' (0 초기화)
    
public:
    static void clear() {
        // 이미 0으로 초기화됨
    }
};

char Buffer::data_[1024];  // 정의

FAQ

Q1: 0 초기화는 언제 발생하나요?

A: 정적/전역 변수는 프로그램 로드 시 자동으로 0 초기화됩니다. 지역 변수는 0 초기화되지 않습니다.

Q2: 지역 변수는 0 초기화되나요?

A: 아닙니다. 지역 변수는 쓰레기 값을 가집니다. 명시적으로 int x{};로 초기화해야 합니다.

Q3: 클래스 멤버는 0 초기화되나요?

A:

  • 정적 멤버: 0 초기화됨
  • 인스턴스 멤버: 기본 초기화 (쓰레기 값), 명시적 초기화 권장
class Widget {
    static int s;  // 0 초기화
    int x;         // 쓰레기 값
    int y = 0;     // 명시적 초기화
};

Q4: 성능 영향은?

A: 프로그램 로드 시 한 번만 수행되며, 일반적으로 성능 영향은 미미합니다. 운영체제가 효율적으로 메모리를 0으로 설정합니다.

Q5: 초기화 순서는?

A:

  1. Zero Initialization: 0으로 설정
  2. Constant Initialization: 컴파일 타임 상수
  3. Dynamic Initialization: 런타임 함수 호출

Q6: 배열은 어떻게 0 초기화되나요?

A: 정적/전역 배열은 모든 요소가 0으로 초기화됩니다.

static int arr[1000];  // 모두 0
static double darr[100];  // 모두 0.0

Q7: 0 초기화 학습 리소스는?

A:

관련 글: 값 초기화, 기본 초기화, 상수 초기화, 집합체 초기화.

한 줄 요약: 0 초기화는 정적/전역 변수를 프로그램 로드 시 자동으로 0으로 설정하는 초기화입니다.

관련 글: 값 초기화, 기본 초기화, 상수 초기화, 집합체 초기화.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ Value Initialization | “값 초기화” 가이드
  • C++ Default Initialization | “기본 초기화” 가이드
  • C++ Constant Initialization | “상수 초기화” 가이드
  • C++ Aggregate Initialization | “집합체 초기화” 가이드

관련 글

  • C++ Dynamic Initialization |
  • C++ Initialization Order 완벽 가이드 | 초기화 순서의 모든 것
  • C++ Aggregate Initialization |
  • C++ Aggregate Initialization 완벽 가이드 | 집합 초기화
  • C++ call_once |