다형성, 가상 함수 (1)

Yama·2024년 1월 15일
0

어소트락 수업

목록 보기
45/55

다형성

  • 하나의 타입으로 여러 형태를 나타낼 수 있다.

다형성을 알려면 상속 + 포인터를 알아야 다형성을 설명 가능.

	int* pInt = nullptr;
  • 주소밖에 저장을 못하지만 저걸 보고 알수 있는것.
    • 이 코드로 알수 있는것은 저 포인터 변수(주소를 저장하는 변수) pInt는 int로 접근한다는 것.
	Parent	p1;
	Child	c1;
    
    Parent* pP = &p1;
	Child* pC = &c1;
  • 내가 알맞은 자료형으로 접근해야 제대로 알 수 있을 것이다.
  • 내가 사용한 데이터랑 매칭되는 포인터를 사용해야 가져와 진다.
    • 알맞은 포인터로 가르켰기 때문에 문제가 없었다.
	float f = 0.f;
	int* pInt = &f;
  • 이것처럼 float형인데 int포인터로 접근할려니까 오류가 발생할 것.
	// (1) 부모 포인터 <= 자식객체 주소
	Parent* pP = &c1; 
	// (2) 자식 포인터 <= 부모객체 주소
    Child* pC = &p1;  
  • 둘다 접근할려는 것의 범위는 틀렸지만 하나는 컴파일 에러가 나지 않는다.
    • 1번은 부모 클래스 타입 포인터로 자식 클래스 객체의 주소를 받았기 떄문이다.
      • 포인터를 이용해서 원본에 접근할 경우 Parent 클래스 영역까지밖에 접근 할 수 없다.
    • 2번은 자식 클래스 타입 포인터로 부모 클래스 객체의 주소를 받으려고 하기 때문이다.
      • 이게 허용될 경우 포인터로 원본 데이터의 크기에 맞지 않게 초과해서 메모리에 접근 할 수 있기 때문에 컴파일러 에러로 친다.
	pP->m_Parent = 10;

  • Parent쪽 까지만 접근이 가능하다. Child쪽 접근 불가능하다.
    • Child가 관리하는곳도 부모 포인터로 접근해서 제대로 접근하는것은 아니지만 컴파일러가 에러날 정도의 문제는 아니다.
	// 1
	int iarr[10];
	iarr[20] = 100;
    // 2
    int* pInt = new int[10];
	pInt[20] = 100;
  • 둘다 매우 위험한 행동.
    • 1번은 오류를 컴파일러가 잡아 주긴 한다.
    • 2번은 컴파일러가 잡아주지 못한다.

다형성 예시

  • 실제 생성된 객체보다 그 클래스를 포함 부모의 포인터로 그 클래스들을 가리킨 수 있다.
    1. a->b->c->d->e상태로 상속을 한 상태에 ea,b,c,d,e포인터들로 다 가리킬수 있다.
    2. 타입은 동물 포인터인데 동물로부터 파생된 누구의 클래스던 동물 포인터로 다 접근 할 수 있다.
    3. 함정 클래스를 만들었다면 함정 클래스를 저장할 백터를 따로 만들게아니라 원래 있던 부모클래스에 쳐 넣어버리면 된다.
      • 부모(a,동물)포인터 하나로 상속 시킨 모든 형태를 받을 수 있다.
      • 새로운것을 만들었다고 컨테이너를 새로 만들러 다닐 필요가 없다.
  • 단일 포인터로 여러가지 형태를 다 다를수 있다.

다형성 단점

  • 부모 포인터로 접근을 할떄 자식들이 누구인지 정체를 알 수 가 없다.
  • 다형성을 쓰면 오버라이딩이 먹통이 된다.

다형성의 문제를 해결하는 가상 함수

	Parent	parent;
	Child	child;
    
    parent.OutputMyData();
	child.OutputMyData();
  • 이것처럼 포인터를 이용해서 알맞은 걸 출력하고 싶다.
// Parent 클래스
	void OutputMyData()
	{
		cout << "Parent" << endl;
		cout << "m_Parent : " << m_Parent << endl;
	}
