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

작성일 : 2014-03-31 이 글은 33460 번 읽혔습니다.

이번 강좌에서는

  • is - ahas - a 관계

  • 오버라이딩(overriding)

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

  • 다형성(polymorphism)

에 대해서 배웁니다.

안녕하세요 여러분! 지난번 강좌는 재미있으셨나요. C++ 이란 산을 넘기 위해 아직도 지나가야 할 관문이 앞에 수 없이 많지만, 그래도 이번 강좌를 통해서 그 관문 하나는 지나갈 수 있으리라 생각합니다. 이번 강좌에서는 객체지향프로그래밍의 핵심 개념 하나에 대해서 배울 것입니다. 기대하세요 :)

 is - a 와 has - a

일단 이야기를 진행하기 전에, 어떠한 경우에서 상속을 사용하는지 생각해봅시다. C++ 에서 상속을 도입한 이유는 단순히 똑같은 코드를 또 쓰는 것을 막기 위한 Ctrl + C, Ctrl + V 방지용으로 위한 것이 아닙니다 (물론 그러한 이유도 약간 있겠지만). 실제 이유는 상속이라는 기능을 통해서 객체지향프로그래밍에서 추구하는 실제 객체의 추상화를 좀 더 효과적으로 할 수 있게 되었습니다.

이게 무슨 말이냐면 상속이란 것이 없던 C 언어에서는 어떠한 구조체 사이의 관계를 표현할 수 있는 방법이 없었습니다. 하지만 C++ 에서 상속이란 것을 도입함으로써, 클래스 사이에 관계를 표현할 수 있게 되었는데, 예를 들어서 ManagerEmployee 를 상속한다;

class Manager : public Employee

의 의미는,

  • Manager 클래스는 Employee 의 모든 기능을 포함한다

  • Manager 클래스는 Employee 의 기능을 모두 수행할 수 있기 때문에 (Manager 에게는 약간 기분 나쁘겠지만) ManagerEmployee 라고 칭해도 무방하다

  • 즉, 모든 ManagerEmployee 이다

  • Manager is a Employee !!

따라서, 모든 상속 관계는 is a 관계라고 볼 수 있습니다. 당연한 점은, 이를 뒤바꾸면 성립되지 않는 다는 점입니다. 즉 ManagerEmployee 이지만 EmployeeManager 가 아닙니다. 이렇기에, ManagerEmployee 로 부를 수 있지만, EmployeeManager 로 (미안하게도) 부를 수 없습니다.

프로그램 설계 시에 클래스들 간의 상속 관계를 도표로 나타내는 경우가 종종 있는데, 많은 경우 파생 클래스가 기반 클래스를 화살표로 가리키게 그립니다.

실제 세상에서 is a 관계로 이루어진 것들은 수 없이 많습니다. 예를 들어, '사람' 이라는 클래스가 있다면, '프로그래머는 사람이다 (A programmer is a human)' 이므로, 만일 우리가 프로그래머 클래스를 만든다면 사람 이라는 클래스를 상속 받을 수 있도록 구성할 수 있습니다.

위는 또 다른 is - a 관계의 예로, BankAccount (은행 계좌) 라는 클래스가 있고 Checking Account (자유롭게 입출금이 가능한 계좌지만 이자가 없다) 와 Savings Account (비교적 자유롭게 입출금이 불가능하지만, 매 달 이자가 붙음) 가 이를 상속 받고 있습니다. 즉, 같은 계좌지만 기능이 약간 씩 다른 두 계좌 클래스들이 좀 더 '일반적인' BankAccount 클래스를 상속 받았지요.

이를 통해서 상속의 또 하나의 중요한 특징을 알 수 있습니다. 바로 클래스가 파생되면 파생될 수 록 좀 더 특수화 (구체화;specialize) 된다는 의미 입니다. 즉, Employee 클래스가 일반적인 사원을 위한 클래스 였다면 Manager 클래스 들은 그 일반적인 사원들 중에서도 좀 더 특수한 부류의 사원들을 의미하게 됩니다.

또, BankAccount 도 일반적인 은행 계좌를 위한 클래스 였다면, 이를 상속 받는 CheckingAccount, SavingsAccount 들은 좀 더 구체적인 클래스가 되지요. 반대로, 기반 클래스로 거슬러 올라가면 올라갈 수 록 좀 더 일반화 (generalize) 된다고 말합니다.

