C++ MODULE04 : クラス継承とポリモーフィズム

2024/06/26に公開


C++のクラス継承とポリモーフィズム
この記事は、C++のモジュール焦点を当て、クラスの継承、ポリモーフィズム、そしてディープコピーとメモリ管理の重要性を探ります。これらのコンセプトは、効率的で堅牢なソフトウェア設計の基盤を形成します。

想像してみてください、ファンタジーの世界で、異なる種族からなるヒーローたちが集う壮大な冒険に挑むゲームをプレイしているとします。各ヒーローには独自の特技や能力があり、これが彼らの「クラス」を定義しています。例えば、勇敢な戦士、賢い魔法使い、敏捷な盗賊などです。この多様なキャラクターをプログラミングの世界で実現するためには、どのようにすれば良いでしょうか?

ここで登場するのが「クラス継承」と「ポリモーフィズム」という二つの強力なプログラミング概念です。クラス継承を使えば、基本となるクラス(例えば、全てのヒーローに共通する特性を持つ「キャラクター」クラス)を作り、それを基に具体的な戦士や魔法使いのクラスを作ることができます。一方、ポリモーフィズムを利用すれば、同一のインターフェース(例えば「攻撃する」や「防御する」といったアクション)に対して、それぞれのキャラクタークラスごとに異なる振る舞いを実装することが可能になります。

この技術によって、ゲームのキャラクターたちはそれぞれの固有の能力を持ちながら、一つの大きな冒険のストーリーに組み込むことができるのです。プログラミングにおけるこれらの概念を理解し、活用することで、リアルな振る舞いを持つダイナミックなゲーム世界を創り出すことができるのです。

1. 導入:動物の基底クラスの実装

I. 基底クラス Animal とその属性 type

基底クラスとは、他のクラス(派生クラス)の基礎となるクラスです。この例での Animal は、犬や猫などの具体的な動物クラスの共通の特徴を定義します。Animal クラスには type という属性があり、これは動物の種類(例:犬、猫)を文字列で保持します。この属性は protected アクセス指定子で宣言されているため、Animal クラス自体と、このクラスから派生するすべてのクラスでアクセス可能です。

class Animal
{
	protected:
		std::string type;
	public:
		Animal();
		virtual ~Animal();
		Animal(const Animal &copy);
		Animal &operator=(const Animal &copy);
		virtual void makeSound() const;
		virtual std::string getType() const;
};

Ⅱ. インスタンス化とは?

インスタンス化は、プログラミングにおいてクラス(設計図)からオブジェクト(実体)を生成するプロセスです。クラスは属性とメソッドを定義するテンプレートのようなもので、インスタンス化を通じて、このテンプレートが具体的なデータと振る舞いを持つオブジェクトへと具現化されます。例えば、C++においてnewキーワードを使用してクラスからオブジェクトを生成することがインスタンス化にあたります。

Ⅲ. なぜAnimalクラスはインスタンス化を禁止すべきか?

Animalクラスがインスタンス化を禁止すべき理由は、主に設計上の意図と実用的な機能性に基づいています。具体的には以下の理由からです:

  • 抽象性の強化:
    Animalクラスは、動物の一般的な特徴を表す抽象的な概念をモデル化していると考えることができます。このクラス自体は具体的な動物の種類を表しているわけではなく、共通の属性や振る舞い(例:動物は音を出す)を定義しています。実際の動物(犬や猫など)はこの一般的な枠組みを具体化したものであるため、Animalクラス自体のインスタンス化は意味を成さない場合が多いです。
  • ポリモーフィズムの活用:
    抽象クラスとしてAnimalを使用することで、派生クラスに対してポリモーフィズム(多様な形態を持つ能力)を提供できます。具体的な動物のクラス(DogやCatなど)がAnimalから派生していれば、Animal型の参照やポインタを通じてこれら具体的なオブジェクトを操作することができ、実行時に適切なメソッド(例えばmakeSound())が動的に呼び出されます。
  • 設計上の安全性:
    Animalを抽象クラスとして設計することで、開発者が直接このクラスをインスタンス化することを技術的に防ぎ、設計意図に沿った使い方を促すことができます。これにより、エラーや不適切なオブジェクト生成を防ぐことが可能です。

