가상 함수 (2), 추상화

Yama·2024년 1월 16일
0

어소트락 수업

목록 보기
46/55

Child 클래스부터 생겨난 함수도 있을것이다.

// Child클래스
	void SetChild(int _child) { m_Child = _child; }
  • 물려받아서 생긴게 아니라 순수하게 자식클래스에서 생긴 함수다.
	Parent* pParent = nullptr;

	Child c5;
	c5.SetChild(10);
    
    pParent = &c5;

  • 부모쪽 포인터 변수로는 자식쪽에 순수하게 구현된 함수에 접근하지 못한다.
	Child* pChild = &c5;
	pChild->SetChild(10);
  • 자식 클래스에서 만든 함수는 자식 포인터로 접근 가능하다.
  • 순수하게 자식클래스에 만든거는 부모쪽에서 접근 못하지만 자식은 부모를 상속받은걸 자식쪽에서 재정의 하거나 그냥 상속 받을수 있다.

정리

  • 부모 클래스 포인터 타입으로 자식 클래스 객체의 주소를 가리킨 경우

    • 가상함수는 테이블에 등록된 함수를 호출하기 떄문에, 각 클래스에서 오버라이딩한 원래 버전을 사용할수 있지만

    • 순수하게 자식 클래스부터 구현된 함수는 부모 클래스 포인터로 알 수 가 없다.

해결 방안

	((Child*)pParent)->m_Child = 10;
  1. 다운 캐스팅
    • 부모 클래스 포인터에 저장된 주소가 특정 자식클래스 객체임이 확실하다면, 해당 자식 클래스 포인터 타입으로 강제로 캐스팅해서 실제 그 객체를 온전히 접근하는 방법
      • pParent에 들어있는게 Child포인터가 아니라 진짜 pParent 포인터 였다면 접근할떄 메모리 범위를 넘어 버려서 확실할때만 사용해야 한다.
		Child* pChild = dynamic_cast<Child*>(pParent);

		if (pChild != nullptr)
		{
			pChild->SetChild(10);
		}
  1. RTTI(Runtime Type Idendification / Informtion)
    • 런타임 도중에 알아야 되는 경우다.
    • dynamic_cast가 RTTI다.
    • 사용예시
      • 오브젝트를 상속받은 몬스터,미사일,플레이어가 있다. 그럼 이 몬,미,플의 클래스는 오브젝트 포인터로 다 가리킬수 있는데 내가 지금 누구를 가리키고 있냐에 따라서 추적할지 말지를 구현했다치면
      • 플레이어가 유도미사일을 쏘면 그걸 몬스터의 주소값이 맞다면 추적을해서 그걸 유도미사일이 몬스터의 주소를 런타임중에 따라 가게한다.
    • Parent에 들어간 주소의 객체가 Child였다면 그 Child 객체의 주소가 들어간다.
      • pParent 들어간 객체가 Child객체가 아니였다면 다이나믹 케스트가 널로 만들어서 널을 반환한다.
  • 둘다 다운 캐스팅의 일종이지만 1번은 무조건 하는 강제 캐스팅이라면 2번은 다운 캐스팅을 할떄 케이스가 성공했는지의 여부를 알 수 있기 때문에 1번인 강제보다는 안전하다.
  • 다운 캐스팅을 할려면 가상 함수 테이블이 필요하기 때문에 가상 함수가 하나는 존재해야 한다.

특정 클래스의 타입 정보를 가져오기

#include <typeinfo>
  • typeid()함수 쓸려고 선언
	const type_info& info = typeid(Parent);
	unsigned __int64 id = info.hash_code();
	const char* pClassName = info.name();
  • Parent의 주소가 저장되는 타입도 존재하고 id 이름 등등이 존재한다.
    • 고유 id, 고유 이름
    • 여러가지가 들어간다.

winapi부터 상속,가상함수,다형성,추상화

  • 본인만의 오브젝트를 만들어서 해보깅 이런 기능을 만들고 싶다면 생각을해보고 이게 맞다면 코드를 치고 이런걸 많이 해봐야한다.
  • 생각접근방법이 맞는지 맞다면 이걸 문법적으로 어케할건지 이런걸 해봐야한다.(복습량증가.)
  • 코드를 쳐보는것보다 설계된 코드를 보는게 좋다.

