초기화 리스트

Jaemyeong Lee·2024년 12월 6일

게임 서버1

목록 보기
43/220

이 Step에서 다루는 것

  • 생성자 : 뒤에 붙는 멤버 이니셜라이저 리스트(초기화 리스트)의 의미
  • “생성자 본문에서 대입” vs “초기화 리스트에서 생성”의 차이(성능/정확성)
  • 생성/초기화 순서 규칙: (중요) “리스트 순서”가 아니라 선언 순서
  • const 멤버 / 참조 멤버가 왜 초기화 리스트를 강제하는지
  • 생성자 위임(Delegating constructor)을 쓰는 이유와 주의점

학습 목표

  • “초기화는 본문 전에 끝난다”를 설명할 수 있다.
  • 멤버 초기화 순서가 “초기화 리스트 순서”가 아니라 “멤버 선언 순서”임을 설명할 수 있다.
  • const/참조 멤버가 왜 본문에서 대입이 불가능한지 설명할 수 있다.

초기화 리스트는 “본문 실행 전 초기화 영역”

생성자에는 크게 두 구간이 있습니다.

  • 초기화 리스트: : 뒤에 오는 부분
    • 멤버/부모(기반) 클래스의 “초기화(생성)”가 여기서 먼저 일어남
  • 생성자 본문: { ... }
    • 이미 초기화된 멤버를 가지고 로직을 수행하는 구간
class Circle {
public:
    Circle() : _color(1) // 여기서 _color가 먼저 초기화됨
    {
        // 여기서는 이미 _color가 1인 상태
    }

private:
    int _color;
};

생성·소멸 / 초기화 순서 (가장 자주 틀리는 규칙)

규칙은 3개로 끝납니다.

  • 상속(기반 클래스): 부모 생성자 → 자식 생성자
  • 멤버 초기화: 선언 순서대로 초기화됨 (초기화 리스트에 적은 순서가 아님)
  • 소멸: 자식 소멸자 → 부모 소멸자 (생성의 역순)

생성·소멸 순서 흐름도

┌─────────────────────────────────────────────────────────────────────────────┐
│ Knight k1;  (Knight : public Player, 멤버: Inventory _inventory)              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                               │
│  [생성 순서]                                                                  │
│     ① Player()     ← 기반(부모) 먼저                                          │
│           │                                                                   │
│     ② Inventory()  ← 멤버(선언 순서대로)                                     │
│           │                                                                   │
│     ③ Knight()     ← 자식 생성자 본문                                        │
│                                                                               │
│  [소멸 순서] (생성의 역순)                                                    │
│     ① ~Knight()    ← 자식 먼저                                                │
│           │                                                                   │
│     ② ~Inventory() ← 멤버                                                    │
│           │                                                                   │
│     ③ ~Player()    ← 기반(부모) 마지막                                        │
│                                                                               │
└─────────────────────────────────────────────────────────────────────────────┘

자주 하는 착각:

  • “초기화 리스트에 적은 순서대로 초기화된다” → 아님. 선언 순서가 우선입니다.

흔한 실수: “리스트 순서대로 될 줄 알았다”

초기화 리스트는 “보기 좋은 순서”로 적을 수 있지만, 실제 초기화는 멤버 선언 순서대로 진행됩니다.

class Foo {
public:
    Foo() : _b(2), _a(1) {} // 이렇게 적어도...

private:
    int _a; // _a가 먼저 초기화됨
    int _b; // 그 다음 _b
};

컴파일러가 경고를 주는 경우도 있으니(예: reorder 관련 경고), “선언 순서 = 초기화 순서”를 기준으로 코드를 읽는 습관을 들이는 게 안전합니다.


생성자 본문 대입 vs 초기화 리스트 (출력으로 증명)

핵심 차이:

  • 본문에서 대입: “이미 한 번 만들어진 객체”에 대입
  • 초기화 리스트: 애초에 원하는 생성자로 한 번만 생성
#include <iostream>

class Inventory {
public:
    Inventory() { std::cout << "Inventory()\n"; }
    explicit Inventory(int gold) : _gold(gold) { std::cout << "Inventory(int)\n"; }

private:
    int _gold = 0;
};

class KnightA {
public:
    KnightA() // 비추천: 본문에서 대입
    {
        _inventory = Inventory(100);
    }

private:
    Inventory _inventory;
};

class KnightB {
public:
    KnightB() : _inventory(100) // 추천: 초기화 리스트
    {
    }

private:
    Inventory _inventory;
};

예상되는 출력 감각:

  • KnightA:
    • Inventory() (기본 생성) → Inventory(int) (임시 생성) → 대입
  • KnightB:
    • Inventory(int) 한 번만 생성

추가로 중요한 포인트:

  • 멤버 타입이 대입이 불가능한 타입(예: const 멤버를 가진 타입, 참조 멤버를 가진 타입 등)이라면, 본문에서 _inventory = ... 자체가 컴파일 불가가 될 수 있습니다.
    이런 경우에도 초기화 리스트가 정답입니다.

생성자 내부 vs 초기화 리스트 비교

┌─────────────────────────────────────────────────────────────────────────────┐
│ 비효율 (본문에서 대입)                    효율 (초기화 리스트)                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                               │
│  KnightA() {                                  KnightB()                       │
│      _inventory = Inventory(100);  // ①      : _inventory(100)  // ①          │
│  }                                             // 한 번만 생성                │
│                                                { }                            │
│                                                                               │
│  ① Inventory() 기본 생성                                                     │
│  ② Inventory(100) 임시 객체 생성                                             │
│  ③ 대입으로 덮어쓰기                                                         │
│                                                                               │
└─────────────────────────────────────────────────────────────────────────────┘

참조형 · const 멤버는 초기화 리스트가 “필수”

  • 참조 멤버(T&): 생성 시점에 “어디를 참조할지”가 확정되어야 함
  • const 멤버(const T): 생성 시점에 값이 확정되어야 함

그래서 둘 다 초기화 리스트에서만 초기화 가능하고, 생성자 본문에서 대입하면 컴파일 에러가 납니다.

class Circle {
public:
    Circle() : _color(1), _colorRef(_color), _colorConst(_color) {}

private:
    int _color;
    int& _colorRef;
    const int _colorConst;
};

주의:

  • 참조 멤버는 “가리키는 대상의 수명”이 클래스 수명보다 짧아지면 위험합니다.

생성자 위임 (Delegating constructor)

#include <iostream>

class Circle {
public:
    Circle() : Circle(3) // 다른 생성자에게 초기화 책임을 위임
    {
        std::cout << "Circle()\n";
    }

    explicit Circle(int a)
    {
        std::cout << "Circle(" << a << ")\n";
    }
};

생성자 위임을 쓰는 이유:

  • 초기화 로직을 한 곳으로 모아 중복을 줄이고, 실수를 줄이기 위해서

체크 질문 (스스로 답해보기)

  • 멤버 초기화 순서는 “초기화 리스트에 적은 순서”일까, “멤버 선언 순서”일까?
  • const 멤버/참조 멤버가 생성자 본문에서 대입이 안 되는 이유는?
  • 멤버 객체를 본문에서 대입하면 “생성이 몇 번” 일어날 수 있을까?

profile
李家네_공부방

0개의 댓글