이 Step에서 다루는 것

  • 생성자/소멸자가 “자동으로” 언제 호출되는지(수명, 스코프)
  • 초기화가 왜 중요한지(디버그에서 보이는 쓰레기 값의 정체)
  • 기본 생성자/복사 생성자 자동 생성 규칙(언제 막히는지)
  • 반복문에서 객체를 어디에 생성하느냐에 따른 비용 차이

학습 목표

  • 생성자/소멸자가 호출되는 타이밍을 예시로 설명할 수 있다.
  • “멤버 초기화는 생성자 초기화 리스트가 정석”임을 알고 적용할 수 있다.
  • 기본 생성자 자동 생성 규칙 때문에 Knight k;가 왜 막히는지 설명할 수 있다.

생성자 (Constructor)

  • 객체가 생성될 때 무조건 호출되는 함수.
  • 이름 = 클래스명, 리턴 타입 없음.
  • Knight() { } → 기본 생성자.
  • 초기화 담당. 초기화하지 않으면(특히 기본 타입 멤버) “쓰레기 값”이 들어갈 수 있음.
class Knight {
public:
    Knight() : _hp(0), _attack(0), _defence(0) {}

private:
    int _hp;
    int _attack;
    int _defence;
};

핵심 포인트:

  • 멤버 변수는 “대입”이 아니라 초기화가 기본입니다.
    그래서 생성자 본문에서 _hp = 0;처럼 넣기보다, 위처럼 초기화 리스트로 초기화하는 습관이 좋습니다.

(실전 팁) 인클래스 멤버 초기화

class Knight {
public:
    Knight() = default; // 기본 생성자

private:
    int _hp = 0;
    int _attack = 0;
    int _defence = 0;
};

이 방식은 “기본값”을 한 곳에 모아두기 좋고, 생성자 오버로딩이 많아질수록 유리합니다.


소멸자 (Destructor)

  • 객체가 소멸될 때 호출되는 함수.
  • ~Knight() { } → 이름 앞에 ~.
  • 정리 작업 (메모리 해제, 로그 등).
~Knight() {
    std::cout << "~Knight()" << '\n';
}

주의:

  • 소멸자는 “반드시 호출되는 정리 단계”라서, 여기서 예외/실패가 나면 디버깅이 어려워집니다.
  • 학습 단계에서는 “호출 타이밍 확인용 로그” 정도로 이해하면 충분합니다.

기타 생성자 (오버로딩)

  • Knight(int hp, int attack, int defence) → 인자를 받는 생성자.
  • 역시 초기화 리스트로 멤버를 초기화하는 게 정석.
Knight(int hp, int attack, int defence)
    : _hp(hp), _attack(attack), _defence(defence)
{
}

객체 생성 방식들

Knight k1(100, 10, 1);         // 직접 초기화
Knight k2 = Knight(100, 10, 1); // 복사 초기화
Knight k3{100, 10, 1};         // 유니폼 초기화 (C++11 이상)

기본 생성자 자동 생성 중단

  • 다른 생성자를 하나라도 직접 만들면, 컴파일러는 기본 생성자를 자동으로 만들어주지 않습니다.
  • 그래서 인자 생성자만 있는 상태에서 Knight k1;처럼 “기본 생성”을 하려고 하면 막힙니다.

📌 오류 C2512: 적절한 기본 생성자가 없습니다.

  • 해결:
    • 기본 생성자를 직접 제공: Knight() { ... }
    • 또는 기본 생성자 요청: Knight() = default;

복사 생성자

  • 동일 타입 객체를 복사해 새 객체 생성 시 호출.
  • Knight(const Knight& other) 형태로 받는 게 기본.
  • Knight k2 = k1; → 복사 생성자 호출.
  • 매개변수는 const 참조로 받아야 원본 수정 방지 + 성능 향상.
Knight(const Knight& other) {
    this->_hp = other._hp;
    this->_attack = other._attack;
    this->_defence = other._defence;
}

(실전 감각) Rule of 0 / 3 / 5

  • 단순히 int 같은 값만 들고 있다면, 보통은 복사 생성자/소멸자를 직접 만들 필요가 없습니다.
    (컴파일러 기본 동작이 충분 → Rule of 0)
  • 반대로 “직접 자원을 관리(new/delete, 파일 핸들 등)”한다면,
    복사/대입/소멸(그리고 이동까지) 규칙을 함께 고려해야 합니다. (Rule of 3/5)

초기화하지 않은 객체의 상태

  • 디버그 환경(MSVC 등)에서 초기화하지 않은 메모리가 0xCCCCCCCC 같은 패턴으로 보이는 경우가 있습니다.
    • 예: _hp = -858993460 (0xCCCCCCCC)
  • 중요한 건 “값이 CCCC냐 아니냐”가 아니라,
    초기화하지 않은 값은 의미가 없고, 상황에 따라 달라질 수 있다는 점입니다.

객체 생성 위치와 생성자/소멸자 호출

  • for문 안에서 객체 생성: 매 반복마다 생성자 → 소멸자 호출 (루프 한 번 끝날 때마다 소멸)
  • for문 밖에서 객체 생성 후 반복 사용: 생성자 1번, 소멸자 1번 (범위 끝날 때)
  • int 같은 기본 타입은 차이 없지만, 클래스 타입은 생성·소멸 비용이 있어 성능 차이 가능.

객체 생성 위치별 생성/소멸 호출

┌─────────────────────────────────────────────────────────────────────────────┐
│ for (int i = 0; i < 10; i++) {        │  Knight k;                           │
│     Knight k;  // 매 반복마다           │  for (int i = 0; i < 10; i++) {     │
│     // ...                             │      // k 사용                       │
│ }  // k 범위 끝 → 소멸자                │  }  // k 범위 끝 → 소멸자 1번         │
├─────────────────────────────────────────────────────────────────────────────┤
│ 생성자 → 소멸자 (10번 반복)             │ 생성자 1번 → 소멸자 1번              │
│ │                                     │ │                                    │
│ ├─ i=0: Knight() → ~Knight()          │ ├─ Knight() 1번                       │
│ ├─ i=1: Knight() → ~Knight()          │ ├─ 루프 10번 (k 재사용)               │
│ ├─ ...                                │ └─ ~Knight() 1번                     │
│ └─ i=9: Knight() → ~Knight()          │                                      │
│ (클래스 타입은 이 방식이 비효율)        │ (동일 용도면 이 방식이 효율)           │
└─────────────────────────────────────────────────────────────────────────────┘
// case 1: 매 반복마다 생성/소멸
for (int i = 0; i < 10; i++) {
    Knight k;  // 생성자 → 소멸자 (반복)
}

// case 2: 한 번만 생성/소멸
Knight k;
for (int i = 0; i < 10; i++) {
    // k 사용
}  // 여기서 소멸자 1번

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

  • 생성자 본문 대입과 초기화 리스트 초기화의 차이는 무엇일까?
  • 인자 생성자만 만들어 둔 클래스에서 Knight k;가 막히는 이유는?
  • 루프 안에서 객체를 만들면 어떤 비용이 반복될까?

profile
李家네_공부방

0개의 댓글