본문 바로가기

생성자와 소멸자

생성자 선택과 멤버 초기화 순서, 소멸 경계, 직접 소유 자원의 Rule of Zero·Three·Five를 이해합니다.

클래스 객체를 초기화하는 생성자(Constructor)와 객체 수명의 끝에서 정리를 수행하는 소멸자(Destructor)를 다룹니다.

자동·정적·스레드 저장 기간 객체와 임시 객체는 각 수명 경계에서 소멸자가 호출됩니다. 반면 new로 만든 동적 객체는 대응하는 delete 표현식이나 RAII 소유자가 수명을 끝내야 소멸자가 호출되고 저장 공간도 해제됩니다.

C++ 객체 생성 순서와 수명 경계

가상 기반→직접 기반→멤버→본문→사용→소멸의 실제 수명 순서이며 생성 실패는 완료된 하위 객체만 역순 정리한다. 기본 후보 존재 검사는 별도 언어 계약으로 남긴다.

사용자 선언 생성자 또는 생성자 템플릿 유무로 기본 생성자 경로를 판정하고, 가상 기반 클래스, 직접 기반 클래스, 비정적 데이터 멤버, 생성자 본문 순서로 초기화한다. 성공하면 불변식을 지키며 사용하고 소멸자 호출로 완전 객체 수명을 끝내며, 생성 중 예외가 나면 완전 객체 소멸자 없이 이미 생성된 하위 객체만 역순 정리한다.

C++ · lifetime · flowchart

객체는 생성자 선택부터 소멸까지 하나의 수명 계약을 따른다

초기화 목록에 적은 순서가 실제 순서를 바꾸지는 않습니다. 기본 생성자 경로를 먼저 판정하고, 하위 객체의 정해진 순서와 생성자 본문을 모두 마쳐야 불변식을 전제로 객체를 사용할 수 있습니다.

기본 생성자 선택과 정해진 초기화 및 소멸 경계원desktop9노드9간선의사용자선언유무두경로가가상기반클래스에서합류하고직접기반→멤버→본문→사용→소멸경계순서다.암시적선언후삭제/호출가능성확인이필요하다.생성중예외롤백은원caption별도범위이며완전객체소멸자를호출한것처럼화살표를그리지않는다.모바일에서도기본후보경로는직접제공후보를건너뛴다.선언한 생성자 없음?생성자 템플릿도 없음기본 생성자 후보암시적 선언·삭제 확인호출할 생성자 필요직접 정의/default/delete가상 기반 클래스최종 파생 객체DFS 왼쪽→오른쪽직접 기반 클래스기반 목록 선언 순서비정적 데이터 멤버멤버 선언 순서생성자 본문하위 객체 초기화 뒤사용할 객체불변식 전제소멸자 호출 시작본문 뒤 멤버·기반완료한 생성의 역순예아니요생성 중 예외: 완전 객체 소멸 없음완료한 하위 객체만 역순 정리
C++ 객체 생성 순서와 수명 경계 사용자 선언 생성자 또는 생성자 템플릿이 없으면 암시적으로 선언된 기본 생성자 후보를 사용하고, 있으면 호출 가능한 생성자가 필요하다. 두 경로는 가장 파생된 객체의 가상 기반 클래스, 직접 기반 클래스의 선언 순서, 비정적 데이터 멤버의 선언 순서, 생성자 본문으로 합쳐진다. 본문이 불변식을 세우면 객체를 사용한다. 소멸자 호출이 시작되면 완전 객체 수명이 끝나지만 본문은 아직 살아 있는 하위 객체를 사용할 수 있고, 이후 멤버와 기반 클래스가 생성의 역순으로 파괴된다. 생성 중 예외가 나면 완전 객체 소멸자는 호출되지 않고 생성 완료된 하위 객체만 역순 정리된다. DEFAULT GATE 사용자 선언 생성자· 생성자 템플릿이 없는가? IMPLICITLY DECLARED 기본 생성자 후보 암시적 선언 · 삭제 여부 확인 USER-DECLARED PATH 호출할 생성자 필요 = default / = delete / 직접 정의 01 · VIRTUAL BASES 가상 기반 클래스 최종 파생 객체 · DFS 좌→우 02 · DIRECT BASES 직접 기반 클래스 기반 클래스 목록 선언 순서 03 · DATA MEMBERS 비정적 데이터 멤버 클래스 선언 순서 초기화 목록 순서 무관 04 · BODY 생성자 본문 하위 객체 초기화 뒤 실행 불변식을 세울 책임 VALID OBJECT 사용 가능한 객체 멤버 함수는 불변식 전제 DESTRUCTION 소멸자 호출 시작 객체 수명 종료 본문 뒤 멤버·기반 역순 정리 YESNO

01 · default gate

사용자 선언 생성자와 생성자 템플릿이 모두 없는가?

그렇다면 매개변수 없는 기본 생성자가 암시적으로 선언되고 defaulted 후보가 됩니다. 하나라도 있다면 호출 가능한 생성자를 직접 제공해야 하며, = default와 = delete도 사용자 선언입니다.