// Child 클래스 
	void OutputMyData()
	{
		cout << "Child" << endl;
		cout << "m_Parent : " << m_Parent << endl;
		cout << "m_Child : " << m_Child << endl;
	}
	Parent* pP = nullptr;
    
	parent.OutputMyData();
	child.OutputMyData();
  • 이것은 오버라이딩되서 잘 되는거 같지만 사실은 그 접근연산자로 지목하니까 그냥 되는것이다.
	Parent* pP = &parent;
    
    pP->OutputMyData();
	// 1
    pP = &child;
	pP->OutputMyData();
  • 우리가 원하는 형태로 출력이 되지않는다
    • 1번에서 pP부모포인터가 child의 주소를 받아가도 부모쪽에 함수가 호출이 된다.
  • 우리가 원한 형태는 child쪽에 주소를 넣어주면 child에 구현된 함수가 호출이 되었어야 한다.
  • 다형성으로 부모쪽걸로 통일을 해서 오버라이딩이 안된다.
	Child* pC = &child;
	pC->OutputMyData();
  • 몰론 그냥 Child 포인터를 사용하면 Child에 있는 OutputMyData 함수에 접근함.
	virtual void OutputMyData()
	{
		cout << "Parent" << endl;
		cout << "m_Parent : " << m_Parent << endl;
	}
  • 부모 클래스에 파생되어 오버 라이딩될 여지가 있는 함수 앞에 virtual키워드를 붙인다.
  • 문제는 해결 되었지만 동작원리도 알아야 한다.

