[디딤돌 C++] 36. 다중 상속

이번에는 여러 개의 기반 형식에서 파생하는 형식을 정의하는 다중 상속을 살펴볼게요.

C++언어에서는 기반 형식을 여러 개를 정의하는 다중 상속을 지원합니다. 많은 이들은 다중 상속을 사용할 때 주의하라고 권하거나 아예 다중 상속을 사용하지 말 것을 권합니다. 실제 Java나 C#에서는 여러 개의 기반 클래스에서 파생하는 형식을 정의하는 문법을 제공하지 않습니다. C++보다 나중에 만들어진 이들 언어에서는 C++언어의 다중 상속 문법의 위험을 알고 난 이후에 만들어져서 이러한 문법을 제외하고 있습니다.

그렇지만 Java나 C#에서도 여러 개의 인터페이스를 기반으로 파생한 형식을 정의하는 문법은 제공하고 있습니다. 여기에서는 C++언어에서 제공하는 다중 상속이 어떠한 위험을 갖고 있는지 살펴보고 Java나 C# 언어처럼 여러 개의 인터페이스를 기반으로 파생한 형식을 정의하는 방법을 알아보기로 할게요.

만약 사람 클래스가 있고 이를 기반으로 파생한 학생 클래스와 야구 선수 클래스가 있다고 가정합시다. 그리고 학생 클래스와 야구 선수 클래스에서 파생한 학생 야구 선수 클래스를 만든다고 가정할게요. 이 때 학생 야구 선수 개체를 생성하면 기반 형식인 학생 생성자와 야구 선수 생성자가 동작하겠죠. 그런데 학생 형식과 야구 선수 형식은 사람 클래스를 기반으로 파생한 형식이죠. 따라서 학생 생성자를 수행 전에 기반 형식인 사람의 생성자를 수행할 거예요. 마찬가지로 야구 선수 생성자를 수행 전에 기반 형식인 사람의 생성자를 수행하겠죠. 결국 학생 야구 선수 개체를 생성할 때 사람 생성자는 두 번 수행합니다. 이러한 특징은 마치 학생 야구 선수 개체에 이름이 두 개 있는 이상한 형식을 정의하여 버그가 만들어질 수 있습니다.

다음은 다중 상속을 통해 학생 야구 선수 클래스를 정의하였을 때 사람 클래스에 정의한 메서드를 호출할 때 발생하는 오류 화면입니다.

다상 상속의 모호함

학생 야구 선수 형식은 기반 형식이 학생 클래스와 야구 선수 클래스입니다. 학생 클래스와 야구 선수 클래스는 모두 사람 클래스에서 파생한 형식이므로 사람 클래스에 정의한 View 메서드가 있는 것이죠. 컴파일러에서는 학생 클래스의 View를 호출하는 것인지 야구 선수 클래스의 View를 호출하는 것인지 판단하지 못하여 모호하다고 오류를 출력하고 있습니다. 우리가 생각할 때 둘 다 사람의 View를 호출하는 것이라고 생각하기 때문에 문제가 없다고 생각하지만 컴파일러는 학생 야구 선수 형식에 View 메서드가 두 개 있는 것으로 판별하고 이와 같은 오류가 발생하는 것입니다.

//다중 상속의 모호함
//Program.cpp
#include <iostream>
#include <string>
using namespace std;
class Man
{
    string name;
public:
    Man(string name)
    {
        this->name = name;
    }
    void View()
    {
        cout<<"이름은 "<<name<<"입니다."<<endl;
    }
};
class Student: public Man
{
public:
    Student(string name):Man(name) {    }
};
class BaseballPlayer: public Man
{
public:
    BaseballPlayer(string name):Man(name){    }
};
class BaseBallPlayerStudent: public Student, public BaseballPlayer
{
public:
    BaseBallPlayerStudent(string name):Student(name), BaseballPlayer(name)
    {
    }
};
int main()
{
    BaseBallPlayerStudent *bbps = new BaseBallPlayerStudent("홍길동");
    bbps->View();
    delete bbps;
    return 0;
}