02 · fixed initialization order

초기화 목록의 표기 순서가 실제 순서를 바꾸지 않는다

① 가상 기반 클래스(최종 파생 객체에서 DFS 좌→우), ② 직접 기반 클래스(기반 클래스 목록의 선언 순서), ③ 비정적 데이터 멤버(클래스 선언 순서), ④ 생성자 본문 순서입니다.

03 · invariant and use

생성자 본문을 마친 뒤에야 불변식을 전제로 사용한다

기반 클래스와 멤버가 모두 초기화된 뒤 생성자 본문이 실행됩니다. 본문이 검증을 마치면 객체의 수명이 시작된 상태에서 멤버 함수가 그 불변식을 전제로 동작합니다.

04 · destruction boundary

소멸자 호출이 시작되면 객체 수명이 끝난다

완전 객체의 형식적 수명은 소멸자 호출과 함께 끝나지만, 소멸자 본문은 특별 규칙에 따라 아직 살아 있는 멤버와 기반 클래스 하위 객체를 사용할 수 있습니다. 본문 뒤에는 각 하위 객체의 수명이 생성 완료의 역순으로 끝납니다.

05 · construction rollback

생성 중 예외가 나면 완전 객체의 소멸자는 호출되지 않는다

이미 생성을 마친 기반 클래스와 멤버 하위 객체만 생성 완료의 역순으로 파괴됩니다. 아직 생성되지 않은 하위 객체와 완전 객체의 소멸자는 호출 대상이 아닙니다.

자동·정적·스레드 저장 기간 객체와 임시 객체는 각 수명 경계에서 소멸자가 호출됩니다. new로 만든 객체는 delete 표현식이나 RAII 소유자가 그 경계를 만들어야 합니다. 생성 중 예외가 나면 완전 객체의 소멸자는 호출되지 않고, 생성 완료된 하위 객체만 역순으로 롤백됩니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명원래 가로 그림의 9개 노드와 9개 간선에서 사용자 선언 유무에 따른 두 경로가 가상 기반 클래스에서 합류하고 직접 기반→멤버→본문→사용→소멸 경계 순서로 이어진다. 암시적 선언 후 삭제/호출 가능성 확인이 필요하다. 생성 중 예외 롤백은 원래 캡션의 별도 범위이며 완전 객체의 소멸자를 호출한 것처럼 화살표를 그리지 않는다. 모바일에서도 기본 후보 경로는 직접 제공 후보를 건너뛴다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
하위 객체 구성

가상 기반·직접 기반·멤버 순으로 구성한다.

  • 순서: 가상 기반→직접 기반→멤버
  • 완료 조건: 각 초기화 성공
  • 초기화 목록에 쓴 순서가 실제 멤버 선언 순서를 바꾸지 않는다.
본문과 사용

전체 하위 객체가 완성된 뒤 생성자 본문을 수행한다.

  • 현재: 완성 객체 사용
  • 기반·멤버: 구성 완료
  • 생성 실패는 완성된 객체의 사용 단계로 진행하지 않는다.
역순 정리

정상 경로에서는 소멸하고, 생성 실패 경로에서는 완성된 하위 객체만 정리한다.

  • 정상: 완성된 구성의 역순
  • 실패: 완료된 하위 객체만 역순
  • 생성에 실패한 전체 객체의 소멸자가 실행되었다고 표시하지 않는다.

C++ 객체 생성 순서와 수명 경계

가상 기반→직접 기반→멤버→본문→사용→소멸의 실제 수명 순서이며 생성 실패는 완료된 하위 객체만 역순 정리한다. 기본 후보 존재 검사는 별도 언어 계약으로 남긴다.

사용자 선언 생성자 또는 생성자 템플릿 유무로 기본 생성자 경로를 판정하고, 가상 기반 클래스, 직접 기반 클래스, 비정적 데이터 멤버, 생성자 본문 순서로 초기화한다. 성공하면 불변식을 지키며 사용하고 소멸자 호출로 완전 객체 수명을 끝내며, 생성 중 예외가 나면 완전 객체 소멸자 없이 이미 생성된 하위 객체만 역순 정리한다.

C++ · lifetime · flowchart

객체는 생성자 선택부터 소멸까지 하나의 수명 계약을 따른다

초기화 목록에 적은 순서가 실제 순서를 바꾸지는 않습니다. 기본 생성자 경로를 먼저 판정하고, 하위 객체의 정해진 순서와 생성자 본문을 모두 마쳐야 불변식을 전제로 객체를 사용할 수 있습니다.

