- 객체지향의 3가지 특징인 다형성의 기초의 이해를 묻는 아주 많이 들었던 질문.


왜 다형성을 가진 C++ 클래스의 소멸자에는 virtual 키워드를 붙일까?

(다형성을 가지지 않는다면 별로 상관없다. virtual 자체가 상속과 다형성에 관련된 키워드이므로)


이를 이해하기 위해서는 먼저 다음을 이해해야 한다.

다음과 같은 상속 관계가 있다.


이 관계에서


Base* base = new Derived()는 컴파일 오류가 나지 않지만 


Derived* derived = new Base()는 컴파일 오류가 나는 것을 이해해야 한다.


이 사실을 제대로 알기전에 나는 항상 생각을 반대로 했었다. 

Derived가 Base를 상속받아 더 확장되었으니 Derived가 Base를 포함할 수 있겠지?


하지만 안타깝게도 코딩은 수학이 아니더라.


*가 붙어있기에

*의 의미는 가리킨다는 것이기에

위에서 아래를 가리키듯이

Base에서 Derived를 가리키는 개념이다.


이에 대한 더 정확한 이해는 상속의 3가지 관계 

is-a

has-a

is implemented of 

3가지 관계 중에서 is-a 관계를 깊게 살펴보면 조금 더 잘 알 수 있다.


이 부분만으로도 기술면접이 아닌 필기테스트까지 합치면 정말 많이 본 문제다.

모든 다형성 이해의 기초가 되는 부분.


결론적으로는 위처럼 컴파일 오류가 난다는 것은 C++ 규칙에 어긋났다는 의미이다.

C++이란 형체가 없는 글로 쓴 규칙이다.


C++ 스펙(취업 스펙이 아닌 명세화된 규칙) 문서를 보기 전까지는 나는 C++이 visual studio인 줄 알았다.


하지만 GCC, Visual Studio같은 컴파일러는 스트롭스트룹 할아버지가 규칙을 만들고 

C++ 표준 위원회에서 더 구체화 환 글로 쓴 규칙, 즉 스펙이다.

즉, Derived *derived = Base(); 란 애초에 스펙과 어긋났다는 소리이다.

(수학으로 치면 정의를 무시했다로 이해하면 된다.)


그럼 애초에 다음과 같이 쓰면 될 것을 왜 저렇게 꼭 받아야 할까?


왜!! Base *base = new Derived() 와 같은 형태가 다형성의 기초가 되는걸까?


물론 위처럼 써도 된다. 문법으로는 어긋날것이 없다.


하지만 그 객체를 받는 것이 Base* base에서 뻗어나간 모든  유도(자식) 객체를 Base에서 가리킬 수 있다는 것은 

설계에서 거대한 이점이 된다. (객체지향의 위엄 중 하나)


그것을 보기 전에 과연 다음과 같은 결과는 어떻게 될까?


당연하듯이 결과는 다음과 같다. 


하지만 다음과 같은 상황에서는 어떨까?

Derived() 객체를 Base*로 받는다면? 위의 결과와 같을까?


어라? (어라는 무슨 다들 알고있듯이 뻔한 결과이다...)


실제 객체는 Derived() 객체이지만 Base*로 이를 받으니 원하던 Derived 행동이 나타나지 않고 Base 행동이 나타났다.

이와 같은 결과가 된다면 다형성이 이뤄진 것이 아니다.


어떻게 할까?


모두 아시다시피 다음처럼 virtual 키워드를 붙인다. 

다음의 결과는?

출력


원하는대로 똑같이 Base* 타입으로 받고 Do() 함수를 호출했지만 그 결과는 다르다.

virtual 키워드를 이용하니 이렇게 Base* 타입 하나로 서로 다른 객체의 행동을 불러올 수 있었다.


이것이 내가 생각으로는 다형성의 기초같다. 그리고 내가 객체지향을 처음으로 제대로 알고싶게 만든 문제이기도 했고.


그럼 돌아와서 중간에 있었던 

Base 객체는 Base* 타입으로 받고, Derived 객체는 Derived* 타입으로 받으면 될 것을! 왜 하나의 Base* 타입으로 받을까?


내가 생각하는 답은 좋은 설계를 위해서이다.

만약 Base 타입이 그로부터 생성된 상속된 Derived를 받을 수 없다면?


단순한 예를 들어보면, 어느 한 함수(메서드)에 Base에서 상속받은 Derived 객체이지만 어느 객체가 들어올지를 모른다면

이를 판단하기 위해서는 아마 다음과 같은 메서드 형태를 만들 것이다.


void PrintMe(Base* b, Derived* d1, DDerived* d2, DDDerived* d3 ...) {

if( ... ) {

}

else if ( ... ) {

}

...

else {

}

}

왜냐하면 객체를 구분하기 위해서. 

그리고 상속이 단순히 Base, Derived로 끝나는 경우는 대학교에서 배울 때 빼곤 없다고 한다.

끝도없이 타고내려오는 경우가 대부분이라 한다. 그러면 상속의 트리가 깊어질수록 코드 작성은 미궁에 빠질 것이다.


하지만 Base*가 Base에서 파생된 모든 객체를 받을 수 있다면? 

위의 코드는 이렇게 변할 것이다.

void PrintMe(Base* b) {

b->Do();

}


정말 간단하다. 심플하다.


이 말고도 다른 이유가 많겠지만, 

나는 이 예시에서 Base*로 파생된 모든 Derived 객체를 받을 수 있다는 것의 이점을 조금 더 알게되었다.


그럼 이제 마지막 본론이다. 


원래의 주제로 돌아와서, 

그럼 왜 다형성을 가진 관계에서 소멸자에 virtual 키워드를 붙여야할까?!!


그 예시는 다음의 두 코드와 결과를 보시라.


(1) 소멸자에 virtual 키워드가 없는 경우 결과

위의 출력 결과에서 이상함을 눈치채셨듯이 Derived 생성자는 있지만 Derived 소멸자는 없다.

이는 C++의 고질적인 무한 디버깅의 원인이되는 메모리 누수의 원인이 된다. 

(프로그램 안에서 수도없이 생성과 소멸하는 객체라면 바로 죽는다. 그리고 그 프로그램에 상용 제품이라면 손해는 ... 하하하)


(2) 소멸자에 virtual 키워드를 넣은 경우


!!!

Derived 소멸자가 호출되었다.


이처럼 위에서 다형성을 가진 행동을 하기 위해서 상속 관계 안에서 각각의 객체를 구분하기 위해 함수(메서드)에 

virtual 키워드를 붙인 것 처럼!


소멸자도 역시 상속 관계 안에서 정말 자신의 객체를 소멸시키기 위하여 소멸자에 virtual 키워드를 붙인다.


잊지말자.


다형성을 가진 상속관계에서는 반드시 소멸자에 virtual 키워드를 붙여야 한다는 것.

(우리나라 대기업 및 주요 IT 기업 필기테스트와 기술면접에서 거의 다 만나본 객체지향의 기초 핵심 중 하나이다.)

-순전히 내 경험 및 내 생각




오랜만에 블로그를 썼더니 좋다.


휴일에 잠만자기보다 좀 햇빛을 받으면서 글을 쓰러나오자.




+ Recent posts