[C++] 09. 가상의 원리와 다중상속

kkado·2023년 10월 14일
0

열혈 C++

목록 보기
9/16
post-thumbnail

💬 윤성우 님의 <열혈 C++ 프로그래밍> 책을 혼자 공부하며 배운 내용을 정리합니다. 글의 모든 내용은 책에서 발췌하였습니다.


지금까지는 객체 내에 멤버 함수가 존재한다고 설명했지만 사실 아니다.
실제로는 동일 클래스로부터 만들어진 여러 객체들이 하나의 함수를 공유하여 사용하고, 조금만 생각해보아도 그 편이 메모리 측면에서 좋다는 생각이 든다.

멤버 함수는 메모리의 한 공간에 클래스와 별도로 위치하고, 이 함수가 정의된 모든 클래스의 객체가 이를 공유하는 형태이다.


가상 함수의 동작 원리와 가상 함수 테이블

이 개념을 알고 나면 가상함수의 특성을 조금 더 잘 이해할 수 있게 된다.

먼저 간단한 예제를 살펴보자.

class AAA
{
private:
    int num1;
public:
    virtual void Func1()
    {
        cout <<"Func1\n";
    }
    virtual void Func2()
    {
        cout << "Func2\n";
    }
};

class BBB : public AAA
{
private:
    int num2;
public:
    virtual void Func1()
    {
        cout <<"BBB::Func1\n";
    }
    void Func3()
    {
        cout << "BBB::Func3\n";
    }
};

int main()
{
    AAA* aptr = new AAA();
    aptr->Func1();

    BBB* bptr = new BBB();
    bptr->Func1();
}

실행 결과는 아래와 같다.

Func1
BBB::Func1

가상 함수가 존재하는 클래스에 대해서, 컴파일러는 가상 함수 테이블을 만든다. 이것은 실제 호출되어야 할 함수의 위치정보를 담고 있는 테이블이다.

아래 사진은 AAA 클래스의 가상 함수 테이블이다.

key-value 형태로 함수가 저장된 메모리 위치가 기록되어 있다.
key는 호출하고자 하는 함수를 구분지어주는 구분자로, Func1 함수가 호출되면 테이블의 key값들 중에서 Func1 함수를 찾아 함수가 있는 메모리 위치로 이동한다.

그리고 아래 사진은 BBB 클래스의 가상 함수 테이블이다.

BBB 클래스는 AAA 클래스를 상속받고 있고 그 중 Func1을 오버라이딩 하고 있다. 테이블을 살펴보면 AAA 클래스의 Func1에 대응되는 key-value 정보가 없는 것을 확인할 수 있다.

오버라이딩 된 가상함수의 주소 정보는 유도 클래스의 가상함수 테이블에 포함되지 않는다.

이 가상함수 테이블은 main 함수가 실행되기 이전에 생성되어 메모리 공간에 할당된다. 가상함수 테이블은 마치 정적 변수처럼 객체의 생성과 상관없이 메모리 공간에 할당된다.

그리고 눈에 보이지 않는 포인터 변수를 통해 테이블 주소를 가지고 있다.


다중 상속에 대한 이해

다중 상속은 득보다 실이 많은 문법이라고 생각하는 프로그래머들이 많다고 한다.
그러나 예외적인 상황에서 아주 제한적인 사용까지 부정할 필요는 없으므로 이를 위해 공부할 필요는 있다는 것이 저자님의 의견이다.

다중상속이란, 둘 이상의 클래스를 동시에 상속하는 것을 말한다.

다중상속의 방법

class BaseOne
{
public:
    void SimpleFuncOne()
    {
        cout << "Func1\n";
    }
};

class BaseTwo
{
public:
    void SimpleFuncTwo()
    {
        cout << "Func2\n";
    }
};

class MultiDerived : public BaseOne, protected BaseTwo
{
public:
    void complexFunc()
    {
        SimpleFuncOne();
        SimpleFuncTwo();
    }
};