Ⅳ. 抽象クラスの実装

C++でクラスを抽象化し、インスタンス化を禁止するには、少なくとも一つの純粋仮想関数(Pure Virtual Function)をクラスに含めます。純粋仮想関数は、派生クラスでのオーバーライドを強制する関数で、以下のように定義されます:

virtual void makeSound() const = 0;

このような設計を採用することで、Animalクラスは直接インスタンス化することができず、派生クラスを通じてのみその機能が利用可能となります。これにより、コードの安全性と保守性が向上します。

2. 派生クラスの紹介:Dog と Cat

I. 派生クラスの基本

Dog と Cat は Animal クラスから派生したクラスで、特定の動物の特性を表現します。これらのクラスは Animal クラスの機能を継承しつつ、各動物固有の振る舞いや特性を追加することで、より具体的な動物のモデルを提供します。

Ⅱ. Type の初期化

派生クラスのコンストラクターでは、基底クラスのコンストラクターが先に呼ばれた後、派生クラス特有の属性が初期化されます。Dog と Cat のクラスでは、Animal の type 属性をそれぞれ "Dog" と "Cat" に設定しています。これにより、各クラスのインスタンスが自身の種類を識別できるようになります。

Cat::Cat() : Animal()
{
	this->type = "Cat";
	std::cout << "call constructer form class Cat" << std::endl;
}
Dog::Dog()
{
	this->type = "Dog";
	std::cout << "call constructer of dog" << std::endl;
}

Ⅲ. 固有のサウンドの実装

ポリモーフィズムの一環として、Dog と Cat は Animal クラスの makeSound() メソッドをオーバーライド(再定義)しています。これにより、Dog インスタンスでは犬の鳴き声("dok haaaaaaao")、Cat インスタンスでは猫の鳴き声("cat miaaaaaaaao")がそれぞれ出力されます。この振る舞いは、Animal のポインタや参照を通じて Dog や Cat のオブジェクトを扱う際にも保持され、動的バインディングによって実行時に適切なメソッドが選択されます。

Ⅳ. コピーコンストラクターと代入演算子のオーバーライド

Dog と Cat クラスはそれぞれ Animal クラスのコピーコンストラクターと代入演算子をオーバーライドしています。これにより、オブジェクトのコピーまたは代入が行われる際に、type 属性が正しくコピーされることが保証されます。コピー時には、オリジナルオブジェクトの type 値が新しいオブジェクトに正しく設定されます。
void makeSound() const;

Dog と Cat の実装は、オブジェクト指向プログラミングにおけるクラス継承、ポリモーフィズム、およびカプセル化の良い例です。これらのクラスを通じて、特定の動物の振る舞いをモデル化しつつ、コードの再利用性と拡張性を向上させることができます。

3. ポリモーフィズムの実践と"WrongCat"クラスの意義

Ⅰ. ポリモーフィズムの基本

ポリモーフィズムとは、オブジェクト指向プログラミングにおいて、異なるクラスのオブジェクトが同一のインターフェイスやメソッドを異なる形で実装することを指します。この特性により、プログラムは異なるデータタイプに対して同じ操作を一貫して行うことが可能になります。

例えば、AnimalクラスにmakeSound()というメソッドがあり、これが犬や猫などの派生クラスで異なるサウンドを出すようにオーバーライド(再定義)されている場合、プログラムはどの動物のインスタンスであってもmakeSound()を呼び出すだけで適切なサウンドを生成することができます。これは動的バインディングによって実現されており、実行時にオブジェクトの型を確認し、適切なメソッドを呼び出す仕組みです。

void Dog::makeSound() const
{
	std::cout << "dog : Woof Woof" << std::endl;
}

Ⅱ. WrongAnimal と WrongCat の役割

WrongAnimalとWrongCatクラスは、ポリモーフィズムが正しく実装されていない場合の挙動を示すために用いられます。これらのクラスは教育的な目的で設計され、ポリモーフィズムの落とし穴を理解するのに役立ちます。通常、これらのクラスでは次のような特徴が見られます:

