-
가상 소멸자 / 가상 소멸자 사용 이유뜯고 또 뜯어보는 컴퓨터/씨쁠쁠 C++ 2022. 10. 26. 12:02반응형
0. 상속이란 개념
c++ 뿐만 아니라 다른 언어에서도 상속(inheritance)이란 기존의 클래스에 기능을 추가하거나 재정의하여 새로운 클래스를 정의하는 것을 의미합니다. 상속을 사용하게 되면, 기존에 정의되어 있는 클래스의 모든 필드와 메서드를 물려받아, 새로운 클래스를 생성할 수 있습니다.
그리고 상속을 하게 되면, 각 계층(부모 또는 자식)에 맞는 메모리 할당이 발생하게 되는데, 이는 다형성(polymorphism)이라는 중요한 성질을 띄게 됩니다. 다형성에 관한 글은 설명이 많아져 따로 작성하겠습니다..! 아무튼 상속을 사용하게 되면 여러가지 장점도 존재하지만, "주의해야 할 점"도 존재합니다. 그중 하나가 가상 소멸자에 관한 내용입니다.
1. 생성자 / 소멸자 호출 순서
파생된 클래스의 생성자 호출 순서는 부모 클래스로부터, 자식 클래스의 생성자를 호출하게 되고, 상속 트리 구조에 따라 위에서부터 아래로 내려오게 됩니다. 그러면 가상 소멸자는 어떻게 호출이 될까요? 가상 소멸자의 경우, 생성자 호출 순서와 반대로, 자식 클래스로부터 부모 클래스로의 순서로 호출이 되게 됩니다.
하지만, 정상적인 경우가 아닌, 참조에 의한 파생 클래스(자식 클래스) 에 할당되는 기본 클래스(부모 클래스)를 삭제 하게 되면, 정의되지 않은 동작이 발생 할 수 있습니다. (위에 설명했듯이, 다형성이라는 성질을 활용하기 때문에, 자식 클래스의 포인터를 부모 포인터로 가지고 있다 소멸시키게 된다면, virtual 소멸자로 설정해주지 않은 자식 클래스에서는 메모리 누수가 일어나게 됩니다.)
예를 들어, 다음과 같은 경우, 자식 클래스의 포인터가 부모 클래스의 포인터로 업캐스팅 되었을시, 소멸자에서 오류가 발생하게 됩니다.
#include <iostream> using namespace std; // 부모 클래스 class base { public: base() { cout << "Constructing base\n"; } ~base() { cout<< "Destructing base\n"; } }; // 자식 클래스 class derived: public base { public: derived() { cout << "Constructing derived\n"; } ~derived() { cout << "Destructing derived\n"; } }; int main() { // 파생된 자식 클래스 derived *d = new derived(); // base -> derived 호출 base *b = d; // 부모 클래스로 업캐스팅 delete b; // 부모 클래스에 캐스팅 된 객체 삭제 getchar(); return 0; }
위의 결과를 보게 된다면,
생성자의 경우, Constructing base -> Constructing derived 을 호출하는 것을 확인할 수 있지만, 소멸자의 경우, Destructing Base만을 호출하여, Destructing derived를 호출하지 않는 것을 확인할 수 있습니다. 이는 자식 클래스에서 동적 객체를 할당해주었을 경우, 제대로 소멸자가 불리지 않아 메모리 누수를 일으키는 주요 원인이 되게 되므로, 꼭 상속객체를 사용시 virtual 키워드를 주어, 제대로 된 소멸자가 호출될 수 있도록 해야합니다.
2. Virtual 소멸자 사용
따라서 다음과 같이 올바르게 코드를 변경 할 수 있습니다.
#include <iostream> using namespace std; class base { public: base() { cout << "Constructing base\n"; } virtual ~base() { cout << "Destructing base\n"; } }; class derived : public base { public: derived() { cout << "Constructing derived\n"; } virtual ~derived() { cout << "Destructing derived\n"; } }; int main() { derived *d = new derived(); base *b = d; delete b; getchar(); return 0; }
위와 같이 virtual 로 가상소멸자를 호출하게 된다면, Destructing derived -> Destructing base 순서로 호출되는 것을 확인할 수 있습니다. 소멸자의 경우 생성자와는 순서가 반대이므로, 자식--> 부모 순으로 호출된다는 것도 유의하시길 바랍니다.
3. 정리
상속을 사용하여, 다형성 및 여러가지 특징들을 손쉽게 활용할 수 있지만, 소멸자를 꼭 virtual 로 지정해주어 소멸자가 제대로 된 순서로 소멸을 하고 메모리가 누수하지 않게끔 하는 것을 설명하는 것이 이번 글의 목적이었습니다.
4. Q&A
<Q> C++에서 생성자 실행의 순서는 어떻게 되는가?
<A> 첫 번째 기본 클래스 생성자(부모)가 실행된 다음 파생 클래스 생성자(자식)가 실행되므로 상속 트리 구조에서 위에서부터 아래로 실행됩니다.
<Q> C++에서 소멸자 실행의 순서는 어떻게 되는가?
<A> 일반적으로 파생 클래스 소멸자가 먼저 호출이 되고, 그 다음에 기본 클래스 소멸자가 호출되게 됩니다. 다만 파생 클래스 객체를 기본 클래스 포인터 (또는 참조 변수)로 가져 오는 경우에는 virtual 키워드로 소멸자를 정의해줘야 합니다.
<Q> 그렇다면 가상 소멸자의 용도는 무엇일까?
<A> 이것은 런타임에 올바른 클래스 소멸자가 호출되는지 확인하기 위한 것입니다. 특히 파생 클래스(자식 클래스) 객체를 유지하기 위해 기본 클래스 포인터 또는 참조를 사용할 때(다형성 활용), 가상 소멸자가 없게 된다면 기본 클래스 소멸자만 호출하게 되어, 메모리 누수가 발생하게 됩니다.< 도움이 되셨다면, 댓글 부탁드립니다..! >
반응형'뜯고 또 뜯어보는 컴퓨터 > 씨쁠쁠 C++' 카테고리의 다른 글
Overloading 오버로딩 - 연산자 (0) 2022.11.02 Shared_ptr 내부 구조 & 주의점 (0) 2022.10.26 [C++] String to Int, Float, Double 자료형 / stoi, stol, stoll (0) 2022.10.26 Map 을 Vector 로 변환하기 (0) 2022.10.20 스마트 포인터 : Unique_ptr (0) 2022.05.08