int main()
{
    MultiDerived mdr;
    mdr.complexFunc();
}

클래스 자체는 정말 간단하고, MultiDerived 클래스에서 : public BaseOne, protected BaseTwo 라고 선언된 부분만 주목하면 된다!


다중상속의 문제점

한 클래스가 다중 상속하는 두 기초 클래스에서 동일한 이름의 멤버가 존재할 경우에는 어떤 클래스의 멤버에 접근하라는 건지 모호한 상황이 발생한다.

이 문제점의 해결책은, 멤버 앞에 클래스 이름을 명시해 주는 것이다. 가령 다음처럼

class BaseOne
{
public:
    void SimpleFunc()
    {
        cout << "Func One\n";
    }
};

class BaseTwo
{
public:
    void SimpleFunc()
    {
        cout << "Func Two\n";
    }
};

class MultiDerived : public BaseOne, protected BaseTwo
{
public:
    void complexFunc()
    {
        BaseOne::SimpleFunc();
        BaseTwo::SimpleFunc();
    }
};

함수 앞에 BaseOne:: 이라고 명시해 줌으로써 모호성 문제를 해결할 수 있다.


가상 상속

class Base
{
public:
    Base()
    {
        cout << "Base Constructor\n";
    }

    void simpleFunc()
    {
        cout << "BaseOne\n";
    }
};

class MiddleDerivedOne : virtual public Base
{
public:
    MiddleDerivedOne() : Base()
    {
        cout << "MiddleDerivedOne Constructor\n";
    }

    void MiddleFuncOne()
    {
        simpleFunc();
        cout << "MiddleDerivedOne\n";
    }
};

class MiddleDerivedTwo : virtual public Base
{
public:
    MiddleDerivedTwo() : Base()
    {
        cout << "MiddleDerivedOne Constructor\n";
    }

    void MiddleFuncTwo()
    {
        simpleFunc();
        cout << "MiddleDerivedTwo\n";
    }
};

class LastDerived : public MiddleDerivedOne, public MiddleDerivedTwo
{
public:
    LastDerived() : MiddleDerivedOne(), MiddleDerivedTwo()
    {
        cout << "LastDerived Constructor\n";
    }

    void complexFunc()
    {
        MiddleFuncOne();
        MiddleFuncTwo();
        simpleFunc();
    }
};

int main()
{
    LastDerived ldr;
    cout << "---------------------\n";
    ldr.complexFunc();
}
Base Constructor
MiddleDerivedOne Constructor
MiddleDerivedOne Constructor
LastDerived Constructor
---------------------
BaseOne
MiddleDerivedOne
BaseOne
MiddleDerivedTwo
BaseOne

Base 클래스가 있고 이 클래스를 Middle 클래스 두 개가 각각 virtual로 상속받는다. 그리고 이 Middle 클래스들을 최종적으로 상속받는 LastDerived 클래스가 있다.

실행 결과는 위와 같다.

여기서 virtual로 상속을 받지 않는다면 어떤 일이 생길까??

Middle 클래스들에서 Base 클래스를 상속 받을 때 각각의 객체 안에 Base 클래스의 멤버들이 존재하게 된다.

이것들을 또다시 상속받으니 한 객체 안에 두 개의 Base 클래스 멤버가 존재한다. 따라서 모호성이 발생한다.

그런데 같은 클래스의 멤버라면 하나만 존재하는 것이 타당할 것이다.
따라서 Base 클래스를 한 번만 상속받게끔 하는 것이 좋을 것인데 이 해결책이 바로 가상으로 상속을 하는 방법이다.

위의 코드 결과를 잘 보면 'Base Constructor' 가 한 번만 출력된 것을 볼 수 있다. 모호성이 해결됐고 하나의 멤버만을 상속받고 있다는 뜻이다.

이를 그림으로 나타내면 다음과 같다.

만약 가상 상속을 하지 않는다면 Base 클래스의 생성자는 두 번 호출된다.


profile
울면안돼 쫄면안돼 냉면됩니다

0개의 댓글