[Modern C++] 6.2. 가상 함수와 다형성

윤정민·2023년 7월 4일
0

C++

목록 보기
19/46

1. is-a와 has-a

c++에서 상속을 도입한 이유를 생각해보자. 단순히 똑같은 코드를 작성하는 것을 방지하는게 아닌 객체지향프로그래밍에서 추구하는 실제 객체의 추상화를 좀 더 효과적으로 하기 위함이다.

c언어에서는 구조체 사이의 관계를 표현할 수 있는 방법이 없었다. 하지만 c++에서 상속이란 것을 도입함으로써, 클래스 사이 관계를 표현할 수 있게 되었다.

1.1. is-a

Manager가 Employee를 상속한다의 의미를 살펴보자.

  • Manager 클래스는 Employee의 모든 기능을 포함함
  • Manager 클래스는 Employee의 기능을 모두 수행할 수 있기 때문에 ManagerEmployee라 칭해도 무방함
  • 즉, 모든 ManagerEmployee라 할 수 있음

위 내용을 Manager is a Employee!로 표현 가능하다.

  • 이는 뒤바꾸면 성립 불가

결국 상속을 통해 클래스를 구체화 가능하다.

1.2. has-a

모든 클래스들의 관계를 is-a로 표현할 순 없다. 자동차 클래스를 생각했을 때 자동차를 구성하는 엔진 클래스, 브레이크 클래스, 오디오 클래스 등 수많은 클래스가 있다. 그렇다고 이들 사이의 관계를 is-a관계로 표현 불가능하다. 대신, 이들 사이는 has-a관계로 쉽게 표현 가능하다.

2. 캐스팅

2.1. 업캐스팅

#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() : s("파생"), Base() { 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();

	return 0;
}

위 코드에서 Base* p_c = &c;를 보면 Derived의 객체를 Base 객체를 가리키는 포인터에 넣었다. 이는 Derived is a Base이기 때문에 가능하다. (단지 Base 객체만 가리키는 포인터이므로 Derived 데이터가 소실되지 않는다.) 이러한 형태의 캐스팅을 업 캐스팅이라고 부른다.

2.2. 다운 캐스팅

#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() : s("파생"), Base() { std::cout << "파생 클래스" << std::endl; }

  void what() { std::cout << s << std::endl; }
};
int main() {
  Base p;
  Derived c;

  std::cout << "=== 포인터 버전 ===" << std::endl;
  Derived* p_p = &p;
  p_p->what();

  return 0;
}

위 코드 Derived* p_p = &p를 보면 Base의 객체인 p를 Derived 객체를 가리키는 포인터에 넣었다. 이 코드를 실행해보면 오류가 발생한다. 그 이유는 Derived 포인터가 Base객체를 가리킬때 p_p->what();Derivedwhat함수가 실행된다. 하지만 객체는 Base이기 때문에 해당 정보가 없어 문제가 발생한다.

따라서 함부로 다운 캐스팅 하는 것은 금지되어 있다.

2.3. 업캐스팅하고 다운캐스팅하기

#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() : s("파생"), Base() { std::cout << "파생 클래스" << std::endl; }

  void what() { std::cout << s << std::endl; }
};
int main() {
  Base p;
  Derived c;

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

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

  return 0;
}

위 코드를 실행하면 오류가 발생한다. 하지만 생각해보면 Derived 객체를 업캐스팅 한것이기 때문에 데이터가 소실되지 않아 다운캐스팅이 가능해야 한다. 하지만 다운캐스팅을 금지하고 있기 때문에 static_cast을 사용해 강제적으로 타입 변환을 하면 된다.(다운 캐스팅이 보장되지 않더라도 강제 캐스팅을 하면 컴파일러가 오류를 찾아내기 힘드니 꼭 확인하고 사용하자.)

2.4. 안전하게 다운 캐스팅 하기

위에서 말했던 문제를 해결하기 위해 dynamic_cast를 사용할 수 있다. 이를 사용하면 컴파일 타임에 오류를 찾아낼 수 있다.

3. virtual 키워드

아래 소스 코드를 실행하게 되면 p_cp_p모두 Base객체를 가리키는 포인터이기 때문에 what()을 호출하면 Base객체의 what()함수가 실행된다.

#include <iostream>

class Base {

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

	void what() { std::cout << "기반 클래스의 what()" << std::endl; }
};
class Derived : public Base {

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

	void what() { std::cout << "파생 클래스의 what()" << std::endl; }
};
int main() {
	Base p;
	Derived c;

	Base* p_c = &c;
	Base* p_p = &p;

	std::cout << " == 실제 객체는 Base == " << std::endl;
	p_p->what();

	std::cout << " == 실제 객체는 Derived == " << std::endl;
	p_c->what();

	return 0;
}

이를 해결하기위해 virtual 키워드를 사용할 수 있다. virtual 키워드를 사용하게 되면 런타임시 컴퓨터가 함수를 호출하는 포인터가 어떤 클래스의 객체인지 확인 후 해당 객체의 함수를 실행하게 된다. 이렇게 런타임 시에 정해지는 일을 가리켜 동적 바인딩이라고 부른다.

virtual키워드가 붙은 함수를 가상함수(virtual function)이라고 부르며 이렇게 파생 클래스의 함수가 기반 클래스의 함수를 오버라이드 하기 위해서는 두 함수의 꼴이 정확히 같아야 한다.

#include <iostream>

class Base {

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

	virtual void what() { std::cout << "기반 클래스의 what()" << std::endl; }
};
class Derived : public Base {

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

	void what() { std::cout << "파생 클래스의 what()" << std::endl; }
};
int main() {
	Base p;
	Derived c;

	Base* p_c = &c;
	Base* p_p = &p;

	std::cout << " == 실제 객체는 Base == " << std::endl;
	p_p->what();

	std::cout << " == 실제 객체는 Derived == " << std::endl;
	p_c->what();

	return 0;
}

4. Override 키워드

C++11에서는 파생 클래스에서 기반 클래스의 가상 함수를 오버라이드 하는 경우, override 키워드를 통해 명시적으로 나타낼 수 있다. 해당 키워드를 사용하면 실수로 오버라이드 하지 않는 경우를 방지할 수 있다.

#include <iostream>
#include <string>

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; }
};
profile
그냥 하자

0개의 댓글