virtual 소멸자
상속시에 소멸자 처리(중요!!)
잘못된 예시코드
#include <iostream>
class Parent {
public:
Parent() { std::cout << "Parent 생성자 호출" << std::endl; }
~Parent() { std::cout << "Parent 소멸자 호출" << std::endl; }
};
class Child : public Parent {
public:
Child() : Parent() { std::cout << "Child 생성자 호출" << std::endl; }
~Child() { std::cout << "Child 소멸자 호출" << std::endl; }
};
int main() {
std::cout << "--- 평범한 Child 만들었을 때 ---" << std::endl;
{ Child c; }
std::cout << "--- Parent 포인터로 Child 가리켰을 때 ---" << std::endl;
{
Parent *p = new Child();
delete p;
}
}
실행결과
--- 평범한 Child 만들었을 때 ---
Parent 생성자 호출
Child 생성자 호출
Child 소멸자 호출
Parent 소멸자 호출
--- Parent 포인터로 Child 가리켰을 때 ---
Parent 생성자 호출
Child 생성자 호출
Parent 소멸자 호출
- 일반적인 객체를 만들때는 문제가 되지 않지만, 소멸자가 호출되지 않아서 문제(메모리 누수)가 발생 한다.
고친 코드
#include <iostream>
class Parent {
public:
Parent() { std::cout << "Parent 생성자 호출" << std::endl; }
virtual ~Parent() { std::cout << "Parent 소멸자 호출" << std::endl; }
};
class Child : public Parent {
public:
Child() : Parent() { std::cout << "Child 생성자 호출" << std::endl; }
~Child() { std::cout << "Child 소멸자 호출" << std::endl; }
};
int main() {
std::cout << "--- 평범한 Child 만들었을 때 ---" << std::endl;
{
// 이 {} 를 빠져나가면 c 가 소멸된다.
Child c;
}
std::cout << "--- Parent 포인터로 Child 가리켰을 때 ---" << std::endl;
{
Parent *p = new Child();
delete p;
}
}
- 상속될 여지가 있는
Base
클래스들은 (위 경우Parent
) 반드시 소멸자를virtual
로 만들어주어야 나중에 문제가 발생할 여지가 없게 됩니다.
- 여기서 왜
Parent
소멸자는 호출이 되었는가를 보면 Child 소멸자를 호출하면서, Child 소멸자가 '알아서' Parent 의 소멸자도 호출해주기 때문입니다. (Child 는 자신이 Parent 를 상속받는다는 것을 알고있기 때문에)
가상함수 구현 원리
그냥 그럼 모든 함수들을 virtual
로 만들어버리면 안되나?
실제로 자바의 경우 모든 함수들이 디폴트로 virtual
함수로 선언한다.
C++ 에서는 virtual
키워드를 이용해 사용자가 직접 virtual
선언하도록 하였을까요? 그 이유는 가상 함수를 사용하게 되면 약간의 오버헤드 (overhead)가 존재하기 때문 (보통의 함수를 호출하는 것 보다 가상 함수를 호출하는 데 걸리는 시간이 조금 더 오래 걸립니다.)
가상함수 테이블
예시
class Parent {
public:
virtual void func1();
virtual void func2();
};
class Child : public Parent {
public:
virtual void func1();
void func3();
};
위 경우 Parent 와 Child 모두 가상 함수를 포함하고 있기 때문에 두 개 다 가상 함수 테이블을 생성하게 되지요. 그 결과 아래 그림과 같이 구성된다.
비 가상함수들은 그냥 단순히 특별한 단계를 걸치지 않고, func3()
을 호출하면 직접 실행됩니다.
하지만, 가상 함수를 호출하였을 때는 그 실행 과정이 다릅니다. 위에서도 보이다 싶이, 가상 함수 테이블을 한 단계 더 걸쳐서, 실제로 어떤 함수를 고를지 결정한다.
예시
Parent* p = Parent();
p->func1();
컴파일러는
p 가 Parent 를 가리키는 포인터 이니까, func1() 의 정의를 Parent 클래스에서 찾아봐야겠다.
func1() 이 가상함수네? 그렇다면 func1() 을 직접 실행하는게 아니라, 가상 함수 테이블에서 func1() 에 해당하는 함수를 실행해야겠다.
그리고 실제로 프로그램 실행시에, 가상 함수 테이블에서 func1() 에 해당하는 함수(Parent::func1()) 을 호출하게 됩니다.
Parent* c = Child();
c->func1();
위 처럼 똑같이 프로그램 실행시에 가상 함수 테이블에서 func1() 에 해당하는 함수를 호출하게 되는데, 이번에는 p 가 실제로는 Child 객체를 가리키고 있으므로, Child 객체의 가상 함수 테이블을 참조하여, Child::func1() 을 호출하게 됩니다. 따라서 성공적으로 Parent::func1() 를 오버라이드 할 수 있습니다.
순수 가상함수
순수가상함수란
순수 가상함수 예시코드
class Animal {
public:
Animal() {}
virtual ~Animal() {}
virtual void speak() = 0;
};
순수 가상함수 특징
추상클래스란 무엇인가
- 추상클래스의 사용이유는 설계도라고 보면 된다. 상속을 다 하지만 즉 동물의 경우 우는 소리가 다 다른경우 다 다르게 구현을 해야하기 때문에 순수 가상함수를 사용하는 것이다.
- 추상 클래스는 객체는 못 만들지만, 추상 클래스를 가리키는 포인터는 문제없이 만들 수 있다!!
- (참고로, private 안에 순수 가상 함수를 정의하여도 문제 될 것이 없습니다. private 에 정의되어 있다고 해서 오버라이드 안된다는 뜻이 아니기 때문이죠. 다만 자식 클래스에서 호출을 못할 뿐입니다.)
다중상속(multiple inheritance)
예시
class A {
public:
int a;
};
class B {
public:
int b;
};
class C : public A, public B {
public:
int c;
};
다중상속의 생성자 호출순서!!
-무조건 상속 순서로 호출 된다.
호출순서 예시
class C : public A, public B
: A생성자 호출 , B 생성자 호출, C 생성자 호출
class C : public B, public A
: B생성자 호출 , A생성자 호출, C 생성자 호출
주의할점 : 모호성 문제
class A {
public:
int a;
};
class B {
public:
int a;
};
class C : public B, public A {
public:
int c;
};
위와 같은 문제에서
int main() {
C c;
c.a = 3;
}
복되는 멤버 변수에 접근한다면 모호성문제가 발생합니다.
주의할점 : 다이아몬드 상속(diamond inheritance)
다이아몬드 상속 문제
class Human {
// ...
};
class HandsomeHuman : public Human {// 여기서도 휴먼 겹침
// ...
};
class SmartHuman : public Human {//여기서 휴먼 겹침
// ...
};
class Me : public HandsomeHuman, public SmartHuman {
// ...
};
사진
이렇게 생긴 그림에서 Human의 멤버를 호출하게 된다면 어떤 Human을 호출해야하는지 찾기가 어려워서 모호성이 발생합니다.
해결방법 :가상상속
가상 상속시에 Me생성자를 통해 HandsomeHuman 과 smartHuman 의 생성자를 호출하는 건 당연하지만 반드시 Human생성자도 호출해야만 한다.
놀랍게도 아래 예시코드로 실행을 하면 자동으로 human의 생성자가 먼저 생성이 되고 그 다음 handsome smart me 의 생성자가 자동으로 생성이 된다. (신기하다!!!)
class Human {
public:
// ...
};
class HandsomeHuman : public virtual Human {//가상상속
// ...
};
class SmartHuman : public virtual Human {
// ...
};
class Me : public HandsomeHuman, public SmartHuman {
// ...
};
Human 을 virtual 로 상속 받는다면, Me 에서 다중 상속 시에도, 컴파일러가 언제나 Human 을 한 번만 포함하도록 지정할 수 있게 됩니다. 참고로, 가상 상속 시에, Me 의 생성자에서 HandsomeHuman 과 SmartHuman 의 생성자를 호출함은 당연하고, Human 의 생성자 또한 호출해주어야만 합니다.
다중상속을 언제 사용할까
c++공식 웹사이트의 FAQ : https://isocpp.org/wiki/faq/multiple-inheritance#virtual-inheritance-where 여기에 가이드 라인이 존재한다.
- 우리는 3가지 방법으로 클래스 디자인이 가능하다.
브리지패턴 (bridge pattern)
- 한가지 카테고리를 멤버 포인터로 설정하고 환경이 1개씩 추가되면 클래스를 한개 더 만들면 된다.
- 클래스의 총 갯수가 N + M개가 된다. 단, 오버라이딩 가지수가 N+M 개라서 가능한 알고리즘의 갯수도 N+ M개라 섬세한 제어가 어렵다.
- 예시로 자동차의 파생클래스로 엔진 엔진의 파생클래스로 가스연료 이런식으로 파생해서 계속해서 생기는 클래스의 패턴을 의미
중첩된 일반화(nested generalization)
- 한가지 계층을 골라 파생클래스 생성한다.
- 각각의 클래스들에 대해서 파생 클래스를 생성한다. M *N 가지의 파생 클래스를 만들 수 있어서 섬세한 제어가 가능하다.
- 최대 N개의 파생 클래스를 만들어줘야하고 만약 공통적으로 사용되는 코드가 있어도 매번 새로 작성해줘야하는 단점이 있다.
- 예를들어 탈것의 파생클래스로 땅에 탈것, 물에 탈것 이렇게 설정. 이후 다른 클래스들에 대해 다른계층에 해당하는 파생클래스 생성한다. 조금씩 중첩시키는 것이다. 땅에타는데 에너지가 가스인것, 땅에 타는데 에너지가 수소연료인것 이런식으로 점점 더 중첩시켜서 구체화 시키는 것이다.
다중상속
- 브리지패턴처럼 각 카테고리에 해당하는 파생클래스를 생성한다.
- 하지만 멤버변수를 없애고 상속을 받는 형식이다.
- 섬세한 제어를 수행하고 모든걸 가리키기가 쉽다는 장점이있다.
- 이건 멤버 변수들을 없애고 동력원과 환경에 해당하는 클래스들을 상속받는 방식이다.
결론
위 3 가지 방식 중에서 절대적으로 우월한 방식은 없다 는 것입니다. 상황에 맞게 최선의 방식을 골라서 사용해야 합니다.
참고 : https://modoocode.com/211
Uploaded by N2T
'CPP' 카테고리의 다른 글
[cpp개념공부]클래스 멤버 포인터 , 함수 포인터(CPP Module02) (5) | 2023.02.28 |
---|---|
[cpp개념공부] throw ,try-catch (2) | 2023.02.18 |
[cpp개념공부] 가상함수와 업 캐스팅 (0) | 2023.02.11 |
[Cpp 개념공부] 상속 (함수 오버라이딩, 상속 접근지시자) (0) | 2023.02.10 |
[cpp 개념공부]연산자 오버로딩 (사용자 지정 연산자, 대입연산자, 입출력 오버로딩 ,첨자 연산자 Wrapper class) (1) | 2023.02.06 |