다중 상속은 두 개 이상의 기초 클래스로부터 상속 받는 것이다.
class SingingWaiter : public Waiter, public Singer
{
...
};
일일이 public같은 키워드를 적어주어야 한다. 그렇지 않으면 private으로 인식하기 때문이다.
다형 상속은 새로운 문제를 발생시킨다. 몇 가지 클래스를 만들어 보자.
class Worker; // 추상 클래스
{
virtual void Set();
...
};
class Singer : public Worker
{
void Set(); // 재정의.
...
};
class Waiter : public Worker
{
void Set(); // 재정의.
...
};
void Singer::Set()
{
cout << "Singer : Set()" << endl;
}
void Waiter::Set()
{
cout << "Waiter : Set()" << endl;
}
int main()
{
Waiter bob();
Singer bev();
Waiter w_temp;
Singer s_temp;
Worker* pw[4] = {&bob, &bev, &w_temp, &s_temp};
for (int i = 0; i < 4; i++)
pw[i]->Set(); // 무엇을 출력할까?
}
저기서 Set 함수가 출력할 문구는 명확하게 알 수 있다. 그러나 다중 상속을 받은 코드는 그렇지 않다.
class SingingWaiter : public Singer, public Waiter
이렇게 다중 상속을 받게 되면, SingingWaiter 클래스는 두 개의 Worker 성분을 갖게 된다. 그러면 다음 코드가 모호해진다.
SingingWaiter ed;
Worker * pw = &ed; // 모호하다.
왜 모호한가? 이런 대입은 보통 파생 클래스 객체의 기초 클래스 객체의 주소로 대입이 되는데, 이 경우에는 SingingWaiter 클래스의 기초 클래스가 2개다. 그래서 선택할 수 있는 주소가 2가지다. 우리는 강제 데이터 변환을 사용할 수 있다.
Worker* pw1 = (Waiter*)&ed;
Worker* pw2 = (Singer*)&ed;
근데 이렇게 하면 또 문제가 생긴다. 그리고 중요한 건 도대체 왜 하나의 객체 복사본이 두 개나 필요하냐는 거다. 해결책은? 바로 가상 기초 클래스를 만들면 된다.
가상 기초 클래스는 이런 다중 상속이 일어날 때, 다중 상속된 클래스의 객체가 유일하게 만들어 준다. 클래스 선언에 virtual을 써서 Worker가 Singer와 Waiter의 가상 기초 클래스로 만든다.
class Singer : virtual public Worker {};
class Waiter : public virtual Worker {}; // public과 virtual 위치는 바뀌어도 상관없다.
이렇게 하면 SingingWaiter는 Worker 객체의 복사본을 하나만 내포한다. 이것은 Singer 객체와 Waiter 객체의 복사본을 하나씩 갖는다는 게 아니라, 하나의 Worker 객체를 공유한다는 뜻이다.
가상 기초 클래스를 사용하면 새로운 생성자 규칙이 요구된다. 왜? 다음과 같은 코드를 보자.
SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other)
: Waiter(wk,p), Singer(wk,v)
{
}
이런 경우에 wk는 서로 다른 두 경로(Waiter와 Singer)를 통해 Worker 객체에 전달된다. 이런 충돌을 방지하기 위해서, C++ 기초 클래스에 정보 전달이 되는 것을 막는다. 그래서 이 경우에는 각각 Waiter, Singer 클래스에 있는 멤버 변수들은 초기화가 되고, Worker의 멤버는 초기화되지 않는다. 그러나 파생 클래스 객체 생성하기 전에 기초 클래스 객체를 만들긴 해야 한다. 그래서 프로그램은 디폴트 생성자를 실행한다. 이런 경우는 이렇게 써야 한다.
SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other)
: Worker(wk), Waiter(wk,p), Singer(wk,v)
{
}
난 못 믿겠어. 실험 진행 중...
Worker::Worker()
: fullname("no name"), id(0L)
{
cout << "Worker::생성자1" << endl;
}
Worker::Worker(const std::string& s, long n)
: fullname(s), id(n)
{
cout << "Worker::생성자2" << endl;
}
SingingWaiter::SingingWaiter(const std::string& s, long n, int p, int v)
: Waiter(s, n, p), Singer(s, n, v)
{
cout << "SingingWaiter::생성자1" << endl;
}
SingingWaiter newhire("Madonna", 2005, 6, 4);
이렇게 하고 실행 해봄. 교재대로라면 Worker 클래스 생성자2 대신 생성자1이 호출된다는 거잖아?
진짜임 ㄷㄷㄷ!
아직 SingingWaiter 클래스로 상속 받은 메서드를 사용할 때의 문제점이 남아있다. 다음 코드를 보자.
SingingWaiter newhire("Madonna", 2005, 6, 4);
newhire.Show() // 어떤 클래스의 함수를 호출하는가?
상속 받은 두 개의 클래스 모두 Show 함수가 있어서 호출이 모호해진다. 이럴 땐 범위 연산자를 사용하면 된다.
SingingWaiter newhire("Madonna", 2005, 6, 4);
newhire.Singer::Show() // Singer의 함수를 호출한다.
혹은 그냥 재정의에서 기초 클래스의 함수를 사용하면 된다.
void SingingWaiter::Show() const
{
cout << "SingingWaiter::Show()" << endl;
Singer::Show();
}