다중 상속과 다이아몬드 문제
한 클래스가 여러 부모를 가지고 있는 경우를 다중 상속이라고 한다
- cf) 자바와 C#은 다중 상속을 허용하지 않음
#include <iostream>
using namespace std;
struct Mom {
int a = 1;
};
struct Dad {
int b = 2;
};
struct Child : Mom, Dad {
int c = 3;
};
int main() {
Child ch;
cout << ch.a << endl;
cout << ch.b << endl;
cout << ch.c << endl;
}
여기까지는 문제가 없다.
만약 Dad의 멤버 변수가 b가 아니라 a라면?
#include <iostream>
using namespace std;
struct Mom {
int a = 1;
};
struct Dad {
int a = 2;
};
struct Child : Mom, Dad {
int c = 3;
};
int main() {
Child ch;
cout << ch.Mom::a << endl;
cout << ch.Dad::a << endl;
cout << ch.c << endl;
}
Mom으로부터 상속받은 a와 Dad로부터 상속받은 a 두가지가 있다.
어느 클래스로 부터 상속 받는 객체인지 표기해줘야 한다.
ch.Mom::a
만약 다음과 같은 구조로 설계되어있다면?
#include <iostream>
using namespace std;
struct Person {
int a;
};
struct Mom : Person {
Mom() {
a = 1;
}
};
struct Dad : Person {
Dad() {
a = 2;
}
};
struct Child : Mom, Dad {
int c = 3;
};
int main() {
Child ch;
cout << ch.Mom::a << endl;
cout << ch.Dad::a << endl;
cout << ch.c << endl;
}
이 예시에서는 문제가 없지만, 이런 다중 상속 구조에는 큰 문제가 존재한다
아래 예시를 봐보자
#include <iostream>
using namespace std;
struct Person {
int age;
void Eat() {
cout << "먹는다..." << endl;
}
};
struct Student : Person {
void Study() {
cout << "공부한다..." << endl;
}
};
struct Worker : Person {
void Work() {
cout << "일한다..." << endl;
}
};
struct Researcher : Student, Worker {
};
int main(void) {
Researcher r;
// r.age는 불가능
r.Student::age = 10;
r.Worker::age = 20;
// r.eat()는 불가능
r.Student::Eat();
r.Worker::Eat();
}
사람의 나이 : 학생으로서의 나이와, 직장인으로서의 나이 두 가지가 존재하게 된다
Eat() 또한 두 가지가 존재하게 된다. r.eat()처럼 그냥 먹을 수가 없음
위 예시를 다형적 클래스 구조로 바꿔보자
#include <iostream>
using namespace std;
struct Person { // 다형적 클래스
int age;
virtual ~Person() {}
void Eat() {
cout << "먹는다..." << endl;
}
};
struct Student : Person {
void Study() {
cout << "공부한다..." << endl;
}
};
struct Worker : Person {
void Work() {
cout << "일한다..." << endl;
}
};
struct Researcher : Student, Worker {
};
int main(void) {
// Person *p = new Researcher;는 불가능
}
Researcher는 Person으로 부터 직접 상속 받은 것이 아니기 때문에 Person 포인터로 가리킬 수 없다
이런 상황에서는 다형성도 사용할 수 없는 단점이 존재한다
이런 문제를 다이아몬드 문제라고 한다
C++에서는 다이아몬드 문제를 방지하기 위해 가상 상속을 제공함
- 가상 상속을 사용하게 되면 Researcher는 Stduent, Worker로부터가 아니라 Person으로부터 직접 상속을 받게 된다
#include <iostream>
using namespace std;
struct Person { // 다형적 클래스
int age;
virtual ~Person() {}
void Eat() {
cout << "먹는다..." << endl;
}
};
struct Student : virtual Person {
void Study() {
cout << "공부한다..." << endl;
}
};
struct Worker : virtual Person {
void Work() {
cout << "일한다..." << endl;
}
};
struct Researcher : Student, Worker {
};
int main(void) {
Researcher r;
r.age = 10;
r.Eat();
}
이러한 방식에도 한계가 있다.
- 만약 Eat() 멤버 함수를 오버라이딩 하게 되면 가상 상속을 했다 하더라도 r.Eat();에서 에러가 발생한다
"다중 상속은 인터페이스로부터만 받는다"라는 약속을 해야 한다
인터페이스 : 모든 메서드가 순수 가상 함수이고 (비정적)멤버 변수는 없는 클래스
cf) 추상 클래스 : 순수 가상 함수가 하나 이상 들어있는 클래스
cf) 다형적 클래스 : 가상 함수가 하나 이상 들어 있는 클래스
cf) 자바에서도 인터페이스가 존재함
부모 클래스를 인터페이스로 바꿔보자
#include <iostream>
using namespace std;
struct IPerson { // 다형적 클래스
virtual ~IPerson() {}
virtual void Eat() = 0;
};
struct IStudent : virtual IPerson {
virtual void Study() = 0;
};
struct IWorker : virtual IPerson {
virtual void Work() = 0;
};
struct Researcher : IStudent, IWorker {
void Eat() {
cout << "먹는다... " << endl;
}
void Study() {
cout << "공부한다... " << endl;
}
void Work() {
cout << "일한다... " << endl;
}
};
int main(void) {
Researcher r;
r.Eat();
}
IStudent와 IWorker 인터페이스는 IPerson으로 부터 기존처럼 가상 상속을 받아야 한다
Researcher는 Eat()를 IWorker와 IStudent가 아닌 IPerson으로 부터 상속을 받는다
이전에 인터페이스가 아닌 클래스로 구현했던 코드에서는, Person의 Eat()가 실체가 있는 함수였기 때문에 오버라이딩을 하게 되면 문제가 발생했었다.
- 하지만 인터페이스에서는 실체가 없는 상황이 되었으므로 Researcher는 Person으로부터 안전하게 상속을 받을 수 있다.
age를 처리하고 싶은데, 인터페이스에서는 비정적 멤버 변수를 포함할 수 없다고 했다. 어떻게 해야할까?
Researcher 클래스에 age를 선언해줘야 한다
- IPerson 포인터를 사용해 다형성을 이용하고자 하는 경우 age 멤버 변수를 사용할 수 없게 된다.
이를 해결하기 위해 IPerson 인터페이스에 virtual int GetAge() = 0;이라는 순수 가상 함수를 만들어 처리할 수 있다.
물론 상속성에는 어긋나는 행동이긴 하다. Researcher2라는 클래스를 만들게 되면 이 클래스 내에 age 멤버 변수를 또 한 번 선언해줘야 한다
하지만 이 방법밖에 없다.
#include <iostream>
using namespace std;
struct IPerson { // 다형적 클래스
virtual ~IPerson() {}
virtual void Eat() = 0;
virtual int GetAge() = 0;
};
struct IStudent : virtual IPerson {
virtual void Study() = 0;
};
struct IWorker : virtual IPerson {
virtual void Work() = 0;
};
struct Researcher : IStudent, IWorker {
void Eat() {
cout << "먹는다... " << endl;
}
void Study() {
cout << "공부한다... " << endl;
}
void Work() {
cout << "일한다... " << endl;
}
int GetAge() {
return age;
}
int age;
};
int main(void) {
Researcher r;
r.age = 10;
r.Eat();
}
Student 타입을 갖는 인스턴스를 만들고 싶다면?
IStudent 인터페이스로부터 하위 클래스 Student를 만들어서 처리한다
Worker의 경우도 마찬가지
#include <iostream>
using namespace std;
struct IPerson { // 다형적 클래스
virtual ~IPerson() {}
virtual void Eat() = 0;
virtual int GetAge() = 0;
};
struct IStudent : virtual IPerson {
virtual void Study() = 0;
};
struct Student : IStudent {
void Eat() {
cout << "먹는다... " << endl;
}
void Study() {
cout << "공부한다... " << endl;
}
int GetAge() {
return age;
}
int age;
};
struct IWorker : virtual IPerson {
virtual void Work() = 0;
};
struct Researcher : IStudent, IWorker {
void Eat() {
cout << "먹는다... " << endl;
}
void Study() {
cout << "공부한다... " << endl;
}
void Work() {
cout << "일한다... " << endl;
}
int GetAge() {
return age;
}
int age;
};
int main(void) {
IPerson *p = new Researcher;
p->GetAge();
delete p;
p = new Student;
p->GetAge();
Student s;
s.Study();
Researcher r;
r.Study();
r.Work();
}
다중 상속에서는 상속성에 위배되고, 코드의 중복이 발생하는 경우가 많다
- 이런 점이 객체 지향의 한계?라고 볼 수 있다
'Programming Language > C++' 카테고리의 다른 글
[C++] 함수 템플릿과 클래스 템플릿 (0) | 2024.06.28 |
---|---|
[C++] 정사각형-직사각형 문제 (0) | 2024.06.28 |
[C++] 객체 지향 프로그래밍의 4대 원칙(원리) (0) | 2024.06.28 |
[C++] 상속에서의 형변환 - RTTI와 dynamic_cast (0) | 2024.06.28 |
[C++] 상속에서의 형변환 - 다운캐스팅 (0) | 2024.06.27 |