기본 생성자 선택과 정해진 초기화 및 소멸 경계원desktop9노드9간선의사용자선언유무두경로가가상기반클래스에서합류하고직접기반→멤버→본문→사용→소멸경계순서다.암시적선언후삭제/호출가능성확인이필요하다.생성중예외롤백은원caption별도범위이며완전객체소멸자를호출한것처럼화살표를그리지않는다.모바일에서도기본후보경로는직접제공후보를건너뛴다.선언한 생성자 없음?생성자 템플릿도 없음기본 생성자 후보암시적 선언·삭제 확인호출할 생성자 필요직접 정의/default/delete가상 기반 클래스최종 파생 객체DFS 왼쪽→오른쪽직접 기반 클래스기반 목록 선언 순서비정적 데이터 멤버멤버 선언 순서생성자 본문하위 객체 초기화 뒤사용할 객체불변식 전제소멸자 호출 시작본문 뒤 멤버·기반완료한 생성의 역순예아니요생성 중 예외: 완전 객체 소멸 없음완료한 하위 객체만 역순 정리
C++ 객체 생성 순서와 수명 경계 사용자 선언 생성자 또는 생성자 템플릿이 없으면 암시적으로 선언된 기본 생성자 후보를 사용하고, 있으면 호출 가능한 생성자가 필요하다. 두 경로는 가장 파생된 객체의 가상 기반 클래스, 직접 기반 클래스의 선언 순서, 비정적 데이터 멤버의 선언 순서, 생성자 본문으로 합쳐진다. 본문이 불변식을 세우면 객체를 사용한다. 소멸자 호출이 시작되면 완전 객체 수명이 끝나지만 본문은 아직 살아 있는 하위 객체를 사용할 수 있고, 이후 멤버와 기반 클래스가 생성의 역순으로 파괴된다. 생성 중 예외가 나면 완전 객체 소멸자는 호출되지 않고 생성 완료된 하위 객체만 역순 정리된다. DEFAULT GATE 사용자 선언 생성자· 생성자 템플릿이 없는가? IMPLICITLY DECLARED 기본 생성자 후보 암시적 선언 · 삭제 여부 확인 USER-DECLARED PATH 호출할 생성자 필요 = default / = delete / 직접 정의 01 · VIRTUAL BASES 가상 기반 클래스 최종 파생 객체 · DFS 좌→우 02 · DIRECT BASES 직접 기반 클래스 기반 클래스 목록 선언 순서 03 · DATA MEMBERS 비정적 데이터 멤버 클래스 선언 순서 초기화 목록 순서 무관 04 · BODY 생성자 본문 하위 객체 초기화 뒤 실행 불변식을 세울 책임 VALID OBJECT 사용 가능한 객체 멤버 함수는 불변식 전제 DESTRUCTION 소멸자 호출 시작 객체 수명 종료 본문 뒤 멤버·기반 역순 정리 YESNO

01 · default gate

사용자 선언 생성자와 생성자 템플릿이 모두 없는가?

그렇다면 매개변수 없는 기본 생성자가 암시적으로 선언되고 defaulted 후보가 됩니다. 하나라도 있다면 호출 가능한 생성자를 직접 제공해야 하며, = default와 = delete도 사용자 선언입니다.

02 · fixed initialization order

초기화 목록의 표기 순서가 실제 순서를 바꾸지 않는다

① 가상 기반 클래스(최종 파생 객체에서 DFS 좌→우), ② 직접 기반 클래스(기반 클래스 목록의 선언 순서), ③ 비정적 데이터 멤버(클래스 선언 순서), ④ 생성자 본문 순서입니다.

03 · invariant and use

생성자 본문을 마친 뒤에야 불변식을 전제로 사용한다

기반 클래스와 멤버가 모두 초기화된 뒤 생성자 본문이 실행됩니다. 본문이 검증을 마치면 객체의 수명이 시작된 상태에서 멤버 함수가 그 불변식을 전제로 동작합니다.

04 · destruction boundary

소멸자 호출이 시작되면 객체 수명이 끝난다

완전 객체의 형식적 수명은 소멸자 호출과 함께 끝나지만, 소멸자 본문은 특별 규칙에 따라 아직 살아 있는 멤버와 기반 클래스 하위 객체를 사용할 수 있습니다. 본문 뒤에는 각 하위 객체의 수명이 생성 완료의 역순으로 끝납니다.

05 · construction rollback

생성 중 예외가 나면 완전 객체의 소멸자는 호출되지 않는다

이미 생성을 마친 기반 클래스와 멤버 하위 객체만 생성 완료의 역순으로 파괴됩니다. 아직 생성되지 않은 하위 객체와 완전 객체의 소멸자는 호출 대상이 아닙니다.

자동·정적·스레드 저장 기간 객체와 임시 객체는 각 수명 경계에서 소멸자가 호출됩니다. new로 만든 객체는 delete 표현식이나 RAII 소유자가 그 경계를 만들어야 합니다. 생성 중 예외가 나면 완전 객체의 소멸자는 호출되지 않고, 생성 완료된 하위 객체만 역순으로 롤백됩니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명원래 가로 그림의 9개 노드와 9개 간선에서 사용자 선언 유무에 따른 두 경로가 가상 기반 클래스에서 합류하고 직접 기반→멤버→본문→사용→소멸 경계 순서로 이어진다. 암시적 선언 후 삭제/호출 가능성 확인이 필요하다. 생성 중 예외 롤백은 원래 캡션의 별도 범위이며 완전 객체의 소멸자를 호출한 것처럼 화살표를 그리지 않는다. 모바일에서도 기본 후보 경로는 직접 제공 후보를 건너뛴다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
하위 객체 구성

