[Effective C++] 항목34 : 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자

Jangmanbo·2024년 5월 20일
0

Effective C++

목록 보기
34/34

public 상속의 종류

  1. 함수 인터페이스의 상속
  2. 함수 구현의 상속
// 순수 가상 함수가 있으므로 추상 클래스
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 클래스는 추상 클래스이므로 파생 클래스만 인스턴스화 가능하다.


순수 가상 함수 (draw)

  • 파생 클래스에게 함수의 인터파이스만을 상속시킨다.
  • 모든 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)

  • 파생 클래스가 함수의 인터페이스와 함수의 기본 구현을 상속받게 한다.
  • 순수 가상 함수와 마찬가지로 함수의 인터페이스를 상속한다.
  • 순수 가상 함수와 달리 파생 클래스가 오버라이드할 수 있는 함수 구현부를 제공한다.

모든 객체는 에러와 마주쳤을 때 호출할 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, ModelBfly함수를 동일하게 작성할 수는 없으므로
Airplane::fly에 A, B의 비행 로직을 작성하였다.


class ModelC : public Airplane			// A, B와 비행 방식이 다른 C 비행기
{
	...									// fly() 함수 선언 X
}

이후 C 모델을 새로 추가했는데 fly 함수를 재정의하는 것을 잊어버렸다.

이때 ModelC기본 동작을 원한다고 명시적으로 밝히지 않았음에도,
알아서 Airplane::fly, 기본 동작을 물려받게 된다.

해결법 1: 가상 함수의 인터페이스와 그 가상 함수의 기본 구현을 잇는 연결 관계를 끊는다.

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

  • protected: 비행기를 사용하는 사용자는 비행 동작이 어떻게 구현되는지는 신경쓰지 말아야 한다.
  • 비가상 함수: 파생 클래스에서 재정의하면 안된다. (항목 36)

해결법 2: 순수 가상 함수로 함수의 인터페이스를, 순수 가상 함수의 구현으로 함수 구현을 물려준다.

flydefaultFly를 별도로 두는 방식이 마음에 들지 않는다면!

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 영역에 있었다.
그러나 flydefaultFly가 합쳐지면서 각기 다른 보호 수준을 부여하지는 못하게 되었다.



비가상 함수 (objectID)

  • 파생 클래스에게 함수 인터페이스와 더불어 함수의 필수적인 구현을 상속시킨다.
  • 클래스 파생에 상관없이 변하지 않는 동작을 지정하는데 사용한다.

objectID를 계산하는 방법이 항상 똑같으므로, 파생 클래스에서 재정의하지 못하도록 한다.



클래스 설계 시 주의점

1. 모든 함수를 비가상 함수로 선언

파생 클래스가 기본 클래스의 동작을 특별하게 만들 만한 여지가 없게 된다.

가상 함수가 성능이 안좋다고 여길 수 있다.
그러나 실제로 전체 실행 시간의 80%에 영향을 주는 것은 전체 코드의 20%이다. (80-20 법칙)
따라서 함수 호출 중 80%를 가상 함수로 두더라도, 전체 성능에는 거의 손실이 없다.

2. 모든 함수를 가상 함수로 선언

분명 파생 클래스에서 재정의가 안되어야 하는 함수가 있을 것이다.
이런 함수의 경우 단호하게 비가상 함수로 선언하자.

(물론 항목 31의 인터페이스 클래스의 경우 모든 함수를 가상 함수로 선언해야 하는 경우도 있다.)



정리

  • public 상속에서 파생 클래스는 항상 기본 클래스의 인터페이스를 상속받는다.
  • 순수 가상 함수: 인터페이스 상속만을 허용
  • 단순 가상 함수: 인터페이스 상속 + 기본 구현의 상속도 가능
  • 비가상 함수: 인터페이스 상속 + 필수 구현의 상속 강제

0개의 댓글