C++ 다중 상속(Multiple inheritance : MI)

프로그래밍/C++ 2019.10.31 댓글 Plorence

다중 상속은 파생 클래스가 여러 기초 클래스를 상속받았을 때 다중 상속이라고 부릅니다.

다중 상속을 사용하게 된다면 다음과 같은 문제가 생깁니다.

  • 두 개 이상의 기초 클래스로부터 이름은 같지만 서로 다른 메서드들을 상속하는 문제

  • 둘 이상의 서로 관련된 인접 기초 클래스들로부터 어떤 클래스의 다중 인스턴스를 상속하는 문제

이러한 다중 상속의 문제 때문에 추가된 기능이 있습니다.

 

다중 상속을 사용하기 전에 고려해야할 사항, 조상 클래스는 어떻게 할 것인가?

다중 상속을 사용할 때 기초 클래스의 N개만큼 포함할 것인지 하나의 독립적인 것으로 만들 것인지 설계해야 합니다.

만약 조상 클래스를 독립적인 형태로 만들려면 새로운 기능을 사용해야 합니다.

class Person {
};
class Student : Person{ //학생
};
class Reporter : Person{ //기자
};
class StudentReporter : public Student,public Reporter { //학생기자
};

이러한 구조를 가지고 있다고 하면 아래와 같습니다.

특별한 문법을 사용하지 않는 한 두 개의 조상 클래스를 가지고 있을 때  하나의 조상 클래스로 공유해야 하는 경우에 문제가 생깁니다.

그래서 업 캐스팅(upcasting)에서 문제가 생기는데 조상 클래스의 참조 변수가 참조하려고 할 때 모호해집니다.

왜냐하면 조상 클래스가 2개 이상이기 때문에 어떠한 것을 참조해야 될지 모르기 때문입니다. (포인터도 마찬가지)

그래서 사용하려면 강제 데이터형 변환을 해줘야 합니다.

StudentReporter Rs = StudentReporter();
Person & per1 = Rs; //에러!!
Person & per2 = (Reporter &)Rs;
Person & per3 = (Student &)Rs;

이러한 방법은 다형을 어렵게 합니다.

 

이처럼 N개의 조상 클래스 객체가 필요하지 않고 하나의 조상 클래스를 공유할 필요가 있을 때를 위하여 가상 기초 클래스(virtual base class)를 추가했습니다.

 

가상 기초 클래스

가상 기초 클래스는 하나의 공통 조상을 공유하는 여러 개의 기초 클래스로부터 공통 조상의 유일 객체를 상속하는 방식으로 객체를 파생시키는 것을 허용합니다.

상속할 때 virtual 키워드를 사용하면 됩니다.

class Student : virtual public Person{ //학생
};
class Reporter : virtual public Person{ //기자
};

하나의 조상 클래스 객체로 만들 파생 클래스에 virtual 키워드를 사용하면 됩니다.

만약 가상 기초 클래스를 사용하게 된다면 클래스 구조는 아래와 같습니다.

조상 클래스가 하나의 객체만 생성됩니다. 이제 하나의 객체만 존재하니 암시적 업 캐스팅이 가능해지면서 다형을 사용할 수 있습니다.

다형이 어렵든 간에 하나의 조상 클래스 객체를 공유하는 게 아닌 기초 클래스만큼의 조상 클래스가 필요할 때도 있을 것입니다.

어떻게 설계하느냐에 따라 필요할 수도, 없을 수도 있습니다.

 

가상 기초 클래스 사용으로 인한 새로운 규칙

가상 기초 클래스를 사용하게 되면 기존에 사용하던 클래스의 코드를 약간 변경해줘야 합니다.

 

생성자

생성자는 가상 기초 클래스를 사용하게 된다면 초기화 부분에서 바꿔줘야 합니다.

왜냐하면 조상 클래스 객체는 하나이고 독립적인데 두 개의 기초 클래스에서 초기화를 하기 때문입니다.(충돌 문제가 발생)

class Person {
};
class Student : virtual public Person{ //학생
public:
       Student() : Person() {
       }
};
class Reporter : virtual public Person{ //기자
public:
       Reporter() : Person() {
       }
};
class StudentReporter: public Student,public Reporter { //학생기자
public:
      StudentReporter() : Student(), Reporter() {
       }
};

이러한 문제점을 피하기 위해서 C++는 기초 클래스를 통해 조상 클래스에 자동으로 정보를 전달하는 기능정지시킵니다.

즉 이 말은 디폴트 생성자가 사용됩니다.

 

조상 클래스를 명시적으로 초기화하고 싶다면 파생 클래스에서 멤버 초기자 리스트를 사용하여 초기화시켜주면 됩니다.

class StudentReporter: public Student,public Reporter { //학생기자
public:
      StudentReporter() : Student(), Reporter(),Person() {
       }
};

 

이름은 같지만 서로 다른 메서드들을 상속하는 문제

#include <iostream>
using namespace std;
class Person {
public:
       void Show() {
              cout << "사람입니다.";
       }
};
class Student : virtual public Person{ //학생
public:
       Student() : Person() {
       }
       void Show() {
              cout << "학생입니다.";
       }
};
class Reporter : virtual public Person{ //기자
public:
       Reporter() : Person() {
       }
       void Show() {
              cout << "기자입니다.";
       }
};
class StudentReporter: public Student,public Reporter { //학생기자
public:
      StudentReporter() : Student(), Reporter(),Person() {
       }
       
};
int main(void) {
       StudentReporterRS =StudentReporter();
       RS.Show(); //에러!!
}

