public 상속의 종류
- 함수 인터페이스의 상속
- 함수 구현의 상속
// 순수 가상 함수가 있으므로 추상 클래스
class Shape
{
public:
virtual void draw() const = 0; // 순수 가상 함수
virtual void error(const string& msg); // 단순 가상 함수
int objectID() const; // 비가상 함수
...
};
class Rectangle: public Shape { ... };
class Ellipse: public Shape { ... };
Shape
클래스는 추상 클래스이므로 파생 클래스만 인스턴스화 가능하다.
Shape
객체는 draw
가 가능해야 한다.직사각형과 타원은 각자 그리는 방법이 다르므로 각 클래스에서 직접 제공해야 하기 때문에 순수 가상 함수를 이용한다.
Shape* ps = new Shape; // Error.
Shape* ps1 = new Rectangle; // Success.
ps1->draw(); // Rectangle::draw 호출
Shape* ps2 = new Ellipse;
ps2->draw(); // Ellipse::draw 호출
ps1->Shape::draw(); // Shape::draw 호출
ps2->Shape::draw(); // Shape::draw 호출
모든 객체는 에러와 마주쳤을 때 호출할 error
함수가 필요하다.
그러나 각 클래스마다 꼭 맞는 방법으로 에러를 처리할 필요는 없으므로 단순 가상 함수를 이용한다.
class Airport { ... };
class Airplane
{
public:
virtual void fly(const Airport& destination);
...
};
void Airplane::fly(const Airport& destination)
{
// 목적지로 비행기가 날아가는 동작
}
// A, B 비행기는 비행 방식이 동일
class ModelA : public Airplane { ... };
class ModelB : public Airplane { ... };
ModelA
, ModelB
의 fly
함수를 동일하게 작성할 수는 없으므로
Airplane::fly
에 A, B의 비행 로직을 작성하였다.
class ModelC : public Airplane // A, B와 비행 방식이 다른 C 비행기
{
... // fly() 함수 선언 X
}
이후 C 모델을 새로 추가했는데 fly
함수를 재정의하는 것을 잊어버렸다.
이때 ModelC
가 기본 동작을 원한다고 명시적으로 밝히지 않았음에도,
알아서 Airplane::fly
, 기본 동작을 물려받게 된다.
class Airplane
{
public:
virtual void fly() = 0;
...
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination)
{
// 비행기의 기본 비행 동작 (A, B)
}
class ModelA : public Airplane // ModelB도 동일
{
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
...
};
class ModelC : public Airplane
{
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly()
{
// C 비행기의 비행 동작
}
defaultFly
에 비행 동작을 기본 구현하지만, 가상 함수 인터페이스는 fly
로 제공한다.
장점
fly
를 구현해야 하므로 프로그래머의 실수를 막을 수 있다.defaultFly
를 통해 코드의 중복을 막고 유지보수도 쉽다.defaultFly
fly
와 defaultFly
를 별도로 두는 방식이 마음에 들지 않는다면!
class Airplane
{
public:
virtual void fly(const Airport& destination) = 0;
...
};
// 순수 가상 함수 구현
void Airplace::fly(const Airport& destination)
{
// 비행기의 기본 비행 동작 (A, B)
}
class ModelA : public Airplane // ModelB도 동일
{
public:
virtual void fly(const Airport& destination)
{ Airplane::fly(destination); }
...
};
class ModelC : public Airplane
{
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly(const Airport& destination)
{
// C 비행기의 비행 동작
}
1번 방법에서는 defaultFly
가 protected 영역에 있었다.
그러나 fly
와 defaultFly
가 합쳐지면서 각기 다른 보호 수준을 부여하지는 못하게 되었다.
objectID
를 계산하는 방법이 항상 똑같으므로, 파생 클래스에서 재정의하지 못하도록 한다.
파생 클래스가 기본 클래스의 동작을 특별하게 만들 만한 여지가 없게 된다.
가상 함수가 성능이 안좋다고 여길 수 있다.
그러나 실제로 전체 실행 시간의 80%에 영향을 주는 것은 전체 코드의 20%이다. (80-20 법칙)
따라서 함수 호출 중 80%를 가상 함수로 두더라도, 전체 성능에는 거의 손실이 없다.
분명 파생 클래스에서 재정의가 안되어야 하는 함수가 있을 것이다.
이런 함수의 경우 단호하게 비가상 함수로 선언하자.
(물론 항목 31의 인터페이스 클래스의 경우 모든 함수를 가상 함수로 선언해야 하는 경우도 있다.)
정리
- public 상속에서 파생 클래스는 항상 기본 클래스의 인터페이스를 상속받는다.
- 순수 가상 함수: 인터페이스 상속만을 허용
- 단순 가상 함수: 인터페이스 상속 + 기본 구현의 상속도 가능
- 비가상 함수: 인터페이스 상속 + 필수 구현의 상속 강제