Computer Science/C & C++

[C++] Multiple Inheritance (MI) | 다중 상속

lww7438 2020. 3. 12. 16:39

Multiple Inheritance (MI)

다중 상속

- Related Immediate Base Class(인접한 기초 클래스)가 두 개 이상인 상속을 지칭하는 용어이다.
- 다수의 기초 클래스로부터 이름이 같은 메서드나 멤버를 상속받았을 경우와 같이 중복의 문제가 있을 수 있다.

// Ex. 다중상속 예시

class Student : private std::string, private std::valarray<double> {
    ...
};
// string과 valarray, 두 라이브러리를 private 타입으로 다중 상속받는 Student 클래스

Syntax (문법)

아래 문법들은 상기 이미지와 같은 상속 구조를 전제하에 서술된다.


- 파생 클래스가 다수의 경로를 통해 어떤 기초 클래스를 두 번 이상 상속받았을 경우, 강제적 데이터형 변환을 통해 그 경로를 지정할 수 있다.

// Ex. 강제적 데이터형 변환을 통한 상속 경로 지정 예시

SingingWaiter ed;
Worker* pw1 = (Waiter*) &ed;     // Waiter가 상속받은 Worker 객체와 연동된다.
Worker* pw2 = (Singer*) &ed;     // Singer가 상속받은 Worker 객체와 연동된다.

Virtual Base Class (가상 기초 클래스)

- 다중 상속 과정에서, 특정 기초 클래스의 멤버를 두 번 이상 상속받는 것을 방지하고 한 묶음의 멤버만 상속받게 하는 기능이다.

- 상속받는 클래스 선언 앞에 \(\texttt{virtual}\) 키워드를 붙여서 활성화할 수 있다.

// Ex. 가상 기초 클래스 선언 예시

class Singer : virtual public Worker {
    ...
};

class Waiter : public virtual Worker {
    ...
};

// 위와 같이 Singer와 Waiter가 각각 Worker를 가상 기초 클래스로 선언해놓으면,
// 아래와 같이 선언했을때, SingingWaiter는 Worker 클래스를 중복없이 딱 한 번 상속받게된다.

class SingingWaiter : public Singer, public Waiter {
    ...
};

※ \(\texttt{virtual}\) 키워드와 접근 레벨 지정자(\(\texttt{public, protected, private}\))와의 순서는 어떠해도 상관없다.

- 가상 기초 클래스를 선언하면, 중간 클래스를 통해 연쇄적으로 상위 클래스에 생성자를 통해 정보를 전달하는 기능이 중지되므로, 가상 기초 클래스의 디폴트 생성자가 호출되기를 원치 않는다면, 가상 기초 클래스의 생성자까지 명시적으로 호출해야 한다. (가상 기초 클래스가 선언되었을 경우, 모든 계층의 클래스 생성자를 명시적으로 호출하는 편이 안전하다.)

// Ex. 가상 기초 클래스 생성자의 명시적 선언 예시

class Worker {
private:
    std::string fullname;
    long id;
public:
    Worker() : fullname(" "), id(0L) { }
    Worker(const std::string& s, long n) : fullname(s), id(n) { }
    virtual ~Worker = 0;      // 기초 클래스에서, 순수 가상 파괴자 함수는 꼭 명시해놓아야 한다.
    virtual void Show() const = 0;
    ...
};


class Singer : public Worker {
protected:
    enum {other, alto, contralto, soprano, bass, baritone, tenor};
    enum {Vtype = 7};
private:
    static char* pv[Vtype];
    int voice;
public:
    Singer(const std::string& s, long n, int v = other) : Worker(s, n), voice(v) { }
    Singer(const Worker& wk, int v = other) : Worker(wk), voice(v) { }
    void Show() const;
    ...
};


class Waiter : public Worker {
    int panache;
public:
    Waiter(const std::string& s, long n, int p = 0) : Worker(s, n), panache(p) { }
    Waiter(const Worker& wk, int p = 0) : Worker(wk), panache(p) { }
    void Show() const;
    ...
};


class SingingWaiter : virtual public Singer, virtual public Waiter {
public:
    SingingWaiter (const std::string& s, long n, int p = 0, int v = Singer::other)
        : Worker(s, n), Waiter(s, n, p), Singer(s, n, v) { }
    SingingWaiter (cosnt Worker& wk, int p = 0, int v = Singer::other)
        : Worker(wk), Waiter(wk, p), Singer(wk, v) { }
    ...
};

- 메서드 중복 문제를 해결하는 방법은 \(\texttt{::}\) 연산자를 통해 프로그래머가 상속 경로를 지정하는 것이다.
(Single Inheritance(단일 상속)에서는 기초 클래스의 메서드를 재정의하지 않는 이상, 가장 인접한 상위 클래스의 메서드가 호출되지만, MI(다중 상속)의 경우에서는, 인접한 클래스의 메서드들 중 어떤 것을 선택할지에 대한 모호한 상황이 발생하게 된다.)

// Ex. :: 연산자를 통해 메서드 선택 모호성을 제거하는 방법 예시

void SingingWaiter::Show() { // :: 연산자를 통해 중복된 메서드를 한 구문에 하나씩 지정한다.
    Singer::Show();          // 물론 둘 중 하나만 호출하는 것 또한 가능하다.
    Waiter::Show();
}                           

※ 위와 같은 연쇄적인 메서드 호출(최하위 클래스에서 최상위 기초 클래스까지)을 자연스럽게 구현하게 위해서는, 각 클래스의 메서드는 자신이 속한 클래스의 멤버만을 조작하도록 구현하는 것이 바람직하다.
ex) 멤버 출력 함수는 자신의 멤버만 출력하게끔 구현한다.

- 다수의 상속 경로 중, 최상위 클래스를 일부는 가상 기초 클래스로 정의하여 상속받고, 나머지는 가상이 아닌 기초 클래스로 부터 상속받게 되면, 가상으로 파생시킨 모든 상속 경로에 대해서는 종합적으로 하나의 객체만 상속받게 되고, 가상이 아닌 경로로 파생되는 경우는 각각의 경로마다 하나의 객체씩 상속받게된다.

 


Dominance (비교 우위)

- 상속 받은 멤버의 이름과, 현재 클래스에서 정의한 이름이 중복될 경우, 현재 클래스에 가장 가까운 클래스에서 정의된 이름이 가장 높은 비교 우위를 가진다.
(물론, 현재 클래스에도 중복된 멤버가 있다면, 그 멤버가 가장 높은 비교 우위를 가지게 된다.)
- 최상위 기초 클래스가 같아도, 상속 경로가 서로 다른 클래스들 끼리 중복되는 멤버의 경우, 범위를 제한하지 않으면 Ambiguity가 발생하게 된다.
- 비교 우위를 선정하는 과정에서 접근 레벨은 고려되지 않는다. 즉, 현재 클래스에서 접근이 가능하든 가능하지않든 (\(\texttt{public, protected}\) 레벨이든 \(\texttt{private}\) 레벨이든) 상속의 계층적 레벨만을 고려한다는 의미이다.
- 역시나, 가장 좋은 방법은 \(\texttt{::}\) 연산자를 통해 어떤 클래스의 멤버를 사용할 지를 명확하게 명시하는 것이다.