이러한 모호함을 해결하기 위해 C++언어에서는 가상 상속을 제공하고 있습니다. 위의 예제와 같을 때 학생 클래스와 야구 선수 클래스를 정의할 때 virtual 키워드를 포함하여 상속을 표현합니다.

class Student:virtual  public Man
{
public:
    Student(string name):Man(name)
    {
    }
};
class BaseballPlayer: virtual public Man
{
public:
    BaseballPlayer(string name):Man(name)
    {
    }
};

그리고 학생 야구 선수 클래스의 생성자에서는 명확하게 사람 클래스의 생성자 초기화를 해 주세요.

class BaseBallPlayerStudent:  public Student, public BaseballPlayer
{
public:
    BaseBallPlayerStudent(string name):Student(name), BaseballPlayer(name), Man(name)
    {
    }
};

이처럼 virtual 키워드를 사용하여 가상 상속을 하면 다중 상속의 모호함을 해결할 수 있습니다.

//virtual 상속을 이용한 다중 상속의 모호함 해결
//Program.cpp
#include <iostream>
#include <string>
using namespace std;
class Man
{
    string name;
public:
    Man(string name)
    {
        this->name = name;
    }
    void View()
    {
        cout<<"이름은 "<<name<<"입니다."<<endl;
    }
};


class Student:virtual  public Man
{
public:
    Student(string name):Man(name)
    {
    }
};
class BaseballPlayer: virtual public Man
{
public:
    BaseballPlayer(string name):Man(name)
    {
    }
};
class BaseBallPlayerStudent:  public Student, public BaseballPlayer
{
public:
    BaseBallPlayerStudent(string name):Student(name), BaseballPlayer(name), Man(name)
    {
    }
};

int main()
{
    BaseBallPlayerStudent *bbps = new BaseBallPlayerStudent("홍길동");
    bbps->View();
    delete bbps;
    return 0;
}

실행 결과

이름은 홀길동입니다.

하지만 이처럼 복잡한 상속 계층 속에서 다중 상속 문법을 사용하는 것은 비용이 많이 발생합니다. 여러분은 다중 상속 문법을 사용할 때는 기반 클래스는 하나만 정하시고 필요하면 기반 인터페이스를 여러 개 사용하길 권합니다.

다음의 예제 코드는 사람 클래스와 IPlay 인터페이스와 IStudy 인터페이스를 기반으로 학생 야구 선수 클래스를 정의한 것입니다. 앞에 예제와 차이점을 살펴보세요.

//인터페이스 다중 상속
//Program.cpp
#include <iostream>
#include <string>
using namespace std;
#define interface struct
interface IPlay
{
    virtual void Play()=0;
};
interface IStudy
{
    virtual void Study()=0;
};
class Man
{
    string name;
public:
    Man(string name)
    {
        this->name = name;
    }
    void View()
    {
        cout<<"이름은 "<<name<<"입니다."<<endl;
    }
    string GetName()
    {
        return name;
    }
};

class Student:public Man, public IStudy
{
public:
    Student(string name):Man(name){    }
    virtual void Study()
    {
        cout<<GetName()<<" 공부하다."<<endl;
    }
};
class BaseBallPlayer:public Man, public IPlay
{
public:
    BaseBallPlayer(string name):Man(name)
    {
    }
    virtual void Play()
    {
        cout<<GetName()<<" 운동하다."<<endl;
    }
};

class BaseBallPlayerStudent:public Man, public IStudy, public IPlay
{
public:
    BaseBallPlayerStudent(string name):Man(name)
    {
    }
    virtual void Play()
    {
        cout<<GetName()<<" 운동하다."<<endl;
    }
    virtual void Study()
    {
        cout<<GetName()<<" 공부하다."<<endl;
    }
};

int main()
{
    BaseBallPlayerStudent *bbps = new BaseBallPlayerStudent("홍길동");
    bbps->View();
    bbps->Play();
    bbps->Study();
    delete bbps;
    return 0;
}

▷ 실행 결과

이름은 홍길동입니다.

홍길동 운동하다.

홍길동 공부하다.