가상 기반·직접 기반·멤버 순으로 구성한다.

  • 순서: 가상 기반→직접 기반→멤버
  • 완료 조건: 각 초기화 성공
  • 초기화 목록에 쓴 순서가 실제 멤버 선언 순서를 바꾸지 않는다.
본문과 사용

전체 하위 객체가 완성된 뒤 생성자 본문을 수행한다.

  • 현재: 완성 객체 사용
  • 기반·멤버: 구성 완료
  • 생성 실패는 완성된 객체의 사용 단계로 진행하지 않는다.
역순 정리

정상 경로에서는 소멸하고, 생성 실패 경로에서는 완성된 하위 객체만 정리한다.

  • 정상: 완성된 구성의 역순
  • 실패: 완료된 하위 객체만 역순
  • 생성에 실패한 전체 객체의 소멸자가 실행되었다고 표시하지 않는다.

생성자 (Constructor)

생성자는 클래스의 객체가 생성될 때 자동으로 호출되는 특별한 멤버 함수입니다.

생성자의 주된 목적은 객체가 올바른 초기 상태를 갖도록 멤버 변수들을 초기화하는 것입니다.

생성자의 특징
  • 선언 문법에서 클래스 이름을 사용합니다. 표준 용어로 생성자는 일반 함수처럼 조회되는 이름을 갖지 않습니다.
  • 반환 타입이 없습니다. (심지어 void도 사용하지 않습니다.)
  • public 접근 지정자로 선언하는 것이 일반적입니다. (외부에서 객체를 생성할 수 있도록)
  • 오버로딩(Overloading)이 가능하여 여러 개의 생성자를 가질 수 있습니다. (매개변수 리스트가 다르면 됨)
생성자 선언 및 정의 형식
#include <string>

// 클래스 선언 (헤더 파일)
class MyClass {
private:
    int number{0};
    std::string text;

public:
    MyClass(); // 1. 기본 생성자 (매개변수가 없는 생성자)
    MyClass(int a, std::string b); // 2. 매개변수가 있는 생성자 (오버로딩)
    // ... 다른 멤버들 ...
};

// 생성자 정의 (소스 파일)
MyClass::MyClass() { // 1. 기본 생성자 정의
    // 멤버 변수 초기화
}

MyClass::MyClass(int a, std::string b) // 2. 매개변수가 있는 생성자 정의
    : number(a), text(b) {}
Book 클래스 생성자 예시
#include <iostream>
#include <string>

class Book {
private:
    std::string title;
    std::string author;
    int year;

public:
    // 1. 기본 생성자 (Default Constructor)
    // 매개변수가 없으며, 객체 생성 시 초기값을 지정하지 않을 때 호출됩니다.
    Book() : title("제목 없음"), author("작가 미상"), year(0) {
        std::cout << "기본 생성자 호출: 책 정보가 초기화되었습니다.\n";
    }

    // 2. 매개변수가 있는 생성자 (Parameterized Constructor)
    // 객체 생성 시 초기값을 전달받아 멤버 변수를 초기화합니다.
    Book(std::string t, std::string a, int y) : title(t), author(a), year(y) {
        std::cout << "매개변수 생성자 호출: " << title << " 책이 생성되었습니다.\n";
    }

    void displayBookInfo() {
        std::cout << "제목: " << title << ", 저자: " << author << ", 출판년도: " << year << std::endl;
    }
};

int main() {
    // 1. 기본 생성자 호출하여 객체 생성
    Book book1; // Book() 생성자 호출
    book1.displayBookInfo(); // 출력: 제목: 제목 없음, 저자: 작가 미상, 출판년도: 0

    std::cout << "--------------------------------\n";

    // 2. 매개변수 생성자 호출하여 객체 생성
    Book book2("어린 왕자", "앙투안 드 생텍쥐페리", 1943); // Book("...", "...", ...) 생성자 호출
    book2.displayBookInfo(); // 출력: 제목: 어린 왕자, 저자: 앙투안 드 생텍쥐페리, 출판년도: 1943

    std::cout << "--------------------------------\n";

    // 힙에 동적 할당 시에도 생성자 호출
    Book* book3 = new Book("구름빵", "백희나", 2004); // Book("...", "...", ...) 생성자 호출
    book3->displayBookInfo(); // 출력: 제목: 구름빵, 저자: 백희나, 출판년도: 2004
    delete book3; // 힙 객체 해제 (이때 소멸자 호출)

    return 0;
}

Book(std::string t, ...) { title = t; ... }처럼 생성자 본문에서 대입하는 방식은 비교를 위한 비권장 패턴입니다. 이 방식은 title을 먼저 기본 생성한 뒤 다시 대입합니다. 위 예시처럼 멤버 초기화 리스트를 사용하면 멤버가 생성되는 순간 원하는 값으로 직접 초기화됩니다.

