7. 상속 + 오버라이딩 + 가상함수

JUSTICE_DER·2024년 1월 27일

❔ C++ QnA

목록 보기
1/2
  • 추상클래스?? 추상메서드 정의하도록 강제되지 않았는데

1. 생성과정

1-1. 부모와 자식의 생성과정

일반적인 Animal과 Dog의 상속관계에서,

	// 1-1. 생성자 호출순서
	Animal* A = new Animal;
	Dog* D = new Dog;	

#생성자
부모(Animal)는 생성시, 본인의 생성자가 당연히 호출되고,
자식(Dog)은 부모의 생성자를 호출하고, 본인의 생성자를 호출한다.

	// 1-5. 소멸자 호출순서
	delete A;
	delete D;

#소멸자
부모(Animal)는 소멸시, 본인의 소멸자가 당연히 호출되고,
자식(Dog)은 본인의 소멸자를 호출하고, 부모의 소멸자를 호출한다.

1-1-1. 그렇게 되는 이유?

2. 오버라이딩

ㄴ왜쓸까?

Animal* AD = new Dog; // Dog : public Animal
AD->Eat();

이 코드 때문에 사용하는 것으로 보인다.

Animal을 상속받는 다양한 동물 Cat, Bear 등이 있을거고,
각각을 사용하기 위해, Cat\*, Bear\* 이렇게 선언하는 것보다,
어짜피 상속하면 메서드의 이름도 같은거
그냥 ★ 공통부모인 Animal*을 통해 동일한 이름으로 더 편하게 접근하겠다는 의도로 사용할텐데

Dog객체로 생성한 Animal* AD는,
일반적으로 Eat을 할 때, Dog의 Eat()으로 동작할 것을 기대한다.

하지만, 일반 오버라이딩을 진행한다면, Animal의 Eat()이 실행된다.

2-1. 정적바인딩

#include <iostream>

using namespace std;

class Animal
{
public:
	Animal()
	{
		cout << "Animal : Init\n";
	}
	~Animal()
	{
		cout << "Animal : Delete\n";
	}
	void Eat()
	{
		cout << "먹었다\n";
	}
};

class Dog : public Animal
{
public:
	Dog()
	{
		cout << "Dog : Init\n";
	}
	~Dog()
	{
		cout << "Dog : Delete\n";
	}
	void Eat()
	{
		cout << "개가 먹었다\n";
	}
};

int nCount = 1;
inline void line() { cout << "\n==========================" << nCount << "\n"; nCount++; }

