근본적으로는 결국 base class의 public method를 통해 초기화하는 것이다.
derived class의 constructor는 다음과 같이 작동한다.
base class의 default constructor가 호출된다.
derived-class의 constructor는 base-class information을 매개변수로 받았을테니, 이를 base class의 생성자를 호출해서 데이터를 저장한다.
derived-class에서 추가된 data를 저장한다.
destructor는 반대로 작동한다.
Special Relationships Between Derived and Base Classes
base-class와 derived class는 특별한 관계성을 가진다.
먼저 base-class의 메소드 중에서 private이 아닌 메소드를 사용할 수 있다.
derived class의 포인터나 레퍼런스는 base-class의 포인터나 레퍼런스로 implicit conversion할 수 있다. 이 rule relaxion은 단방향이다. 즉, base-class의 포인터나 레퍼런스는 derived class의 포인터나 레퍼런스로 implicit conversion이 불가하다. dynamic_cast를 통한 explicit conversion은 가능하다.
Inheritance: An Is-a Relationship
Inheritance는 기본적으로 Is-a 관계를 만들어낸다. Is-a 관계는 Is-a-kind-of의 뜻이라고 생각하면 더 편하다.
포도나 오렌지는 과일이다. 하지만 lunch는 fruit를 포함하겠지만 과일이라고는 할 수 없다.
다시 강조하자면, Inheritance는 클래스의 특성을 추가할 뿐이다.
The Need for Virtual Destructors
만약, destructor가 virtual이 아니라면 base-class의 destructor만 부르게 되는 상황이 발생한다. 만약 derived-class의 생성자에서 new라도 했다가는 leak이 발생하는 것이다.
Static and Dynamic Binding
"function call이 있을 때, 어떤 function을 실행해야 하는가?"는 컴파일러가 대답해야 한다. C에서는 이걸 결정하는 게 아주 쉬웠다. 이름만 찾아서 매칭해주면 끝이었다. C++은 다르다. 함수 오버로딩 등이 있기 때문이다. 하지만 함수 오버로딩 등을 포함해서 왠만해서 이런 매칭은 컴파일 타임에 일어난다.
함수 콜과 실제 함수 코드를 매칭해주는 것을 Binding이라고 부른다. Binding이 컴파일 타임에 일어나면 Static Binding, 런타임에 일어나면 Dynamic Binding이다. virtual function은 Dynamic Binding이다.
Dynamic Binding은 런타임에 타입에 따른 함수를 매칭해주기 때문에 편하다. 그렇다면 두가지 질문이 생길 것이다.
그런데 왜 Dynamic Binding이 Default가 아닌가?
Virtual function은 어떻게 작동하는가?
Why Two Kinds of Binding and Why Static Is the Default?
여기엔 두 가지 이유가 있다. 효율성(efficiency)과 클래스 디자인(a conceptual model)이다.
Virtual Function의 작동 방식을 살펴보면 알겠지만, Dynamic Binding은 기존 함수 호출에 한단계가 더 추가되면서 오버헤드가 발생한다. Static Binding은 이를 컴파일에 다 해결해놓기 때문에 비용이 더 싸다. Stroustrup은 C++ 원칙 중 하나로 '사용하지 않는 기능으로 인한 비용을 지불하지 않는다'(you shouldn't have to pay for features you don't use)를 말했다.
클래스 디자인 측면에서 virtual을 함수에 넣지 않음으로써 이 함수가 재정의되기를 원하지 않는다는 의도를 강조할 수 있다.
How Virtual Functions Work
C++은 Virtual 함수의 특징을 서술하지만 어떻게 작동해야 하는지는 컴파일러에게 맡긴다. 그래서 컴파일러마다 작동방식은 다르다.
보통 컴파일러는 virtual function을 관리하기 위해서 각 오브젝트마다 숨겨진 멤버 변수를 추가한다. 이 변수는 virtual function들을 담은 table을 가리키는 포인터다. 포인터를 vptr, 테이블을 vtable이라고 부른다. vtable은 해당 클래스의 가상 함수의 주소를 담고 있다.
출처: C++ Primer Plus 6th 741p
virtual function을 쓴다는 것은 다음의 side effect를 일으킨다.
각 오브젝트의 크기가 vptr 등으로 인해 증가한다.
각 클래스마다 컴파일러가 vtable을 만든다.
virtual function을 호출할 때마다 vtable을 조회하는 추가적인 절차가 생긴다.
Redefinition Hides Methods
다음 상속 관계를 보자.
class Dwelling
{
public:
virtual void showperks(int a) const;
...
};
class Hovel : Dwelling
{
public:
virtual void showperks() const; // 재정의했는데, argument가 다르다.
}
이러면 showperks는 int를 받는 버전과 받지 않는 버전 이렇게 2개가 생기는 것일까? 그렇지 않다. 위와 같은 코드는 warning을 만든다.