第1章:継承
はじめに
CPP Module 03では、C++の継承(Inheritance)を学びます。継承はオブジェクト指向プログラミングの三大要素(カプセル化、継承、ポリモーフィズム)の1つであり、コードの再利用と階層的な設計を可能にします。
---
1. 継承の概念
1.1 歴史的背景
継承の概念はSimula(1967年、Ole-Johan DahlとKristen Nygaard)で初めて導入されました。Simulaは「シミュレーション言語」として開発され、現実世界のオブジェクト階層をモデル化するために継承を使用しました。
Smalltalk(1970年代、Alan Kay)がこの概念を発展させ、すべてがオブジェクトという設計哲学を確立しました。C++はこれらの言語から継承の概念を取り入れ、C言語との互換性を保ちながら実装しました。
1.2 「is-a」関係
継承は「is-a」関係を表現します:
- 犬は動物である(Dog is an Animal)
- 車は乗り物である(Car is a Vehicle)
- ScavTrapはClapTrapである(ScavTrap is a ClapTrap)
class Animal {
// 基底クラス
};
class Dog : public Animal {
// 派生クラス
// DogはAnimalの一種
};
1.3 継承の種類
| 継承タイプ | 基底のpublic | 基底のprotected | 基底のprivate | |-----------|-------------|-----------------|---------------| | public継承 | public | protected | アクセス不可 | | protected継承 | protected | protected | アクセス不可 | | private継承 | private | private | アクセス不可 |
42の課題ではpublic継承のみを使用します。
---
2. 継承の構文
2.1 基本的な継承
// 基底クラス
class ClapTrap {
protected:
std::string _name;
int _hitPoints;
int _energyPoints;
int _attackDamage;
public:
ClapTrap();
ClapTrap(const std::string& name);
ClapTrap(const ClapTrap& other);
ClapTrap& operator=(const ClapTrap& other);
~ClapTrap();
void attack(const std::string& target);
void takeDamage(unsigned int amount);
void beRepaired(unsigned int amount);
};
// 派生クラス
class ScavTrap : public ClapTrap {
public:
ScavTrap();
ScavTrap(const std::string& name);
ScavTrap(const ScavTrap& other);
ScavTrap& operator=(const ScavTrap& other);
~ScavTrap();
void attack(const std::string& target); // オーバーライド
void guardGate(); // 新しいメソッド
};
2.2 アクセス修飾子
private:
- そのクラス内からのみアクセス可能
- 派生クラスからはアクセス不可
protected:
- そのクラスと派生クラスからアクセス可能
- 外部からはアクセス不可
public:
- どこからでもアクセス可能
class Base {
private:
int privateVar; // Derivedからアクセス不可
protected:
int protectedVar; // Derivedからアクセス可能
public:
int publicVar; // どこからでもアクセス可能
};
class Derived : public Base {
void foo() {
// privateVar = 1; // エラー!
protectedVar = 2; // OK
publicVar = 3; // OK
}
};
---
3. コンストラクタとデストラクタの継承
3.1 コンストラクタチェーン
派生クラスのコンストラクタは、基底クラスのコンストラクタを最初に呼び出します:
class ClapTrap {
public:
ClapTrap() {
std::cout << "ClapTrap default constructor" << std::endl;
}
ClapTrap(const std::string& name) : _name(name) {
std::cout << "ClapTrap constructor called" << std::endl;
}
};
class ScavTrap : public ClapTrap {
public:
ScavTrap() : ClapTrap() {
std::cout << "ScavTrap default constructor" << std::endl;
}
ScavTrap(const std::string& name) : ClapTrap(name) {
std::cout << "ScavTrap constructor called" << std::endl;
}
};
実行順序:
ScavTrap s("Robot");
// 出力:
// ClapTrap constructor called
// ScavTrap constructor called
3.2 デストラクタの順序
デストラクタはコンストラクタの逆順で呼び出されます:
class ClapTrap {
public:
~ClapTrap() {
std::cout << "ClapTrap destructor" << std::endl;
}
};
class ScavTrap : public ClapTrap {
public:
~ScavTrap() {
std::cout << "ScavTrap destructor" << std::endl;
}
};
実行順序:
{
ScavTrap s("Robot");
} // スコープ終了
// 出力:
// ScavTrap destructor
// ClapTrap destructor
3.3 初期化リストでの基底クラス初期化
ScavTrap::ScavTrap(const std::string& name) : ClapTrap(name) {
// ClapTrap(name)を呼び出してから、ScavTrapの初期化
_hitPoints = 100; // ClapTrapの10から変更
_energyPoints = 50; // ClapTrapの10から変更
_attackDamage = 20; // ClapTrapの0から変更
std::cout << "ScavTrap " << name << " is created!" << std::endl;
}
---
4. メソッドのオーバーライド
4.1 オーバーライドとは
派生クラスで基底クラスと同じシグネチャのメソッドを定義することをオーバーライドといいます:
class ClapTrap {
public:
void attack(const std::string& target) {
std::cout << "ClapTrap " << _name << " attacks " << target << std::endl;
}
};
class ScavTrap : public ClapTrap {
public:
void attack(const std::string& target) {
std::cout << "ScavTrap " << _name << " attacks " << target << std::endl;
}
};
4.2 基底クラスのメソッドを呼び出す
派生クラスから基底クラスのメソッドを呼び出すには、スコープ解決演算子を使用します:
class ScavTrap : public ClapTrap {
public:
void attack(const std::string& target) {
// 基底クラスの処理を呼び出す
ClapTrap::attack(target);
// 追加の処理
std::cout << "ScavTrap adds extra damage!" << std::endl;
}
};
4.3 オーバーロードとオーバーライドの違い
| 項目 | オーバーロード | オーバーライド | |------|--------------|---------------| | 定義 | 同名で異なるパラメータ | 同名で同じシグネチャ | | クラス | 同じクラス内 | 派生クラスで | | ポリモーフィズム | 静的(コンパイル時) | 動的(実行時) |
注: 動的ポリモーフィズムにはvirtualキーワードが必要(CPP Module 04で学習)
---
5. 多重継承
5.1 多重継承とは
C++では、1つのクラスが複数のクラスを継承できます:
class ClapTrap { /* ... */ };
class ScavTrap : public ClapTrap { /* ... */ };
class FragTrap : public ClapTrap { /* ... */ };
// 多重継承
class DiamondTrap : public ScavTrap, public FragTrap {
// ScavTrapとFragTrap両方を継承
};
5.2 ダイヤモンド問題
多重継承で最も有名な問題がダイヤモンド問題です:
ClapTrap
/ \
ScavTrap FragTrap
\ /
DiamondTrap
DiamondTrapは2つのClapTrapを持つことになります:
- ScavTrap経由のClapTrap
- FragTrap経由のClapTrap
class DiamondTrap : public ScavTrap, public FragTrap {
void test() {
_hitPoints = 100; // どちらのClapTrapの_hitPoints?
// 曖昧さによりコンパイルエラー
}
};
5.3 仮想継承
ダイヤモンド問題を解決するために仮想継承を使用します:
class ClapTrap { /* ... */ };
class ScavTrap : virtual public ClapTrap { /* ... */ };
class FragTrap : virtual public ClapTrap { /* ... */ };
class DiamondTrap : public ScavTrap, public FragTrap {
// ClapTrapのインスタンスは1つだけ
};
virtualキーワードにより、ClapTrapは共有された1つのインスタンスになります。
5.4 仮想継承でのコンストラクタ
仮想継承では、最も派生したクラスが仮想基底クラスのコンストラクタを呼び出す責任を持ちます:
class DiamondTrap : public ScavTrap, public FragTrap {
public:
DiamondTrap(const std::string& name)
: ClapTrap(name + "_clap_name"), // 仮想基底クラスを直接初期化
ScavTrap(name),
FragTrap(name),
_name(name) {
// DiamondTrapの初期化
}
};
---
6. 42課題での実装ポイント
6.1 ClapTrap(ex00)
基底クラスとしてのClapTrap:
class ClapTrap {
protected:
std::string _name;
int _hitPoints; // 10
int _energyPoints; // 10
int _attackDamage; // 0
public:
ClapTrap();
ClapTrap(const std::string& name);
ClapTrap(const ClapTrap& other);
ClapTrap& operator=(const ClapTrap& other);
~ClapTrap();
void attack(const std::string& target);
void takeDamage(unsigned int amount);
void beRepaired(unsigned int amount);
};
6.2 ScavTrap(ex01)
ScavTrapはClapTrapを継承し、異なる初期値を持ちます:
| 属性 | ClapTrap | ScavTrap | |------|----------|----------| | hitPoints | 10 | 100 | | energyPoints | 10 | 50 | | attackDamage | 0 | 20 |
6.3 FragTrap(ex02)
FragTrapも異なる初期値を持ちます:
| 属性 | ClapTrap | FragTrap | |------|----------|----------| | hitPoints | 10 | 100 | | energyPoints | 10 | 100 | | attackDamage | 0 | 30 |
6.4 DiamondTrap(ex03)
DiamondTrapは以下の値を使用:
| 属性 | 継承元 | |------|--------| | hitPoints | FragTrap (100) | | energyPoints | ScavTrap (50) | | attackDamage | FragTrap (30) | | _name | 独自 | | ClapTrap::_name | name + "_clap_name" |
---
7. 継承のベストプラクティス
7.1 継承 vs コンポジション
継承は「is-a」関係、コンポジションは「has-a」関係:
// 継承: CarはVehicleである
class Car : public Vehicle { };
// コンポジション: CarはEngineを持つ
class Car {
Engine _engine; // has-a
};
一般原則: 継承よりもコンポジションを優先する(Favor composition over inheritance)
7.2 Liskov置換原則(LSP)
派生クラスは基底クラスと置換可能でなければなりません:
void process(ClapTrap& trap) {
trap.attack("target");
}
// ScavTrapはClapTrapとして使用可能
ScavTrap scav("Robot");
process(scav); // OK
7.3 protectedメンバの使用
派生クラスでアクセスが必要なメンバはprotectedにします:
class ClapTrap {
protected:
std::string _name; // 派生クラスでアクセス必要
int _hitPoints;
private:
int _secretValue; // 派生クラスでもアクセス不要
};
---
まとめ
本章で学んだこと:
- 継承の概念: is-a関係、コードの再利用
- 継承の構文: public/protected/private継承
- コンストラクタチェーン: 基底→派生の順序
- メソッドオーバーライド: 派生クラスでの再定義
- 多重継承とダイヤモンド問題: 仮想継承による解決
- ベストプラクティス: LSP、継承 vs コンポジション
次章では、各演習の詳細な実装を解説します。