로또
32. public 상속 모형은 반드시 "is-a(...는 ...의 일종이다)"를 따르도록 만들자. 본문
다루는 내용
클래스들 사이에 맺을 수 있는 관계들 (is-a, has-a, is-implemented-in-terms-of) 중 is-a의 의미를 가지는 public 상속에 대해 알아보자.
모든 Derived는 Base의 일종이다.
클래스 D(Derived)를 클래스 B(Base)로부터 public 상속을 통해 파생시켰다면, D타입의 모든 객체는 또한 B타입의 객체이지만 그 반대는 성립하지 않는다. 즉, B타입의 객체가 쓰이는 곳에 D타입의 객체도 마찬가지로 쓰일 수 있지만 D타입의 객체가 필요한 부분에 B타입의 객체는 쓰일 수 없다.
class Person{ ... };
class Student public Person { ... }; // public 상속 - 모든 학생은 사람이다.
void eat(const Person& p);
void study(const Student& s);
int main(){
Person p;
Student s;
eat(p);
eat(s); // 가능, 모든 학생은 사람이다.
study(p); // 불가능, 모든 사람은 학생이 아니다.
study(s);
}
모든 새는 날 수 있다?
"public 상속 == is-a 관계" 라는 이야기는 직관적이지만 판단을 잘못하는 경우도 존재한다.
'새' 라는 개념을 보았을 때, 새는 날 수 있고, 펭귄은 새의 일종이다. 때문에 다음과 같이 코드를 작성하여 표현할 수 있을 것이다.
class Bird{
public:
virtual void fly();
};
class Penguin: public Bird{ ... };
...
Penguin pg
pg.fly(); // 펭귄이 난다.
펭귄은 실제로 날 수 없음에도, 펭귄은 fly 함수를 통해 날 수 있다. 하지만 이는 맞지 않다.
'새는 날 수 있다.' 가 '모든 새는 날 수 있다.' 라는 의미를 가진 것은 아니기 때문이다.
새도 날 수 있는 새가 있고, 날지 못하는 새가 있을 것이다. 그러니 다음과 같이 코드를 수정할 수 있다.
class Bird{...};
class FlyingBird: public Bird{
public:
virtual void Fly();
};
class Penguin: public Bird{...};
이러한 방식은 Penguin 객체에 Fly 함수를 호출하는 일이 있더라도 컴파일 단계에서 에러를 발생시켜줄 것이다.
모든 정사각형은 직사각형이다.
모든 새가 날 수 있다는 명제는 펭귄으로 인해 틀렸지만, 모든 정사각형은 직사각형이라는 것은 많은 사람들이 알고 있는 사실이다. 모든 정사각형은 직사각형인데, 직사각형은 정사각형이 아닐 수도 있으니 상속 관계를 만들기에 정말 좋아보인다.
class Rectangle{
public:
virtual void SetHeight(int new_height);
virtual void SetWidth(int new_width);
virtual int GetHeight() const;
virtual int GetWidth() const;
...
};
void MakeBigger(Rectangle& r){
int old_height = r.GetHeight(); // 현재 세로 길이
r.SetWidth(r.GetWidth() + 10); // 가로 길이 + 10
assert(r.GetHeight() == old_height); // r의 세로 길이는 변하지 않음을 확인
}
MakeBigger 함수는 Rectangle 객체를 받아 가로 길이를 10만큼 늘려주는 역할을 한다.
가로 길이를 늘린 후, 세로 길이를 비교하여 세로 길이가 변하면 문제가 있다고 판단하는 단정문이 존재한다.
이제, Rectangle 클래스를 상속한 Square 클래스를 만들고, MakeBigger 함수를 사용해보자.
class Square: public Rectangle{...};
int main(){
Square s;
assert(s.GetWidth() == s.GetHeight()); // 모든 정사각형의 가로와 세로의 길이는 같다.
MakeBigger(s); // 가로 길이를 10 늘린다.
assert(s.GetWidth() == s.GetHeight());
}
무언가 이상한 점이 보인다.
- MakeBigger 함수를 호출하기 전에, s의 세로 길이는 가로 길이와 같아야 한다.
- MakeBigger 함수가 실행되는 중에, s의 가로 길이는 변하지만 세로 길이는 유지되어야 한다.
- MakeBigger 함수 호출이 끝난 후, s의 세로 길이는 가로 길이와 같아야 한다.
모든 정사각형은 직사각형이지만 위와 같은 코드에서는 분명 단정문에 의해 코드가 종료되고 말 것이다.
public 상속은 기본 클래스 객체가 가진 모든 것들이 파생 클래스 객체에도 그대로 적용된다고 단정한다.
이것만은 잊지 말자!
- public 상속의 의미는 "is-a(...는 ...의 일종)"이다. 기본 클래스에 적용되는 모든 것들이 파생 클래스에 그대로 적용되어야 한다. 왜냐하면 모든 파생 클래스 객체는 기본 클래스 객체의 일종이기 때문이다.
'책 > Effective C++' 카테고리의 다른 글
| 34. 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자. (1) | 2023.12.02 |
|---|---|
| 33. 상속된 이름을 숨기는 일은 피하자. (0) | 2023.12.01 |
| 30. 인라인 함수는 미주알고주알 따져서 이해해 두자. (0) | 2023.11.29 |
| 29. 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자! (2) | 2023.11.28 |
| 28. 내부에서 사용하는 객체에 대한 '핸들'을 반환하는 코드는 되도록 피하자. (1) | 2023.11.27 |