프로그래밍 공부
작성일
2023. 5. 21. 02:55
작성자
WDmil
728x90

C++ 의 상속은 이중상속이 가능하다.

 

C++ 의 상속과정에서 상속자를 두개 넣으면 두개의 class의 public 과 protected에 대한 값을 사용할 수 있다.

다음은 이러한 이중상속 에 대한 코드이다.

#include <iostream>
// 이중상속 에 대한 이야기
using namespace std;

class USBDevice
{
	int id;

public:
	USBDevice() = default;

	USBDevice(int id) : id(id) {}

	auto GetId() const { return id; }
	void PlugAndPlay() {}
};

class NetworkDevice
{
	int id;

public:
	NetworkDevice() = default;

	NetworkDevice(int id) : id(id) {}

	auto GetId() const { return id; }
	void Networking() {}
};

class USBNetworkDevice : public USBDevice, public NetworkDevice // 상속받는것 이 두가지로 이중상속을 한다.
{
public:
	USBNetworkDevice(int usbId, int netId)
		: USBDevice(usbId), NetworkDevice(netId)
	{}

	USBNetworkDevice(int id)
		: USBDevice(id), NetworkDevice(id)
	{}

};

int main()
{
	USBNetworkDevice device(1, 2);

	device.PlugAndPlay();
	device.Networking();

	device.USBDevice::GetId();
	device.NetworkDevice::GetId();

	return 0;
}

위와같이 class를 두개 상속받아 이중상속 할 수 있다.


C++ 에서 final을 통해 어떠한 클래스의 값을 상속시킬수 없게 하거나, virtual 키워드를 통해 함수 오버라이딩이 가능하다.

 

다음은 final과 virtual을 사용한 코드 예시이다.

#include <iostream>

using namespace std;

class A //final
	// final = 클래스를 상속시킬 수 없게 제한하는 키워드 이다. 이걸 붙이면 
	// 나는 더이상 이 함수를 상속하지 않겠다. 라는 의미이다.
{
public:
	virtual void Print(int x) { cout << "A" << endl; }
	// virtual = 가상함수 라는 의미로, 하위클레스에서 이 함수를 재정의 해줄 수 있음을 명시해주는 것.

	int i = 0;
};

class B /*final*/ : public A
	// B옆에 붙이면, B를 이후에 상속시키지 않겠다. 라는 의미.
{
public:
	void Print(int x) override /*final*/ { cout << "B" << endl; }
	// override 는 이 함수가 부모의 함수를 덮어씌워즈는 역활 이라는 것 을 명시해주는것.

	// 이때 인자값 과 반환형은 언제나 같아야 한다.
	// 함수에 final을 붙이면, 함수 한정해서 상속을 시키지 않을 수 있다.
};

class C : public B
{
public:
	void Print(int x) override { cout << "C" << endl; }
};

int main()
{
	A a;
	B b;
	C c;

	a.Print(1);
	b.Print(2);
	c.Print(3);

	A* aptr = new B();
	// B* bptr = aptr;

	B* bb = new B();
	A* aa = bb; // A포인터가 B를 가리킬때 A것이 출력되고 B가 출력되지 않는다.
	// 이는 오버라이딩 기준이 호출자 기준으로 정해지기 때문이다.

	return 0;
}

위와같이 Print를 통해 a와 b 그리고 c의 함수를 함수오버라이딩을 통해 출력했다.

 

원래라면, 상속받은 b와 c의 함수가 동일함으로 모호성문제가 발생하거나 함수출력에 다한 값을 지정해주어야 할 수 있지만, 

 

virtual선언을 A class 에서 해주었음으로 A의 Print함수는 가상함수 라고 컴파일러가 이해하였기 때문에, override되어,

 

각 class에서는 자기가 가지고있는 Print를 사용하여 출력하게 되는것이다.


또한, final을 class에 붙이게되면, 해당 class의 값을 다음 상속자에게 넘겨주지 않겠다고 선언하는. 명시해주는 역활로,

 

final을 B에 붙이게되면, C에서는 컴파일 오류가 나타난다.

위와같이, final 클래스 형식을 상속받을 수 없음이 컴파일러 오류로 나타난다.


