C++ Value Initialization | "값 초기화" 가이드

C++ Value Initialization | "값 초기화" 가이드

이 글의 핵심

값 초기화는 빈 () 또는 {}로 객체를 만들 때 적용되는 규칙입니다. 기본 타입은 0·nullptr, 클래스는 기본 생성자입니다. 기본 초기화와 달리 지역 `int`도 안전한 기본값을 만들 수 있고, `{}`와 `()`의 미묘한 차이(좁히기, 파싱)를 알아두면 실수를 줄일 수 있습니다.

값 초기화란?

값 초기화(Value Initialization) 는 빈 괄호 {} 또는 ()로 변수를 초기화하는 방법입니다. 기본 타입은 0으로 초기화되고, 클래스 타입은 기본 생성자가 호출됩니다.

int x{};     // 0
int* ptr{};  // nullptr
double d{};  // 0.0

왜 필요한가?: 기본 초기화는 쓰레기 값을 남길 수 있지만, 값 초기화는 안전한 기본값을 보장합니다.

// 기본 초기화: 쓰레기 값
int x;  // 예: 0x7fff5fbff5e0 (쓰레기)

// 값 초기화: 0
int y{};  // 0

다른 초기화와의 관계: 언제 “값 초기화”인가?

값 초기화는 문맥에 따라 다음처럼 나타납니다.

  • 빈 중괄호 T{}, T{ } — 리스트 초기화 문법이지만, 인자가 없으면 객체에 대해 값 초기화 규칙으로 이어집니다(리스트 초기화 참고).
  • 빈 괄호 T() — 임시 객체를 만들 때 값 초기화입니다. new T()도 마찬가지입니다.
  • T t; — 초기화자가 없으면 기본 초기화이지, 값 초기화가 아닙니다(지역 기본 타입은 쓰레기 가능).

즉, “0을 보장하고 싶다”면 기본 초기화가 아니라 값 초기화(또는 0 초기화가 포함된 경로) 를 선택해야 합니다.

{} vs (): 둘 다 값 초기화일 때

많은 경우 int()int{}는 같은 값 초기화 결과(정수 0)를 줍니다. 차이는 문법적 함정리스트 초기화 규칙에 있습니다.

항목(){}
값 초기화 (예: int)int() → 0int{} → 0
좁히기(narrowing)int(3.14)는 허용(잘림)int{3.14}는 일반적으로 컴파일 거부
가장 성가신 파싱T f();함수 선언일 수 있음T f{};는 객체 한 개
std::vectorvector<int>(10, 0)는 크기·값vector<int>{10, 0}요소 두 개
int a = int();   // 값 초기화, 0
int b{};         // 값 초기화, 0
// int c(3.14);  // 좁히기 허용 → 3
// int d{3.14};  // 에러: narrowing

// 가장 성가신 파싱 회피
Widget w1;       // 기본 초기화(Widget이 어떻게 정의됐는지에 따름)
Widget w2{};     // 값 초기화(기본 생성자)
// Widget w3();  // Widget w3() — 함수 선언!

auto p1 = new double;    // 기본 초기화(쓰레기 가능)
auto p2 = new double(); // 값 초기화 → 0.0
auto p3 = new double{};  // 값 초기화 → 0.0

실무 팁: “기본값 0·빈 객체”가 목적이면 지역 변수와 멤버에는 **{}**를 우선 고려하면, 기본 초기화로 인한 쓰레기 값을 줄이면서도 좁히기 실수를 컴파일 단계에서 잡을 수 있습니다.

클래스 vs 기본 타입에서의 값 초기화

  • 기본 타입 (int, char, 포인터 등): 값 초기화의 결과는 0 또는 nullptr 등으로 정해진 값입니다.
  • 클래스 타입 T: 기본 생성자가 호출됩니다. 이때 기본 생성자가 없거나 접근 불가하면 컴파일 오류입니다.
  • 집합체(aggregate) 인 클래스: 값 초기화 T{}는 종종 0 초기화 후 집합체 규칙과 맞물립니다(집합체 초기화, 0 초기화 참고).