그렇다면 모든 클래스들의 관계를 is - a 로만 표현할 수 있을까요? 당연히 그렇지 않습니다. 어떤 클래스들 사이에서는 is - a 대신에 has - a 관계가 성립하기도 합니다. 예를 들어서, 간단히 자동차 클래스를 생각해봅시다. 자동차 클래스를 구성하기 위해서는 엔진 클래스, 브레이크 클래스, 오디오 클래스 등 수 많은 클래스들이 필요합니다. 그렇다고 이들 사이에 is a 관계를 도입 할 수 없습니다. (자동차 is a 엔진? 자동차 is a 브레이크?) 그 대신, 이들 사이는 has - a 관계로 쉽게 표현할 수 있습니다.

즉, 자동차는 엔진을 가진다 (자동차 has a 엔진), 자동차는 브레이크를 가진다 (자동차 has a 브레이크) 이와 같이 말이지요. 이런 has - a 관계는 우리가 흔히 해왔듯이 다음과 같이 클래스로 나타내면 됩니다.

class Car {
 private:
  Engine e;
  Brake b;  // 아마 break 아니냐고 생각하는 사람들이 있을 텐데 :)
  ....
};

또 다른 예로 바로 우리의 EmployeeList 를 들을 수 도 있습니다. EmployeeListEmployee 들과 has - a 관계 이지요. 따라서, 실제로 EmployeeList 클래스를 보면

class EmployeeList {
  int alloc_employee;        // 할당한 총 직원 수
  int current_employee;      // 현재 직원 수
  Employee **employee_list;  // 직원 데이터

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

  return 0;
}

성공적으로 컴파일 하였다면

실행 결과

 === 기반 클래스 생성 ===
기반 클래스
기반
 === 파생 클래스 생성 ===
기반 클래스
파생 클래스
파생

이미 저번 강좌에서도 이야기 했었지만, Base 에서 what 을 호출하면 당연히 Basewhat 이 실행되어서 '기반' 라고 나오고, Base 를 상속받는 Derived 클래스에서 what 을 호출하면, DerivedwhatBasewhat 을 오버라이드 해서 Derivedwhat 이 호출되게 됩니다.

이번에는 코드를 약간 변형해보도록 하겠습니다.

#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;
}

성공적으로 컴파일 하였다면

실행 결과

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

이번에는 Derived 의 객체 cBase 객체를 가리키는 포인터에 넣었습니다.

Base* p_c = &c;

어떤 분들은 이와 같은 대입이 가능하냐고 물을 수 있습니다. BaseDerived 는 다른 클래스 이니까요. 하지만, 그 분들이 간과하고 있는 점은 DerivedBase 를 상속 받고 있다는 점입니다. 상속 받는다면 뭐죠? Derived is a Base

즉 (말이 조금 이상하지만) Derived 객체 c 도 어떻게 보면 Base 객체이기 때문에 Base 객체를 가리키는 포인터가 c 를 가리켜도 무방하다는 것입니다. 이를 그림으로 표현한다면 아래와 같습니다.

그 대신 p 는 엄연한 Base 객체를 가리키는 포인터 입니다. 따라서, pwhat 을 실행한다면 p 는 당연히 '아 Basewhat 을 실행해 주어야 겠구나' 하고, Basewhat 을 실행해서, BasewhatBases 를 출력 하게 됩니다. 따라서 위 처럼 '기반' 가 출력됩니다.

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

위 그림을 보면 왜 업 캐스팅이라 부르는지 이해가 확 되지요.

그렇다면 업 캐스팅의 반대인 다운 캐스팅도 있을까요?

#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;
}

컴파일 한다면 다음과 같은 오류 메세지를 볼 수 있습니다.

컴파일 오류

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

사실 위와 같은 오류가 발생한 이유는 간단합니다.

만일 Derived* 포인터가 Base 객체를 가리킨다고 해봅시다. 그렇다면 p_p->what() 하게 된다면 Derivedwhat 함수가 호출되어야만 하는데, 이는 불가능 합니다. (왜냐하면 p_p 가 가리키는 객체는 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() : 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;
}

컴파일 하였다면

컴파일 오류

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

