C++ Learning

第42回 継承の基本

既存のクラスを土台にして新しいクラスを作る仕組み、それが継承(inheritance)。「犬は動物である」「三角形は図形である」のようなis-a 関係をコードで表現します。本回は継承の書き方、アクセス制御、メモリレイアウト、そして「継承すべきか / 合成(has-a)で済ませるべきか」の判断を学びます。

このページで押さえること
✅ 最低限ここだけ覚える
  • class Derived : public Base { }; で継承
  • 基底クラスのメンバが派生クラスに引き継がれる
  • 基底のコンストラクタは初期化子リストで呼ぶ
  • 通常は public 継承のみ使う(is-a 関係)
⭐ 余裕があれば読む
  • メモリレイアウト(base 部分 + derived 部分)
  • protected / private 継承(ほぼ使わない)
  • is-a vs has-a(継承 vs 合成)
  • スライシング問題

1. まず触ってみる ― Animal と Dog

「動物は食べる」「犬は動物」「犬は吠える」を表現するのに、継承を使います。

Animal
+ name
+ eat()
継承: "Dog is-a Animal"
Dog
(+ name / eat() 継承)
+ bark()

最小コード

first_inherit.cpp最小例
class Animal { protected: std::string name_; public: Animal(std::string n) : name_(std::move(n)) {} void eat() { std::cout << name_ << " は食べている\n"; } }; class Dog : public Animal { // ← 継承 public: Dog(std::string n) : Animal(std::move(n)) {} // 基底のctorを呼ぶ void bark() { std::cout << name_ << " は吠えた!\n"; } }; Dog d{"ポチ"}; d.eat(); // 継承した関数が使える d.bark(); // Dog 独自の関数
ここまでで覚えること(3 つ):
  • 継承は class 派生 : public 基底 {}
  • 基底のメンバ(関数・変数)が派生に引き継がれる
  • 基底コンストラクタは初期化子リストで呼ぶ:Dog(args) : Animal(args) {}

よくある素朴な疑問

Q. 何を継承するの?
→ 基底クラスのすべてのメンバ(public と protected)。private メンバは技術的には継承されますが、派生クラスから触れないので「見えない」。

Q. 継承は悪い設計と聞いたけど?
深い継承階層安易な継承は確かに避けるべき。「is-a 関係」が本当にあるときだけ使い、できれば合成(has-a)を優先するのが現代の設計思想(§5)。

2. 継承の書き方

基本構文C++
class Base { public: int x; void f(); }; class Derived : public Base { public: int y; void g(); }; Derived d; d.x = 1; // 基底から引き継いだメンバ d.f(); // 基底から引き継いだ関数 d.y = 2; // 派生独自 d.g(); // 派生独自
基底のコンストラクタ初期化子リスト
class Base { public: Base(int x) : x_(x) {} protected: int x_; }; class Derived : public Base { public: // : Base(x) で基底の ctor を明示的に呼ぶ Derived(int x, int y) : Base(x), y_(y) {} private: int y_; };

コンストラクタ・デストラクタの呼ばれ順

派生オブジェクトを作ると、基底 → 派生の順にコンストラクタが呼ばれます。破棄は逆順:派生 → 基底。これはメモリ上で「基底の部分」が先に構築され、その上に「派生の部分」が積まれるから。

呼ばれ順ログで確認
Animal::Animal() { std::cout << "Animal ctor\n"; } Animal::~Animal() { std::cout << "Animal dtor\n"; } Dog::Dog() { std::cout << "Dog ctor\n"; } Dog::~Dog() { std::cout << "Dog dtor\n"; } { Dog d; } // 出力: // Animal ctor // Dog ctor // Dog dtor // Animal dtor

3. アクセス制御と継承

基底クラスのメンバは、派生クラスから見える/見えないがアクセス指定子で決まります。

基底のメンバ派生クラスから外部から(派生オブジェクト経由)
public
protected×
private××

protected は「派生クラス専用の public」のような位置付け。基底から子孫が直接触れる必要があるメンバを置きます。

継承の種類(public / protected / private)

public 継承推奨
class D : public B { }; // is-a 関係 // B の public → D でも public // B の protected → D でも protected
private 継承特殊用途のみ
class D : private B { }; // is-implemented-in-terms-of 関係 // B の public → D では private // 外から D を B として扱えない
原則:ほぼ常に public 継承。 private/protected 継承は「実装の再利用のためだけ」に継承するケースで使いますが、ほとんどの場合合成(メンバ変数として持つ)のほうが良い設計。

4. メモリレイアウト

派生オブジェクトは、メモリ上で「基底部分 + 派生部分」の順に並びます。

Dog のメモリレイアウト(概念):
[Animal 部分] std::string name_ (継承)
[Dog 独自部分] int age_

このレイアウトのおかげで:

スライシング問題

派生オブジェクトを基底の「値型」に代入すると、派生部分が切り落とされます(スライシング)。

スライシング落とし穴
Dog d{"ポチ"}; Animal a = d; // ← Dog 部分が切り落とされる! // a は Animal なので、Dog の bark() は使えない // 多態を使いたいときは参照・ポインタ・スマートポインタで持つ Animal& ref = d; // OK: Dog のまま扱える Animal* p = &d; // OK std::unique_ptr<Animal> up = std::make_unique<Dog>("ポチ"); // OK
モダン C++ の原則: 多態性(次章)を活かしたいなら、派生オブジェクトは必ず参照・ポインタ・スマートポインタで扱う。値のコンテナ std::vector<Animal> は Dog を入れるとスライシングで壊れます。std::vector<std::unique_ptr<Animal>> が正解。
ここまでで継承の基本は OK
最後の §5 は設計思想。継承を使うか合成を使うかの判断は、現代 C++ でとても重要。

5. is-a vs has-a(継承 vs 合成)

「新クラスに既存クラスの機能を取り込みたい」ときの 2 つの手段:

継承(is-a)厳選
class Dog : public Animal { }; // Dog is-a Animal // Dog は Animal として使える
合成(has-a)推奨
class Car { Engine engine_; // ← メンバとして持つ Wheel wheels_[4]; }; // Car has-a Engine // Car は Engine として使えない(当然)

判断フロー

典型的な誤用:
  • 「内部で Vector のような処理が欲しい」→ class MyClass : public std::vector<T> にしない。std::vector をメンバに持つ
  • 「Player に HP と MP と Inventory を持たせたい」→ それぞれを継承しない。メンバとして持つ
継承は「本当に is-a 関係が成立する」場合だけ。 疑わしきは合成です。
Scott Meyers の格言:「継承よりも合成を優先せよ (Prefer composition over inheritance)」― Effective C++ の基本原則のひとつ。
広告スペース

確認クイズ

継承の基本を 4 問で確認。

Q1. class Dog : public Animal { }; のとき、Dog から見える Animal の private メンバは?

全部見える
public メンバと同じ扱いで見える
protected と同じ扱いで見える
見えない(アクセスできない)
private メンバは派生クラスからもアクセス不可。派生に触らせたいなら protected にします。基底の private は基底クラス内部からのみ。

Q2. コンストラクタの呼ばれ順として正しいのは? (Dog : Animal 継承)

Dog ctor → Animal ctor
Animal ctor → Dog ctor
同時
呼び出し側が選ぶ
基底 → 派生の順。派生クラスは基底のメンバを使えるので、先に基底が初期化されている必要があります。破棄は逆順(Dog dtor → Animal dtor)。

Q3. Animal a = dog; (Dog を Animal 値に代入)で起こるのは?

ポインタに変換
a は Dog として機能する
スライシング(Dog 部分が切り落とされる)
コンパイルエラー
値型に代入すると派生部分が切り落とされ、純粋な Animal になります(スライシング)。多態性を使いたければ参照・ポインタ・unique_ptr を使う。

Q4. 設計のベストプラクティスとして正しいのは?

機能を共有したければ常に継承する
private 継承を積極的に使う
is-a 関係のときだけ継承。疑わしきは合成
深い継承階層を作ると再利用性が上がる
「継承よりも合成を優先せよ」。継承は本当に is-a 関係があるときだけ。合成(メンバとして持つ)のほうが変更に強く、テストしやすい。