상세 컨텐츠

본문 제목

C++ 다중 상속

C++/C++98

by deulee 2023. 8. 7. 13:06

본문

다중 상속

다중 상속은 말 그대로 복수의 부모 클래스를 가진 자식 클래스를 의미한다.

 

예를 들면 다음과 같은 상황이라고 할 수 있다.

 

 

이렇게 될 경우 자식 클래스 "C"는 부모 클래스 "A"와 "B"의 멤버를 모두 상속받게 된다.

 

 

 

아래는 예시 코드며 위의 상황이 어떻게 보이는지 알 수 있다.

 

#include <iostream>

class A
{
public:
	int a;
	A(int _a) : a(_a)
	{
		std::cout << "A Constructor" << std::endl;
	}
	void fA()
	{
		std::cout << "A: " << a << std::endl;
	}
	~A()
	{
		std::cout << "A Destructor" << std::endl;
	}
};

class B
{
public:
	int b;
	B(int _b) : b(_b)
	{
		std::cout << "B Constructor" << std::endl;
	}
	void fB()
	{
		std::cout << "B : " << b << std::endl;
	}
	~B()
	{
		std::cout << "B Destructor" << std::endl;
	}
};

class C : public A, public B
{
public:
	int c;
	C(int _a, int _b, int _c) : A(_a), B(_b), c(_c)
	{
		std::cout << "C Constructor" << std::endl;
	}
	void fC()
	{
		fA();
		fB();
		std::cout << "C: " << c << std::endl;
	}
	~C()
	{
		std::cout << "C Destructor" << std::endl;
	}
};

int main(void)
{
	C c(1, 2, 3);

	c.fC();
	return 0;
}

출력 결과

 

출력 결과를 통해 알 수 있지만 부모의 생성자가 먼저 호출되고 자식의 파괴자가 먼저 호출되는 것을 볼 수 있다.

 

또한, 양쪽 부모 모드의 멤버에 접근이 가능한 것도 알 수 있다.


다중 상속의 문제점

그럼 어떤 경우에 문제가 생기게 될까?

 

바로 이같은 상황에서 문제가 된다는 것이다.

 

#include <iostream>

class A
{
public:
	int a;
	A(int _a) : a(_a)
	{
		std::cout << "A Constructor" << std::endl;
	}
	void fA()
	{
		std::cout << "A: " << a << std::endl;
	}
	~A()
	{
		std::cout << "A Destructor" << std::endl;
	}
};

class B : public A
{
public:
	int b;
	B(int _a, int _b) : A(_a), b(_b)
	{
		std::cout << "B Constructor" << std::endl;
	}
	void fB()
	{
		fA();
		std::cout << "B : " << b << std::endl;
	}
	~B()
	{
		std::cout << "B Destructor" << std::endl;
	}
};

class C : public A
{
public:
	int c;
	C(int _a, int _c) : A(_a), c(_c)
	{
		std::cout << "C Constructor" << std::endl;
	}
	void fC()
	{
		fA();
		std::cout << "C: " << c << std::endl;
	}
	~C()
	{
		std::cout << "C Destructor" << std::endl;
	}
};

class D : public B, public C
{
public:
	int d;
	D(int _a, int _b, int _c, int _d) : B(_a, _b), C(_a, _c), d(_d) 
	{
		std::cout << "D Constructor" << std::endl;
	}
	void fD()
	{
    	// fA(); 오류
		fB();
		fC();
		std::cout << "D: " << d << std::endl;
	}
	~D()
	{
		std::cout << "D Constructor" << std::endl;
	}
};

int main(void)
{
	D d(1, 2, 3, 4);

	d.fD();
	return 0;
}

출력 결과

클래스 "B"와 "C"는 클래스 "A"로 부터 상속을 받았고 클래스 "D"는 클래스 "B"와 "C"로 부터 상속을 받고 있다.

 

이런 경우를 다이아몬드(마름모) 계층도라고 부른다.

 

이렇게 될 경우 멤버가 다음과 같이 상속이 되는데 이게 문제를 일으키게 된다.

 

 

 

즉, 클래스 "D"는 클래스 "A"를 간접적으로 두 번 상속 받게 되며 클래스 "A"의 멤버를 두 번 물려받게 된다.

 

이렇게 될 경우 메모리 공간이 쓸데없이 낭비되고 클래스 "D"에서 클래스 "A"의 멤버 "a"를 참조할 때 컴파일러 입장에서 우선 순위를 매길 수 없서 에러 메시지가 출력될 것이다. 이는 비단 멤버 변수 뿐만이 아니라 멤버 함수에도 똑같이 적용이 된다.

 

물론 다음과 같이 오류를 피할 수 있다.

B::a;
C::a;

위와 같이 소속 기반 클래스를 명시함으로써 두 변수를 구분할 수는 있지만 무척 번거롭다.


