KoreanFoodie's Study

C++ 기초 개념 6-2 : 가상(virtual) 함수와 다형성 본문

Tutorials/C++ : Beginner

C++ 기초 개념 6-2 : 가상(virtual) 함수와 다형성

GoldGiver 2022. 1. 6. 16:22

모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 C++ 강좌를 참고해 주세요!

 


다운캐스팅

이전 글에서 사용했던 Base 클래스와 Derived 클래스를 약간 변형해 보자.

#include <iostream>
#include <string>

class Base {
	std::string s;

public:
	Base() : s("기반") { std::cout << "기반 클래스" << std::endl; }

	void what() { std::cout << s << std::endl; }
};


class Derived : public Base {
	std::string s;

public:
	Derived() : Base(), s("파생") { std::cout << "파생 클래스" << std::endl; }

	void what() { std::cout << s << std::endl; }
};


int main() {

	Base p;
	Derived c; 

	std::cout << "=== 포인터 버전 ===" << std::endl;
	Base* p_c = &c;
	p_c->what();
}

위의 코드를 실행하면, 다음과 같은 결과가 나온다.

기반 클래스
기반 클래스
파생 클래스
=== 포인터 버전 ===
기반

 

p_c 처럼, 파생클래스의 메모리를 받되 타입을 부모 클래스로 설정해주는 것을 업 캐스팅(파생 클래스에서 기반 클래스로 캐스팅하는 것)이라고 한다. 이때, p_c는 타입이 Base이기 때문에, Base 클래스의 what( )을 호출하게 된다. 따라서 '파생'이 아닌 '기반'이 출력되게 된다.

 

그렇다면 다음과 같이 Derived 클래스로 포인터를 만들어주면 어떨까?

Derived* p_p = &p;
p_p->what();

하지만  위 코드는 다음과 같은 오류가 발생하게 된다.

error C2440: 'initializing' : cannot convert from 'Base *' to 'Derived *'

p_p는 Base 클래스 타입의 객체를 참조하는데, 실제 타입은 Derived 클래스이다. 따라서 Derived 클래스의 what 함수를 호출하려고 하지만, 실제 메모리 상에서는 (p는 Base 클래스 이므로) Derived 클래스의 what이 정의되지 않아 오류가 발생하는 것이다. 이는 전형적인 다운 캐스팅 오류이다.

 

만약 굳이 다운캐스팅을 하고 싶다면, 아래와 같이 static_cast 를 활용하면 된다.

  Base p;
  Derived c;

  std::cout << "=== 포인터 버전 ===" << std::endl;
  Base* p_p = &c;

  Derived* p_c = static_cast<Derived*>(p_p);
  p_c->what();

만약 static_cast 가 없다면, Base* 타입에서 Derived* 타입으로 변환할 수 없다는(다운 캐스팅이 안된다는) 에러 메시지가 출력되게 된다(비록 p_p가 실제로 가리키는 녀석은 Derived 객체일지라도).

 

만약 p_p가 c가 아닌 Base 클래스 객체인 p를 가리킨다면 어떨까?

Base* p_p = &p;

그렇게 되면 컴파일은 되지만, 런타임에서 what()을 호출하는 과정에서 에러가 발생하게 된다. 따라서 다운캐스팅은 그 작동이 보장되는 경우가 아니라면 권장되지 않는 방식이다.

 

 

dynamic_cast

위에서 설명한 상속 관계에 있는 클래스들 사이의 캐스팅 실수를 미연에 방지하기 위해, C++에서는 dynamic_cast를 활용한다.

Derived* p_c = dynamic_cast<Derived*>(p_p);

위의 코드에서 dynamic_cast 는 실패 시 nullptr 를 리턴하므로, 캐스팅이 실제로 이루어질 수 있는지를 판단할 수 있다.

다만 downcasting 을 dynamic_cast 를 통해 시도할때, compile time 에서 에러가 나올 수도 있다. 이 경우는 Base class 가 "polymorphic" 하지 않을 경우(즉, 가상함수가 없을 경우) 에 한정된다. 그 외의 경우는, 컴파일 타임에서 에러를 내지 않고 대신 nullptr 값을 리턴한다. 또한, 레퍼런스 타입으로 캐스팅하려 할 경우는 bad_cast 를 throw 하게 된다.

즉, 위의 예시에서는 컴파일 오류가 발생하는데, 그 이유는 Base 클래스와 Derived 클래스 모두 virtual 함수가 없기 때문이다. 만약 what 을 virtual 하게 만든 후 실행을 시키면 컴파일 에러가 발생하지는 않지만 p_c 에서 실제로 what( ) 함수를 실행할 수 없게 됨을 확인할 수 있다(p_c 가 nullptr 일 것이므로).

 

 

가상(virtual) 함수의 필요성

앞서 만든 Base 클래스와 Derived 클래스를 모두 받는 Base 타입 배열이 있다고 가정해 보자. 이때, 배열의 각 객체들에 대해 what함수를 호출하게 되면, 배열의 타입이 Base이므로 Base의 what만 계속 실행되게 된다.

하지만 만약 what 함수를 다음과 같이 virtual로 바꿔주게 되면, Base의 객체는 Base의 what 함수를, Derived 객체는 Derived의 what을 호출하게 된다.

class Base {
  std::string s;

 public:
  Base() : s("기반") { std::cout << "기반 클래스" << std::endl; }

  virtual void what() { std::cout << s << std::endl; }
};
class Derived : public Base {
  std::string s;

 public:
  Derived() : s("파생"), Base() { std::cout << "파생 클래스" << std::endl; }

  void what() override { std::cout << s << std::endl; }
};

int main() {

	Base** base_list;
	base_list = new Base*[2];

	Base b;
	Derived d;

	Base* p_b = &b;
	Derived* p_d = &d;


	base_list[0] = p_b;
	base_list[1] = p_d;

	std::cout << "=== what 함수 호출 ===" << std::endl;

	base_list[0]->what();
	base_list[1]->what();
}

 

출력은 다음과 같다.

기반 클래스
기반 클래스
파생 클래스
=== what 함수 호출 ===
기반
파생

이때 what 함수에 virtual을 붙여주지 않으면 what 함수는 두 경우 모두 '기반'을 출력한다.

또한, Derived 클래스의 what 함수에서 overrides는 상속받은 클래스가 기반 클래스의 가상함수를 오버라이드한다는 것을 명시적으로 나타내준다. 이는 실수로 오버라이드하는 경우를 막을 수 있다. 왜냐하면 virtual 함수가 오버라이드 되려면, 형식이 똑같아야 하기 때문이다. 예를 들어, Derived 클래스에서 what 을 다음과 같이 정의한다면, override가 불가능하다.

void what() const { /* Do something */ }

왜냐하면 상수함수와 그냥 함수는 다른 녀석으로 간주되기 때문이다.

 

 

바인딩

이처럼, 함수가 실행될 때 어떤 녀석이 실행될지 런타임에 실제 객체를 보고 결정되는 경우를 동적 바인딩(dynamic binding)이라고 한다. virtual이 적용된 함수들이 이에 해당한다.

정적 바인딩(static binding)은 컴파일 타임에 정해지는 것으로, 일반 함수들을 생각하면 된다.

 

Comments