struct Pod { int x; int y; };
Pod p{};  // x, y 모두 0에 가깝게(값/집합 규칙에 따라)

class NonDefault {
    NonDefault(int) {}
};
// NonDefault nd{};  // 기본 생성자 없음 → 오류

기본 타입

int x{};        // 0
double d{};     // 0.0
bool b{};       // false
char c{};       // '\0'
int* ptr{};     // nullptr

실전 예시

예시 1: 배열 초기화

int arr1[5];     // 쓰레기 값
int arr2[5]{};   // 모두 0

struct Point {
    int x, y;
};

Point points[3]{};  // 모두 {0, 0}

예시 2: 클래스

class Widget {
    int value;
    
public:
    Widget() : value{} {}  // value = 0
};

int main() {
    Widget w{};  // 기본 생성자 호출
}

예시 3: new 연산자

// 기본 초기화
int* p1 = new int;     // 쓰레기 값

// 값 초기화
int* p2 = new int();   // 0
int* p3 = new int{};   // 0

// 배열
int* arr1 = new int[5];    // 쓰레기 값
int* arr2 = new int[5]();  // 모두 0
int* arr3 = new int[5]{};  // 모두 0

예시 4: 멤버 초기화

class Data {
    int x{};      // 0
    double y{};   // 0.0
    int* ptr{};   // nullptr
    
public:
    Data() = default;
};

int main() {
    Data d;
    // d.x = 0, d.y = 0.0, d.ptr = nullptr
}

초기화 비교

// 기본 초기화
int x;        // 쓰레기 값

// 값 초기화
int y{};      // 0
int z = int(); // 0

// 직접 초기화
int w(10);    // 10

초기화 방법 정리:

초기화 방법문법결과 (기본 타입)결과 (클래스)
기본 초기화int x;쓰레기 값기본 생성자
값 초기화int x{};0기본 생성자
직접 초기화int x(10);10생성자 호출
복사 초기화int x = 10;10복사 생성자

실무 권장:

  • 안전성 우선: {} 사용 (값 초기화)
  • 명시적 값: = value 또는 (value) 사용
// ✅ 안전한 패턴
int counter{};  // 0으로 시작
int* ptr{};     // nullptr로 시작

// ✅ 명시적 값
int max_size = 100;
std::string name("Alice");

자주 발생하는 문제

문제 1: 쓰레기 값

// ❌ 초기화 안함
int x;
std::cout << x << std::endl;  // 쓰레기 값

// ✅ 값 초기화
int x{};
std::cout << x << std::endl;  // 0

문제 2: 배열

// ❌ 일부만 초기화
int arr[5] = {1, 2};  // {1, 2, 0, 0, 0}

// ✅ 모두 0
int arr[5]{};  // {0, 0, 0, 0, 0}

문제 3: 포인터

// ❌ 초기화 안함
int* ptr;
if (ptr) {}  // 정의되지 않은 동작

// ✅ nullptr
int* ptr{};
if (ptr) {}  // OK

문제 4: 클래스 멤버

// ❌ 초기화 안함
class Bad {
    int value;  // 쓰레기 값
};

// ✅ 값 초기화
class Good {
    int value{};  // 0
};

성능 고려사항

// 큰 배열
int arr[1000000]{};  // 모두 0으로 초기화 (시간 소요)

// 필요한 경우만 초기화
int arr[1000000];
for (int i = 0; i < 1000000; i++) {
    arr[i] = compute(i);
}

성능 분석:

#include <chrono>
#include <iostream>

using namespace std::chrono;

// 값 초기화 (모두 0)
auto start = steady_clock::now();
int arr1[10000000]{};
auto elapsed1 = steady_clock::now() - start;