Derived* p_cBase * 를 대입하면 안된다는 똑같은 오류가 발생합니다. 하지만 우리는 p_p 가 가리키는 것이 Base 객체가 아니라 Derived 객체라는 사실을 알고 있습니다. 그렇기 때문에 비록 Base * 포인터를 다운 캐스팅 함에도 불구하고 p_p 가 실제로는 Derived 객체를 가리키기 때문에

Derived* p_c = p_p;

를 해도 전혀 문제가 없습니다. 이를 위해서는 아래 처럼 강제적으로 타입 변환을 하면 됩니다.

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

비록 약간은 위험하지만 (만일 p_p 가 사실은 Derived 객체를 가리키지 않는다면?) 컴파일 오류를 발생시키지 않고 성공적으로 컴파일 할 수 있습니다. 그렇다면 만일 p_p 가 사실 Base 객체를 가리키는데 강제적으로 타입 변환을 해서 what 을 실행한다면 어떨까요?

#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 = &p;

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

  return 0;
}

성공적으로 컴파일 하였다면

와 같은 런타임 오류가 발생하게 됩니다.

이러한 강제적으로 다운 캐스팅을 하는 경우, 컴파일 타임에서 오류를 찾아내기 매우 힘들기 때문에 다운 캐스팅은 작동이 보장되지 않는 한 매우매우 권장하지 않는 바입니다.

dyanmic_cast

이러한 캐스팅에 따른 오류를 미연에 방지하기 위해서, C++ 에서는 상속 관계에 있는 두 포인터들 간에 캐스팅을 해주는 dynamic_cast 라는 것을 지원합니다. 이를 사용하는 방법은 static_cast 와 거의 동일합니다.

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

하지만 위의 경우 컴파일 하게 된다면

컴파일 오류

test.cc: In function 'int main()':
test.cc:27:44: error: cannot dynamic_cast 'p_p' (of type 'class Base*') to type 'class Derived*' (source type is not polymorphic)
   Derived* p_c = dynamic_cast<Derived*>(p_p);

와 같이 캐스팅 할 수 없다는 컴파일 오류가 발생하게 됩니다.

 EmployeeList 다시 보기

자, 그럼 이제 위에서 다룬 내용을 가지고 EmployeeList 를 어떻게 하면 좀 더 간단하게 만들 수 있을 지 생각해봅시다.

class EmployeeList {
  int alloc_employee;  // 할당한 총 직원 수

  int current_employee;  // 현재 직원 수
  int current_manager;   // 현재 매니저 수

