본문 바로가기
C++

[C++] 객체지향과 클래스에 대해 - 다형성(Polymorphism)

by rehtorb_s 2020. 6. 26.

이전 포스팅의 상속(Inheritance)에서 모바일 핸드폰을 예시로 들었는데요..! 파생클래스가 기본클래스의 기능을 포함하는 형태로 확장되는 것이라고 말씀드렸습니다. 동시에 파생클래스는 기본클래스 멤버를 자기자신으로 가져오는 것이 아니라 각각의 영역을 지키고 있다고 했습니다.

 

 

 

상속관계에서 파생클래스가 가지는 구조(이전 포스팅 참조)

 

 

그럼 파생클래스 객체가 기본클래스가 가지고 있는 멤버함수를 커스터마이징을 하고 싶다면 어떻게 할까요?

가령 전화기능을 사용하기 위해서 스마트폰 이전 세대의 모바일핸드폰은 물리적인 자판을 통해 번호를 입력합니다. 그런데 스마트폰은 디스플레이에 나타나는 가상의 키보드를 사용하구요. 입력 방식에 대한 차이가 드러납니다.

 

여기서 키포인트

번호를 입력하는 함수가 기본클래스에 정의되어 있다고 가정 합시다. 그 기본클래스를 상속 받은 스마트폰이라는 파생클래스가 번호를 입력하는 함수를 호출한다면? 물리자판이 없네요? 가상의 키패드를 띄워주는 함수를 따로 구현을 해서 번호입력 함수에서 호출해야 할까요? 

 

또 한가지 

스마트폰에는 음성인식 기능이 있습니다. 스마트폰 클래스를 상속받는 애플의 아이폰이나, 삼성의 갤럭시가 있다고 칩시다. 애플은 시리, 갤럭시는 빅스비를 사용하죠.

음성인식 기능을 호출한다는 대전제는 같지만 음성인식 함수가 호출하는 녀석들은 각기 다르죠!!

그럼 스마트폰에 정의된 음성인식 함수를 그대로 쓸 수 있을까요? 

 

 

 

정답은 새로 만들어 줘야 합니다. 단 기본 클래스의 멤버함수로 함수 재정의를 이용해서요. 그 중 오버라이딩(overriding)이요 !! (오버로딩은 추후에 다뤄봅시다)

오버라이딩은 기본클래스의 함수와 동일한 반환 값, 이름, 매개변수를 가지고 내용을 다르게 정의하는 것입니다. 갤럭시의 빅스비와 아이폰의 시리처럼 파생클래스가 원하는대로 기능들이 다양한 형태로 갈라지는 것이죠. 이게 다형성 개념의 기본 토대입니다.

코드예제를 한번 보겠습니다. 

//.h 선언부
class CSmartPhone
{
public:
	void RecogVoice();
};

class CIPhone : public CSmartPhone
{
	void RecogVoice();
};

class CGalaxy : public CSmartPhone
{
	void RecogVoice();
};

//.cpp 구현 및 정의부
void CSmartPhone::RecogVoice()
{
	std::cout << "음성을 들으면 분석한다" << std::endl;
}

void CIPhone::RecogVoice()
{
	std::cout << "시리 호출" << std::endl;
}

void CGalaxy::RecogVoice()
{
	std::cout << "빅스비 호출" << std::endl;
}

//main.cpp 메인문
int main(void)
{
	CSmartPhone smartPhone;
	CIPhone iPhone;
	CGalaxy galaxy;

	smartPhone.RecogVoice();
	iPhone.RecogVoice();
	galaxy.RecogVoice();

	return 0;
}

 

 

 

 

 


결과는??  각 객체들이 본인의 RecogVoice()를 호출 했습니다.

 

 

각 객체들의 구조

 

 

각 객체들의 구조는 위와 같습니다. 그런데 각 객체가 RecogVoice()를 호출할 때 기본클래스와 파생클래스 중에 어떤걸 호출해야할지 어떻게 알까요? 정답은 정적바인딩(Static binding) 때문입니다. 객체의 타입(자료형) 정보를 컴파일러가 알고있기 때문에 그에 해당하는 함수를 호출하는 것이죠. 컴파일러 입장에서 생각해보세요. 자신이 아는길로 안내해줄까요 모르는 길로 안내해줄까요?

 

//main.cpp
int main(void)
{
	CSmartPhone* pPhone;		//CSmartPhone타입의 객체 포인터 pPhone 선언
	CIPhone iPhone;			//CIphone 타입의 iPhone 객체 생성

	pPhone = &iPhone;		//pPhone이 iPhone을 가리킨다 up-casting
	pPhone->RecogVoice();		

	return 0;
}

 

 

 

 


