객체지향프로그래밍(20) - 가상 소멸자

가상 소멸자란?

Featured image

🔚 짧게 하는 복습

✅ 1. 오버라이드 키워드의 용도와 사용 이유를 안다.

✅ 2. 순수 가상 함수의 특징과 용도를 안다.

✅ 3. 추상 클래스의 정의를 안다.

✅ 4. 순수 가상 함수와 추상 클래스의 장단점을 안다.

혹시 기억이 안 난다면, 다시 돌아가자


저번 시간에는 순수 가상 함수와 추상 클래스에 대해 다루었다.

읽어볼 거리에서는 더욱 특별한 추상 클래스인, 인터페이스에 대해서도 알아보았다.

그런데 우리가 하나 크게 까먹은 부분이 있다.

모든 오버라이딩이 필요한 메소드는 다 가상 함수로 바꾸고, 오버라이딩까지 잘한 것 같은데 어디를 실수했을까?


숨겨진 버그

사실 우리가 저번 시간까지 작성한 코드에서는 프로그램을 끝까지 수행하지 않았다.

해야 될 것을 하나 크게 놓친 것이 있는데, 바로 할당 해제이다.

우리는 동적 할당만 해놓고, 할당 해제를 아직 하지 않았다.

그런 의미에서 코드를 수정하고 할당 해제를 시켜보자.


#include <iostream>

enum { Fire, Water, Grass };

class pokemon {
public:
    pokemon() {}

    ~pokemon() { std::cout << "다 보냈습니다~" << std::endl; }
    //소멸자 추가

    virtual void attack() =0; // 순수 가상 함수


}; // 추상 클래스

class bukein : public pokemon {
    int type;

public:
    bukein() : pokemon(), type(Fire) {}

    ~bukein() { std::cout << "브케인 보냈습니다~" << std::endl; }
    //소멸자 추가

    void attack() override { //override 됬음을 명시적으로 보여줌
        std::cout << "불 공격합니다~" << std::endl;
        std::cout << "확률에 따라 화상으로 추가 데미지를 줍니다." << std::endl;
    }
}; // 불 속성 브케인은 확률에 따라 추가 데미지를 주게하고 싶다.

class riako : public pokemon {
    int type;

public:
    riako() : pokemon(), type(Water) {}

    ~riako() { std::cout << "리아코 보냈습니다~" << std::endl; }
    //소멸자 추가

    void attack() override { //override 됬음을 명시적으로 보여줌
        std::cout << "물 공격합니다~" << std::endl;
        std::cout << "확률에 따라 자신의 hp를 회복합니다." << std::endl;
    }
}; // 물 속성 리아코는 확률에 따라 자신의 hp를 회복시키고 싶다.

class chicorita : public pokemon {
    int type;

public:
    chicorita() : pokemon(), type(Grass) {}

    ~chicorita() { std::cout << "치코리타도 보냈습니다~" << std::endl; }
    //소멸자 추가

    void attack() override { //override 됬음을 명시적으로 보여줌
        std::cout << "풀 공격합니다~" << std::endl;
        std::cout << "확률에 따라 보호막으로 다음 공격을 무시합니다." << std::endl;
    }
}; // 풀 속성 치코리타는 확률에 따라 보호막을 만들어 다음 공격을 무시하고 싶다.

int main() {
    pokemon** pomons = new pokemon*[3]; // 포인터를 저장하는 배열이어야하니!
    chicorita* pomon1 = new chicorita();
    bukein* pomon2 = new bukein();
    riako* pomon3 = new riako();


    pomons[0] = pomon1;
    pomons[1] = pomon2;
    pomons[2] = pomon3;

    /*
    for (int i = 0; i < 3; i++) {
        pomons[i]->attack();
    }
    */

    for (int i = 0; i < 3; i++) {
        delete pomons[i];
    } // 각각의 요소 해제

    delete[] pomons;
    //배열도 할당 제제
}

결과는??

 보냈습니다~
 보냈습니다~
 보냈습니다~

어림 없이 당연히 안 된다., 이제는 virtual을 배웠기 때문에 여러분들이 무엇을 놓쳤는지 알 것이다.

업 캐스팅된 채로 소멸자를 호출하니 상위 클래스의 소멸자가 호출될 수밖에 없다.

그렇다면 어떻게 해결해야 할까?


가상 소멸자

간단하다. 소멸자 앞에 virtual 키워드를 이용하면 된다!

이를 가상 소멸자라고 하고, 알아서 동적 바인딩 되어 자신의 소멸자를 호출해준다.

class pokemon {
public:
    pokemon() {}

    virtual ~pokemon() { std::cout << "다 보냈습니다~" << std::endl; }
    //가상 소멸자 추가

    virtual void attack() = 0; // 순수 가상 함수


}; // 추상 클래스

결과

치코리타도 보냈습니다~
 보냈습니다~
브케인 보냈습니다~
 보냈습니다~
리아코 보냈습니다~
 보냈습니다~

어라..? 되긴 하는데 상위 클래스의 소멸자도 같이 호출된다.

이는 생성자의 반대 순서라고 생각하면 편하다.

하위 클래스의 생성자를 호출하면 상위 클래스의 생성자가 먼저 호출된다.

반면에 가상 소멸자를 사용하면, 하위 클래스의 소멸자가 끝나면 다음 상위 클래스의 생성자가 순서대로 호출된다.


강의를 마치며

프로그래머들이 상속 관계에 있는 클래스를 작성할 때 항상 가상 소멸자를 사용한다.

이는 안정적인 다형성 구현을 위해서 필수이다.

당연히 가상 소멸자 역시 단점도 있다.

가상 함수이다 보니 동적 바인딩에서 시간적 손해가 있을 순 있다.

하지만 가상 소멸자가 주는 다형성과 안정성에 비하면 이는 거의 미비한 편이라 무시할 수 있는 정도이다.


📖 오늘의 핵심(다 알기 전까지는 넘어가지 말자❗)

✅ 1. 가상 소멸자의 정의와 활용을 안다.

✅ 2. 가상 소멸자 이용 시 소멸자 호출 순서를 안다.

⚠️ 상속 구조를 이용할 시, 안정성을 위해 상위 클래스의 소멸자는 가상 소멸자를 이용해야 한다.

💣 과제, 없음