void main()
{
	// 1. 정적바인딩 - 함수에 virtual을 붙이지 않은경우

	line();

	// 1-1. 생성자 호출순서
	Animal* A = new Animal;
	Dog* D = new Dog;
	
	line();

	// 1-2. 업캐스팅
	A->Eat();				// 먹었다
	D->Eat();				// 개가 먹었다
	((Animal*)D)->Eat();	// 먹었다		

	line();

	// 1-3. 핵심
	Animal* AD = new Dog;
	AD->Eat();				// 먹었다		 

	line();

	// 1-4. 다운캐스팅
	((Dog*)AD)->Eat();		// 개가 먹었다	

	line();

	// 1-5. 소멸자 호출순서
	delete A;
	delete D;

	cout << "\n";
    
	//delete AD;
	delete (Dog*)AD;

정적바인딩은 변수를 생성하면 자료형을 바꾸지 않는다.
★ = 명시한 자료형에 맞게 정직하게 동작

1-2. ((Animal*)D)->Eat(); 에서도
D의 실제 타입은 Dog지만, Animal로 명시적 업캐스팅을 했기에,
Animal의 Eat으로 동작.

1-3. AD의 실제 타입은 Dog지만, Animal로 명시되어있으므로,
같은 이유로 Animal의 Eat으로 동작.
(생성자의 실행과정을 보면, Animal->Dog으로 실행)

1-4. 업캐스팅을 다시 다운캐스팅하면
(실제 개체가 Dog이었기 때문에 가능)
Dog으로 명시되어있으므로, Dog의 Eat으로 동작

1-5. delete AD를 하면 AD가 Animal이므로 Animal의 소멸자 호출.

그렇다면 delete (Dog*)AD는 Dog로 명시했으니,
자식인 Dog, 부모인 Animal순으로 소멸자가 호출될 것이다.

그렇게 된다면 문제가 보인다.
Animal* AD = new Dog로 선언을 했고,
delete AD로 해제를 하는게 뭔가 당연해 보인다.
하지만 그렇게 하면, AD는 실제로 Dog로 생성됐음에도,
Animal의 delete만 진행되어 문제가 발생.

그래서 애초에 정적바인딩은
Animal* AD = new Dog; 이런 구문을 사용하지 않는다
Dog* D = new Dog; delete D; 이런식으로 직접 명시할 것이다.

그런데 그거 하기 싫으니까 virtual 붙여서 overriding한다.

2-1-1. 다운캐스팅

ㄴ왜쓸까?

2-2. 동적바인딩

ㄴ왜쓸까?

class Animal
{
public:
	Animal()
	{
		cout << "Animal : Init\n";
	}
	virtual ~Animal()
	{
		cout << "Animal : Delete\n";
	}

	virtual void Eat()
	{
		cout << "먹었다\n";
	}
	virtual void Fly()				// 추가
	{
		cout << "날았다\n";
	}
};

class Dog : public Animal
{
public:
	Dog()
	{
		cout << "Dog : Init\n";
	}
	~Dog()
	{
		cout << "Dog : Delete\n";
	}
	void Eat()
	{
		cout << "개가 먹었다\n";
	}
	/*
	void Fly()						// 구현하지 않는다.
	{
		cout << "개가 날 수 있을까? \n";	
	}
	*/
};

{
	// 2. 동적바인딩 - 함수에 virtual을 붙인 경우 - 가상함수테이블

	line();

	// 1-1. 생성자 호출순서
	Animal* A = new Animal;
	Dog* D = new Dog;
	
	line();

	// 1-2. 업캐스팅
	A->Eat();				// 먹었다
	D->Eat();				// 개가 먹었다
	((Animal*)D)->Eat();	// 개가 먹었다	<< 부모의 "먹었다" 가 동작하지 않는다

	D->Animal::Eat();		// 먹었다		<< 이렇게 직접 명시하면 부모의 "먹었다" 동작

	line();

	// 1-3. 핵심
	Animal* AD = new Dog;
	AD->Eat();				// 개가 먹었다	<< 동적바인딩되어 Dog의 Eat()

	AD->Fly();				// 자식에서 구현되지 않은경우, 부모의 것이 호출됨

	line();

	// 1-4. 다운캐스팅
	((Dog*)AD)->Eat();		// 개가 먹었다	<< 되는이유?

	line();

	// 1-5. 소멸자 호출순서
	delete A;
	delete D;

	cout << "\n";

	//delete (Dog*)AD;
	delete AD;				// 이것도 virtual이기 때문에, Dog의 소멸자 호출	
}

1-2. ((Animal*)D)->Eat()를 했는데도, 부모의 Eat이 동작하지 않았다.
이건 왜그렇지??
Animal* D = new Dog 와 같은 원리인 것 같다.

1-3. 기본적으로 이 행위를 위해 동적바인딩을 사용한다고 생각한다.
Animal로 하위 클래스들을 동일하게 관리할 수 있게 된다.

virtual이 자식에서 구현되지 않은 경우는,
부모의 virtual 함수가 그대로 실행된다.

만약 부모의 virtual 함수에도 구현이 되어있지 않다면,
오류발생

2-2-1. 업캐스팅

ㄴ왜쓸까?

2-2-1. 가상함수테이블

2-2-2. 가상소멸자

Animal* AD = new Dog
생성자는 애초에 객체를 new하여 생성하면,
부모 - 자식 순으로 알아서 생성을 해준다.

소멸자는 그렇지 않았다.
delete AD라는 것은, delete (Animal*) AD라는 것이고,
소멸자는 대입하는 과정이 없기 때문에, 실제 객체의 타입을 인식할 수 없으므로,
Animal의 소멸자만 호출.

따라서 부모클래스에서 가상함수를 사용한다는 것은,
부모클래스의 소멸자도 반드시 virtual이어야 한다는 말.

가상으로 소멸자를 해두지 않는다면,
소멸자를 정적바인딩처럼 볼 수 있고,
Animal, 즉 부모의 것으로만 인식하게 될 것이다.
virtual이라고 다른 소멸자가 있을 것으로 판단할 여지를 주지 않았기 때문.
(C++는 기본 정적바인딩, JAVA는 기본 동적바인딩)

2-2-3. 가상생성자?

그러면 가상생성자라는걸 사용한다면 어떻게 될까

생각해본다.

profile
Time Waits for No One

0개의 댓글