ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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

    댓글

Designed by Tistory.