virtual ~Animal();
  • メソッドの非仮想化: WrongAnimalクラスのmakeSound()メソッドが仮想関数ではない場合、WrongCatクラスでこのメソッドをオーバーライドしても、WrongAnimal型のポインタからWrongCatのオブジェクトを操作しているときに親クラスのメソッドが呼ばれることがあります。これは、期待されるWrongCatのサウンドではなく、WrongAnimalのサウンドが出力されることを意味します。
  • ポリモーフィズムの欠如: WrongAnimal から派生したクラスが正しくポリモーフィズムを実装していない場合、基底クラスの挙動がそのまま子クラスに引き継がれ、カスタマイズされた動作が期待通りに動作しない可能性があります。

Ⅲ. ポリモーフィズムの誤用を避けるための教訓

この例は、プログラマがオブジェクト指向プログラミングにおいてポリモーフィズムを正しく使用するためには、基底クラスでのメソッドの仮想化が必要であることを示唆しています。仮想関数を使用することで、派生クラスのオブジェクトに対して基底クラスのポインタや参照を使用した場合でも、派生クラスのオーバーライドしたメソッドが適切に呼び出されるようになります。

WrongAnimal と WrongCat の設計は、正しくポリモーフィズムを実装するために避けるべき典型的な間違いを浮き彫りにします。これにより、学習者はオブジェクト指向設計の原則をより深く理解し、より堅牢で拡張可能なソフトウェアを設計するための洞察を得ることができます。

4. ディープコピーの必要性

Dog と Cat が Brain クラスを持つことの意義。
ディープコピーとシャローコピーの違い、及びコピー操作が適切に行われることの確認方法。

Ⅰ. DogとCatクラスにおけるBrainの役割

C++においてクラスの設計は、データの構造と動作の両方を管理することを可能にする。特に、DogとCatクラスにBrainクラスを持たせることには、それぞれの動物が個別の思考や記憶を持つことを表現する重要な意義がある。Brainクラスは、動物ごとに100個のアイデアを保存する配列を保持し、これにより各インスタンスがユニークな状態を持つことができる。

Ⅱ. ディープコピーとシャローコピーの違い

オブジェクトのコピーを作成する際には、ディープコピーとシャローコピーの二つの方法が存在する。シャローコピーはオブジェクトの各フィールドを単純にコピーするが、これにより参照型のフィールドが同じリソースを指すことになり、予期せぬ副作用やバグの原因となる。一方、ディープコピーはすべてのフィールドを個別にコピーし、特に参照型で新しいインスタンスを生成するため、オブジェクト間でデータの独立性が保たれる。

Ⅲ. コピー操作の適切な実装

DogとCatクラスでは、コピーコンストラクタと代入演算子を通じてディープコピーが実装されている。これは、新しいBrainオブジェクトを割り当て、その内容を既存のオブジェクトからコピーすることにより行われる。この方法により、元のオブジェクトが破棄された際に他のオブジェクトに影響を与えることなく、各オブジェクトが独自の状態を維持することが可能となる。

Ⅳ. コピー操作の確認方法

適切なディープコピーの実装を確認するためには、コピーされたオブジェクトが独立して動作することをテストする必要がある。これには、コピー後に元のオブジェクトを変更し、その変更が新しいオブジェクトに影響を与えないことを検証するテストが有効である。また、プログラムの終了時にメモリリークが発生しないことも重要なチェックポイントである。

private: Brain* attribute;
: Brain オブジェクトへのポインタをクラスのプライベートメンバとして宣言。プライベートメンバは、クラスの外部から直接アクセスすることができず、クラス自身のメソッドを通じてのみ操作される。これにより、クラスの内部状態のカプセル化と保護が行われ、外部からの不正な変更を防ぐ。

this->attribute = new Brain();
: 新しいBrainオブジェクトをヒープ上に動的に割り当て、そのポインタをattributeメンバに代入。newを使用することで、プログラムの実行中に必要なメモリを確保し、オブジェクトの寿命をプログラマが管理できる。これは特に、オブジェクトのサイズが実行時まで不明である場合や、オブジェクトの寿命を関数やブロックのスコープを超えて延長する必要がある場合に有効。