new로 만든 book3은 delete book3가 수명을 끝내면서 소멸자를 호출합니다. delete를 생략하면 프로그램이 포인터를 잃은 뒤에는 그 동적 객체의 소멸자와 저장 공간 해제가 자동으로 따라오지 않습니다.

멤버 초기화 리스트

생성자에서 멤버 변수를 초기화하는 또 다른 방법이자 권장되는 방법은 멤버 초기화 리스트를 사용하는 것입니다.

멤버 초기화 리스트 형식
class MyClass {
private:
    int member;

public:
    explicit MyClass(int value);
};

MyClass::MyClass(int value) : member(value) {}
  • 생성자의 매개변수 리스트 뒤에 콜론(:)을 붙이고, 멤버 변수 이름과 괄호 안에 초기화할 값을 지정합니다.
  • 실제 초기화 순서는 목록에 적은 순서가 아닙니다. 가장 파생된 객체의 가상 기반 클래스, 직접 기반 클래스의 선언 순서, 비정적 데이터 멤버의 선언 순서, 생성자 본문 순서입니다.
  • 장점
    • 효율성: 멤버가 생성되는 시점에 바로 초기화되므로, 기본 초기화 후 다시 대입하는 과정을 피할 수 있습니다. 생성자 인자로 값을 받는 const 멤버와 참조자(reference) 멤버는 본문 대입이 아니라 멤버 초기화 리스트에서 초기화해야 합니다. 유효한 기본 멤버 초깃값이 있다면 그 값을 사용할 수도 있습니다.
    • 명확성: 어떤 멤버 변수가 어떤 값으로 초기화되는지 한눈에 파악하기 쉽습니다.
멤버 초기화 리스트 사용 예시
#include <iostream>
#include <string>

class Student {
private:
    std::string name;
    int id;
    const double GPA; // const 멤버 변수는 반드시 초기화 리스트에서 초기화해야 함

public:
    // 멤버 초기화 리스트를 사용한 생성자
    Student(std::string n, int i, double g) : name(n), id(i), GPA(g) {
        std::cout << "Student 객체 생성: " << name << std::endl;
    }

    void displayInfo() {
        std::cout << "이름: " << name << ", 학번: " << id << ", GPA: " << GPA << std::endl;
    }
};

int main() {
    Student s1("김민수", 2023001, 3.85);
    s1.displayInfo();

    // GPA를 목록에서 빠뜨린 별도 생성자를 선언하면 const 멤버를 초기화하지 못해 오류입니다.
    return 0;
}

이제부터는 특별한 이유가 없는 한 멤버 변수를 초기화할 때 멤버 초기화 리스트를 사용하는 것을 강력히 권장합니다.

예시

기반 클래스 없이도 멤버의 선언 순서와 정리 역순을 관찰할 수 있습니다. 아래는 원 Book·Student 예제와 별개의 완전한 프로그램입니다. 초기화 목록을 일부러 뒤집어 적었으므로 GCC의 순서 경고도 읽은 뒤, 실행 로그를 예상해 보세요.

멤버 생성과 소멸의 순서를 관찰하기
#include <iostream>

struct Trace {
    const char* name;
    explicit Trace(const char* n) : name(n) { std::cout << "+" << name << "\n"; }
    ~Trace() { std::cout << "-" << name << "\n"; }
};

class Order {
    Trace first;
    Trace second;
public:
    Order() : second("second"), first("first") { std::cout << "body\n"; }
    ~Order() { std::cout << "~Order\n"; }
};

int main() {
    { Order value; std::cout << "use\n"; }
    std::cout << "after\n";
}

출력은 +first, +second, body, use, ~Order, -second, -first, after의 여덟 줄입니다. 생성자 본문 전에 두 멤버가 생성되고, 소멸자 본문 뒤에 멤버가 역순으로 정리됩니다. 정상 초기화·정상 블록 종료의 관찰이며 생성 중 예외 경로는 별도의 규칙입니다. 실제 코드에서는 경고를 없애도록 초기화 목록도 first·second의 선언 순서로 맞춥니다.

선언 한 곳만 바꾸기

멤버 선언을 second 다음 first 순서로 바꾸고 초기화 목록은 그대로 두면 생성과 멤버 소멸 로그는 어떤 순서인가요? Order() : first("first"), second("second")로 목록만 바꾸면 어떤가요?

순서를 정하는 위치

second·first로 선언한 경우 생성은 +second, +first, 멤버 정리는 -first, -second입니다. 원래 first·second 선언을 유지하고 목록만 맞추면 최초 여덟 줄의 순서가 그대로입니다. 목록의 순서나 소멸자 본문에 적은 로그가 멤버의 선언 순서를 바꾸지 않습니다. MyArray 예제도 main 끝의 arr1과 명시적 delete의 arr2라는 서로 다른 종료 경계를 먼저 표시한 뒤 읽으면 정리 시점을 구분할 수 있습니다.


소멸자 (Destructor)

소멸자는 클래스 객체의 수명이 끝나는 경계에서 정리를 수행하는 특별한 멤버 함수입니다.

생성자와 반대로, 소멸자의 주된 목적은 객체가 사용하던 리소스(예: 동적으로 할당된 메모리, 파일 핸들, 네트워크 연결 등)를 해제하고 정리하는 것입니다.