가상함수의 동작원리

  • 가상 함수 왜씀?
    • 다형성을 사용하면 주소값을 넘겨줘도 오버라이딩이 안되서 그걸 해결하기 위해서 사용한다.
      • 부모 포인터로 자식 객체의 주소를 받을 수있게 되지만 부모 포인터로 자식의 객체의 주소를 받아서 가리키고 함수를 호출하면 부모쪽의 함수가 호출되기 떄문에 가상함수를 사용해서 문제를 해결해준다.
  • virtual 키워드를 붙이면 일반 함수와 작동 방식이 달라진다.
  • virtual 키워드가 단 하나라도 있다면 그 클래스에는 타입 정보(type info)가 생긴다.
    • 타입정보 안 내용
      • 가상함수만 모아두는 가상 함수 주소 테이블이 하나 생긴다.
  • A클래스와 A클래스를 상속 받은 B클래스가 있다.
    1. A클래스에 가상함수(virtual)를 만들면 타입정보 안 에 가상 함수 테이블이 생긴다.
    2. A클래스에 상속을 시킨다면 B클래스에도 타입정보 안 에 가상 함수가 생긴다.
    3. A클래스에 가상함수가 생기면 접근 할 때 가상 함수 테이블에 접근 한 후 가상 함수 테이블에 있는 A클래스::OutputMyData() 함수를 출력하게 바뀐다.
      • A클래스::OutputMyData()
      • B클래스::OutputMyData()
      • 가상 함수 테이블에서 함수를 호출한다.
	Parent p1;
	Parent p2;
	Parent p3;
	Parent p4;
  • 코드처럼 A클래스 객체를 만들때마다 p1,p2... 만들때마다 객체 하나하나 마다 A클래스 가상함수 테이블을 가리키는 포인터가 추가되서 그 포인터 들이 하나의 A클래스 가상함수 테이블을 가리킨다.
    (Parent::OutputMyData())
    • 가상 함수 테이블은 클래스당 하나 생긴다.
	Child c1;
	Child c2;
	Child c3;
	Child c4;

  • 모든 차일드 클래스 객체들도 차일드 타입정보가 만들어진 가상 함수 테이블을 가상 함수 가리키는 포인터로 가리킨다.
  • Parent포인터가 자기한테 들어온 객체를 정확히 아는게 아니라 어차피 너는 Parent까지 밖에 접근을 못하니까 Parent파트쪽에 자기 타입 정보를 가리킬수 있는 포인터를 하나 두고 그 객체가 생성될때마다 그곳을 자기 타입에 맞는 테이블에 주소를 넣는 것이고 주소에 접근해서 그랜드 차일드라는걸 보고 그랜드 차일드쪽에 함수를 참조 한다.
  • 가상함수테이블이 Parent에 생긴 이유?
    • 부모 클래스에 접근을 해서 가상함수테이블에 접근해야되니까 부모쪽에 생겨야 테이블에 접근을 해서 주소값으로 자기에 맞는 테이블에 접근이 가능하니 부모쪽에 생겨야한다.
  • Parent(부모)가 진짜 정체를 알필요 없는 이유?
    • 부모파트안에 어차피 테이블 주소가 있을것이고 거기에 들러서 오버라이딩된 진짜 함수를 호출하기때문이다.
   	/*virtual*/ void VirtualFunc()
	{

	}
  • virtual키워드를 가상 함수를 안붙인 부모쪽 함수를 자식쪽에 상속 시키고 오버라이딩 했다면?
    • 다형성의 문제가 발생한다.
  • virtual 키워드를 붙이면 가상함수 테이블에 인덱싱되서 들어간다
    • 부모쪽에 [1]에 있는 주소랑 자식쪽에 [1] 있는 주소가 다르다.
  • 부모가 등록을 했지만 자식에서 오버라이딩을 하지 않는다면(부모의 기능을 그대로 쓰겠다.
    • 부모쪽에 가상 함수 테이블에 접근해서 부모걸 사용한다.
    • 부모쪽에 [1]에 있는 주소랑 자식쪽에 [1] 있는 주소가 같다.
	class ChildChild
		: public Child
	{
	
	};
  • 자식을 상속받은 손자클래스
	Parent* pPP = &c1;
	pPP->VirtualFunc();

	ChildChild cc;
	pPP = &cc;
	pPP->VirtualFunc();
  • 자식 클래스를 상속받았으니 손자의 가상함수 테이블에는 딱히 오버라이딩 안했다면 부모걸 호출하는게 아니라 자식걸 호출해준다.
    • 자식의 가상 함수 테이블에 있는 주소랑 손자의 접근한 버츄얼함수의 주소값이 같다.

다형성의 문제점 정리

  • 부모 클래스 포인터 변수로 자식 클래스 객체의 주소를 받았을 때
    • 포인터가 부모 클래스 타입이기 때문에, 부모 클래스의 기능까지밖에 접근이 안된다.
    • 가상함수를 통해서 자식 클래스에서 오버라이딩한 함수를 호출할 수 있게 한다.

문제점 해결법 정리.

  • 가상함수가 1개 이상 있으면 각 클래스는 타입 정보가 만들어진다.
    • 타입 정보에는 해당 클래스가 보유하고 있는 가상함수의 주소를 모아놓은 가상함수 테이블도 있다.
  • 부모 클래스에 보이지 않는 맴버(가상함수 테이블 포인터)가 추가된다.
  • 가상함수를 호출할 경우, 부모클래스의 맴버인 가상함수테이블 포인터를 이용해서 가상함수 테이블로 접근,
    • 가상함수 테이블에서 호출하려는 함수를 호출(탐색 x, 인덱싱 o)

강의 코드

main_01.cpp

#include <iostream>

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

class Parent
{
public:
	int m_Parent;

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

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


class Child
	: public Parent
{
public:
	int	m_Child;

public:
	void OutputMyData()
	{
		//cout << "m_Parent : " << m_Parent << endl;
		Parent::OutputMyData();
		cout << "m_Child : " << m_Child << endl;
	}

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

int main()
{
	Parent	p(10);
	Child	c(10, 20);

	p.OutputMyData();

	c.OutputMyData();
	c.Parent::OutputMyData();

	Parent p1;
	Child  c1;

	Parent* pP = &c1;		//(1)  부모 포인터 <= 자식객체 주소
	//Child* pC = &p1;		//(2)  자식 포인터 <= 부모객체 주소	

	//pP->m_Parent = 10;
	//pC->m_Child = 10;

	return 0;
}

main_02.cpp

#include <iostream>

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) {}
};


class Child
	: public Parent
{
public:
	int	m_Child;

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

	void VirtualFunc()
	{

	}

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


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();

	return 0;
}

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

0개의 댓글