*(this->attribute) = *(copy.attribute);
: ディープコピーの一環として実装。ここでは、コピー元オブジェクトのattributeポインタが指すBrainオブジェクトの内容を新しく作成されたBrainオブジェクトにコピー。(this->attribute)は、このインスタンスのattributeポインタが指すBrainオブジェクトを表し、(copy.attribute)はコピー元のBrainオブジェクトを指す。この操作により、Brainオブジェクトの実データが新しいオブジェクトに完全にコピーされるため、元のオブジェクトと新しいオブジェクトが互いに独立した状態を保つことができる。

このようにして、C++におけるディープコピーの概念とその実装方法を理解し、安全で効率的なプログラムを設計するための基礎を固めることができる。

5. メモリ管理とリークの防止

C++でのクラス継承とポリモーフィズムを実践する際、動的メモリ管理は避けて通れない重要な要素です。newとdeleteを正しく使うことで、メモリリークを防止し、プログラムの安定性を確保することができます。

動的メモリ管理において、new演算子を使用してメモリを確保し、delete演算子で適切に解放することが基本です。しかし、継承関係にあるクラスでは、デストラクタの呼び出し順序にも注意が必要です。派生クラスのオブジェクトを削除する際に、基底クラスのデストラクタが正しく呼び出されるようにするためには、基底クラスのデストラクタを仮想デストラクタとして宣言する必要があります。

例えば、Animalクラスを基底クラスとし、DogやCatクラスを派生クラスとする場合、Animalクラスのデストラクタを仮想デストラクタとして宣言します。

class Animal {
public:
    virtual ~Animal() {
        // Clean up resources
    }
};

class Dog : public Animal {
public:
    ~Dog() override {
        // Clean up Dog-specific resources
    }
};

上記のように仮想デストラクタを宣言することで、派生クラスのオブジェクトを削除する際に、基底クラスのデストラクタが適切に呼び出されます。これにより、リソースの確保と解放が正しく行われ、メモリリークを防ぐことができます。

6. まとめとベストプラクティス

C++のクラス継承とポリモーフィズムを通じて、さまざまな概念や技術を学びました。これらの概念は、ソフトウェア開発全般において広く応用される重要な技術です。
まず、基底クラスと派生クラスの関係を明確に設計することが重要です。基底クラスには共通の属性やメソッドを持たせ、派生クラスでそれらを拡張・具体化します。また、基底クラスのデストラクタは仮想デストラクタとして宣言し、正しいデストラクタの呼び出し順序を確保しましょう。

次に、ポリモーフィズムの利用は、コードの再利用性や柔軟性を高めるために有効です。基底クラスのポインタや参照を使って派生クラスのオブジェクトを操作することで、動的な型の切り替えが可能になります。ただし、ポリモーフィズムを適切に使うためには、正しい設計と実装が不可欠です。例えば、WrongCatクラスのように、基底クラスが持つべきメソッドを正しく実装しないと、期待通りに動作しないことがあります。

また、コピーコンストラクタや代入演算子のオーバーロードを適切に行うことも、クラスの設計において重要です。ディープコピーとシャローコピーの違いを理解し、必要に応じてディープコピーを実装することで、データの不整合やメモリリークを防ぐことができます。

メモリ管理においては、newとdeleteを正しく使用し、必要に応じてスマートポインタを活用することで、メモリリークを防ぎます。また、リソースの確保と解放を明確に管理し、エラー処理やパフォーマンスの最適化も考慮することが求められます。

効果的なクラス設計のためのベストプラクティス

  • クラスの責務を明確にし、単一責任原則を遵守する。

  • 継承関係を適切に設計し、必要に応じて仮想関数を活用する。

  • メモリ管理とエラー処理を適切に行い、スマートポインタを活用する。

  • コードの再利用性と拡張性を考慮し、ポリモーフィズムを効果的に活用する。

    以上、ありがとうございました。

Discussion