자동 저장 기간 객체는 선언된 블록을 벗어날 때 소멸자가 암시적으로 호출됩니다. new 표현식으로 만든 객체는 delete 표현식이 소멸자를 호출한 뒤 저장 공간을 해제하므로, 동적 객체에는 소유권을 끝낼 경로가 반드시 필요합니다.

완전 객체의 형식적인 수명은 소멸자 호출이 시작될 때 끝납니다. 다만 소멸 과정에서는 [class.cdtor]의 특별 규칙에 따라 소멸자 본문이 아직 살아 있는 멤버와 기반 클래스 하위 객체를 사용할 수 있습니다. 각 하위 객체의 수명은 이후 자신의 소멸이 시작되는 시점에 끝나며, 멤버와 기반 클래스는 생성 완료의 역순으로 파괴됩니다.

소멸자의 특징
  • 선언 문법에서 클래스 이름 앞에 ~(틸드)를 붙입니다. (예: ~Book())
  • 반환 타입이 없습니다. (심지어 void도 사용하지 않습니다.)
  • 매개변수를 가질 수 없습니다.
  • 일반적인 비템플릿 클래스에서는 보통 ~클래스이름() 하나만 선언합니다. C++20부터는 클래스 템플릿에서 제약 조건이 다른 후보 소멸자(prospective destructor)를 여러 개 선언할 수 있고, 클래스 정의가 끝날 때 오버로드 해석으로 그 특수화가 사용할 선택된 소멸자(selected destructor) 하나를 결정합니다.
  • public 접근 지정자로 선언하는 것이 일반적입니다.
소멸자 선언 및 정의 형식
// 클래스 선언 (헤더 파일)
class MyClass {
public:
    MyClass(); // 생성자
    ~MyClass(); // 소멸자
    // ... 다른 멤버들 ...
};

// 소멸자 정의 (소스 파일)
MyClass::~MyClass() {
    // 리소스 해제 코드
}
MyArray 클래스에 소멸자 추가 (동적 메모리 해제)
#include <iostream>
#include <stdexcept>

class MyArray {
private:
    int* data; // 동적으로 할당된 정수 배열을 가리키는 포인터
    int size;

public:
    // 생성자: 동적으로 메모리 할당
    MyArray(int s) : data(nullptr), size(s) {
        if (size > 0) {
            data = new int[size]; // 힙에 size만큼의 int 배열 할당
            std::cout << "MyArray 생성자 호출: " << size << "개의 정수 배열 할당됨. 주소: " << data << std::endl;
            for (int i = 0; i < size; ++i) {
                data[i] = 0; // 초기화
            }
        } else {
            data = nullptr;
            size = 0;
            std::cout << "MyArray 생성자 호출: 유효하지 않은 크기. 배열 할당되지 않음.\n";
        }
    }

    // raw 포인터를 직접 소유하므로 얕은 복사를 금지합니다.
    MyArray(const MyArray&) = delete;
    MyArray& operator=(const MyArray&) = delete;
    MyArray(MyArray&&) = delete;
    MyArray& operator=(MyArray&&) = delete;

    // 소멸자: 동적으로 할당된 메모리 해제
    ~MyArray() {
        if (data != nullptr) {
            delete[] data; // new[]로 할당한 메모리는 delete[]로 해제
            std::cout << "MyArray 소멸자 호출: 할당된 메모리 " << size * sizeof(int) << " 바이트 해제됨.\n";
        } else {
            std::cout << "MyArray 소멸자 호출: 해제할 메모리가 없습니다.\n";
        }
    }

    // 배열 요소에 접근하는 함수 (안전하게)
    int& operator[](int index) { // [] 연산자 오버로딩 (뒤에서 자세히 학습)
        if (index < 0 || index >= size) {
            throw std::out_of_range("배열 인덱스 범위를 벗어났습니다.");
        }
        return data[index];
    }

    void display() {
        std::cout << "배열 내용: [";
        for (int i = 0; i < size; ++i) {
            std::cout << data[i] << (i == size - 1 ? "" : ", ");
        }
        std::cout << "]\n";
    }
};

int main() {
    // 1. 스택에 MyArray 객체 생성
    std::cout << "--- 스택 객체 생성 시작 ---\n";
    MyArray arr1(5); // MyArray 생성자 호출 (5개 정수 배열 할당)
    arr1[0] = 10;
    arr1[4] = 50;
    arr1.display(); // 출력: 배열 내용: [10, 0, 0, 0, 50]
    std::cout << "--- 스택 객체 생성 끝 ---\n";
    // main 함수 종료 시 arr1의 소멸자가 자동으로 호출되어 메모리 해제

    std::cout << "\n--- 힙 객체 생성 시작 ---\n";
    // 2. 힙에 MyArray 객체 동적 생성
    MyArray* arr2 = new MyArray(3); // MyArray 생성자 호출 (3개 정수 배열 할당)
    (*arr2)[0] = 100; // arr2->operator[](0)
    arr2->display();  // 출력: 배열 내용: [100, 0, 0]

    std::cout << "--- 힙 객체 사용 중 ---\n";
    delete arr2; // delete 표현식이 소멸자를 호출한 뒤 객체 저장 공간을 해제
    arr2 = nullptr;
    std::cout << "--- 힙 객체 소멸 완료 ---\n";

    // 범위 벗어난 인덱스 접근 시도 (오류 발생 예시)
    // MyArray bad_arr(2);
    // bad_arr[10] = 100; // 오류 발생!

    return 0; // main 함수 종료 시 arr1의 소멸자가 호출됨
}

