모두의코드 씹어먹는 C++ - <6 - 2. 가상(virtual) 함수와 다형성>

YP J·2022년 6월 21일
0

모두의코드 C++

목록 보기
7/11
  • is - a 와 has - a 관계

  • 오버라이딩(overriding)

  • virtual 키워드와 가상함수(virtual function)

  • 다형성(polymorphism)

is -a 와 has -a

  • 단순 코드 반복을 줄이려고 상속개념 도입보단.

  • 상속이라는 기능을 통해 객체지향프로그래밍에서 추구하는 실제 객체의
    추상화를 좀더 효과적으로 할수있게끔 하려고

  • 무슨말이냐면 상속이 없던 C언어에서는 어떤 구조체들의 사이의 관계를 표현할수있는 방법이 없었다.

  • C++에서 상속을 도입함으로써 클래스 사이의 관계를 표현할수있는데

  • 예를들어

class Manager : public Employee

의 의미는,

  • Manager 클래스는 Employee 의 모든 기능을 포함한다.
  • Manager 클래스는 Employee 의 기능을 모두 수행할 수 있기 때문에 Manager 를 Employee 라고 칭해도 무방하다.
  • 즉 , 모든 Manager 은 Employee 이다. ( Manager 가 Employee 에 포함된다?)
  • Manager is a Employee !!
  • 따라서, 모든 상속 관계는 is a 관계라 볼수 있다.
  • 하지만 이를 뒤바꾸면 성립되지 않는다.
  • 즉, Manager 는 Employee 이지만 Employee 는 Manager 가 아닙니다.
  • Manager 를 Employee 로 부를 수 있지만, Employee 는 Manager 로 부를 수 없습니다.

  • 이렇게 파생클래스가 기반 클래스를 가리키게 그린다.

  • ex). 사람 <- 프로그래머 , 만약 프로그래머 클래스를 만든다면 사람이라는 클래스를 상속받을수 있도록 구성할수 있다.

상속의 중요한 특징

  • 클래스가 파생되면 파생될 수록 좀 더 특수화(구체화;specialize)된다.
  • 즉, Employee 클래스가 일반적인 사원을 위한 클래스 였다면
  • Manager클래스 들은 그 일반적인 사원들 중에서도 좀더 specialize한 사원을 의미한다.

Q:그렇다면 모든 클래스들의 관계를 is - a 로만 표현할수 있을까?
A: Nope

  • 어떤 클래스들 사이에서는 has-a 관계가 성립하기도 한다.

  • 예) 자동차 클래스 를 구성하기 위해 엔진 클래스 , 브레이크 클래스, ...

    • 자동차 is a 엔진 (X)
    • 자동차 has a 엔진 (0)

이런 has-a 클래스 관계

class Car
{
	private:
    	Egine e;
        Brake b;
        ...
};

또 다른 예로

class EmployeeList {
  int alloc_employee;        // 할당한 총 직원 수
  int current_employee;      // 현재 직원 수
  Employee **employee_list;  // 직원 데이터
  • EmployeeList 는 Employee 들과 has-a. 관계.
  • EmployeeList 클래스 는 Employee 를 포함하고 있다.

업캐스팅

#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()
{
	std::cout << " === 기반 클래스 생성 === " << std::endl;
	Base p;

	p.what();

	std::cout << " === 파생 클래스 생성 === " << std::endl;
	Derived c;

	c.what();

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

	return 0;
}

>> 
=== 기반 클래스 생성 ===
기반 클래스
기반
 === 파생 클래스 생성 ===
기반 클래스
 파생클래스
파생
 === 포인터 버젼 ===
기반
Base* p_c =&c;

Derived 의 객체 c 를 Base 객체를 가리키는 포인터에 넣었다.

벗!.

  • Base 와 Derived 는 다른 클래스 아닌가요?!
    • Dervied 가 Base 를 상속 받는다
    • Derived is a Base
  • 즉 Derived 객체 c 도 어떻게 보면 Base객체이기 때문에 Base 객체를 가리키는 포인터가 c를 가리켜도 무방.

  • p는 Base 객체를 가리키는 포인터이다.

  • 따라서 p의 what 을 실행하면 Base의 what함ㅎ수를 실행한다.

  • what 은 Base의 s 를 출력하게 된다.

  • 따라서 '기반' 이 출력된다.

  • 이러한 캐스팅을 , 즉 파생 클래스에서 기반 클래스로 캐스팅하는것을 업 캐스팅 이라고 부릅니다.

다운캐스팅

int main() 
{
  Base p;
  Derived c;

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

  return 0;
}

에러발생한다.

이유는

  • Derived* 포인터가 Base객체를 가리킨다고 하면
  • p_p->what() 하게되면
  • Derived 의 what함수가 호출 되어야 하는데
  • 이는 불가능 하다
  • 왜냐면 p_p 가 가리키는 객체는 Base 객체 이므로 Derived에 대한 정보가 없다.

int main() {
  Base p;
  Derived c;

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

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

  return 0;
}
  • error C2440: 'initializing' : cannot convert from 'Base ' to 'Derived '

  • Derived p_c 에 Base 를 대입하면 안 된다는 오류

  • p_p 가 가리키는것이 Base 객체가 아니라 Dervied 객체라는 사실을 알고 있다.