상속 다형성 정리.

  • 상속

    • 부모 클래스의 기능을 물려받음, 코드 재사용성
    • 오버 라이딩, 부모 클래스의 기능을 재정의
  • 상속을 남발하다보면 클래스 종류와 갯수가 많아지고 클래스에서 만든 객체들이 존재하니까 파생되서 상속이 존나 많이되고 일괄적으로 관리가 어려워진다.

    • 이 문제를 해결하기 위해 다형성을 사용.
  • 다형성

    • 부모 클래스 포인터 타입으로, 파생되는 모든 자식 클래스 객체들의 부모 포인터로 주소를 가리켜서 하나의 포인터로 모든걸 클래스들을 관리가 가능햇당.
  • 그렇지만 기능을 접근하는데 문제가 발생 부모포인터로 접근하면 부모쪽만 접근해서 자식쪽에 오버라이딩한게 접근이 안된다.

    • 가상함수로 해결.
  • virtual (가상함수)

    • 가상함수를 통해서 그 가상 함수 테이블에 등록되어있는 것으로 가게된다.
  • 자식 클래스에서부터 추가된 함수에 대한 접근을 할때 문제 발생.

    • 다운 캐스팅으로 해결.
  • 다운 캐스팅(강제 캐스팅, dynamic_cast)

    • 자식클래스에서부터 추가된걸 부모포인터로 지목하면 자식클래스에 추가된걸 모르니까 다운캐스팅으로 알게한다.

추상화

  • 추상 클래스(Abstract Class)
  • 반대어
    • 구체적, 명확하다.
  • 상속을 목적으로, 파생되는 자식클래스들의 공통분모 역할을 하는 클래스 구체성이 없기 때문에 해당 클래스로 객체가 생성되는 것을 방지해야 함
    • 동물클래스와 조류를 클래스의 객체를 만들면 그건 무슨 클래스인지 구체적인건 따로있다 닭, 비둘기 등등..
    • 동물과 조류는 추상 클래스가 된다.
class Animal
{
public:
	// 1
	virtual void Move()
	{
		cout << "Move" << endl;
	}
};
class Bird
	: public Animal
{
	// 2
	void Move()
	{
		cout << "Fly" << endl;
	}
};
// 3
class Eagle
	: public Bird
{
	// 3
	// void Move()
	//{
	//	cout << "Fly" << endl;
	//}


};
  • 1번은 동물 클래스를 만들어서 움직임 가상 함수를 만든다.
  • 2번은 새는 날아다니까 가상 함수를 통해 함수 오버라이딩을 한다.
  • 3번은 독수리도 날아다니는건 같으니까 2번을 안적어서 걍 새의 움직임을 상속받으면 된다.
  • 문제가 존재한다
	Animal animal;
	Bird bird;
	Eagle eagle;
  • Animal 클래스와 Bird 클래스 Eagle 클래스에서 모두 객체를 만들수 있다.
    • 3개의 클래스를 봤을떄 새와 동물은 추상 클래스 일것이기 때문에 구체적인 클래스로 사용되면 안된다.
      • 순수 가상 함수로 해결.
  • 순수 가상함수(pure virtual function) 이 정의되어 있는 클래스는 추상 클래스로 취급한다.
    • 객체 생성이 불가능
// 애니멀 클래스에 순수 가상 함수 만듬.
virtual void Eat() = 0;
  • 동물은 먹는 기능이 있어서 상속을 받지만 기능은 다 다를것이기 때문에 공통적으로 상속받는 동물 클래스에서는 공통분모가 아니기 때문에 구현을 할 필요가 없지만 먹는다는 기능은 꼭 있어야 한다.
  • 위의 코드는 인터페이스를 제시한다 고 한다.
// Bird 클래스에 구현되어 있다.
	void Eat()
	{
		cout << "pack" << endl;
	}
  • Animal을 상속받는 모든 자식 클래스들은 추상 클래스를 벗어나기 위해서 순수 가상 함수를 구현해야 한다.
  • 애니멀에서 상속받은 Eat을 구현했기 때문에 추상클래스에서 탈출했다.
    • Bird, Eagle클래스 객체 생성 가능해짐.
