/ c++

Interface / Implementation Inheritance

Comments

본 포스트는 Effective C++ item 34를 참고하여 작성함.

인터페이스 상속과 구현 상속의 차이를 네대로 파악하고 구별하자

  • 상속은 사실 두가지!
    인터페이스 상속 / 구현 상속
    함수 선언 / 정의 의 관계와 유사함

예시

class Shape {
public:
    virtual void draw() const =0;
    virtual void error(const std::string& msg);
    int objectID() const;
    ...
};

순수 가상함수인 draw -> Shape는 추상 클래스 -> Shape 자체를 인스턴스화 불가능, public 상속에 의해 상속받은 파생 클래스가 draw를 override 해야 인스턴스화 가능

인스턴스화 조차 안되는 이 Shape는 (public) 상속을 한 파생 클래스는 기본 클래스에 해당하는 함수를 모두 사용 가능하므로 영향이 가히 절대군주와 맞먹음!!

1. pure virtual function

    virtual void draw() const =0;

pure virtual function을 선언하는 목적은 파생 클래스에게 함수의 인터페이스만을 물려주려고하는 것

  • 순수 가상 함수의 특징 : 상속 받은 파생 클래스가 순수 가상 함수를 반드시 다시 선언해야 하며, 추상 클래스 내부에서는 선언만 있을 뿐 직접적인 구현은 없어도 된다.(있어도 되지만, 호출하려면 반드시 클래스 이름을 한정자로 붙여 주여야함...)

  • 순수 가상 함수를 선언하는 목적은 인터페이스만을 상속하고 싶을 때 사용, 즉 같은 기능을 제공해야 하지만 구체적인 구현 행동은 다를 때 사용하면 된다.

  • 예시 : 직사각형과 타원의 경우 둘 다 draw 함수를 지원하는 것은 동일하지만 그리는 방법은 전혀 다름

2. virtual function

    virtual void error(const std::string& msg);

virtual function을 선언 하는 목적은 파생 클래스에게 함수의 인터페이스뿐만 아니라 기본 구현도 물려주려고하는 것

  • 가상함수의 특징 : 상속 받은 파생 클래스가 가상 함수를 override해서 구현을 바꿀 수도 있고 그렇지 않다면 기본 클래스가 정의한 함수를 그대로 이용한다.

  • 가상 함수를 선언하는 목적은 인터페이스와 기본 구현도 상속하되, 파생 클래스가 기본 구현을 바꿀 수 있는 기회를 준다.

  • 예시 : 에러의 경우 파생 클래스마다 다른 에러처리를 할 수도 있지만 굳이 구별할 필요가 없는 경우에는 기본 버전을 그대로 사용하면 된다

가상 함수의 위험성

    class Airplane {
    public :
        virtual void fly(const Airport& destination);
    };
 
    void Airplane::fly(const Airport& destination) {
          // 비행기를 날리는 코드
   }

장점: Airplane을 상속 받은 비슷한 모델의 비행기 A,B가 있다고 했을 때 모델 A와 B가 같은 방식으로 fly를 구현해야 한다면 그대로 Airplane을 상속받고 다시 중복해서 구현할 필요가 없으므로 효율적. (입에 거품 물고 객체 지향을 좋아라 하는 이유!!)

위험성: fly구현이 전혀 다른 비행기 C 모델을 추가했는데 깜빡하고 fly override를 깜빡했다면 문제가 커지지만 compile시에는 문제가 되지므로 실수를 방지하지 못하는 위험성이 있다. (좀 억지 인듯. 걍 프로그래머가 병신인거 아닌가)

해결 방법 1

    class Airplane {
    public :
        virtual void fly(const Airport& destination) = 0;
 
    protected :
        void defaultFly(const Airport& destination);
    };
 
    void Airplane::defaultFly(const Airport& destination) {
          // 비행기를 날리는 기본 코드
   }
class ModelA : public Airplance {
public :
    virtual void fly(const Airport& destination){
        defaultFly(destination);
    }
};
class ModelB : public Airplance {
public :
    virtual void fly(const Airport& destination){
        defaultFly(destination);
    }
};

pure virtual function 써서 강제로 구현하게하고, default를 call해서 중복은 피함.
인터페이스(fly)와 기본 구현(defaultFly)을 제공하는데, 이를 싫어하는 사람도 있음. 안 중요한 관계로 얽힌 함수 이름들이 군웅할거 하면서 클래스가 더러워져서...

해결 방법 1

    class Airplane {
    public :
        virtual void fly(const Airport& destination) = 0;
    };
 
    void Airplane::fly(const Airport& destination) {
          // 비행기를 날리는 코드
   }
class ModelA : public Airplance {
public :
    virtual void fly(const Airport& destination){
        Airplane::fly(destination);  // 기본 클래스의 fly 함수를 가져다 씀. 
    }
};
class ModelB : public Airplance {
public :
    virtual void fly(const Airport& destination){
        Airplane::fly(destination);
    }
};

이름 더러워지는게 싫으면, pure virtual function 의 기본 구현을 만들고 이를 쓰면 됨.

3. non virtual function

        int objectID() const;

non virtual function을 선언하는 목적은 파생 클래스에게 함수의 인터페이스와 함수의 mandatory implementation을 물려주려고하는 것

  • 파생 클래스의 종류에 상관없이 똑같이 동작해야 하는 경우 인터페이스와 함께 구현을 물려받게 한다. 이 함수는 파생 클래스가 override할 수 없기 때문에 늘 같은 동작을 하게 된다.

이것만은 기억하기

  1. 인터페이스 상속이랑 구현 상속은 다름.
  2. pure virtual function은 인터페이스 상속만 허용
  3. virtual function은 인터페이스 상속과 기본 구현도 상속하도록
  4. non virtual function은 인터페이스 상속과 필수 구현도 상속하도록.

결론은, 저 3가지 알아서 잘 쓰자!