그러나, 함수 오버라이딩 을 통하여 함수를 다음함수로 넘겨주어 자식클래스의 함수를 사용하게 된다고 하더라도, 포인터로 접속시 부모클래스를 통해 자식클래스로 접속하여 출력하게 되면, override된 함수가 출력되지 않는다.

 

다음은 class의 포인터로 부모클래스에대한 포인터에 자식클래스를 넣었을 때 의 코드이다.

#include <iostream>

using namespace std;
// 자기자신을 가리키는 포인터를 출력할 때 는 상속자 기준으로 출력이 아니라 포인터 기준으로 출력된다.

class A
{
public:
	virtual A* GetThisPointer() { cout << "A" << endl; return this; }
	void print() { cout << "A" << endl; }
};

class B : public A
{
public:
	B* GetThisPointer() override { cout << "B" << endl; return this; }
	void print() { cout << "B" << endl; }
};

int main()
{
	B b;

	b.print();
	cout << "Address : " << b.GetThisPointer() << endl;
	cout << "typeid : " << typeid(b.GetThisPointer()).name() << endl;

	A& ref = b;
	ref.print(); // A가 참조되어서, override가 되지 않았기 때문에, A가 출력된다.
	cout << "Adress : " << ref.GetThisPointer() << endl; // B를 출력하기 때문에 B가 출력되는것 이 맞다.
	cout << "typeid : " << typeid(ref.GetThisPointer()).name() << endl; // 타입 자체는 A이기 때문에 A의 타입이 반환되는것 이다.

	return 0;
}

 

위와같이, 자기자신의 포인터를 출력하였지만, Class의 포인터위치값은 같게 나타난다. 그러나, class의 타입이 A에서부터 B로  들어가게되면, A로 나타나는것 을 볼 수 있다.


다음은, viertual ( 가상함수 ) 를 활용한 코드의 예시이다.

#include <iostream>
// 가상함수 특징을 활용한 코드예시
using namespace std;

class Animal
{
	string name;

public:
	Animal(string name) : name(name) {}

	virtual void Speak() const { cout << "??" << endl; }
	
};

class Cat : public Animal
{
public:
	Cat(string name) : Animal(name) {}
	void Speak() const override { cout << "야옹" << endl; }
};

class Dog : public Animal
{
public:
	Dog(string name) : Animal(name) {}
	void Speak() const override { cout << "멍멍" << endl; }
};

int main()
{
	Cat cats[]{ Cat("cat1"),Cat("cat2"), Cat("cat3"), Cat("cat4"), Cat("cat5") };
	Dog dogs[]{ Dog("dog1"),Dog("dog2"), Dog("dog3"), Dog("dog4"), Dog("dog5") };

	for (const auto& cat : cats)
		cat.Speak();

	for (const auto& dog : dogs)
		dog.Speak();

	Animal* animals[]
	{
		&cats[0],
		&cats[1],
		&cats[2],
		&cats[3],
		&cats[4],
		&dogs[0],
		&dogs[1],
		&dogs[2],
		&dogs[3],
		&dogs[4]
	};

	for (const auto& animal : animals)
		animal->Speak();

	return 0;
}

위와 같이, 부모자식클래스를 활용하여 어떠한 그룹을 만들어, 출력할 수 있는 코드를 작성할 수 있다.

 

위 코드에서는 Animal이라는 부모클래스를 구성하고, Cat과 Dog라는 class의 배열을 생성하여, 생성자로 이름을 구분지어주었다.

 

그후, for문을 통하여 배열 을 전체검출하고, 검출마다 해당 class의 함수를 출력하도록 만들었다.

 

또한, 이러한 Animal class의 함수가 virtual 로 선언되어있음으로, override되어, 정상적인 함수의 출력값이 나타나게 되는것이다.


다음은 가상함수에서 재정의되어 출력되는 코드예시이다.

#include <iostream>
// 가상함수 에서 재정의하여 출력하는 예시.
using namespace std;

class A
{
public:
	void Print() { cout << "A" << endl; }
	virtual void Print1()		{ cout << "A1" << endl; }
	virtual void Print2()		{ cout << "A2" << endl; }
	virtual void Print3()		{ cout << "A3" << endl; }
	virtual void Print4() final { cout << "A4" << endl; }
};