가상 기반 클래스(Virtual Base Class)

그럼 두 번 상속받는 문제를 한 번만 상속받도록 할 수 없을까?

 

이런 문제를 해결하기 위해 가상 기반 클래스(Virtual Base Class)가 있다. 이렇게 지정된 클래스는 간접적으로 두 번 상속되더라도 결과 클래스에는 자신의 멤버를 한 번만 상속시킨다.

 

#include <iostream>

class A
{
public:
	int a;
	A(int _a) : a(_a)
	{
		std::cout << "A Constructor" << std::endl;
	}
	void fA()
	{
		std::cout << "A: " << a << std::endl;
	}
	~A()
	{
		std::cout << "A Destructor" << std::endl;
	}
};

class B : virtual public A
{
public:
	int b;
	B(int _a, int _b) : A(_a), b(_b)
	{
		std::cout << "B Constructor" << std::endl;
	}
	void fB()
	{
		std::cout << "B : " << b << std::endl;
	}
	~B()
	{
		std::cout << "B Destructor" << std::endl;
	}
};

class C : virtual public A
{
public:
	int c;
	C(int _a, int _c) : A(_a), c(_c)
	{
		std::cout << "C Constructor" << std::endl;
	}
	void fC()
	{
		std::cout << "C: " << c << std::endl;
	}
	~C()
	{
		std::cout << "C Destructor" << std::endl;
	}
};

class D : public B, public C
{
public:
	int d;
	D(int _a, int _b, int _c, int _d) : A(_a), B(_a, _b), C(_a, _c), d(_d) 
	{
		std::cout << "D Constructor" << std::endl;
	}
	void fD()
	{
		fA();
		fB();
		fC();
		std::cout << "D: " << d << std::endl;
	}
	~D()
	{
		std::cout << "D Constructor" << std::endl;
	}
};

int main(void)
{
	D d(1, 2, 3, 4);

	d.fD();
	return 0;
}

출력 결과

위의 예시처럼 가상 부모 클래스를 지정할 때는 상속문의 부모 클래스앞에 "virtual" 키워드를 사용해주면 된다.

 

이렇게 수정한 뒤 컴파일하게 되면 클래스 "D"에서 클래스 "A"의 멤버 함수 "fA"를 호출해도 전혀 모호함 없다.

 

또한 주목해야 할 점은, 클래스 "D"의 생성자에서 멤버 초기화 리스트로 "A"를 초기화 하는 것을 알 수 있다.

 

중간 단계의 부모 클래스 "B"와 "C"는 최상위 계층의 부모 클래스인 "A"의 생성자를 호출하지 않는다.

 

가상 기반 클래스를 사용하게 되면 클래스 "A"는 현재 한 번만 메모리에 할당되어 있는 상태이다. 그 말은 클래스 "B"와 클래스 "C"가 참조하는 클래스 "A"는 똑같은 메모리를 가진 객체라는 것이다. 즉, 중복으로 가리키고 있다고 생각하면 된다.

 

이 상태에서 클래스 "B"와 클래스 "C"가 동시에 "A"를 초기화하면 어떻게 될까?

 

순서에 따라 값이 덮어질 것이다.

 

이를 방지하기 위해서 가상 기반 클래스로 상속된 중간 단계의 부모 클래스는 최상위 계층의 부모 클래스의 생성자를 호출하지 않는 것이다.

 

만약 "D"가 "A"의 생성자를 호출하지 않으면 "A"의 디폴트 생성자가 호출된다.


가상 포인터

이러한 가상 상속을 받으면 중복된 멤버가 한 번밖에 나타나지 않으므로 객체의 크기는 줄어들 것이라고 생각하지만 실제로는 그렇지 않다.

 

중복된 멤버의 관리를 위해 숨겨진 포인터가 추가되기 때문이다.

 

중복 멤버의 관리 방법은 컴파일러마다 다르지만 가상 기반 클래스 하나에 숨겨진 포인터의 크기(8바이트)씩 더 추가되므로 중복된 멤버의 크기가 8바이트 이상일 때만 객체 크기가 줄어든다.

 

즉, 가상 기반 클래스가 문자열이나 대규모 배열을 가질 경우 객체 크기는 극적으로 작아질 것이다.


출처

http://www.soen.kr/lecture/ccpp/cpplec.htm

 

C/C++ 강좌

 

www.soen.kr

 

 

 

 

'C++ > C++98' 카테고리의 다른 글

C++ 가상 함수  (0) 2023.08.07
C++ [Has a] 관계  (0) 2023.08.07
C++ 오버라이딩  (0) 2023.08.07
C++ 상속  (0) 2023.08.07
C++ 다양한 연산자 오버로딩의 예시  (0) 2023.08.05

관련글 더보기