모든 상속 관계는 is-a 관계이다.
이를 뒤바꾸면 성립되지 않는다.
클래스가 파생되면 파생될 수록 좀 더 특수화가 된다.
반대로, 기반 클래스로 거슬러 올라가면 올라갈 수록 좀 더 일반화된다.
또 다른 클래스 간의 관계로, has-a 관계 역시 존재한다.
has-a 관계는 보통 아래와 같이 객체를 프로퍼티로 가지는 경우를 말한다.
class Car {
private:
Engine e;
Brake b; // 아마 break 아니냐고 생각하는 사람들이 있을 텐데 :)
};
파생 클래스의 객체를 기반 클래스의 포인터로 가리키는 것
-> 파생 클래스의 객체를 기반 클래스의 객체처럼 다룰 수 있게 함
따지고 보면 Derived is a Base 이기 때문에,
Base 객체를 가리키는 용도의 포인터(p_c라고 하자)가 Derived 객체(c라고 하자)도 가리킬 수 있다.
Base* p_c = &c;
이를 그림으로 표현하면 아래와 같음.
대신 p_c는 엄연한 Base 객체를 가리키는 포인터임.
따라서 p_c는 Base 클래스의 what함수를 호출한다(Derived의 what 함수를 호출할 것 같지만 아니다).
그렇기 때문에 Base 클래스에 what함수가 존재해야 한다.
반대로, 아래와 같이 다운 캐스팅을 시도하고 what()을 호출한다면 에러가 뜬다.
Base* p_p = &c;
Derived* p_c = p_p;
p_c->what();
그러나 우리는 what이 오버라이딩된 메소드이기 때문에 현재 상태에서는 저렇게 호출해도 아무 문제 없다는 것을 잘 안다.
따라서 아래와 같이 강제적으로 타입 변환을 할 수 있다.
static_cast가 아닌 dynamic_cast로 해야 함!
이럴 경우, 컴파일 오류를 발생시키지 않고 성공적으로 컴파일할 수 있다.
그러나 강제적으로 다운 캐스팅을 하는 경우 컴파일 타임에서 오류를 찾아내기 매우 힘들기 때문에 권장하지 않음!
#include <iostream>
class Base {
public:
Base() { std::cout << "기반 클래스" << std::endl; }
virtual void what() { std::cout << "기반 클래스의 what()" << std::endl; } };
class Derived : public Base { public:
Derived() : Base() { std::cout << "파생 클래스" << std::endl; } void what() { std::cout << "파생 클래스의 what()" << std::endl; }
};
int main() {
Base p;
Derived c;
Base* p_c = &c;
Base* p_p = &p;
std::cout << " == 실제 객체는 Base == " << std::endl; p_p->what();
// 기반 클래스의 what()
std::cout << " == 실제 객체는 Derived == " << std::endl; p_c->what();
// 파생 클래스의 what()
return 0;
}
따라서 p_p -> what()과 p_c-> what()을 하면 모두 Base 객체의 what() 함수가 실행되어서 둘다 기반이라고 출력이 되어야만 함.
그런데 이때 virtual 키워드로 동적 바인딩을 행하게 한다.
동적 바인딩 = 컴파일 시에 어떤 함수가 실행될 지 정해지지 않고 런타임 시에 정해지는 일을 가리킴
즉, p_c는 Base 포인터: Base의 what
실행하려다가 -> virtual
키워드 발견
-> 실제 Base 객체가 아니라는 걸 확인(Derived 객체) -> Derived의 what
을 실행
Parent 클래스를 상속 받는 Child 클래스의 객체가 소멸할 때
1. Parent 생성자 호출
2. Child 생성자
3. Child 소멸자
4. Parent 소멸자
순으로 호출됨
그러나 Parent 포인터가 Child 객체를 가리킬 때는 다른 양상을 보임
std::cout << "--- Parent 포인터로 Child 가리켰을 때 ---" << std::endl; {
Parent *p = new Child();
delete p;
}
delete p를 해도 사실상 Child 객체이기 때문에(p는) Child 소멸자가 호출이 되어야 하는데, 호출되지 않음.
-> 메모리 누수(memory leak)와 같은 문제가 생김
문제 해결: Parent의 소멸자를 virtual로 만들어버리면 된다.
그러면 p가 소멸자를 호출할 때 Child의 소멸자를 성공적으로 호출할 수 있음.
virtual ~Parent() { std::cout << "Parent 소멸자 호출" << std::endl; } };
레퍼런스 역시 가능하다.
함수를 virtual로 선언했을 때(즉 가상 함수를 사용할 때) 약간의 오버헤드가 발생
이 오버헤드가 생기는 과정을 이해해보자!
class Parent {
public:
virtual void func1();
virtual void func2();
};
class Child : public Parent {
public:
virtual void func1();
void func3();
};
c++ 컴파일러는 가상 함수 테이블을 만듦.
가상 함수 테이블 = 함수의 이름 - 실제 어떤 함수가 대응되는 지에 대한 테이블
이를 그림으로 나타내면 다음과 같다.
가상함수 호출할 경우: 가상 함수 테이블을 한 단계 더 거쳐서, 실제로 어떤 함수를 고를 지 결정함
Parent* p = Parent();
p->func1();
위 예에서 컴파일러는
가상 함수 테이블에서 func1()에 해당하는 함수
를 실행해야 함++ p-> Child 객체를 가리킨다면(Parent 포인터), Child가 가리키는 가상 함수 테이블을 따라가서 그에 해당하는 함수를 실행함.
이와 같이 행동을 함.
-> 즉 2번에 걸쳐서 행동을 하기 때문에 미미하지만 시간이 더 오래 걸림
순수 가상 함수:
=0;
으로 처리되어있으며, 반드시 오버라이딩 되어야만 하는 함수
#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();
}
위 코드에서 Animal 객체를 생성하려고 하면 컴파일 오류 생김!
이렇게 순수 가상 함수를 최소 한개 포함하고 있는- 반드시 상속 되어야 하는 클래스를 가리켜 추상 클래스 (abstract class)라고 부름.
생성자 호출 순서는 상속 순서에 좌우됨.
virtual로 상속 받으면 1번만 호출할 수 있음(생성자를)
그런데
H1 -> human 상속 받음
H2 -> human 상속 받음
H3 -> H1, H2 상속 받음
이럴 경우 H3는 human 생성자를 2번 호출한다.
이때 애초에 H1, H2 에서 human 클래스를 상속 받을 때 virtual 키워드로 명시해 주면 1번만 호출해 줄 수 있다.