  • 그렇게 때문에 Base * 포인터를 다운캐스팅 함에도 불구하고 p_p가 실제로는 Derived객체를 가르키기 때문에

Dervied* p_c = p_p;

  • 를 해도 문제 없다 . 이를 위해서는 강제적으로 타입 변환 해야한다.
    Derived* p_c = static_cast<Dervied*>(p_p)

만약 p_p가 사실 Base객체를 가리키는 데 강제적으로 타입 변환 해서 what 실행하면 ??

int main() {
  Base p;
  Derived c;

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

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

  return 0;
}
  • 런타임에러 <다운캐스팅은 작동이 보장되지 않는한 매우 권장하지 않는다>

dynamic_cast

  • 이러한 캐스팅에 따른 오류 방지를 위해
  • C++에서는 상속 관계에 있는 두 포인터들 간에 캐스팅을 해주는 dynamic_cast라는 것을 지원한다.

virtual 키워드

#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;
}
>> 
기반 클래스
기반 클래스
파생 클래스
 == 실제 객체는 Base == 
기반 클래스의 what()
 == 실제 객체는 Derived == 
파생 클래스의 what()
  • p_c 와 p_p 모두 Base 객체를 가리키는 포인터.

  • 따라서 p_c->what() , p_p->what() 둘다 Base에서 호출 되어야한다.

  • 그런데 실제 p_p와 p_c가 각각 Base, Dervied 와 결합했는지 아는것처럼 적절한 what함수를 호출한다

  • 그게 가능한이유는 바로

class Base {

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

  virtual void what() { std::cout << "기반 클래스의 what()" << std::endl; }
};
  • 이 virtual 키워드 하나 때문이다.

  • p_c->what();

    • p_c는 Base포인터 닌까 Base 의 what() 실행 하야지!
    • 근데 what 이 virtual 이네?
    • 이거 실제Base맞아? 아니네 Derived 객체네!
    • 그럼 Dervied의 what실행 해야지
  • p_p->what();

    • p_p는 Base포인터닌까 Base 의 what()실행해야지
    • what 이 virtual이네?
    • 이거 실제 Base맞아? 맞네!
    • Base 의 what()실행
  • 이렇게 컴파일 시에 어떤 함수가 실행될지 정해지지 않고
  • 런타임 시에 정해지는 일을 가리켜서 동적 바인딩(dynamic binding)이라 부른다

ex)

  • // i 는 사용자로부터 입력받는 변수
    if (i == 1) {
      p_p = &c;
    } else {
      p_p = &p;
    }
    p_p->what();
    • 이런 경우도 동적 바인딩이라한다. 왜냐면 런타임때 입력 받은 i에 따라 달라지닌까.
  • 정적 바인딩 은 컴파일에서 어떤 함수가 호출될지 정해지는것.

  • 가상함수 는 파생클래스의 함수가 기반 클래스의 함수를 오버라이드 하기 위해서는 두 함수의 꼴이 정확히 같아야한다.

  • 기반클래스에서 virtual로 선언한 함수는 파생클래스에 있는 같은 시그니처의 함수들에게 자동으로 virtual로 선언됨!

override 키워드

  • override 키워드는 c++11부터 나온 키워드. 파생클래스에서 기반클래스의 가상함수를 오버라이드 하는 경우 override 키워드를 통해 명시적으로 나타낼 수 있게 됨.

  • 오버라이드를 하려면 함수형이 완전히 같아야 함. override 키워드를 쓰면 오버라이드가 이뤄진 게 맞는지 더 정확하게 판단할 수 있음.(함수꼴이 달라 오버라이드가 이뤄지지 않는 경우 오류 발생함)

  • 자식 클래스에서 명시적으로 override를 하는 경우 암묵적으로 virtual이라는 가정 하에 작성하기 때문에 virtual 키워드를 사용하지 않음

    • 아~ 이말이 부모에서 virtual키워드를 안 써도 된다는 말이 아니라
    • 자식에서 또 virtual을 붙일 필요가 없다는것.

Q. 그럼 모든 함수를 가상함수로 만들면 안 되나?

  • https://modoocode.com/211
  • 실제로 자바에선 모든 함수들이 디폴트로 virtual로 선언됨.
    오버헤드 문제가 있음. 최적화와 관련한 문제라고 생각하면 됨. 위에서 vtable사용 방법에서 봤듯이 가상함수를 이용하게 되면 함수를 사용할 때 약간 더 느릴 수밖에 없음.

다중상속

  • c++은 다중상속을 허용한다.
  • 다중상속시 생성자 호출 순서는 상속 순서에 따른다.

가상상속

  • 다이아몬드 상속을 하려다 보면 변수 접근에도 문제가 생기고 생성자, 소멸자가 두 번씩 호출되게 된다.
  • 이를 해결하기 위해 사용할 수 있는 것이 가상상속
  • https://hwan-shell.tistory.com/224
  • 가상상속을 사용하게 되면 생성자, 소멸자를 중복으로 호출하는 문제와 변수 접근 문제를 해결할 수 있지만 대신 데이터 크기는 더 커지게 된다.
  • 그 이유는 virtual base table을 생성하고 그 테이블을 이용하여 클래스에 접근하게 되기 때문.
    이는 성능저하를 야기할 수 있음
profile
be pro

0개의 댓글