포인터 객체 pPhone은 CSmartPhone 타입인데 CIPhone타입객체 iPhone을 가리키고 있습니다. (상속 관계의 객체들은 서로를 가로지르는 타입 캐스팅이 가능합니다.) 당연히 pPhone의 타입이 CSmartPhone이기 때문에 정적바인딩이 일어나고, CSmartPhone에 정의된 RecogVoice()를 호출합니다. 

 

 

 

 

 

 

 

아니근데 왜 굳이 포인터변수를 선언해서 이리 가리키고 저리 가리키고 하나요?

[C++] 포인터를 쓰는 이유와 업 캐스팅, 다운 캐스팅 


정적바인딩이 있다면 동적바인딩(Dynamic binding)도 있습니다. 멤버함수에 virtual 키워드를 붙여 선언해서 내가 접근하고자 하는 멤버가 어떤 클래스에서 정의가 됐는지 실행시간(run-time) 중에 찾아서 호출하는 방식인데요. 즉 가리키고 있는 객체의 타입이 뭐냐! 가 됩니다. 이 때 virtual이 붙은 함수를 가상함수라고 합니다. 이 가상함수를 재정의 한다면 이제 다형성의 기능을 십분 활용한다 라고 할 수 있겠습니다. 코드를 보시죠. 바로위 코드 클래스 선언부에 virtual만 붙혀줬습니다.

 

//.h
class CSmartPhone
{
public:
	virtual void RecogVoice();
};

class CIPhone : public CSmartPhone
{
public:
	virtual void RecogVoice();	//가상함수를 상속받아 재정의(오버라이딩)
};

class CGalaxy : public CSmartPhone
{
public:
	virtual void RecogVoice();	//가상함수를 상속받아 재정의(오버라이딩)
};
//main.cpp
int main(void)
{
	CSmartPhone* pPhone;		//CSmartPhone타입의 객체 포인터 pPhone 선언
	CIPhone iPhone;			//CIphone 타입의 iPhone 객체 생성

	pPhone = &iPhone;		//pPhone이 iPhone을 가리킨다 up-casting
	pPhone->RecogVoice();		

	return 0;
}

 

 

 

 

pPhone이 iPhone을 가리키고~ (업 캐스팅이죠)

시리를 호출합니다.

 

 

 

 

CSmartPhone 타입객체 pPhone이 CSmartPhon::RecogVoice()를 호출했지만(착각하시면 안되는게 기본클래스로 파생클래스를 가리키게 되면 기본클래스의 포인터객체는 기본클래스 내에 정의된 멤버만 호출할 수 있다는 점, 컴파일러는 업 캐스팅 사실을 아직 모릅니다 ㅎㅎ) virtual 키워드가 밀어내서 CIPhone 타입에 속한 CIPhone::RecogVoice() 가 호출되었습니다. 

여기서 중요한 점은 CSmartPhone 포인터가 CSmartPhone(기본클래스)를 상속한 모든 객체들에 대해 업 캐스팅이 가능함으로써, 일종의 인터페이스(Interface) 역할을 한다는 것입니다. 이 인터페이스 역할은 순수가상함수(pure virtual function)를 적용함으로써 더욱 극대화 될 수 있는데요. 

 

순수가상함수란 무엇일까요? 간단합니다. virtual 키워드가 붙은 멤버 함수 원형에 = 0을 붙여 선언하는데 이 기본클래스에 속한 순수가상함수는 절대로 정의(구현)하지 않습니다. 구현이 되어 있지 않기 때문에 객체로도 선언이 불가능 합니다. 그 말인 즉 완전히 인터페이스 혹은 상속만을 위한 용도로 사용하겠다는 의미죠. 아래 예제코드 보시죠.

//.h
class CSmartPhone
{
public:
	virtual void RecogVoice() = 0;
};

class CIPhone : public CSmartPhone
{
public:
	virtual void RecogVoice();
};

class CGalaxy : public CSmartPhone
{
public:
	virtual void RecogVoice();
};

또한 순수가상함수를 포함한 클래스는 추상클래스가 되며, 추상클래스를 상속한 파생클래스 역시 상속받은 순수가상함수를 구현하지 않으면 객체로 선언이 불가능합니다. 추상클래스를 만드는 근본적인 이유는 설계와 구현을 나누기 위함이고 다형성의 기능을 더 깔끔하게 100% 활용하기 위함이지요. 

 

이전에 어떤 물체든 사람이든 공통적인 기능을 추스려 클래스로 만드는걸 추상화라고 했었죠. 그럼 추상클래스는 그 클래스들의 추상화가 되겠네요^^

 

다음 포스팅은 생성자와 소멸자에 대해 알아보겠습니다. 감사합니다. 

 


포스팅 내용에 오류가 있거나 지적사항이 있다면 댓글로 달아주세요. 배움을 멈추지 않는 Good Programmer가 되겠습니다. 감사합니다.

댓글