C++ 기초부터 마스터까지 — 8화
상속과 다형성, 객체 수명과 RAII
본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.
Warrior와 Archer는 둘 다 캐릭터이지만 공격 방식은 다르다. 공통 데이터는 부모 클래스에 두고, 달라지는 동작은 자식 클래스에서 구현할 수 있다.
상속과 다형성
class Character
{
public:
Character(const std::string& name)
: name(name)
{
}
virtual ~Character() = default;
virtual int Attack() const = 0;
const std::string& GetName() const
{
return name;
}
private:
std::string name;
};
class Warrior : public Character
{
public:
Warrior(const std::string& name)
: Character(name)
{
}
int Attack() const override
{
return 20;
}
};
class Archer : public Character
{
public:
Archer(const std::string& name)
: Character(name)
{
}
int Attack() const override
{
return 15;
}
};
Attack() = 0처럼 구현이 없는 순수 가상 함수를 가진 클래스는 추상 클래스가 된다. Character 자체는 만들 수 없고 자식 클래스가 Attack을 구현하도록 강제한다.
override를 붙이면 부모 함수의 시그니처와 다를 때 컴파일러가 오류를 잡아준다. 철자가 틀렸는데 새 함수를 만든 것으로 처리되는 실수를 막을 수 있다.
부모 포인터로 Attack을 호출해도 실제 객체가 Warrior인지 Archer인지에 따라 다른 함수가 실행된다. 이것이 런타임 다형성이다.
가상 소멸자
상속용 부모 클래스의 소멸자는 virtual로 두는 것이 안전하다.
virtual ~Character() = default;
부모 포인터를 통해 자식 객체를 삭제할 때 가상 소멸자가 없다면 자식 소멸자가 호출되지 않을 수 있다. 자식이 파일이나 메모리 같은 자원을 들고 있었다면 정리가 빠질 수 있다.
객체 수명과 RAII
지역 객체는 블록이 끝날 때 자동으로 파괴된다.
void Battle()
{
Warrior warrior{"Rose"};
std::cout << warrior.Attack() << '\n';
} // 여기서 warrior의 소멸자가 호출된다.
객체 생성과 자원 획득을 연결하고, 객체 소멸과 자원 해제를 연결하는 방식을 RAII라고 부른다. 함수 중간에 return을 하더라도 지역 객체의 소멸자는 호출된다.
좋다. 누가 언제 정리하는지 코드만 보고 알 수 있게 되었다. 반대로 new와 delete가 여기저기 흩어져 있으면 누가 소유자인지 알기 어려워진다.
다형성이 필요한 객체는 값으로 부모 타입에 저장하면 안 된다. Character는 순수 가상 함수를 가진 추상 클래스라 값으로는 만들 수 없어서, std::vector<Character>는 슬라이싱 이전에 컴파일 오류가 난다. 기반 클래스가 구체 클래스였다면 컴파일은 되지만 자식 부분이 잘려 나가는 객체 슬라이싱이 발생한다.
따라서 다음과 같이 포인터 형태로 보관해야 한다.
std::vector<std::unique_ptr<Character>> party;
이제 포인터가 객체를 소유하면서도 자동으로 정리되게 만드는 스마트 포인터가 필요해진다.