-
[C++] 다중 상속C++/이것이 C++이다 2021. 9. 24. 16:45
다중 상속은 한 클래스가 두 개 이상의 클래스를 동시에 상속받는 것을 의미합니다.
잘 사용한다면, 여러 클래스를 섞은 새로운 클래스를 쉽게 만들어 낼 수 있을 것 입니다.
하지만 그렇게 잘 사용하는 것 보다, 잘못될 가능성이 더 큰 기능이 다중 상속 입니다.
초기 개발 단계까지는 쉽고 빠른 길 처럼 보일 수 있지만, 이후에는 구조적 결함이 더 커질 확률이 높습니다.
이번 글에서는 다중 상속이 어떤 것이며, 왜 위험한 것인지를 중점으로 살펴 볼 것입니다.
1. 다중 상속
더보기서론에 의하면, "그렇게 위험한 것이면 문법적으로 제한하면 되지 않는가?" 라는 질문이 나올 수 있습니다.
실제로 JAVA, C# 등의 다른 객체 지향적 프로그래밍 언어는 다중 상속을 문법적으로 제한하고 있기 때문입니다.
하지만 C++는 개발자의 자유도를 최대한 보장해주는 언어이기 때문에, 이러한 방법과는 어울리지 않습니다.
이것이 지식적으로 다중 상속에 대하여 숙지해야 하는 이유입니다.
예제를 살펴보도록 하겠습니다.
#include <iostream> using namespace std; class MyImage { public: MyImage(int height, int width) : my_height(height), my_width(width) { cout << "MyImage(int, int)\n"; } int get_height() const { return my_height; } int get_width() const { return my_width; } int function() const { return 0; } protected: int my_height; int my_width; }; class MyShape { public: MyShape(int type) : my_type(type) { cout << "MyShape(int)\n"; } int get_type() const { return my_type; } int function() const { return 0; } protected: int my_type; }; class MyPicture : public MyImage, public MyShape { public: MyPicture() : MyImage(100, 50), MyShape(1) { cout << "MyPicture()\n"; } }; int main() { MyPicture a; cout << "Width : " << a.get_width() << "\n"; cout << "Height : " << a.get_height() << "\n"; cout << "Type : " << a.get_type() << "\n"; //Compile Error : Line 45 //C2385 : 'function' 액세스가 모호합니다. //cout << "Function : " << a.function() << "\n"; return 0; }
MyImage(int, int) MyShape(int) MyPicture() Width : 50 Height : 100 Type : 1
다중 상속을 통해 두 개 이상의 클래스를 상속받은 클래스는
생성자, 소멸자 호출 과정에서, 상속받은 클래스들의 생성자, 소멸자를 호출하게 됩니다.
여기까지는 당연하게 받아들일 수 있는 특징일 수 있습니다.
하지만, 주석처리 된 Line 45를 살펴보도록 하겠습니다.
주석처리 된 function()함수는 상위 클래스 2개에 모두 정의되어 있는 함수이기 때문에 하위 클래스에서 묵시적으로 호출할 수 없기 때문에 컴파일 에러가 발생하는 것 입니다.
이 또한 다중 상속의 특징이겠지만, 이렇게 호출이 모호해질 수 있다는 것 하나만으로도 다중 상속을 지양해야 하는 충분한 이유가 될 것이라고 생각합니다.
참고로, 위와 같은 경우는
a.MyImage::function() a.MyShape::function()
과 같은 식으로 명시적으로 클래스를 지정해 줄 경우 문제없이 컴파일됩니다.
2. 가상 상속
더보기1의 예제는 상속 관계에서의 모호성을 살펴보았습니다.
이번에는 조금 다른 방향의 문제점을 살펴보도록 하겠습니다.
예제를 보겠습니다.
#include <iostream> using namespace std; class MyObject { public: MyObject(int a) { cout << "MyObject() : " << a << "\n"; } virtual ~MyObject() {} }; class MyImage : public MyObject { public: MyImage() : MyObject(0) { cout << "MyImage()\n"; } }; class MyShape : public MyObject { public: MyShape() : MyObject(1) { cout << "MyShape()\n"; } }; class MyPicture : public MyImage, public MyShape { public: MyPicture() { cout << "MyPicture()\n"; } }; int main() { MyPicture a; return 0; }
MyObject() : 0 MyImage() MyObject() : 1 MyShape() MyPicture()
1의 예제에서 중요하지 않은 부분을 제외하고, 상위 클래스인 MyObject를 추가한 예제입니다.
출력을 보면 무엇이 문제인지 한번에 알 수 있을 것 입니다.
선언된 네 개의 클래스 중 최상위에 있는 MyObject 클래스의 생성자가 두번 호출되는 것을 볼 수 있습니다.
이것의 문제점에 대해서 논하기 전에, 해결방법을 먼저 이야기하도록 하겠습니다.
위와 같이 생성자가 여러번 호출되는 것은 가상 상속으로 해결할 수 있습니다.
#include <iostream> using namespace std; class MyObject { public: MyObject(int a) { cout << "MyObject() : " << a << "\n"; } virtual ~MyObject() {} }; class MyImage : virtual public MyObject { public: MyImage() : MyObject(0) { cout << "MyImage()\n"; } }; class MyShape : virtual public MyObject { public: MyShape() : MyObject(1) { cout << "MyShape()\n"; } }; class MyPicture : public MyImage, public MyShape { public: MyPicture() : MyObject(3) { cout << "MyPicture()\n"; } }; int main() { MyPicture a; return 0; }
MyObject() : 3 MyImage() MyShape() MyPicture()
모든 클래스의 생성자가 한번만 호출된 것을 볼 수 있습니다.
이것으로 생성자가 두번 호출되는 문제는 해결되었습니다.
하지만 만약에 MyShape, MyImage의 함수 중 MyObject의 초기화와 밀접한 관련이 있는 함수가 있을 경우 혹은 각 클래스가 MyObject의 멤버를 수정하는 함수를 가지고 있을 경우 등 구조적으로 불안정하고, 많은 버그가 발생할 수 있습니다.
3. 인터페이스 다중 상속
더보기컴퓨터 과학에서 인터페이스란 서로 다른 두 개의 시스템 (장치, 프로토콜 등)에서 정보나 신호를 주고받는 방법, 통로 등을 의미하는 말입니다.
구체적인 예시로 키보드, 마우스 등 서로 다른 기능을 하는 여러 기기들이 USB라는 하나의 인터페이스를 통해 작동하는 것을 생각 해 볼 수 있겠습니다.
다중 상속이 유의미하게 긍정적으로 사용되는 상황 중 하나가 바로 이 인터페이스입니다.
C++에서는 가상 클래스를 다른 언어의 인터페이스와 같이 이용하는데, 자세한 내용은 이전 글(가상 함수) 에 기술되어 있습니다.
예제를 살펴보도록 하겠습니다.
#include <iostream> using namespace std; class MyUSB { public: virtual void usb_connect() = 0; protected: int usb_port_version; }; class MyHDMI { public: virtual void display() = 0; }; class MyLaptop : public MyUSB, public MyHDMI { public: void usb_connect() { cout << "USB : Laptop\n"; } void display() { cout << "Display : Laptop\n"; } }; void something_usb(MyUSB& machine) { machine.usb_connect(); } int main() { MyLaptop a; something_usb(a); return 0; }
USB : Laptop
USB와 HDMI포트를 가지고 있는 노트북을 정의한 소스코드입니다.
USB와 HDMI 클래스는 인터페이스만 제공하고, 상세한 구현은 하위 클래스에서 구현했습니다.
중간에 호출된 something_usb는 파라미터를 MyLaptop이 아닌 MyUSB로 받았습니다.
이와 같이 인터페이스를 제공하는 용도로 다중 상속을 긍정적으로 이용할 수 있습니다.
개발 과정에서 설계상 다중 상속이 필요해지는 상황이 발생할 수 있습니다.
서론에서 언급하였듯이, 여러 기능을 하나로 모으는 것이 긍정적으로 보일 수 있습니다.
하지만, 본문에서 다룬 것과 같은 문제들이 발생하지는 않는지 면밀히 검토하고
더 나은 설계방법이 없는지 생각해보는 것이 프로젝트의 수명과 본인의 개발실력 함양에 더 도움이 될 것이라 생각합니다.
다음 시간에는 객체 지향 프로그래밍에서 객체 간의 관계 형성에 도움이 되는 문법을 살펴보겠습니다.
읽어주셔서 감사합니다.
'C++ > 이것이 C++이다' 카테고리의 다른 글
[C++] 템플릿 (0) 2021.10.26 [C++] 객체 간 관계 (0) 2021.10.19 [C++] 형 변환 연산자 (0) 2021.09.19 [C++] 가상 함수 (0) 2021.09.05 [C++] 상속 기본 (0) 2021.08.13