第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継承
  • 次章では、ポリモーフィズムについてさらに深く学びます。

    ---

    練習問題

  • 以下のクラス階層を実装してください:
- Shape(抽象基底クラス): area(), perimeter() - Rectangle: width, height - Circle: radius

  • 仮想デストラクタが必要な場合を説明してください。
  • 以下のコードの出力を予測してください:
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;
}

  • ダイヤモンド問題とは何か、どう解決するか説明してください。