C++입문자들이 가상 소멸자에대해서 잘 모르는 것을 많이 봤다.
또, 어느정도 했다는 프로그래머중에서도 정확히는 모르는 사람들이 꽤 있드라…
“Effective C++ 항목7” 에 이 부분에 대한 설명이 있지만, 초보자들이 이해하기에는 다소 어려움이 있어 보여, 이부분에 대해 이해도를 높여 보고자 한다.
(다형성을 가진 virtual class에서) 가상 소멸자(virtual destructor)를 사용하지 않으면 문제는 무엇인가?
라는 질문을 했을때 다음과 같은 대답을 한다면 70점을 줄 것이다.
메모리 릭이 발생합니다
소멸자의 호출여부를 제대로 알고 있어야 100점이라고 할 수 있다. 대략
(파생 클래스의) 소멸자가 제대로 호출되지 않을 수 있어, 메모리릭을 포함해서 부분 소멸될 수 밖에 없는 문제점이 발생할 수 있습니다
C++은 생성자, 소멸자가 굉장히 중요하다는 것을 잊어서는 안된다.
생성자에서 락일 걸고, 소멸자에서 락을 해제하는 방법을 사용하며, 생성자에서 파일을 열고, 소멸자에서 파일을 닫는 방법을 사용하기도 한다.
위의 예에서 소멸자가 호출되지 않는다면 일어나는 문제점은 잘 알 수 있을 것이다.
메모리 릭이 발생한다고 대답하는 것은 왜 70점짜리 대답인지 자세히 알아보면,
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
class Abstract { public: Abstract() { std::cout << "Abstract()" << std::endl; } ~Abstract() { std::cout << "~Abstract()" << std::endl; } }; class Foo : public Abstract { public: Foo() { std::cout << "Foo()" << std::endl; } ~Foo() { std::cout << "~Foo()" << std::endl; } private: std::array<char, 1024*1024> _data; }; class Bar : public Abstract { public: Bar() : _data(std::make_unique<char[]>(1024 * 1024)) { std::cout << "Bar()" << std::endl; } ~Bar() { std::cout << "~Bar()" << std::endl; } private: std::unique_ptr<char[]> _data; }; |
Abstract의 소멸자에는 virtual 키워드가 없다.
이를 상속받은 Foo, Bar 2개의 class가 있는데,
Foo 클래스는 메모리릭 문제가 존재하지 않는다.
왜냐하면, Foo가 가지고 있는 데이터는 순전히 HeapManager에 의해서 할당/해제가 되기 때문에 소멸자의 호출여부와는 무관하기 때문
소멸자에서 하는 일이라고는 “~Foo()”를 출력하는 것 뿐
하지만, Bar 클래스는 메모리릭 문제가 발생 할 수 있다.
왜냐하면, char[]에대한 해제는 소멸자가 호출되어야만 발생 할 수 있기 때문
VS2015의 Heap Profile을 이용해서 확인해 보면,
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
int main() { ::Sleep(1000); { // 1. 정상적인 소멸자 호출로 메모리 릭이 발생하지 않는다. Foo* p = new Foo(); ::Sleep(1000); delete p; } std::cout << std::endl; ::Sleep(1000); { // 2. ~Foo(); 가 호출되지는 않지만 메모리 릭은 발생하지 않는다. Abstract* p = new Foo(); ::Sleep(1000); delete p; } std::cout << std::endl; ::Sleep(1000); { // 3. 정상적인 소멸자 호출로 메모리 릭이 발생하지 않는다. Bar* p = new Bar(); ::Sleep(1000); delete p; } std::cout << std::endl; ::Sleep(1000); { // 4. ~Bar(); 가 호출되지도 않고, 메모리릭도 발생! Abstract* p = new Bar(); ::Sleep(1000); delete p; } std::cout << std::endl; ::Sleep(1000); return 0; } |

~Foo()의 소멸자 호출과는 관계없이 메모리가 해제되는 2번째 파동을 볼 수 있다.
하지만, ~Bar()의 소멸자가 호출되지 않고 메모리가 해제된다면 생기는 4번째 파동은 볼 수 없다.
정상적으로 Abstract의 소멸자에 virtual을 붙인다면 어떨까?

참고
Effective C++, Third Edition, 2005. Scott Meyers
codemore code
~~~~