대답할 질문
- 상속을 통해 어떤 일을 할 수 있는가?
- 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는 다음과 같이 작동한다.
- 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은 해당 클래스의 가상 함수의 주소를 담고 있다.
- 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을 만든다.
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는 다음 상황에서 호출된다.
- 같은 클래스인 다른 오브젝트로부터 새로운 오브젝트를 생성할 때
- 오브젝트가 pass by value로 함수로 넘겨질 때
- 함수가 오브젝트를 value로 리턴할 때
- 컴파일러가 임시 오브젝트를 생성할 때
References
- C++ Primer Plus 6th, Chapter 13
'Coding > Unreal, C++' 카테고리의 다른 글
[C++ Primer Plus] 15. Friends, Exceptions, and More (0) | 2022.06.24 |
---|---|
[C++ Primer Plus] 14. Reusing Code in C++ (0) | 2022.06.23 |
[C++ Prime Plus] 12. Classes and Dynamic Memory Allocation (0) | 2022.06.21 |
[C++ Prime Plus] 11. Working with Classes (0) | 2022.06.21 |
[C++ Primer Plus] 9. Memory Models and Namespaces (0) | 2022.06.19 |