  Employee **employee_list;  // 직원 데이터
  Manager **manager_list;    // 매니저 데이터
  // ...

위와 같은 구성에서 가장 문제가 되는 것이 각 클래스 별로 데이터를 따로 보관해야 된다는 것입니다. 즉 Employee 들은 Employee * 가 가리켜야 하고, Manager 들은 Manager * 가 가리켜야 합니다. 만일 무한 상사에서 클래스 하나를 더 추가해달라고 연락이 왔다간 때릴 지도 모르겠지요.

하지만, 한 가지 위에서 배운 사실은, 업 캐스팅은 매우 자유롭게 수행될 수 있다는 점입니다. 즉, Employee *Manager 객체를 가리켜도 별 문제가 없다는 것이지요. 그렇다면 manager_list 를 그냥 지워 버리고, employee_listEmployee, Manager 상관없이 가리키게 해도 될까요? 그러면 참 좋겠지만 다음과 같은 문제점이 있습니다.

void print_employee_info() {
  int total_pay = 0;
  for (int i = 0; i < current_employee; i++) {
    employee_list[i]->print_info();
    total_pay += employee_list[i]->calculate_pay();
  }
  ...

바로 여기서, employee_list[i]->print_info() 를 하게 되면 무조건 Employee 클래스의 print_info 함수가 호출된다는 것입니다. 왜냐하면 위에서도 이야기 하였듯이, employee_list[i]Employee 객체를 가리키는 포인터 이기 때문에 자신이 가리키는 객체가 Employee 객체라고 생각합니다.

하지만 우리는 Manager 객체와 Employee 객체 모두 Employee* 가 가리키도록 하였으므로, 만일 employee_list[i] 가 가리키는 것이 Manager 객체 일 때, Manager 의 print_info 함수가 아니라 Employeeprint_info 함수가 호출되서 다른 결과를 냅니다.

마찬가지로 calculate_pay 함수도 Managercalculate_pay 가 호출 되어야 하는데 Employeecalculate_pay 가 호출되어서 (월급이 더 적게나오는 ㅠㅠ) 틀린 결과가 나옵니다. 나쁜 회사였으면 환영할 일이였겠지만 착한 우리의 입장에선 이 문제를 꼭 해결해야 합니다.

실제로,

#include <iostream>
#include <string>

class Employee {
 protected:
  std::string name;
  int age;

  std::string position;  // 직책 (이름)
  int rank;              // 순위 (값이 클 수록 높은 순위)

 public:
  Employee(std::string name, int age, std::string position, int rank)
      : name(name), age(age), position(position), rank(rank) {}

  // 복사 생성자
  Employee(const Employee& employee) {
    name = employee.name;
    age = employee.age;
    position = employee.position;
    rank = employee.rank;
  }

  // 디폴트 생성자
  Employee() {}

  void print_info() {
    std::cout << name << " (" << position << " , " << age << ") ==> "
              << calculate_pay() << "만원" << std::endl;
  }
  int calculate_pay() { return 200 + rank * 50; }
};

class Manager : public Employee {
  int year_of_service;

 public:
  Manager(std::string name, int age, std::string position, int rank,
          int year_of_service)
      : year_of_service(year_of_service), Employee(name, age, position, rank) {}

  // 복사 생성자
  Manager(const Manager& manager)
      : Employee(manager.name, manager.age, manager.position, manager.rank) {
    year_of_service = manager.year_of_service;
  }

  // 디폴트 생성자
  Manager() : Employee() {}

  int calculate_pay() { return 200 + rank * 50 + 5 * year_of_service; }
  void print_info() {
    std::cout << name << " (" << position << " , " << age << ", "
              << year_of_service << "년차) ==> " << calculate_pay() << "만원"
              << std::endl;
  }
};
class EmployeeList {
  int alloc_employee;        // 할당한 총 직원 수
  int current_employee;      // 현재 직원 수
  Employee** employee_list;  // 직원 데이터

 public:
  EmployeeList(int alloc_employee) : alloc_employee(alloc_employee) {
    employee_list = new Employee*[alloc_employee];

    current_employee = 0;
  }
  void add_employee(Employee* employee) {
    // 사실 current_employee 보다 alloc_employee 가 더
    // 많아지는 경우 반드시 재할당을 해야 하지만, 여기서는
    // 최대한 단순하게 생각해서 alloc_employee 는
    // 언제나 current_employee 보다 크다고 생각한다.
    // (즉 할당된 크기는 현재 총 직원수 보다 많음)
    employee_list[current_employee] = employee;
    current_employee++;
  }
  int current_employee_num() { return current_employee; }

  void print_employee_info() {
    int total_pay = 0;
    for (int i = 0; i < current_employee; i++) {
      employee_list[i]->print_info();
      total_pay += employee_list[i]->calculate_pay();
    }
    std::cout << "총 비용 : " << total_pay << "만원 " << std::endl;
  }
  ~EmployeeList() {
    for (int i = 0; i < current_employee; i++) {
      delete employee_list[i];
    }
    delete[] employee_list;
  }
};
int main() {
  EmployeeList emp_list(10);
  emp_list.add_employee(new Employee("노홍철", 34, "평사원", 1));
  emp_list.add_employee(new Employee("하하", 34, "평사원", 1));
  emp_list.add_employee(new Manager("유재석", 41, "부장", 7, 12));
  emp_list.add_employee(new Manager("정준하", 43, "과장", 4, 15));
  emp_list.add_employee(new Manager("박명수", 43, "차장", 5, 13));
  emp_list.add_employee(new Employee("정형돈", 36, "대리", 2));
  emp_list.add_employee(new Employee("길", 36, "인턴", -2));
  emp_list.print_employee_info();
  return 0;
}

성공적으로 컴파일 하였다면

실행 결과

노홍철 (평사원 , 34) ==> 250만원
하하 (평사원 , 34) ==> 250만원
유재석 (부장 , 41) ==> 550만원
정준하 (과장 , 43) ==> 400만원
박명수 (차장 , 43) ==> 450만원
정형돈 (대리 , 36) ==> 300만원
길 (인턴 , 36) ==> 100만원
총 비용 : 2300만원 

와 같이 전부다 Employeeprint_infocalculate_pay 함수가 호출되서 원래 결과와 달라집니다.

그런데 놀랍게도 이러한 문제를 5초 만에 해결할 수 있는 방법이 있습니다.

 virtual 키워드

EmployeeList 문제를 해결하기 전에 좀 더 간단한 예시로 살펴보겠습니다.

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

어라? 위 결과를 보셨다면 놀라움을 금치 못하셨을 것입니다.

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

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

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

분명히 여기서 p_pp_c 모두 Base 객체를 가리키는 포인터 입니다. 따라서, p_p->what()p_c->what() 을 하면 모두 Base 객체의 what() 함수가 실행되서 둘 다 '기반' 라고 출력이 되어야만 했습니다.

그런데, 놀랍게도, 실제 p_pp_c 가 무엇과 결합해 있는지 아는 것 처럼 (p_pBase 객체를 가리키고, p_cDerived 객체를 가리킴) 이에 따른 적절한 what 함수를 호출해준 것입니다.

이와 같은 일이 가능해진 이유는 바로;

class Base {

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

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

virtual 키워드 하나 때문입니다. 이 virtual 키워드는, 다음과 같은 역할을 합니다.

p_c->what();

위 코드를 실행시에 (런타임), 컴퓨터 입장에서;

"흠, p_c 는 Base 포인터니까 Base 의 what() 을 실행해야지"
"어 근데 what 이 virtual 이네?"

"잠깐. 이거 실제 Base 객체 맞어? 아니네 Derived 객체네"
"그럼 Derived 의 what 을 실행해야지"

반면에

p_p->what();

였을 경우에는

"흠, p_c 는 Base 포인터니까 Base 의 what() 을 실행해야지"
"어 근데 what 이 virtual 이네?"

"잠깐. 이거 실제 Base 객체 맞어? 어 맞네."
"Base 의 what 을 실행하자"

이렇게 컴파일 시에 어떤 함수가 실행될 지 정해지지 않고 런타임 시에 정해지는 일을 가리켜서 동적 바인딩(dynamic binding) 이라고 부릅니다. 즉,

p_c->what();

에서 Derivedwhat 을 실행할지, Basewhat 을 실행하지 결정은 런타임에 이루어지게 됩니다. 물론 위 코드에선 컴파일 시에 무조건 p_c->what()Derivedwhat 이 실행되도록 정해진 거 아니냐고 물을 수 있지만 다음과 같은 상황을 생각해보세요.

// i 는 사용자로부터 입력받는 변수
if (i == 1) {
  p_p = &c;
} else {
  p_p = &p;
}
p_p->what();

이렇게 된다면 p_p->what() 이 어떤 what 일지에는 런타임에 정해지겠지요? 물론 동적 바인딩의 반대말로 정적 바인딩(static binding) 이란 말도 있습니다. 이는 컴파일 타임에 어떤 함수가 호출될 지 정해지는 것으로 여태까지 여러분이 알고 오셨던 함수에 해당합니다.

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

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

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

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

위 경우 Derived 클래스의 what 함수는 Base 클래스의 what 함수를 오버라이드 하므로, override 키워드를 통해 이를 알려주고 있습니다.

override 키워드를 사용하게 되면, 실수로 오버라이드를 하지 않는 경우를 막을 수 있습니다. 예를 들어서;

#include <iostream>
#include <string>

class Base {
  std::string s;

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

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

 public:
  Derived() : Base(), s("파생") {}

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

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

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

  std::cout << " == 실제 객체는 Derived == " << std::endl;
  p_c->incorrect();
  return 0;
}

성공적으로 컴파일 하였다면

실행 결과

기반 클래스
기반 클래스
 == 실제 객체는 Base == 
기반 클래스 
 == 실제 객체는 Derived == 
기반 클래스 

와 같이 incorrect 함수가 제대로 오버라이드 되지 않았음을 알 수 있습니다. 그 이유는 Baseincorrect 함수와 Derivedincorrect 함수는 거의 똑같이 생기기는 했지만 사실 다르기 때문입니다. 왜냐하면 Derivedincorrect 함수는 상수 함수 이고, Baseincorrect 는 아니기 때문이지요.

따라서 컴파일러 입장에서 두 함수는 다른 함수로 간주되므로, p_c->incorrect() 를 하였을 때 Derivedincorrect 함수가 Baseincorrect 함수를 오버라이드 하는 것이 아니라, 그냥 Baseincorrect 함수를 호출하는 셈이 됩니다.

만약에 여러분의 의도가 Derivedincorrect 함수가 기반 클래스를 오버라이드 하는 것이였다면 큰 문제가 될 것입니다. 이 버그는 컴파일 타임에 잡을 수 없게 되니까요.

하지만, 실제로 Derivedincorrect 함수를 기반 클래스의 incorrect 함수를 오버라이드 하기 위해서 만들었다면, override 키워드를 써야겠지요.

#include <iostream>
#include <string>

class Base {
  std::string s;

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

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

 public:
  Derived() : Base(), s("파생") {}

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

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

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

  std::cout << " == 실제 객체는 Derived == " << std::endl;
  p_c->incorrect();
  return 0;
}

컴파일 하였다면

컴파일 오류

test.cc:19:8: error: 'void Derived::incorrect() const' marked 'override', but does not override

위와 같이 Derivedincorrect 함수가 override 한다고 써있지만, 실제로는 아무것도 오버라이드 하지 않는다고 오류가 발생하게 됩니다. 만일 const 키워드를 지워준다면

#include <iostream>
#include <string>

class Base {
  std::string s;

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

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

 public:
  Derived() : Base(), s("파생") {}

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

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

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

  std::cout << " == 실제 객체는 Derived == " << std::endl;
  p_c->incorrect();
  return 0;
}

성공적으로 컴파일 하였다면

실행 결과

기반 클래스
기반 클래스
 == 실제 객체는 Base == 
기반 클래스 
 == 실제 객체는 Derived == 
파생 클래스  

제대로 실행됨을 알 수 있습니다.

그렇다면 Employee 문제를 어떻게 해결할까

이제 여러분은 그동안 골머리를 썩여왔던 EmployeeList 문제도 해결할 수 있게 되었습니다. 단순히 Employee 클래스의 calculate_pay 함수와 print_info 함수 앞에 virtual 만 붙여주면 깔끔하게 정리 되지요.

#include <iostream>
#include <string>

class Employee {
 protected:
  std::string name;
  int age;

  std::string position;  // 직책 (이름)
  int rank;              // 순위 (값이 클 수록 높은 순위)

 public:
  Employee(std::string name, int age, std::string position, int rank)
      : name(name), age(age), position(position), rank(rank) {}

  // 복사 생성자
  Employee(const Employee& employee) {
    name = employee.name;
    age = employee.age;
    position = employee.position;
    rank = employee.rank;
  }

  // 디폴트 생성자
  Employee() {}

  virtual void print_info() {
    std::cout << name << " (" << position << " , " << age << ") ==> "
              << calculate_pay() << "만원" << std::endl;
  }
  virtual int calculate_pay() { return 200 + rank * 50; }
};

class Manager : public Employee {
  int year_of_service;

 public:
  Manager(std::string name, int age, std::string position, int rank,
          int year_of_service)
      : year_of_service(year_of_service), Employee(name, age, position, rank) {}

  int calculate_pay() override { return 200 + rank * 50 + 5 * year_of_service; }
  void print_info() override {
    std::cout << name << " (" << position << " , " << age << ", "
              << year_of_service << "년차) ==> " << calculate_pay() << "만원"
              << std::endl;
  }
};
class EmployeeList {
  int alloc_employee;        // 할당한 총 직원 수
  int current_employee;      // 현재 직원 수
  Employee** employee_list;  // 직원 데이터

 public:
  EmployeeList(int alloc_employee) : alloc_employee(alloc_employee) {
    employee_list = new Employee*[alloc_employee];
    current_employee = 0;
  }
  void add_employee(Employee* employee) {
    // 사실 current_employee 보다 alloc_employee 가 더
    // 많아지는 경우 반드시 재할당을 해야 하지만, 여기서는
    // 최대한 단순하게 생각해서 alloc_employee 는
    // 언제나 current_employee 보다 크다고 생각한다.
    // (즉 할당된 크기는 현재 총 직원수 보다 많음)
    employee_list[current_employee] = employee;
    current_employee++;
  }
  int current_employee_num() { return current_employee; }

  void print_employee_info() {
    int total_pay = 0;
    for (int i = 0; i < current_employee; i++) {
      employee_list[i]->print_info();
      total_pay += employee_list[i]->calculate_pay();
    }

    std::cout << "총 비용 : " << total_pay << "만원 " << std::endl;
  }
  ~EmployeeList() {
    for (int i = 0; i < current_employee; i++) {
      delete employee_list[i];
    }
    delete[] employee_list;
  }
};
int main() {
  EmployeeList emp_list(10);
  emp_list.add_employee(new Employee("노홍철", 34, "평사원", 1));
  emp_list.add_employee(new Employee("하하", 34, "평사원", 1));

  emp_list.add_employee(new Manager("유재석", 41, "부장", 7, 12));
  emp_list.add_employee(new Manager("정준하", 43, "과장", 4, 15));
  emp_list.add_employee(new Manager("박명수", 43, "차장", 5, 13));
  emp_list.add_employee(new Employee("정형돈", 36, "대리", 2));
  emp_list.add_employee(new Employee("길", 36, "인턴", -2));
  emp_list.print_employee_info();
  return 0;
}

성공적으로 컴파일 하였다면

실행 결과

노홍철 (평사원 , 34) ==> 250만원
하하 (평사원 , 34) ==> 250만원
유재석 (부장 , 41, 12년차) ==> 610만원
정준하 (과장 , 43, 15년차) ==> 475만원
박명수 (차장 , 43, 13년차) ==> 515만원
정형돈 (대리 , 36) ==> 300만원
길 (인턴 , 36) ==> 100만원
총 비용 : 2500만원 

와 같이 비록 Employee* 가 가리키고 있음에도 불구하고 ManagerManager 의 함수를, EmployeeEmployee 의 함수를 잘 호출하고 있음을 알 수 있습니다. 물론 바뀐 것은 단 두 단어. virtual 키워들을 Employee 의 함수들 앞에 추가해놓은 것 뿐이지요.

employee_list[i]->print_info();
total_pay += employee_list[i]->calculate_pay();

이 두 부분은 employee_list[i]EmployeeManager 에 따라서 다르게 동작하게 됩니다. 이렇게 같은 print_info 함수를 호출했음에도 불구하고 어떤 경우는 Employee 의 것이, 어떤 경우는 Manager 의 것이 호출되는 일; 즉 하나의 메소드를 호출했음에도 불구하고 여러가지 다른 작업들을 하는 것을 바로 다형성(polymorphism) 이라고 부릅니다.

참고로, 다형성을 뜻하는 영어 단어인 polymorphism 은,여러개를 의미하는 그리스어 'poly' 와, 모습, 모양을 뜻하는 그리스어 'morphism' 에서 온 단어로 '여러가지 형태' 라는 의미 입니다.

자 그러면 이번 강좌는 여기에서 마치도록 하겠습니다. 아마도 virtual 키워드를 처음 접했더라면 머가 어떻게 되는지 많이 헷갈릴 수 있는데 꼭 여러번 테스트 프로그램을 만들어보아서 확실히 이해하고 넘어가도록 합시다 :)

생각해보기

문제 1

그렇다면 프로그램 내부적으로 virtual 함수들은 어떻게 처리될까요? 즉, 이 포인터가 어떠한 객체를 가리키는지 어떻게 알 수 있을까요? (난이도 : 上)

강좌를 보다가 조금이라도 궁금한 것이나 이상한 점이 있다면 꼭 댓글을 남겨주시기 바랍니다. 그 외에도 강좌에 관련된 것이라면 어떠한 것도 질문해 주셔도 상관 없습니다. 생각해 볼 문제도 정 모르겠다면 댓글을 달아주세요.

현재 여러분이 보신 강좌는 <6 - 2. 가상(virtual) 함수와 다형성> 입니다. 이번 강좌의 모든 예제들의 코드를 보지 않고 짤 수준까지 강좌를 읽어 보시기 전까지 다음 강좌로 넘어가지 말아주세요
댓글이 47 개 있습니다!
프로필 사진 없음
강좌에 관련 없이 궁금한 내용은 여기를 사용해주세요