第6章:継承
はじめに
継承はSimula(1967年)で導入されたオブジェクト指向の中核概念です。継承により、既存のクラスを拡張して新しいクラスを定義できます。
本章では、継承の概念、仮想関数、多重継承について、理論的背景と実践的な実装を学びます。
---
1. 継承の概念
1.1 is-a関係
継承は「is-a」関係を表現します:
Dog is a Animal(犬は動物である)
Car is a Vehicle(車は乗り物である)
Circle is a Shape(円は形状である)
この関係をC++で表現:
class Animal {
protected:
std::string name;
public:
Animal(const std::string& n) : name(n) {}
void eat() { std::cout << name << " is eating" << std::endl; }
};
class Dog : public Animal { // DogはAnimalを継承
public:
Dog(const std::string& n) : Animal(n) {}
void bark() { std::cout << name << " is barking" << std::endl; }
};
1.2 基底クラスと派生クラス
用語の整理:
| 用語 | 別名 | 説明 | |------|------|------| | 基底クラス | 親クラス、スーパークラス | 継承元 | | 派生クラス | 子クラス、サブクラス | 継承先 |
1.3 継承によるコードの再利用
class Vehicle {
protected:
int speed;
std::string brand;
public:
Vehicle(const std::string& b) : speed(0), brand(b) {}
void accelerate(int delta) {
speed += delta;
std::cout << brand << " accelerating to " << speed << " km/h" << std::endl;
}
void brake() {
speed = 0;
std::cout << brand << " stopped" << std::endl;
}
};
class Car : public Vehicle {
int numDoors;
public:
Car(const std::string& b, int doors) : Vehicle(b), numDoors(doors) {}
// accelerate()とbrake()は自動的に使える
};
class Motorcycle : public Vehicle {
public:
Motorcycle(const std::string& b) : Vehicle(b) {}
void wheelie() { std::cout << brand << " doing a wheelie!" << std::endl; }
};
---
2. アクセス指定子と継承
2.1 三種類の継承
class Base {
public:
int pub;
protected:
int prot;
private:
int priv;
};
// public継承
class DerivedPublic : public Base {
// pub → public
// prot → protected
// priv → アクセス不可
};
// protected継承
class DerivedProtected : protected Base {
// pub → protected
// prot → protected
// priv → アクセス不可
};
// private継承
class DerivedPrivate : private Base {
// pub → private
// prot → private
// priv → アクセス不可
};
2.2 継承方式の選択
| 継承方式 | 意味 | 使用場面 | |----------|------|----------| | public | is-a | 最も一般的 | | protected | 実装の継承 | まれ | | private | has-a的な実装 | 実装詳細の隠蔽 |
実践的には、ほとんどの場合public継承を使います。
---
3. コンストラクタとデストラクタの呼び出し順序
3.1 構築順序
派生クラスのオブジェクトを構築する際:
- 基底クラスのコンストラクタが先に呼ばれる
- 派生クラスのコンストラクタが後に呼ばれる
class Base {
public:
Base() { std::cout << "Base constructor" << std::endl; }
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived constructor" << std::endl; }
};
int main() {
Derived d;
// 出力:
// Base constructor
// Derived constructor
return 0;
}
3.2 破棄順序
破棄は構築の逆順:
class Base {
public:
~Base() { std::cout << "Base destructor" << std::endl; }
};
class Derived : public Base {
public:
~Derived() { std::cout << "Derived destructor" << std::endl; }
};
int main() {
Derived d;
// スコープ終了時:
// Derived destructor
// Base destructor
return 0;
}
3.3 基底クラスコンストラクタの呼び出し
class Base {
int value;
public:
Base(int v) : value(v) {
std::cout << "Base(" << v << ")" << std::endl;
}
};
class Derived : public Base {
int extra;
public:
// 初期化リストで基底クラスのコンストラクタを呼ぶ
Derived(int v, int e) : Base(v), extra(e) {
std::cout << "Derived extra=" << e << std::endl;
}
};
---
4. 仮想関数(Virtual Functions)
4.1 問題:静的バインディング
class Animal {
public:
void speak() { std::cout << "Some sound" << std::endl; }
};
class Dog : public Animal {
public:
void speak() { std::cout << "Woof!" << std::endl; }
};
int main() {
Dog dog;
Animal* ptr = &dog;
ptr->speak(); // "Some sound" ← Dog::speak()が呼ばれない!
return 0;
}
4.2 解決:動的バインディング
virtualキーワードを使用:
class Animal {
public:
virtual void speak() { std::cout << "Some sound" << std::endl; }
};
class Dog : public Animal {
public:
void speak() override { std::cout << "Woof!" << std::endl; }
};
int main() {
Dog dog;
Animal* ptr = &dog;
ptr->speak(); // "Woof!" ← Dog::speak()が呼ばれる!
return 0;
}
4.3 vtable(仮想関数テーブル)
仮想関数はvtableを通じて実現されます:
オブジェクトのメモリレイアウト:
+------------------+
| vptr (vtableへ) | ← 8バイト(64ビット環境)
+------------------+
| メンバ変数 |
+------------------+
vtable(各クラスに1つ):
+------------------+
| &speak() | ← 実際の関数アドレス
+------------------+
| &other_virtual() |
+------------------+
オーバーヘッド:
- 空間: 各オブジェクトにvptrが追加(通常8バイト)
- 時間: 関数呼び出し時に間接参照が発生
4.4 override指定子(C++11)
class Base {
public:
virtual void foo() {}
virtual void bar(int) {}
};
class Derived : public Base {
public:
void foo() override {} // OK
void bar(int) override {} // OK
// void baz() override {} // エラー:基底クラスにbaz()がない
// void bar(double) override {} // エラー:シグネチャが違う
};
overrideを使うことで、意図しないオーバーライドミスを防げます。
4.5 final指定子(C++11)
class Base {
public:
virtual void foo() final {} // これ以上オーバーライドできない
};
class Derived : public Base {
public:
// void foo() override {} // エラー:finalな関数をオーバーライドできない
};
class FinalClass final {}; // このクラスは継承できない
// class Other : public FinalClass {}; // エラー
---
5. 純粋仮想関数と抽象クラス
5.1 純粋仮想関数
class Shape {
public:
// 純粋仮想関数(= 0)
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual void draw() const = 0;
virtual ~Shape() {} // 仮想デストラクタ
};
5.2 抽象クラス
純粋仮想関数を持つクラスは抽象クラスとなり、インスタンス化できません:
// Shape shape; // エラー:抽象クラスはインスタンス化できない
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
double perimeter() const override {
return 2 * 3.14159 * radius;
}
void draw() const override {
std::cout << "Drawing circle with radius " << radius << std::endl;
}
};
// 全ての純粋仮想関数を実装したので、インスタンス化可能
Circle c(5.0);
5.3 インターフェースとしての抽象クラス
// インターフェース的な使い方
class Drawable {
public:
virtual void draw() const = 0;
virtual ~Drawable() {}
};
class Printable {
public:
virtual void print() const = 0;
virtual ~Printable() {}
};
class Document : public Drawable, public Printable {
public:
void draw() const override { /* ... */ }
void print() const override { /* ... */ }
};
---
6. 仮想デストラクタ
6.1 なぜ必要か
class Base {
public:
~Base() { std::cout << "Base destructor" << std::endl; }
};
class Derived : public Base {
int* data;
public:
Derived() : data(new int[100]) {}
~Derived() {
delete[] data;
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // Base::~Base()のみ呼ばれる → メモリリーク!
return 0;
}
6.2 仮想デストラクタの使用
class Base {
public:
virtual ~Base() { std::cout << "Base destructor" << std::endl; }
};
class Derived : public Base {
int* data;
public:
Derived() : data(new int[100]) {}
~Derived() override {
delete[] data;
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // Derived::~Derived()が呼ばれ、その後Base::~Base()
return 0;
}
ルール: 基底クラスとして使う可能性があるクラスは、デストラクタをvirtualにする。
---
7. 多重継承
7.1 基本的な多重継承
class Swimmer {
public:
void swim() { std::cout << "Swimming" << std::endl; }
};
class Flyer {
public:
void fly() { std::cout << "Flying" << std::endl; }
};
class Duck : public Swimmer, public Flyer {
public:
void quack() { std::cout << "Quack!" << std::endl; }
};
int main() {
Duck duck;
duck.swim(); // Swimmerから継承
duck.fly(); // Flyerから継承
duck.quack(); // Duck自身のメソッド
return 0;
}
7.2 ダイヤモンド問題
class Animal {
protected:
std::string name;
public:
Animal(const std::string& n) : name(n) {}
};
class Mammal : public Animal {
public:
Mammal(const std::string& n) : Animal(n) {}
};
class Bird : public Animal {
public:
Bird(const std::string& n) : Animal(n) {}
};
class Platypus : public Mammal, public Bird {
// Animalが2回継承される!
// Mammal::name と Bird::name が別々に存在
};
Animal
/ \
Mammal Bird
\ /
Platypus
7.3 virtual継承による解決
class Animal {
protected:
std::string name;
public:
Animal(const std::string& n = "Unknown") : name(n) {}
};
class Mammal : virtual public Animal { // virtual継承
public:
Mammal(const std::string& n) : Animal(n) {}
};
class Bird : virtual public Animal { // virtual継承
public:
Bird(const std::string& n) : Animal(n) {}
};
class Platypus : public Mammal, public Bird {
public:
// virtual継承した基底クラスは最派生クラスで初期化
Platypus(const std::string& n)
: Animal(n), Mammal(n), Bird(n) {}
};
注意点:
- virtual継承はオーバーヘッドがある
- 可能であれば多重継承を避ける設計を検討
- 継承の概念: is-a関係、コードの再利用
- アクセス指定子と継承: public, protected, private継承
- コンストラクタ/デストラクタの順序: 構築と破棄の順序
- 仮想関数: 動的バインディング、vtable
- 純粋仮想関数と抽象クラス: インターフェース定義
- 仮想デストラクタ: メモリリークの防止
- 多重継承: ダイヤモンド問題とvirtual継承
- 以下のクラス階層を実装してください:
---
まとめ
本章で学んだこと:
次章では、ポリモーフィズムについてさらに深く学びます。
---
練習問題
- 仮想デストラクタが必要な場合を説明してください。
- 以下のコードの出力を予測してください:
class A { public: A() { std::cout << "A"; } ~A() { std::cout << "a"; } };
class B : public A { public: B() { std::cout << "B"; } ~B() { std::cout << "b"; } };
class C : public B { public: C() { std::cout << "C"; } ~C() { std::cout << "c"; } };
int main() {
C c;
return 0;
}
- ダイヤモンド問題とは何か、どう解決するか説明してください。