스토리텔링 개발자

[Effective C++] 34. 인터페이스 상속과 구현 상속의 차이 본문

개발/Effective C++

[Effective C++] 34. 인터페이스 상속과 구현 상속의 차이

김디트 2024. 7. 3. 10:59
728x90

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

 

 

 

상속이 가지는 두 가지 의미
  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 { ... };
  • 추상 클래스의 인터페이스는 항상 상속되게 되어 있기 때문에 그 파생 클래스들에 지대한 영향을 끼친다.
  • 위 예제를 보면 추상 클래스는 각 멤버함수를 다른 방식으로 정의했는데 이에 대해 정리해본다.

 

 

 

draw (순수 가상 함수)
  • 순수 가상 함수의 특징
    1. 상속받는 구체 클래스는 이를 다시 선언해야 한다.
    2. 전형적으로 추상 클래스 안에서 정의를 갖지 않는다.
  • 즉, 순수 가상 함수는 파생 클래스에게 함수의 인터페이스만 물려주기 위해서 사용한다.
  • "draw 함수는 여러분이 직접 제공하도록 하시우. 하지만 당신이 어떻게 구현할지에 대해선 난 아무 생각 없소."
  • 사실 순수 가상 함수도 정의를 제공할 수 있다.(컴파일 에러도 발생하지 않는다.)
    • 단, 구현이 붙은 순수 가상 함수를 호출하려면 반드시 클래스 이름을 한정자로 붙여주어야 한다.
    • 추상 클래스 인스턴스란 존재할 수 없으므로 당연한 이치.
    • Shape* ps = new Shape;
      Shape* ps1 = new Rectangle;
      
      ps1->draw(); // Rectangle::draw가 호출된다.
      ps1->Shape::draw(); // Shape::draw가 호출된다.

 

 

 

error (단순 가상 함수)
  • 파생 클래스에서 인터페이스에 더해 오버라이드 할 수 있는 함수 구현부도 제공한다.
  • "error 함수는 여러분이 지원해야 한다우. 그러나 굳이 새로 만들 생각이 없다면 Shape 클래스에 있는 기본 버전을 그냥 쓰시구려."
  • 단순 가상 함수가 인터페이스 + 구현을 제공하는 것이 위험할 때도 있다.
    • class Airport { ... }; // 공항 클래스
      
      class Airplane
      {
      public:
          virtual void fly(const Airport& destination)
          {
          	// 주어진 목적지로 비행기를 날려 보내는 기본 동작 코드
          }
          ...
      };
      
      class ModelA : public Airplane { ... };
      class ModelB : public Airplane { ... };
      
      // 기존 비행기들과 전혀 다른 방식으로 나는 비행기가 도입된다.
      class ModelC : public Airplane
      {
          // 허나 실수로 fly 함수가 선언되지 않았다!
      };
      
      Airport PDX {...};
      Airplane* pa = new ModelC;
      ...
      pa->fly(PDX); // Airplane::fly 함수가 호출되는 문제!!!
    • 이처럼, 파생 클래스에서 요구하지 않았는데도 기능이 제공되므로 실수의 여지가 있다.
  • 가상 함수의 구현이 강제로 상속되는 문제의 해결법
    • 기본 동작을 원한다고 명시적으로 밝힐 때만 기능을 물려주도록 하면 된다. 
    • 순수 가상 함수로 구현하고, 기본 구현을 별도의 함수로 제공한다.
    • class Airplane
      {
          virtual void fly(const Airport& destination) = 0; // 순수 가상 함수로 변경
          ...
      protected:
          void defaultFly(const Airport& destination); // 파생 클래스로 기능 제공
      };
    • 순수 가상 함수의 구현부 제공이 가능하다는 점을 활용하여 따로 기본 구현용 함수를 준비하지 않는 방법도 가능하다.
      • 허나 이 경우 보호 수준이 날아간다. fly는 public 이므로, protected로 기본 구현용 함수를 따로 제공하는 것보다는 보호 수준이 낮다.

 

 

 

objectID (비가상 함수)
  • 비가상 함수를 선언하는 목적은, 파생 클래스가 함수 인터페이스 + 필수적인 구현을 물려받게 하는 것이다.
  • 필수적인 구현이란, 클래스 파생에 상관 없이 변하지 않는 동작이다.
  • "Shape 및 이것에서 파생된 모든 객체는 객체의 식별자를 내어 주는 함수를 갖게 되겠지. 객체 식별자를 계산하는 방법은 항상 똑같겠군. 실제 계산 방법은 Shape::objectID의 정의에서 결정되고, 파생 클래스는 이것을 바꿀 수 없겠는걸."
  • 비가상 함수는 클래스 파생에 상관없는 불변동작과 같으므로 절대로 파생 클래스에서 재정의할 수 없다.(항목 36 참조)

 

 

 

멤버 함수 선언 시 결정적인 실수
  1. 모든 멤버 함수를 비가상 함수로 선언한다
    • 비가상 소멸자가 문젯거리가 될 수 있다.(항목 7 참조)
    • 기본 클래스로 쓰이는 클래스는 십중팔구 가상 함수를 갖고 있어야 한다.(항목 7 참조)
    • 가상 함수의 비용이 크기 때문에 이렇게 하고 싶을 수는 있다. 허나... 80-20법칙을 생각하자.(항목 30 참조)
  2. 모든 멤버 함수를 가상 함수로 선언한다
    • 분명히 파생 클래스에서 재정의가 안 되어야 하는 함수가 있을 것이다.
    • 클래스 제작자는 변경 불가능하다고 비가상 함수로 선언함으로써, 입장을 명확히 밝히는 것이 소임이다.
728x90
Comments