가상 소멸자(Virtual Destructor)
class First
{
...
public:
virtual ~First() { ... }
};
class Second: public First
{
...
public:
virtual ~Second() { ... }
};
class Third: public Second
{
...
public:
virtual ~Third() { ... }
};
int main(void)
{
First *ptr = new Third();
delete ptr;
...
}
위 코드에서 클래스의 소멸자에 virtual 이 명시된 것을 볼 수 있다.
만약 virtual 명시가 없을 경우, 생성자와는 다르게 소멸자는 타입으로 설정된 Fisrt 의 소멸자만 호출된다.
그렇게되면 객체의 전체 레이아웃은 해제되나, 자식 클래스 영역에 할당된 자원들은 해제되지 않아서 leak 문제가 발생한다.
이전 챕터에서 다웠던 가상 함수랑 유사하면서 조금 다른 메커니즘으로 소멸자를 호출하는데
가상 함수는 virtual 을 사용함으로써 생성된 객체의 클래스를 기준으로 가장 마지막 오버라이딩된 함수를 호출한다면
https://code-jh.tistory.com/102
가상 소멸자는 상속 과정에서 거친 모든 클래스의 소멸자를 호출하여 각각의 해당하는 파트의 자원을 해제시켜준다
참조자의 참조 가능성
C++ 에서 AAA 형 참조자는 AAA 객체 또는 AAA 를 직적 혹은 간접적으로 상속하는 모든 객체를 참조할 수 있다.
class First
{
public:
void FirstFunc() { cout << "FirstFunc()" << endl; }
virtual void SimpleFunc() ( cout << "First's SimpleFunc()" << endl; }
}
class Second: public First
{
public:
void SecondFunc() { cout << "SecondFunc()" << endl; }
virtual void SimpleFunc() ( cout << "Second's SimpleFunc()" << endl; }
};
class Third: public Second
{
public:
void ThirdFunc() ( cout << "ThirdFunc()" << endl; }
virtual void SimpleFunc() ( cout << "Third's SimpleFunc()" << endl; }
};
int main(void)
{
Third obj;
obj.FirstFunc();
obj.SecondFunc();
obj.ThirdFunc();
obj.SimpleRunc();
Second& rsef = obj;
sref.FirstFunc();
sref.SecondFunc();
srdf.ThirdFunc();
sref.SimpleFunc();
First& fref = obj;
fref.FirstFunc();
fref.SimpleFunc();
return ;
}
----------------------------------------------------------------
// 결과
FirstFunc()
SecondFunc()
ThirdFunc()
Third's SimpleFunc()
FirstFunc()
SecondFunc()
Thrid's SimpleFunc()
FirstFunc()
Third's SimpleFunc()
[ 가상의 원리와 다중상속]
멤버함수와 가상함수의 동작 원리
객체 안에 정말로 멤버함수가 존재하는가?
// 클래스 Data를 흉내 낸 영역
typedef struct Data
{
int data;
void (*ShowData)(Data*);
void (*Add)(Data*, int);
} Data;
void ShowData(Data* THIS) { cout << "Data: " << THIS->data << endl; }
void Add(Data *THIS, int num) { THIS->data += num; }
// 적절히 변경된 main 함수
int main(void)
{
Data obj1 = {15, ShowData, Add};
Data obj2 = {7, ShowData, Add};
obj1.Add(&obj1, 17);
obj2.Add(&obj2, 9);
obj1.ShowData(&obj1);
obj2.ShowData(&obj2);
return 0;
}
구조체가 같은 함수 포인터를 가지고 있을지라도, 인자로 던져주는 THIS 포인터를 구조체 각각의 주소를 전달함으로써,
각자에게 맞는 결과를 취할 수 있도록 유도되는 메커니즘이
C++ 에서 객체의 멤버 함수와 인스턴스에서 활용되는 메서드의 기본 메커니즘이다.
가상함수의 동작원리와 가상함수 테이블
class AAA
{
private:
int num1;
public:
virtual void Func1() { cout << "Func1" << endl; }
virtual void Func2() { cout << "Func2" << endl; }
};
class BBB: public AAA
{
private:
int num2;
public:
virtual void Func1() { cout << "BBB::Func1" << endl; }
void Func3() { cout << "Func3" << endl; }
};
int main(void)
{
AAA *aptr = new AAA();
aptr->Func1();
BBB *bprt = new BBB();
bptr->Func1();
return 0;
}
---------------------------------------------------------
// 결과
Func1
BBB::Func1
하나 이상의 가상함수가 멤버로 포함되면 위와 같은 형태의 V-Table 이 생성되고 매 함수 호출시마다 이를 참조하게 된다.
BBB 클래스에서 오버라이딩 된 Func1 로 인해서 BBB 클래스의 가상테이블에는 기존의 AAA::Func1은 사라지고,
재정의된 BBB::Func1 이 남아있는 것을 주의깊게 볼 필요가 있다.
업캐스팅된 객체에서 하위 클래스에 해당하는 요소를 직접적으로 활용할 수 없는게 기본 상태이지만,
상위에서 부터 virtual 로 오버라이딩된 함수일 경우엔 하위 클래스의 함수를 호출할 수 있게 된다.
(업캐스팅된 객체의 클래스 구조의 상위에서 virtual 없이 명시된 함수를
하위에서 virtual 을 명시한 후 해당 함수를 호출할 경우엔
이를 오버라이딩 된 것으로 간주하지 않고, 별도의 함수로 취급하게 된다.
상위에선 해당 함수가 virtual 이 아니기 때문에 v-table 을 확인하지 않고, 바로 상위의 함수를 호출한다는 것이다.
격이 맞는 객체로(클래스 == 객체) 생성됐을 경우엔 virtual 취급이 가능하지만 크게 의미가 있는가 싶은 대목이다)
각 클래스는 virtual 로 오버라이딩 된 함수가 가지는 주소는 클래스 별로 생성된 v-table 의 주소이다.
다중 상속(Multiple Inheritance) 에 대한 이해
다중 상속의 설명에 앞서
- 많은 사람들이 다중상속은 득보다 실이 더 많은 문법이라고들 한다.
- 일반적인 경우 다중상속은 다양한 문제를 동반한다
- 가급적 사용하지 않아야 하는 의견에 동의율이 많다
- 그러나 예외적으로 매우 제한적인 사용까지 부정할 필요는 없다고 본다.
class BaseOne
{
public:
void SimpleFuncOne() { cout << "BaseOne" << endl; }
};
class BaseTwo
{
public:
void SimpleFuncTwo() { cout << "BaseTwo" << endl; }
};
class MultiDerived: public BaseOne, protected BaseTwo
{
public:
void ComolexFunc()
{
SimpleFuncOne();
SimpleFuncTwo();
}
};
int main(void)
{
MultiDerived mdr;
mdr.ComplexFunc();
return 0;
}
---------------------------------------------
// 결과
BaseOne
BaseTwo
다중 상속은 말 그대로 둘 이상의 클래스를 상속하는 형태이고,
이로 인해서 유도 클래스의 객체는 모든 기초 클래스의 멤버를 포함하게 된다.
다중상속에서 메서드를 통일할 경우에 다중상속의 모호성을 확인 할 수 있다.
class BaseOne
{
public:
void SimpleFunc() { cout << "BaseOne" << endl; }
};
class BaseTwo
{
public:
void SimpleFunc() { cout << "BaseTwo" << endl; }
};
class MultiDerivde: public BaseOne, protected BaseTwo
{
public:
void ComplexFunc()
{
BaseOne::SimpleFunc(); // 호출 대상 구분
BaseTwo::SimpleFunc(); // 호출 대상 구분
}
};
이렇게 선 뜻 구분할 수 없기 때문에 호출의 대상을 구분해서 명시해야 하는 불편함이 동반된다.
이보다 더한 모호한 상황을 확인해보자면
class Base
{
public:
Base() { cout << "Base Constructor" << endl; }
void SimpleFunc() { cout << "BaseOne" << endl; }
};
class MiddleDerivedOne: virtual public Base
{
public:
MiddelDerivedOne(): Base()
{
cout << "MiddelDErivedOne Constructor" << endl;
}
void MiddleFuncOne()
{
SimpleFunc();
cout << "MiddelDerivedOne" << endl;
}
};
class MiddleDerivedTwo: virtual public Base
{
public:
MiddleDerivedTwo(): Base()
{
cout << "MiddelDerivedTwo Constructor" << endl;
}
void MiddleFuncTow()
{
SimpleFunc();
cout << "MiddleDerivedTwo" << endl;
}
};
class LastDerived: public MiddleDerivedOne, public MiddleDerivedTwo
{
public:
LastDerived(): MiddleDerivedOne(), MiddleDerivedTwo()
{
cout << "LastDerived Constructor" << endl;
}
void ComplexFunc()
{
MiddleFuncOne();
MiddleFuncTwo();
SimpleFunc();
}
};
LastDerived 는 Base 를 상속받은 MiddleDerivedOne 과 MiddleDerivedTwo 로 인해 2개의 Base 를 갖게 된다.
(이는 다이아몬드 상속이라고도 부른다)
이와 같인 동일한 Base 의 멤버가 중복적인 것은 좋은 프로그램을 작성하는 방법과는 거리가 있는 형태이다.
다중 상속을 받더라도, 기초 클래스의 멤버 중복을 제한하고자 사용되는 게 virtual 상속이다.
virtual 상속은 상속 계층에서 같은 기초 클래스를 여러 경로를 통해 상속 받을 때,
해당 기초 클래스의 인스턴스를 단 하나만 공유하도록 명시하는 상속 방식이다.
virtual 상속의 실질적인 효과는 파생 클래스의 공통 조상이 중복 상속되는 상황에서만 드러나기 때문에
조부모 기준으로 중복 인스턴스가 제거된다.
'C++ > 윤성우의 열혈 프로그래밍 C++' 카테고리의 다른 글
윤성우의 열혈 C++ chpt 12. (0) | 2025.07.28 |
---|---|
윤성우의 열혈 C++ chpt 11. (0) | 2025.07.28 |
윤성우의 열형 C++ chpt.10 (2) | 2025.07.25 |
윤성우의 열혈 C++ chpt9. (4) | 2025.07.21 |
윤성우의 열혈 C++ chpt8. (0) | 2025.07.02 |