// main.24에 있는 Child클래스 내부
public:
	void SetChild(int _child) { m_Child = _child; }

	void OutputMyData() 
	{
		cout << "Child" << endl;
		cout << "m_Parent : " << m_Parent << endl;
		cout << "m_Child : " << m_Child << endl;
	}

	void VirtualFunc() 
	{

	}
  • 3개의 함수를 봤을떄 일반 함수인지 가상 함수인지 딱 봤을떄 구분이 안된다.
	virtual void OutputMyData()
	{
		cout << "Child" << endl;
		cout << "m_Parent : " << m_Parent << endl;
		cout << "m_Child : " << m_Child << endl;
	}

	virtual void VirtualFunc() 
	{

	}
  • 가상함수인 친구들은 가독성을 높여주기 위해서 virtual을 붙여주는게 좋다.
    • 의도적으로 붙여서 딱 봤을떄 구분하기 위해서 안 적어도 문법오류는 아니지만 붙이자.
	virtual void Test()
	{

	}
  • Child 클래스부터 생겨난 가상 함수이다.
  • Parent클래스의 가상 함수는 OutputMyData과 VirtualFunc를 Child에서는 받아서 재정의를 하고 있다. Test는 Child 클래스부터 생겨난 가상 함수이다.
    • Parent는 a.b 함수 Child a,b함수를 재정의해서 받고 Child 클래스에 추가된 c함수가 있다고 가정.
    • Child클래스를 상속받은 other클래스가 있다고 가정하고 other에서는 a함수를 재정의하고 b는 그냥 상속받고 c도 재정의 해서 받는 상황
    • Parent쪽 포인터로 접근하면 other의 c함수에 접근을 하지 못한다.
      • a는 재정의한 other의 함수 b는 상속받은 Child클래스 함수를 호출 할 것이다.
    • 몰론 다운 캐스팅을해서 ChildChild의 포인터로 접근하면 c함수 접근 가능.
  • 가상 함수도 부모걸 재정의 한것인가 아니면 이번 클래스에서 새로 만든 가상 함수인지 구분을 하지 못한다.
    • override를 붙여서 가독성을 챙긴다.
	// 1
	virtual void Test() //override
	{

	}
    // 2
	virtual void OutputMyData() override
	{
		cout << "Child" << endl;
		cout << "m_Parent : " << m_Parent << endl;
		cout << "m_Child : " << m_Child << endl;
	}

	// 3
	virtual void VirtualFunc() override
	{

	}
  • 1번은 Child클래스에 추가된 (가상)함수니까 override를 붙여줄 필요가 없고 2번과 3번은 override붙여서 상속받은 가상함수를 재정의 했다는걸 명시적으로 보여준다.
    • 1번쪽에 override 붙이면 오류가 발생한다.

상속에서의 가상함수를 쓰는 경우 소멸자 문제.

  • 지역변수에서는 객체를 만들경우는 객체를 지울때 컴파일러가 알아서 크기를 파악해 놔서 코드가 끝나면 메모리 해제를 자동으로 해줬다
  • 소멸할때도 자동적으로 부모 클래스를 호출하는 기능도 있었다.
    • 연쇄적으로 소멸을 하기 위해서.
	Child* pChild = new Child;
	delete pChild;
  • but 동적할당 클래스에서는 자기가 직접 해줘야했다.
  • 직접 지워주면 아무 문제 없다. 그 클래스에서 만들어진걸 그 클래스의 포인터로 지우기 때문에 문제 없다.
  • Child의 소멸자를 호출하고 부모의 소멸자를 호출해주기 때문에 문제 없다.
    • 연쇄적으로 소멸을 하기 위해서.
	Parent* pParent = new Child;
	delete pParent;
  • Child 객체를 만들었는데 Parent 포인터로 접근해서 보기때문에 pParent파트쪽만 지운다.(Child공간이 지워지지 않는다)
  • 진짜 생성된 객체는 Child이기 때문에 문제가 발생한다.
    • 문제 해결을 위해서는 시작점을 자식쪽으로 보내주어야 한다.
  	virtual ~Parent()
	{

	}
  • 이런 한 이유때문에 부모쪽에 소멸자에는 무조건 가상 함수를 붙여줘야 한다.
    • 굳이 붙이지 않아도 소멸자이기 때문이다.

정리