// 기본 초기화 (쓰레기 값)
start = steady_clock::now();
int arr2[10000000];
auto elapsed2 = steady_clock::now() - start;

std::cout << "값 초기화: " << duration_cast<milliseconds>(elapsed1).count() << "ms\n";
std::cout << "기본 초기화: " << duration_cast<milliseconds>(elapsed2).count() << "ms\n";
// 값 초기화가 더 느림 (0으로 채우는 비용)

실무 권장:

  • 작은 배열/변수: 항상 값 초기화 (안전성)
  • 큰 배열: 즉시 덮어쓸 예정이면 기본 초기화 고려
  • 의심스러우면: 값 초기화 (안전성 > 성능)
// ✅ 작은 변수: 항상 초기화
int counter{};
double sum{};

// ✅ 큰 배열: 즉시 덮어쓰기
int buffer[1000000];
read_from_file(buffer, sizeof(buffer));  // 즉시 덮어씀

// ✅ 큰 배열: 부분 사용
int cache[1000000]{};  // 0으로 초기화 (안전)

실무 패턴

패턴 1: 안전한 카운터

class Counter {
    int count_{};  // 0으로 초기화
    
public:
    void increment() { ++count_; }
    int get() const { return count_; }
};

패턴 2: 옵셔널 포인터

class Resource {
    int* data_{};  // nullptr로 초기화
    
public:
    void allocate(int size) {
        data_ = new int[size]{};  // 배열도 0으로 초기화
    }
    
    ~Resource() {
        delete[] data_;  // nullptr 삭제는 안전
    }
};

패턴 3: 통계 구조체

struct Statistics {
    int count{};
    double sum{};
    double min{};
    double max{};
    
    void add(double value) {
        if (count == 0) {
            min = max = value;
        } else {
            if (value < min) min = value;
            if (value > max) max = value;
        }
        sum += value;
        ++count;
    }
    
    double average() const {
        return count > 0 ? sum / count : 0.0;
    }
};

FAQ

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

A: {} 또는 ()로 초기화하는 방법입니다. 기본 타입은 0, 포인터는 nullptr, 클래스는 기본 생성자가 호출됩니다.

Q2: 기본 초기화와 차이는?

A:

  • 기본 초기화 (int x;): 쓰레기 값 가능
  • 값 초기화 (int x{};): 0 또는 기본값

Q3: 성능 영향은?

A: 0 초기화 비용이 있습니다. 큰 배열은 주의하되, 안전성이 더 중요하면 값 초기화를 사용하세요.

Q4: 언제 사용해야 하나요?

A:

  • 안전한 초기화가 필요할 때
  • 0 또는 기본값으로 시작해야 할 때
  • 쓰레기 값을 방지하고 싶을 때

Q5: 클래스는 어떻게 되나요?

A: 기본 생성자가 호출됩니다. 기본 생성자가 없으면 컴파일 에러입니다.

class Widget {
public:
    Widget() { std::cout << "Default constructor\n"; }
};

Widget w{};  // "Default constructor" 출력

Q6: {} vs () 차이는?

A: 대부분 동일하지만, {}좁히기 변환(narrowing conversion) 을 방지합니다.

int x(3.14);  // OK: 3 (소수점 버려짐)
int y{3.14};  // 에러: narrowing conversion

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

A:

관련 글: Zero Initialization, Default Initialization, List Initialization.

한 줄 요약: 값 초기화는 {}로 변수를 안전하게 0 또는 기본값으로 초기화하는 방법입니다.


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

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

  • C++ Zero Initialization | “0 초기화” 가이드
  • C++ Default Initialization | “기본 초기화” 가이드
  • C++ List Initialization | “리스트 초기화” 가이드

관련 글

  • C++ Aggregate Initialization |
  • C++ Aggregate Initialization 완벽 가이드 | 집합 초기화
  • C++ call_once |
  • C++ Copy Initialization |
  • C++ default와 delete |