상속 시 소멸자를 가상 함수로 만들어야 함
#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 소멸자 호출
delete p;를 하더라도 p가 가리키는 것은 Parent 객체가 아닌 Child 객체이기 때문에
보통의 Child 객체가 소멸되는 것과 같은 순서로 생성자와 소멸자들이 호출되어야 함
-> 그러나 실제로는 Child의 소멸자가 호출되지 않음
Child 객체에서 동적으로 할당하는 메모리가 있고, 소멸자에서 해제하는데
소멸자가 호출되지 않는다면 메모리 누수(memory leak)가 발생됨
=> virtual 키워드를 통해 해결할 수 있음
#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;
}
}
실행 결과
--- 평범한 Child 만들었을 때 ---
Parent 생성자 호출
Child 생성자 호출
Child 소멸자 호출
Parent 소멸자 호출
--- Parent 포인터로 Child 가리켰을 때 ---
Parent 생성자 호출
Child 생성자 호출
Child 소멸자 호출
Parent 소멸자 호출
Child 소멸자를 호출하면서 Child 소멸자가 알아서 부모의 소멸자도 호출해 주기 때문에
Parent 소멸자도 호출이 됨
반면 Parent 소멸자를 먼저 호출하면, Parent는 Child가 있는지 없는지 모르므로
Child 소멸자를 호출 해 줄 수 없음
class Parent
{
public:
virtual void func1();
virtual void func2();
};
class Child : public Parent
{
public:
virtual void func1();
void func3();
};
컴파일러는 가상 함수가 하나라도 존재하는 클래스에 대해서 가상 함수 테이블을 만들게 됨
가상 함수를 호출할 때는 해당 테이블을 거쳐서 어떤 함수를 사용할지 정하게 됨
Parent* p = Parent();
p->func1();
컴파일러는
(1) p가 Parent를 가리키는 포인터니까, func1()의 정의를 Parent 클래스에서 찾아봐야지
(2) func1()은 가상함수네? func1()을 직접 실행하는 게 아니라 가상 함수 테이블에서 func1() 에 해당하는 함수를 실행해야지
(3) 프로그램 실행 시에 가상 함수 테이블에서 func1()에 해당하는 함수를 호출함
-> 즉 일반적인 함수 보다 약간 더 시간이 오래 걸림
C++에서는 디폴트로 모든 멤버 함수를 가상 함수가 되도록 설정하지 않고
필요 시 virtual 키워드를 붙여서 사용함
#include <iostream>
class Animal {
public:
Animal() {}
virtual ~Animal() {}
virtual void speak() = 0;
};
class Dog : public Animal {
public:
Dog() : Animal() {}
void speak() override { std::cout << "왈왈" << std::endl; }
};
class Cat : public Animal {
public:
Cat() : Animal() {}
void speak() override { std::cout << "야옹야옹" << std::endl; }
};
int main() {
Animal* dog = new Dog();
Animal* cat = new Cat();
dog->speak();
cat->speak();
}
가상 함수에 =0;을 붙여서 반드시 오버라이딩 되도록 만든 함수를 순수 가상 함수라고 부름
순수 가상 함수는 본체가 없기 때문에 이 함수를 호출하는 것은 불가능함
-> 그렇기 때문에 Animal 객체를 생성하는 것 또한 불가능함
// 즉, 아래와 같은 코드는 불가능
Animal a;
a.speak();
Animal처럼 순수 가상 함수를 최소 한 개 이상 포함하고 있는 클래스는
객체를 생성할 수 없으며 인스턴스화 시키기 위해서는 이 클래스를 상속 받는
클래스를 만들어서 모든 순수 가상 함수를 오버라이딩 해 주어야만 함
-> 이렇게 순수 가상 함수를 최소 한 개 이상 포함하고 있는 클래스를 가리켜
추상 클래스라고 부름
(참고)
private 안에 순수 가상 함수를 정의하여도 문제 될 것이 없음
private 에 정의되어 있다고 해서 오버라이드 안된다는 뜻이 아니기 때문
(다만 자식 클래스에서 호출 불가)
추상 클래스는 비록 객체는 생성할 수 없지만 추상 클래스를 가리키는 포인터는
문제 없이 생성 가능함
Animal* dog = new Dog();
Animal* cat = new Cat();
dog->speak();
cat->speak();
한 클래스가 여러 개의 클래스를 상속 받는 것
생성자 호출 순서는 상속하는 순서에 좌우됨
#include <iostream>
class A
{
public:
int a;
A() { std::cout << "A 생성자 호출" << std::endl; }
};
class B
{
public:
int b;
B() { std::cout << "B 생성자 호출" << std::endl; }
};
class C : public A, public B
{
public:
int c;
C() : A(), B() { std::cout << "C 생성자 호출" << std::endl; }
};
int main()
{ C c; }
실행 결과
A 생성자 호출
B 생성자 호출
C 생성자 호출
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;
}
컴파일 시 오류 발생
c.a가 A의 a인지 B의 a인지 구분 할 수 없기 때문
// 다이아몬드 상속
class Human
{
// ...
};
class HandsomeHuman : public Human
{
// ...
};
class SmartHuman : public Human
{
// ...
};
class Me : public HandsomeHuman, public SmartHuman
{
// ...
};
상속이 되는 두 개의 클래스가 공통의 베이스 클래스를 포함하고 있는 형태를 가리켜서
다이아몬드 상속이라고 부름
만약 Human에 name이라는 멤버 변수가 있다면 HandsomeHuman과 SmartHuman 모두 name이라는 변수를 가짐
그런데 Me 가 이 두 개의 클래스를 상속 받으니 name 이라는 변수가 겹치게 됨
-> 해결 방법
Human을 virtual로 상속 받음
class Human
{
public:
// ...
};
class HandsomeHuman : public virtual Human
{
// ...
};
class SmartHuman : public virtual Human
{
// ...
};
class Me : public HandsomeHuman, public SmartHuman
{
// ...
};
virtual 형태로 Human을 상속 받으면 Me에서 다중 상속 시에도
컴파일러가 언제나 Human을 한 번만 포함하도록 지정할 수 있게 됨
(참고)
가상 상속 시 Me의 생성자에서 HandsomeHuman, SmartHuman, Human의 생성자를 호출해주어야만 함