가상함수 사용시 주의점

  • 상속 과정에서 부모 클래스의 가상함수를 오버라이딩 한 경우

    • vvirtual 키워드를 붙이지 않아도 되지만, 일반 맴버함수와의 구별을 위해서 virtual 키워드를 붙이는 것이 좋다.
  • 가상함수를 오버라이딩 한 경우와, 새로 해당 클래스에서 구현한 가상함수 끼리 구별하기 위해

    • 함수 뒤에 override 키워드를 붙여서 부모의 가상함수를 오버라이딩한 것인지, 새로 구현된 가상함수인지 구별해 주는것이 가독성이 좋다.
  • 상속구조의 클래스를 동적할당하는 경우

    • 다형성을 위해서 부모 포인터로 자식 객체의 주소를 받는 상황이 많은데, 이때 객체를 소멸할때 부모 포인터로 delete 를 요청하기 때문에, 소멸자에 virtual 을 붙이지 않았으면, 최상위 부모 클래스의 소멸자만 호출되고 자식 클래스에 구현한 소멸자들은 호출되지 않는 문제가 있다.
  • 상속구조를 설계할 때에는 최상위 부모 클래스의 소멸자에 virtual 키워드를 붙이는 것을 반드시 잊으면 안된다.

강의 코드

main_01.cpp

#include <iostream>
#include <typeinfo>

using std::cout;
using std::endl;

class Parent
{
public:
	int m_Parent;

public:
	// 가상 함수
	virtual void OutputMyData()
	{
		cout << "Parent" << endl;
		cout << "m_Parent : " << m_Parent << endl;
	}

	virtual void VirtualFunc()
	{

	}

public:
	Parent() : m_Parent(0) {}
	Parent(int _P) : m_Parent(_P) {}

	virtual ~Parent()
	{

	}
};


class Child
	: public Parent
{
public:
	int	m_Child;

public:
	// Child 클래스 맴버함수
	void SetChild(int _child) { m_Child = _child; }

	// Child 클래스에서 추가된 가상함수
	virtual void Test()
	{

	}

	// 부모 클래스의 가상함수를 재정의 한 경우
	virtual void OutputMyData() override
	{
		cout << "Child" << endl;
		cout << "m_Parent : " << m_Parent << endl;
		cout << "m_Child : " << m_Child << endl;
	}

	// 부모 클래스의 가상함수를 재정의 한 경우
	virtual void VirtualFunc() override
	{

	}

public:
	Child() :m_Child(0) {}
	Child(int _P, int _C) : Parent(_P), m_Child(_C) {}

	~Child()
	{

	}
};


class ChidlChild
	: public Child
{

};

int main()
{
	Parent	parent;
	Child	child;

	Parent* pP = &parent;
	parent.OutputMyData();
	pP->OutputMyData();

	pP = &child;
	child.OutputMyData();
	pP->OutputMyData();


	Child* pC = &child;
	pC->OutputMyData();

	Parent p1;
	Parent p2;
	Parent p3;
	Parent p4;

	Child c1;
	Child c2;
	Child c3;
	Child c4;

	Parent* pPP = &c1;
	pPP->VirtualFunc();

	ChidlChild cc;
	pPP = &cc;
	pPP->VirtualFunc();

	Parent* pParent = nullptr;

	Child c5;
	c5.SetChild(10);

	pParent = &c5;

	Child* pChild = &c5;
	pChild->SetChild(10);

	((Child*)pParent)->SetChild(10);

	{
		Child* pChild = dynamic_cast<Child*>(pParent);

		if (pChild != nullptr)
		{
			pChild->SetChild(100);
		}
	}

	// 특정 클래스의 타입 정보를 가져오기
	const type_info& info = typeid(Parent);
	unsigned __int64 id = info.hash_code();
	const char* pClassName = info.name();



	// 동적할당 클래스
	Child* pChild = new Child;
	delete pChild;

	Parent* pParent = new Child;
	delete pParent;


	return 0;
}

main.02.cpp

#include <iostream>
using std::cout;
using std::endl;

class Animal
{
public:
	virtual void Eat() = 0; // 인터페이스

	virtual void Move()
	{
		cout << "Move" << endl;
	}
};

class Bird
	: public Animal
{

public:
	//virtual void Eat()
	//{
	//	cout << "Pack" << endl;
	//}

	virtual void Move()
	{
		cout << "Fly" << endl;
	}
};

class Eagle
	: public Bird
{
private:



public:
	virtual void Move()
	{
		cout << "Fly" << endl;
	}
};

int main()
{
	//Animal animal;
	//Bird  bird;
	//Eagle eagle;
	return 0;
}

1차 24.01.16
2차 24.01.17
3차 24.01.18
4차 24.01.19
5차 24.01.22

0개의 댓글