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自体は作成できず、子クラスにAttakを実装するように強制します。
overrideを付けると、親関数のシグネチャと異なるときにコンパイラがエラーをキャッチします。 綴りが間違っていますが、新しい関数を作成したものとして扱われる間違いを防ぐことができます。
親ポインタで Attack を呼び出しても、実際のオブジェクトが Warrior か Archer かによって異なる関数が実行される。 これがランタイム多型です。
仮想デストラクタ
継承用の親クラスのデストラクタは仮想として置くのが安全です。
virtual ~Character() = default;
親ポインタを介して子オブジェクトを削除するときに仮想デストラクタがない場合、子デストラクタは呼び出されない可能性があります。 子がファイルやメモリなどのリソースを持っていた場合、整理が欠ける可能性があります。
オブジェクト寿命とRAII
ローカルオブジェクトはブロックの終わりに自動的に破壊されます。
void Battle()
{
Warrior warrior{"Rose"};
std::cout << warrior.Attack() << '\n';
} //ここでwarriorのデストラクタが呼び出されます。
オブジェクト生成とリソース獲得を結びつけ、オブジェクト消滅とリソース解放をつなぐ方式をRAIIと呼ぶ。 関数の途中でreturnをしても、ローカルオブジェクトのデストラクタは呼び出されます。
いいですね。誰がいつ整理するのかコードだけを見て知ることができるようになった。 逆にnewとdeleteがあちこちに散らばっていると誰が所有者なのか分かりにくくなる。
多型が必要なオブジェクトは、値として親型に格納しないでください。 std::vector<Character>に Warrior を値として入れると、子部分が切り捨てられるオブジェクトスライシングが発生することがある。
したがって、次のようにポインタの形式で保存する必要があります。
std::vector<std::unique_ptr<Character>> party;
これで、ポインタがオブジェクトを所有しながらも自動的に整理されるようにするスマートポインタが必要になります。