위 예시에서 MyArray 클래스는 int* data 멤버 변수를 가집니다.

data는 힙에 동적으로 할당된 배열을 가리킵니다.

생성자에서는 new int[size]를 사용하여 메모리를 할당하고, 소멸자에서는 delete[] data를 호출하여 할당된 메모리를 안전하게 해제합니다.

만약 소멸자가 없거나 delete[]를 호출하지 않으면, MyArray 객체가 소멸되더라도 동적으로 할당된 data 배열은 해제되지 않아 메모리 누수가 발생합니다.

소멸자만 추가한 채 컴파일러의 얕은 복사를 허용하면 두 객체가 같은 data를 가리키고 두 번 해제할 수 있습니다. 이 예시는 복사와 이동을 모두 = delete하여 단일 소유 객체로 제한했습니다. 실전에서는 std::vector<int>처럼 자원을 직접 관리하는 멤버에 소유권을 맡겨 Rule of Zero로 설계하는 편이 더 안전합니다.

소멸 직전 data = nullptr로 바꾸더라도 다른 곳에 복사된 포인터가 있다면 그 외부 별칭의 댕글링 상태까지 막을 수는 없습니다. 그래서 곧 끝날 MyArray 내부 포인터를 다시 설정하지 않고, delete arr2 뒤에는 실제로 계속 존재하는 호출자 쪽 포인터 arr2를 nullptr로 바꿨습니다. 또한 소멸자의 std::cout은 호출 시점을 관찰하기 위한 학습용 로그입니다. 스트림 예외가 활성화된 환경에서는 출력이 예외를 던져 암시적으로 noexcept인 소멸자에서 프로그램이 종료될 수 있으므로, 실제 정리 코드에서는 실패 가능한 로깅을 소멸자 밖으로 분리하거나 내부에서 처리해야 합니다.


기본 생성자 (Default Constructor)

클래스에 사용자 선언 생성자(user-declared constructor) 또는 생성자 템플릿이 하나도 없으면, 컴파일러는 매개변수 없는 기본 생성자(Default Constructor)를 암시적으로 선언합니다.

이 생성자는 초기화 목록과 본문이 비어 있는 사용자 작성 기본 생성자와 같은 초기화를 수행합니다. 클래스 타입 멤버는 자신의 기본 생성자를 호출하고 기본 멤버 초깃값도 적용되지만, 기본 멤버 초깃값이 없는 기본 타입 멤버를 Point pt;처럼 기본 초기화하면 값이 정해지지 않습니다.

컴파일러가 자동 생성하는 기본 생성자
  • 클래스에 사용자 선언 생성자나 생성자 템플릿이 전혀 없을 때만 암시적으로 선언됩니다.
  • 멤버들을 기본 초기화합니다. 기본 멤버 초깃값은 적용되고 클래스 타입은 해당 타입의 기본 생성자를 호출합니다.

기본 생성자의 암시적 선언 여부는 생성자를 정의했는가가 아니라 생성자 또는 생성자 템플릿을 사용자가 선언했는가를 기준으로 판단합니다. 빈 상태를 허용할지에 따라 = default 또는 = delete로 의도를 명시할 수도 있습니다.

컴파일러가 생성하는 기본 생성자 예시
#include <iostream>
#include <string>

class Point {
public:
    int x{0};
    int y{0};
    // 사용자 선언 생성자가 없음 -> Point()가 암시적으로 선언됨
};

class RequiredPoint {
public:
    RequiredPoint(int x, int y) : x(x), y(y) {}

    int x;
    int y;
};

class Line {
public:
    Point p1; // Point 객체 (기본 생성자 호출됨)
    Point p2;
    // 사용자 선언 생성자가 없음 -> Line()이 암시적으로 선언됨
};

int main() {
    Point pt; // 암시적으로 선언된 Point() 기본 생성자 호출
    std::cout << "Point.x: " << pt.x << ", Point.y: " << pt.y << std::endl; // 0, 0

    RequiredPoint required(10, 20);
    std::cout << "RequiredPoint.x: " << required.x << std::endl;
    // RequiredPoint missing; // 오류: 호출할 기본 생성자가 없음

    Line l; // 암시적으로 선언된 Line(), 이어서 p1과 p2의 Point() 호출
    std::cout << "Line.p1.x: " << l.p1.x << ", Line.p1.y: " << l.p1.y << std::endl;

    return 0;
}