class B : public A
{
public:
	void Print() { cout << "B" << endl; }
	virtual void Print1()		{ cout << "B1" << endl; }
	virtual void Print2()		{ cout << "B2" << endl; }
	virtual void Print3() final { cout << "B3" << endl; }
//	virtual void Print4() final { cout << "A4" << endl; }
};

class C : public B
{
public:
	void Print() { cout << "C" << endl; }
	virtual void Print1()		{ cout << "C1" << endl; }
	virtual void Print2() final { cout << "C2" << endl; }
//	virtual void Print3() final { cout << "C3" << endl; }
//	virtual void Print4() final { cout << "C4" << endl; }
};

class D : public C
{
public:
	void Print() { cout << "D" << endl; }
	virtual void Print1() final	{ cout << "D1" << endl; }
//	virtual void Print2() final { cout << "D2" << endl; }
//	virtual void Print3() final { cout << "D3" << endl; }
//	virtual void Print4() final { cout << "D4" << endl; }
};
int main()
{
	A a;
	a.Print1();

	B b;
	b.Print1();

	C c;
	c.Print1();

	D d;
	d.Print1();

	cout << endl;

	A& refB = b;
	A& refC = c;
	A& refD = d;

	cout << "######### A" << endl;
	a.Print();
	a.Print1();
	a.Print2();
	a.Print3();
	a.Print4();

	cout << "######### refB" << endl;
	refB.Print();
	refB.Print1();
	refB.Print2();
	refB.Print3();
	refB.Print4();

	cout << "######### refC" << endl;
	refC.Print();
	refC.Print1();
	refC.Print2();
	refC.Print3();
	refC.Print4();

	cout << "######### refD" << endl;
	refD.Print();
	refD.Print1();
	refD.Print2();
	refD.Print3();
	refD.Print4();

	D d1;
	D d2;
	// d1 과 d2의 주소는 같다.
	// 가상함수 테이블은 함수 포인터 배열이라고 생각하면 된다. 가상함수의 배열의 순서로 접근하게 된다.
	// 가상함수 테이블은 변수주소와는 다르다고 생각하면 된다.
	// 선입선출 방식으로 덮어씌워지면서 생성이된다.

	return 0;
}

위와같이, 상속, 상속, 상속하여. 계속해서 자신의 함수로 대체되는 과정을 반복하고, virtual되지 않을 함수를 지정하기 위해 명시적 선언자인 final을 사용하여 상속되지 않음을 표현하였다. [ 주석처리는 시각적으로 나타내기 위한 코드줄 ]

 

이때, virtual 선언된 함수가 대체될때 가상함수 테이블이 생성되어 d1과 d2의 주소가 같게 나타난다.

 

이는, 함수가 코드영역에 존재함으로써 나타나는 최적화 방식중 하나인데,

어떠한 class가 여러개 생성되더라도 함수는 언제나 같은 함수가 호출되기 때문에, 함수에 대한 저장영역을 여러개 만들어 줄 필요가 없기 때문이다.

 

그리하여 class가 여러개 생성되었을 때 각각 class는 자신의 가상함수 테이블이 생성되고, 해당되는 변수가 호출될 때 가상함수 테이블에서 지정된 가상함수테이블 의 함수를 가져오게 된다.

 

부모 자식 클래스간의 함수 override 가 다르기 때문에, 예를들어 부모클래스 A와 부모이자 자식인 B, B를 상속받는 C가 있다고 한다면,

 

B와 C는 각각 A의 함수를 override하여 대체한다.

 

그렇다면, 가상함수테이블 에는

 

A std::cout<< "A" << std::endl
B std::cout<< "B" << std::endl
C std::cout<< "C" << std::endl

 

로 저장된다고 축약해보자.

 

이때 A a1, A a2, B b1, B b2, C c1

이 생성된다면, 각각의 가상함수테이블 호출은 다음과 같을것 이다.

A std::cout<< "A" << std::endl a1
a2
B std::cout<< "B" << std::endl b1
b2
C std::cout<< "C" << std::endl c1

그리고, b1과 c1이 호출된다면, 다음과 같은 경로로 나타난다.

b1 = A -> B

c1 = A -> B -> C

728x90