CPP Module 03
“CPP Module 03에 대하여”
Inheritance
ex00: Aaaaand… OPEN!
- 명세에 맞게 ClapTrap을 구현해보자. Hit points, Energy Point, Attack Damage 등을 설정하고,
void attack(const std::string& target);
,void takeDamage(unsigned int amount);
,void beRepaired(unsigned int amount);
등도 구현하자. 본인은 모듈 01에서 PhoneBook을 구현했을 때와 마찬가지로, STATUS, ATTACK, REPAIR, ANNOY, EXIT의 다섯 명령어를 표준 입력 받아서 그에 해당하는 처리를 구현했다.


ex01: Serena, my love!
클래스의 상속
클래스의 상속에 대해 알아볼 시간이다. 상속에 대한 자세한 정보는 씹어먹는 C++ - <6 - 1. C++ 표준 문자열 & 부모의 것을 물려쓰자 - 상속>을 참고해보자.
상속은 사용하는 요령을 알고 나면, 쓰는 것 자체는 크게 어렵지 않다. 빠르게 상속의 장점에 대해 짚어보자면,
첫 번째로, 간결한 코드를 작성할 수 있다. 중복된 부분이 줄어들게 되므로, 코드가 간결해진다.
두 번째로, 클래스 간 계층 분류 및 관리에 유용하다. 상속이 없는 상태에서, 클래스 간의 관계는 수평적이다(일차원적이다). 그러나 클래스의 상속 개념이 들어오면 클래스 간의 위계가 생기게 되며, 그 관계는 수직적으로 변하게 된다(이차원적이다). 이것은 클래스의 관리 측면에서 상당한 혁신이라 볼 수 있다. 클래스의 상속을 통해 클래스를 수직/수평적으로, 입체적으로 관리할 수 있게 된다.
세 번째로, 재사용과 확장을 통해 생산성을 향상시킬 수 있다. 코드의 양이 적어지고, 들어가는 품이 줄어들어 재사용과 확장에 용이하고, 생산성을 향상시킬 수 있다.
새로운 접근 지시자 protected
- scavtrap은 claptrap을 상속받고, claptrap의 멤버 변수에 접근해야 한다. 그런데 claptrap의 멤버 변수가 private이라면 scavtrap이 claptrap을 상속받았다 하더라도 접근할 수 없다. private 멤버 변수는 자기 클래스 말고는 접근할 수 있는 방법이 없다. 하지만 protected 멤버 변수는 상속받는 클래스에서 접근 가능하고 그 외에서는 접근이 불가능한 중간자적인 지시자라고 볼 수 있다. 따라서, claptrap의 멤버 변수들을 private에서 protected로 변경하여 scavtrap이 멤버 변수에 접근하는 것을 허용해야 한다.
class scavtrap : public claptrap
claptrap을 public으로 상속받게 되면, 기반 클래스(claptrap)의 접근 지시자들은 그대로 작동한다. public은 public이고, protected는 protected다.
protected로 상속받게 되면 public은 protected가 되고 나머지는 그대로 작동한다.
private으로 상속받게 되면 기반 클래스의 모든 접근 지시자들이 private이 된다.
대부분의 경우, public으로 상속받게 될 것이다.
기반 클래스와 파생 클래스의 생성과 소멸
scavtrap의 생성자를 호출하기 전 반드시 claptrap의 생성자를 호출하고, scavtrap이 소멸하고 난 이후엔 반드시 claptrap의 소멸자가 호출된다.
이렇게 되는 이유는 상속을 받을 때 ClapTrap의 데이터를 지닌 ScavTrap이 생성되는 것이 아니라 ClapTrap이 생성되고 그 주소를 지닌 ScavTrap이 생성되기 때문에, 만약 순서가 뒤바뀐다면 CalpTrap의 변수를 ScavTrap에서 초기화 하기가 굉장히 불편할 것이고, 반대로 소멸에서는 ClapTrap이 먼저 소멸된다면, ScavTrap의 소멸자에서 부모의 함수나 변수를 사용하지 못하는 상황이 생기기 때문에 이런 순서로 동작한다.
virtual ~ClapTrap();
- 기반 클래스의 소멸자에 virtual을 붙여서 순서대로 소멸자를 호출하게 만들자. 메모리 관리… 해야지? (virtual의 내부 작동에 대해서는 다음 모듈에서 이야기할 예정)
ex02: Repetitive work
바인딩(binding)과 다형성
가상 함수를 통한 동적 바인딩(dynamic binding)으로 subtype polymorphism(run time polymorphism)을 구현할 수 있다. 어떤 방식으로 작동할까?
바인딩(binding)에 대해 알아야 한다. 바인딩은 프로그램 소스에 있는 요소 및 식별자들에 대한 값 또는 속성을 확정시키는 작업이다. 정적 바인딩(static binding)을 보면서 바인딩의 의미를 파악해보자.
정적 바인딩
#include <iostream>
class Base
{
public:
void print_b(void)
{
std::cout << "I am base" << std::endl;
}
};
class Derived : public Base
{
public:
void print_d(void)
{
std::cout << "I am derived" << std::endl;
}
};
int main(void)
{
Base* base = new Derived(); // OK!
base->print_d(); // complie error
// ...
}
어디선가 봤을 수도 있는 코드다. 잘라 말하자면,
Base* base = new Derived();
는 말이 되는 라인이고,base->print_d()
는 컴파일이 안되는 라인이다.Base* base = new Derived();
는 기반 클래스 포인터로 파생 클래스 포인터를 가리킬 수 있다는 다형성의 기본적인 원리에 따라 성립하는 라인이다. 물론 반대는 성립하지 않는다. 단순히 생각해봐도, 파생 클래스는 기반 클래스 + α인데, 파생 클래스가 기반 클래스를 가리키게 되면, 기반 클래스에서 +α인 부분을 찾을 수가 없으니 문제가 생길 것이 뻔하다.근데
base->print_d()
는 왜 안되는 걸까? 결국 base 포인터 안에 들어있는 본체는 Derived인데, 그러면 print_d()를 가리킬 수 있는 것이 아닐까하는 생각이 들기 때문이다. 근데 컴파일이 안되는 것은 이미Base* base
는Derived*
가 아니라Base*
로 컴파일 타임에 이미 정해져버렸기 때문이다. 이것이 정적 바인딩이다. 이렇게 컴파일 타임에 객체의 타입이 결정되어버리면 이후에는 변경이 불가능하다. 자원 소모가 적지만, 다형성을 100% 활용할 수 없다.
가상 함수(virtual function)와 동적 바인딩
- 정적 바인딩 시, 컴파일 타임에 타입 정보의 손실이 일어나기 때문에(Derived -> Base), Base 포인터가 가리키는 실제의 객체 내용
Derived
의 함수를 호출하기 위해서는 동적 바인딩을 해야하고, 그러기 위해서 가상 함수를 사용해야 한다. 아래를 보자.
#include <iostream>
class Base
{
public:
virtual void print(void) // virtual 키워드의 사용
{
std::cout << "I am base" << std::endl;
}
};
class Derived : public Base
{
public:
void print(void)
{
std::cout << "I am derived" << std::endl;
}
};
int main(void)
{
Base* base = new Derived(); // OK!
base->print();
delete base;
}
❯ ./a.out
I am derived
Base* base
의 print를 호출했더니, 실제로 가리키는 Derived의 print가 잘 실행되었다. 가상 함수의 사용으로 런타임에 포인터가 가리키는 실제 객체를 파악했기 때문에 가능한 일이다. virtual 키워드를 통해 가상 함수를 사용할 수 있다.
키워드: virtual
- 기반 클래스의 함수에 virtual 키워드를 붙이게 되면, 이 함수가 자식 클래스에서 재정의될 수 있다는 것을 컴파일러에 알리게 되며 해당 함수는 가상 함수(virtual function)가 된다.
virtual void print(void) // virtual 키워드의 사용
기반 클래스의 멤버 함수에 한번 virtual 키워드가 붙게 되면, 파생되는 클래스들에는 더 이상 virtual 키워드를 붙일 필요가 없다. 즉 Base를 기반으로 하는 Derived 말고도, Derived를 상속하는 그 밑의 파생 클래스들에도 virtual 키워드를 붙일 필요가 없다는거다.
가상 함수를 사용하면 컴파일러는 런타임에 올바른 멤버 함수를 호출하기 위해 동적 바인딩을 수행한다. 즉, 객체의 실제 타입에 맞는 오버라이딩된 함수를 호출한다. 이렇게 컴파일 시에 어떤 함수가 실행될 지 정해지지 않고 런타임 시에 정해지는 일을 가리켜서 동적 바인딩(dynamic binding)이라고 한다.
번외, virtual과 v-table
- 모듈 3의 주제는 클래스의 상속으로, 이는 c++ subtype polymorphism(서브타입 다형성, 런타입 다형성)에 대한 물꼬를 트는 주제라 볼 수 있다. 그런데 모듈 3에서 다룬 내용만으로는 서브타입 다형성의 전부를 알 수가 없다. 예를 들어 virtual이 내부적으로는 어떻게 동작하는지(v-table), 순수 가상함수는 무엇인지, 인터페이스는 무엇인지에 관한 것들이 있다. 이런 것들은 다음 모듈에서 자세하게 다루게 되는데, 평가를 진행할 때 간혹 v-table에 대해 이야기하게 되는 경우가 있어서 v-table에 대해서는 꼭 알아두고 가는 편이 좋다. 물론 v-table은 뒤에 dynamic cast를 공부할 때 다시 이야기가 나오기도 하고, 컴파일러의 RTTI(Runtime Type Information)기능과 더불어 c++ 런타임 다형성의 정수인만큼 다음 모듈에서 바로 다뤄볼 것이다.