public 상속은 두 가지로 나눌 수 있다.
1.함수 인터페이스의 상속
2.함수 구현의 상속
둘의 차이는 함수 선언 과 함수 정의의 차이라고 생각하면 된다.
추상 클래스는 인스턴스를 만들지 못하고, 추상 클래스의 파생 클래스만 인스턴스화가 가능하다.
class Shape
{
public:
virtual void Draw() const = 0;
virtual void Error(const std::string& msg);
int ObjectID() const;
};
class Rectangle : public Shape { };
class Ellipse : public Shape { };
순수 가상 함수인 Draw 함수로 인해 Shape 클래스는 추상 클래스가 되었고, Rectangle과 Ellipse 클래스는 Shape 클래스를 public 상속하고 있다. Shape 클래스는 인스턴스화가 불가능하고, Rectangle과 Ellipse 클래스는 인스턴스화가 가능하다.
Draw 함수는 순수 가상 함수이고, Error 함수는 가상 함수이고, ObjectID 함수는 비가상 함수로 선언했다.
순수 가상 함수는 파생 클래스에게 함수의 인터페이스만을 물려주는 것이다.
가상 함수는 파생 클래스에게 함수의 인터페이스뿐만 아니라 그 함수의 기본 구현도 물려받게 한다.
비가상 함수는 파생 클래스가 함수 인터페이스와 더불어 그 함수의 필수적인 구현을 물려받게 한다.
사실 비가상 함수와 가상 함수의 차이는 이해하고 있다고 생각하고 있지만, 비가상 함수도 파생 클래스에서 재정의가 가능한데 무슨 차이가 있을까요? 라는 질문에 대답을 못할 것 같다. 이 책에서 설명해준다고 하니 그때 이해해보자(36장 참고).
예전에는 순수 가상 함수와 가상 함수의 차이가 이해가 안됐던 때가 있었다.
둘의 차이를 비유해보자면, 순수 가상 함수는 "파생아 이 함수는 니가 반드시 필요한데 구현은 니가해"이고, 가상 함수는 "파생아 이 함수는 니가 반드시 필요한데 구현까진 내가 해줄게. 상황에 따라서 니가 맘대로 바꿔 쓰던가해" 이런 상황이라고 보면 된다.
파생 클래스를 추가하면서 가상 함수를 파생 클래스에 맞게 오버라이드 해야하는데 깜박하는 경우가 생길 수 있다.
컴파일적으로 아무 문제가 없기 때문에 지나치기 쉬운 실수이다. 그래서 나는 클래스를 설계할 때 최상단은 생성자와 소멸자를 두고 가상 함수들을 소멸자 아래에 두는 편이다. 스크롤 내릴필요 없이 볼 수 있어야하고, 모여있어야 복붙해서 가져오기 편하기 때문이다.
각설하고 파생 클래스에 가상 함수를 오버라이드 하지 않았다면, 기본 클래스의 함수를 사용할 것이다.
이런 실수를 없애기 위해선 무조건 오버라이드 해야 한다는 강제성을 갖고 있어야 할텐데, 이는 순수 가상함수가 떠오른다. 즉, 가상 함수를 순수 가상 함수로 바꾸고, 가상 함수였던 구현부를 다른 함수로 대체하면 된다.
class Shape
{
public:
...;
virtual void Error(const std::string& msg);
protected:
void ErrorHelper(const std::string& msg);
};
class Rectangle : public Shape
{
public:
...
virtual void Error(const std::string& msg) override
{
ErrorHelper(msg); //구현부 호출
}
};
그리고 이전 가상 함수였던 구현부가 들어가 있는 함수를 호출하면 된다.
솔직히 이 방법 굉장히 별로라고 생각한다. 코드 길이는 물론이고, 결국 클래스 사용자는 기본 클래스를 봐야 구현부가 따로 구현되어 있는 것을 알텐데 그럴빠에 가상 함수들을 모아두고 실수를 안하게끔 하든 주석으로 적어두는게 낫다고 본다.
순수 가상 함수의 구현부를 구현해선 안된다는 것은 아니다. 명백히 구현부를 기본 클래스에서 구현할 수 있다.
나도 처음 안 사실이다. 순수 가상 함수는 파생 클래스가 무조건 오버라이드 해야 하는 것이지, 구현부를 구현할 수 없다는 특징은 없다고 깨달았다.
순수 가상 함수의 오버라이드 강제성과 기본 클래스에서 정의할 수 있는 점을 살리면 가상 함수의 느낌을 살릴 수 있다.
class Shape
{
public:
...;
virtual void Error(const std::string& msg) = 0;
};
void Shape::Draw() const
{
//구현
}
class Rectangle : public Shape
{
public:
...
virtual void Error(const std::string& msg) override
{
Shape::Error(msg); //기본 클래스의 Error 순수 가상 함수 사용
}
};
class Ellipse : public Shape
{
public:
...
virtual void Error(const std::string& msg) override
{
//Ellipse 클래스에 맞게 구현
}
};
Error 함수는 순수 가상 함수이기에 파생 클래스는 무조건 오버라이드 해야 하지만 기본 클래스의 성질을 그대로 상속 받고 싶다면 기본 클래스의 함수를 불러오고, 바꿔야 한다면 재정의하면 그만이다.
이것만은 잊지 말자
인터페이스 상속은 구현 상속과 다르다. public 상속에서 파생 클래스는 항상 기본 클래스의 인터페이스를 모두 물려받는다.
순수 가상 함수는 인터페이스 상속만을 허용한다.
가상 함수는 인터페이스 상속과 더불어 기본 구현의 상속도 가능하도록 지정한다.
비가상 함수는 인터페이스 상속과 더불어 필수 구현의 상속도 가하도록 지정하자.
'서적 정리 > Effective C++' 카테고리의 다른 글
38."has-a(...는 ...를 가짐)" 혹은 "is-implemented-in-terms-of(...는 ...를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자 (0) | 2021.12.22 |
---|---|
37.어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자 (0) | 2021.12.22 |
36.상속받은 비가상 함수를 파생 클래스에서 재정의하는 것은 절대 금물! (0) | 2021.12.22 |
35.가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자 (0) | 2021.12.21 |
33.상속된 이름을 숨기는 일은 피하자 (0) | 2021.12.20 |
32.public 상속 모형은 반드시 "is-a(...는 ...의 일종이다)"를 따르도록 만들자 (0) | 2021.12.20 |
31.파일 사이의 컴파일 의존성을 최대로 줄이자 (0) | 2021.12.20 |
30.인라인 함수는 미주알고주알 따져서 이해해 두자 (0) | 2021.12.19 |
댓글