icon
9장 : 객체 지향 프로그래밍 기초

생성자와 소멸자

클래스에서 객체가 생성되고 소멸될 때 자동으로 호출되는 특별한 멤버 함수인 생성자(Constructor)소멸자(Destructor) 에 대해 알아보겠습니다.

이들은 객체의 생명주기(Lifetime)와 밀접하게 관련되어 있으며, 객체 지향 프로그래밍에서 매우 중요한 역할을 합니다.


생성자 (Constructor)

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

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

생성자의 특징

  • 클래스 이름과 동일한 이름을 가집니다.
  • 반환 타입이 없습니다. (심지어 void도 사용하지 않습니다.)
  • public 접근 지정자로 선언하는 것이 일반적입니다. (외부에서 객체를 생성할 수 있도록)
  • 오버로딩(Overloading)이 가능하여 여러 개의 생성자를 가질 수 있습니다. (매개변수 리스트가 다르면 됨)
생성자 선언 및 정의 형식
// 클래스 선언 (헤더 파일)
class MyClass {
public:
    MyClass(); // 1. 기본 생성자 (매개변수가 없는 생성자)
    MyClass(int a, std::string b); // 2. 매개변수가 있는 생성자 (오버로딩)
    // ... 다른 멤버들 ...
};

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

MyClass::MyClass(int a, std::string b) { // 2. 매개변수가 있는 생성자 정의
    // 전달받은 매개변수로 멤버 변수 초기화
}
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";
    }
    
    // (선택 사항) 멤버 초기화 리스트를 사용한 생성자 (권장)
    // 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;
}

멤버 초기화 리스트

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

멤버 초기화 리스트 형식
클래스이름::클래스이름(매개변수1, ...) : 멤버1(인자1), 멤버2(인자2) {
    // 생성자 본문
}
  • 생성자의 매개변수 리스트 뒤에 콜론(:)을 붙이고, 멤버 변수 이름과 괄호 안에 초기화할 값을 지정합니다.
  • 멤버 초기화 리스트는 멤버 변수가 선언된 순서대로 초기화됩니다.
  • 장점
    • 효율성: 멤버 변수가 생성되는 시점에 바로 초기화되므로, 기본 생성자로 초기화된 후 다시 대입되는 비효율적인 과정을 피할 수 있습니다. 특히 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();

    // Student s2("박지민", 2023002); // 컴파일 오류: const 멤버 GPA를 초기화하지 않음
    return 0;
}

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


소멸자 (Destructor)

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

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

소멸자의 특징

  • 클래스 이름 앞에 ~ (틸드)를 붙인 이름을 가집니다. (예: ~Book())
  • 반환 타입이 없습니다. (심지어 void도 사용하지 않습니다.)
  • 매개변수를 가질 수 없습니다. (오버로딩 불가)
  • 오직 하나만 존재할 수 있습니다.
  • public 접근 지정자로 선언하는 것이 일반적입니다.
소멸자 선언 및 정의 형식
// 클래스 선언 (헤더 파일)
class MyClass {
public:
    MyClass(); // 생성자
    ~MyClass(); // 소멸자
    // ... 다른 멤버들 ...
};

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

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

public:
    // 생성자: 동적으로 메모리 할당
    MyArray(int s) : 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";
        }
    }

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

    // 배열 요소에 접근하는 함수 (안전하게)
    int& operator[](int index) { // [] 연산자 오버로딩 (뒤에서 자세히 학습)
        if (index < 0 || index >= size) {
            std::cerr << "오류: 배열 인덱스 범위를 벗어났습니다.\n";
            // 실제 앱에서는 예외를 던지거나 다른 적절한 오류 처리
            exit(1); // 예시를 위해 프로그램 종료
        }
        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; // MyArray 소멸자 명시적으로 호출 (메모리 해제)
    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 배열은 해제되지 않아 메모리 누수가 발생합니다.


기본 생성자 (Default Constructor)

만약 프로그래머가 클래스에 어떤 생성자도 명시적으로 정의하지 않으면, 컴파일러는 자동으로 기본 생성자(Default Constructor) 를 생성합니다.

이 기본 생성자는 아무런 매개변수도 받지 않으며, 대부분의 경우 멤버 변수를 초기화하지 않습니다.

컴파일러가 자동 생성하는 기본 생성자

  • 클래스에 다른 생성자가 전혀 없을 때만 생성됩니다.
  • 멤버 변수들을 기본 초기화합니다. (기본 타입은 쓰레기 값, 클래스 타입은 해당 클래스의 기본 생성자 호출)
컴파일러가 생성하는 기본 생성자 예시
#include <iostream>
#include <string>

class Point {
public:
    int x;
    int y;
    // 어떤 생성자도 명시하지 않음 -> 컴파일러가 기본 생성자 Point()를 자동 생성
};

class Line {
public:
    Point p1; // Point 객체 (기본 생성자 호출됨)
    Point p2;
    // Line에 어떤 생성자도 명시하지 않음 -> 컴파일러가 기본 생성자 Line()을 자동 생성
};

int main() {
    Point pt; // 컴파일러가 생성한 Point() 기본 생성자 호출
    std::cout << "Point.x: " << pt.x << ", Point.y: " << pt.y << std::endl; // 쓰레기 값이거나 0 (환경에 따라 다름)

    // 만약 Point에 Book처럼 사용자 정의 생성자가 있다면, Point pt; 는 컴파일 오류가 됩니다.
    // 이 경우 Point pt(0,0); 처럼 명시적으로 호출해야 합니다.

    Line l; // 컴파일러가 생성한 Line() 기본 생성자 호출, 그 안에 p1, p2의 Point() 기본 생성자 호출
    std::cout << "Line.p1.x: " << l.p1.x << ", Line.p1.y: " << l.p1.y << std::endl;

    return 0;
}

만약 클래스에 매개변수를 받는 생성자를 하나라도 명시적으로 정의했다면, 컴파일러는 더 이상 기본 생성자를 자동으로 생성해주지 않습니다.

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


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

클래스가 동적으로 할당된 메모리나 파일 핸들 같은 자원(Resource) 을 소유하는 경우, 생성자와 소멸자 외에 다음 특별 멤버 함수들도 명시적으로 정의하거나 = default, = delete를 사용해야 합니다.

이를 규칙 3(Rule of Three) 이라고 합니다.

  1. 소멸자 (Destructor): 자원 해제
  2. 복사 생성자 (Copy Constructor): 객체 복사 시 깊은 복사 (Deep Copy)
  3. 복사 대입 연산자 (Copy Assignment Operator): 객체 대입 시 깊은 복사

C++11 이후에는 이동 생성자 (Move Constructor)이동 대입 연산자 (Move Assignment Operator) 가 추가되어 규칙 5(Rule of Five) 가 되었고, 현대 C++에서는 스마트 포인터 등을 사용하여 자원 관리를 자동화하여 이들을 직접 정의할 필요가 없도록 하는 규칙 0(Rule of Zero) 를 권장합니다.

이 내용은 이후 고급 객체 지향 프로그래밍에서 더 자세히 다루겠습니다.

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