[C++ Primer Plus] 13. Class Inheritance
Coding/Unreal, C++

[C++ Primer Plus] 13. Class Inheritance

대답할 질문

  • 상속을 통해 어떤 일을 할 수 있는가?
  • derived-class의 constructor와 destructor는 어떻게 작동하는가?
  • Is-a relationship은 무엇인가?
  • dynamic binding이 default가 아닌 이유가 무엇인가?
  • virtual function은 어떻게 작동하는가?
  • derived-class에서 함수를 재정의할 때 무엇을 유의해야 하는가?
  • Abstract Base Class는 어떻게 만들 수 있는가? 왜 만드는가?

Class Inheritance

  • OOP의 목적 중 하나가 코드 재사용성이다. Inheritance(상속)는 이 특성을 강화한다.
  • C library를 생각해보자. 따로 소스 코드가 제공되지 않는 라이브러리라면 우리는 특정 함수를 입맛대로 바꾸기 어려울 것이다. 하지만 C++은 Class를 제공함으로써 이를 해결한다. Class Inheritance를 통해 우리는 세가지 일을 할 수 있다.
    • 기존 클래스에 새로운 기능을 추가한다.
    • 기존 클래스의 기능을 변경한다.
    • 클래스에 새로운 데이터를 추가한다.

Beginning with a Simple Base Class

  • 상속 관계에서 기존 클래스를 base class,  상속받는 클래스를 derived class라고 부른다.
  • derived class는 base class의 private 멤버에게 접근할 수 있는 권한이 없다. 그렇다보니 derived class의 생성자를 만들 때, 애매한 경우가 생길 수 있는데, 이는 base class의 생성자를 통해 해결할 수 있다.
RatedPlayer::RatedPlayer(unsigned int r, const string & fn, const string& ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
	rating = r;
}
  • 근본적으로는 결국 base class의 public method를 통해 초기화하는 것이다. 
  • derived class의 constructor는 다음과 같이 작동한다.
    1. base class의 default constructor가 호출된다.
    2. derived-class의 constructor는 base-class information을 매개변수로 받았을테니, 이를 base class의 생성자를 호출해서 데이터를 저장한다.
    3. 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은 런타임에 타입에 따른 함수를 매칭해주기 때문에 편하다. 그렇다면 두가지 질문이 생길 것이다.
    1. 그런데 왜 Dynamic Binding이 Default가 아닌가?
    2. 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를 일으킨다.
    1. 각 오브젝트의 크기가 vptr 등으로 인해 증가한다.
    2. 각 클래스마다 컴파일러가 vtable을 만든다.
    3. 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을 만든다.
Warning: Hovel::showperks(void) hides Dwelling::showperks(int)
  • 상속 관계에서 재정의는 함수 오버로딩을 추가하는 형태로 이뤄지지 않는다. 아예 이전 내용을 모두 가리고 그 위에 새로 쓰는 방식이다.
  • 이로 인해 재정의를 할 때 지켜야 하는 규칙이 생긴다.
    • 만약 base-class의 메소드를 재정의해야 한다면 함수 시그니쳐를 동일하게 가져가야 한다. base-class -> derived-class로의 변화는 괜찮다.
    • 만약 메소드가 오버로딩이 있다면 그 오버로딩을 전부 재정의해야 한다.

Access Control: protected

  • 기본적으로 protected는 private과 비슷하다. protected인 멤버는 바깥에서 접근할 수 없다. 하지만 상속관계에서는 public과 같다. derived-class가 base-class의 protected 멤버에 접근할 수 있다.

Abstract Base Classes (ABC)

  • 어떤 클래스가 ABC가 되려면 적어도 한 개이상의 pure virtual function이 있어야 한다. pure virtual function은 다음처럼 생겼다.
virtual void Move(int nx, int ny) = 0;
  • pure virtual function은 사실상 시그니쳐만 있는 함수 껍데기이므로 해당 함수를 가진 클래스는 인스턴스화될 수 없다.
  • ABC를 통해 클래스의 인터페이스를 정해줄 수 있다.

Class Design Review

  • Copy Constructor는 다음 상황에서 호출된다.
    1. 같은 클래스인 다른 오브젝트로부터 새로운 오브젝트를 생성할 때
    2. 오브젝트가 pass by value로 함수로 넘겨질 때
    3. 함수가 오브젝트를 value로 리턴할 때
    4. 컴파일러가 임시 오브젝트를 생성할 때 

References

  • C++ Primer Plus 6th, Chapter 13