위와 같이 클래스를 설계했을 때 Show메서드를 호출하게 되면 모호하다고 에러가 발생하게 됩니다.

단일 상속에서는 Show메서드를 다시 정의하지 않으면 가장 가까운 파생 클래스의 정의가 사용됩니다.

하지만 다중 상속에서는 두 개의 메서드가 있기 때문에 어떤 메서드를 호출해야 하는지 모호해집니다.

 

첫 번째 방법

사용 범위 결정 연산자를 사용합니다.

       StudentReporter RS =StudentReporter();
       RS.Reporter::Show();

 

두 번째 방법

파생 클래스에서 Show메서드를 재정의하고 사용할 Show메서드 버전을 명시적으로 지정합니다.

class StudentReporter: public Student,public Reporter { //학생기자
public:
       StudentReporter() : Student(), Reporter(),Person() {
       }
       void Show() {
              Reporter::Show();
       }
       
};

이게 첫 번째 방법보다 더 나은 방법입니다.

 

 

점층적 접근 방식을 모듈 접근 방식으로 바꾸기

#include <iostream>
using namespace std;
class Person {
public:
       void Show() {
              cout << "사람입니다.";
       }
};
class Student : virtual public Person{ //학생
public:
       Student() : Person() {
              
       }
       void Show() {
              Person::Show();
              cout << "학생입니다.";
       }
};
class Reporter : virtual public Person{ //기자
public:
       Reporter() : Person() {
       }
       void Show() {
              Person::Show();
              cout << "기자입니다.";
       }
};
class StudentReporter: public Student,public Reporter { //학생기자
       StudentReporter() : Student(), Reporter(),Person() {
       }
       void Show() {
              Reporter::Show();
              Student::Show();
       }
       
};

이게 점층적 접근 방식인데 파생 클래스에서 호출할 시에 조상 클래스의 Show메서드가 두 번 호출되기 때문에 의도치 않은 출력이 됩니다.

#include <iostream>
using namespace std;
class Person {
public:
       void Show() {
              cout << "사람입니다.";
       }
};
class Student : virtual public Person{ //학생
public:
       Student() : Person() {
              
       }
       void Show() {
              
              cout << "학생입니다.";
       }
};
class Reporter : virtual public Person{ //기자
public:
       Reporter() : Person() {
       }
       void Show() {
              
              cout << "기자입니다.";
       }
};
class StudentReporter: public Student,public Reporter { //학생기자
public:
      StudentReporter() : Student(), Reporter(),Person() {
       }
       void Show() {
              Reporter::Show();
              Student::Show();
              Person::Show(); //여기서 출력함
       }
       
};

위처럼 바꿔주면 한 번만 출력될 것입니다.

다중 상속에 관한 추가적인 문제

가상 기초 클래스와 가상이 아닌 기초 클래스의 혼합 사용

class Person {
public:
       void Show() {
              cout << "사람입니다.";
       }
};
class Student : virtual public Person{ //학생
public:
       Student() : Person() {
              
       }
       void Show() {
              
              cout << "학생입니다.";
       }
};
class Reporter : public Person{ //기자
public:
       Reporter() : Person() {
       }
       void Show() {
              
              cout << "기자입니다.";
       }
};
class StudentReporter: public Student,public Reporter { //학생기자
public:
      StudentReporter() : Student(), Reporter(),Person() {
       }
       void Show() {
              Reporter::Show();
              Student::Show();
              Person::Show();
       }
       
};

하나는 가상 기초 클래스고 하나는 가상이 아닌 기초 클래스 일 때는 그대로 동작하게 됩니다.

클래스 구조는 아래와 같습니다.

이 경우에는 Person객체에 대해 모호함을 가질 것입니다.

Reporter객체의 Person객체냐, StudentReporter객체의 Person객체를 말하는 것인지 알 수 없습니다.

그래서 ::연산자를 통해 Reporter 객체인지 Student객체인지 명시해야 합니다.

클래스 구조

별개의 객체를 가지게 되며 접근은 ::연산자를 사용해야 합니다.

 

비교 우위(dominance)

가상이 아닌 기초 클래스를 사용할 때에는 이름이 같은 멤버를 상속받을 때 호출한다면 사용이 모호해지지만 가상 기초 클래스를 사용할 때는 다릅니다.

파생 클래스에 있는 이름은 조상 클래스에 있는 동일한 이름보다 비교 우위를 가집니다.

이때 가상 모호성 규칙은 접근 규칙을 고려하지 않기 때문에 private 접근 제한자여도 문제가 생깁니다.

class Person {
public:
       void Show() {
              cout << "사람입니다.";
       }
};
class Student : virtual public Person{ //학생
public:
       Student() : Person() {
              
       }
       void Show() {
              
              cout << "학생입니다.";
       }
};
class StudentReporter: public Student { //학생기자
public:
      StudentReporter() : Student(),Person() {
              Show(); //Student::Show메서드를 호출한다.
       }
};

댓글