만약 클래스에 매개변수를 받는 생성자를 하나라도 사용자 선언했다면, 컴파일러는 더 이상 기본 생성자를 암시적으로 선언하지 않습니다.

이 경우 매개변수가 없는 클래스이름() 형식의 객체 생성이 필요하다면, 프로그래머가 기본 생성자를 직접 선언해 정의하거나 = default해야 합니다.


규칙 3 (Rule of Three / Five / Zero) (심화)

Rule of Three/Five/Zero는 컴파일러가 강제하는 문법 규칙이 아니라, 복사·이동·소멸 의미를 일관되게 설계하기 위한 지침입니다.

C++ · OWNERSHIP POLICY

자원 소유 방식에 맞춰 복사·이동·소멸 정책을 함께 정한다

Rule of Zero/Three/Five는 설계 지침입니다. 직접 소유하는 raw 자원이 있는지 확인하고, 실제로 제공할 동작과 금지할 동작을 결정합니다.

Rule of Zero · 소유권을 멤버에 맡김

vector, string, 스마트 포인터 같은 RAII 멤버가 자원을 관리하면 특별 멤버를 직접 정의하지 않는 설계를 우선합니다.

std::vector<int> data;

바깥 클래스의 복사·이동 가능성은 멤버의 정책과 언어 규칙을 따릅니다. unique_ptr 멤버라면 기본 복사는 불가능합니다.

Rule of Three · 직접 소유

raw 메모리·파일 핸들을 직접 소유하면 소멸자와 두 복사 연산의 의미를 함께 검토합니다.

~MyArray();
MyArray(const MyArray&) = delete;
MyArray& operator=(
  const MyArray&) = delete;

값 복사가 필요하면 깊은 복사를 구현하고, 복사할 수 없는 소유권이면 명시적으로 금지합니다.

Rule of Five · 이동 정책도 결정

소유권 이전을 제공한다면 이동 생성자와 이동 대입까지 함께 검토합니다.

MyArray(MyArray&&) = delete;
MyArray& operator=(
  MyArray&&) = delete;

본문 MyArray는 이동도 금지합니다. 이동을 구현한다면 원본·대상의 소멸과 대입을 안전하게 유지하는 후상태를 문서화합니다.

이동 후 상태 · 타입의 계약

사용자 타입의 이동 후 상태는 구현자가 보장해야 합니다. 생략과 = delete가 호출 결과에 미치는 영향도 확인합니다.

표준 라이브러리 타입은 달리 명시되지 않으면 유효하지만 값은 미지정인 상태를 보장합니다. 사용자 타입에 같은 상태가 자동으로 생기지는 않습니다.

이 그림의 MyArray 선언은 직접 소유 예제에서 복사·이동을 금지하는 정책을 보여 줍니다. 특별 멤버의 실제 암시적 선언·삭제 여부는 C++ 언어 규칙을 따릅니다.

클래스가 동적으로 할당된 메모리나 파일 핸들 같은 자원(Resource)을 직접 소유한다면, 소멸자와 복사·이동 특별 멤버 함수의 의미를 함께 검토해야 합니다. 멤버별 동작이 원하는 소유권 의미와 같을 때만 = default를 사용하고, 지원하지 않을 동작은 = delete로 막습니다.

소멸자, 복사 생성자, 복사 대입 연산자 중 하나를 직접 선언해야 한다면 나머지 둘도 함께 검토하는 지침을 규칙 3(Rule of Three)이라고 합니다.

소멸자 (Destructor): 자원 해제

복사 생성자 (Copy Constructor): 객체 복사 시 깊은 복사(Deep Copy)를 구현하거나 = delete

복사 대입 연산자 (Copy Assignment Operator): 객체 대입 시 깊은 복사를 구현하거나 = delete

C++11 이후 이동을 지원하는 직접 소유 타입은 이동 생성자(Move Constructor)와 이동 대입 연산자(Move Assignment Operator)까지 함께 검토하므로 규칙 5(Rule of Five)라고 부릅니다. 사용자 선언 소멸자나 복사 연산은 암시적 이동 연산의 생성에도 영향을 주므로, 이동을 제공할지 명시적으로 결정해야 합니다.

현대 C++에서는 문자열, 컨테이너, 스마트 포인터처럼 스스로 자원을 관리하는 멤버에 소유권을 맡기고 특별 멤버 함수를 직접 선언하지 않는 규칙 0(Rule of Zero)를 우선합니다. 이때 바깥 클래스의 복사·이동 가능 여부는 멤버 타입의 정책을 그대로 따릅니다. 예를 들어 std::unique_ptr 멤버가 있으면 기본 복사는 허용되지 않습니다.

이 내용은 10장의 ‘Rule of Three, Five, Zero 실전’에서 실제 코드로 더 자세히 다룹니다.

지금은 생성자와 소멸자의 중요성을 이해하는 것에 집중하세요.

객체 수명은 생성, 사용, 정리, 복사/이동 정책이 한 번에 맞물려야 안전해집니다.

직접 자원을 소유하는 순간에는 생성자와 소멸자만 보지 말고 복사와 이동까지 같은 계약으로 읽어야 합니다.