C++のクラス継承について

AI実装検定のご案内

C++のクラス継承とは、既存のクラスが持つデータや機能を、新しいクラスへ引き継ぐ仕組みです。

継承元となるクラスを基底クラス、基底クラスを継承して作成するクラスを派生クラスと呼びます。

たとえば、「犬」と「猫」はそれぞれ異なる特徴を持っていますが、どちらも「動物」の一種です。

この関係をクラスで表すと、次のようになります。

#include <iostream>

class Animal {
public:
    void eat() const {
        std::cout << "食事をします\n";
    }
};

class Dog : public Animal {
public:
    void bark() const {
        std::cout << "ワンワン\n";
    }
};

DogクラスはAnimalクラスを継承しているため、Dog自身が持つbark()だけでなく、Animalで定義されたeat()も使用できます。

int main() {
    Dog dog;

    dog.eat();
    dog.bark();
}

実行結果は次のとおりです。

食事をします
ワンワン

継承を利用すると、複数のクラスに共通する処理を基底クラスへまとめながら、派生クラスごとに独自の機能を追加できます。

目次

継承の基本構文

C++における継承の基本構文は、次のとおりです。

class 派生クラス名 : 継承時のアクセス指定子 基底クラス名 {
    // 派生クラスのメンバー
};

具体的には、次のように記述します。

class Dog : public Animal {
};

それぞれの意味は次のとおりです。

  • Animal:基底クラス
  • Dog:派生クラス
  • public:継承方法を指定するアクセス指定子

継承時には、主に次の3種類のアクセス指定子を使用します。

public
protected
private

継承方法を省略した場合

classで継承方法を省略した場合、デフォルトではprivate継承になります。

class Dog : Animal {
};

このコードは、次の記述と同じ意味です。

class Dog : private Animal {
};

一方、structで継承方法を省略した場合は、public継承になります。

struct Dog : Animal {
};

これは、次の記述と同じです。

struct Dog : public Animal {
};

継承方法を明確にするため、実際のコードではpublicなどを省略せずに記述するのが一般的です。

基底クラスと派生クラス

基底クラス

基底クラスは、複数のクラスに共通するデータや機能をまとめるためのクラスです。

#include <iostream>
#include <string>

class Person {
public:
    std::string name;

    void introduce() const {
        std::cout << "名前は" << name << "です\n";
    }
};

派生クラス

派生クラスは、基底クラスの機能を利用しながら、独自のデータや機能を追加するクラスです。

class Student : public Person {
public:
    int studentId;

    void study() const {
        std::cout << name << "が勉強しています\n";
    }
};

使用例は次のとおりです。

int main() {
    Student student;

    student.name = "田中";
    student.studentId = 1001;

    student.introduce();
    student.study();
}

Studentクラスにはnameintroduce()を直接定義していませんが、Personクラスを継承しているため使用できます。

より厳密に説明すると、Studentオブジェクトの内部には、Personに相当する基底クラス部分が含まれています。

メンバーのアクセス指定子

C++のクラスメンバーには、次の3種類のアクセス指定子があります。

アクセス指定子クラス外部派生クラスクラス内部
publicアクセス可能アクセス可能アクセス可能
protectedアクセス不可アクセス可能アクセス可能
privateアクセス不可直接アクセス不可アクセス可能

publicメンバー

publicメンバーは、クラスの外部、派生クラス、クラス内部のすべてからアクセスできます。

class Base {
public:
    int value = 10;
};
int main() {
    Base object;
    object.value = 20;
}

protectedメンバー

protectedメンバーは、クラスの外部からはアクセスできませんが、派生クラスからはアクセスできます。

class Base {
protected:
    int value = 10;
};

class Derived : public Base {
public:
    void show() const {
        std::cout << value << '\n';
    }
};

privateメンバー

privateメンバーは、定義したクラスの内部からのみ直接アクセスできます。

派生クラスのオブジェクト内にも基底クラスのprivateメンバーは存在しますが、派生クラスから名前を使って直接アクセスすることはできません。

class Person {
private:
    std::string name;

public:
    void setName(const std::string& value) {
        name = value;
    }

    const std::string& getName() const {
        return name;
    }
};

class Student : public Person {
public:
    void showName() const {
        // std::cout << name;
        // エラー:privateメンバーへ直接アクセスできない

        std::cout << getName() << '\n';
    }
};

派生クラスから基底クラスのprivateメンバーを扱う場合は、基底クラスが提供するpublicまたはprotectedのメンバー関数を利用します。

継承時のアクセス指定子

継承時に指定するpublicprotectedprivateは、基底クラスのメンバーが派生クラス側でどのように扱われるかを決めます。

public継承

public継承は、最も一般的な継承方法です。

class Derived : public Base {
};

public継承では、基底クラスのアクセス権が次のように引き継がれます。

基底クラス側派生クラス側での扱い
publicpublic
protectedprotected
private直接アクセス不可
class Base {
public:
    int publicValue = 10;

protected:
    int protectedValue = 20;

private:
    int privateValue = 30;
};

class Derived : public Base {
public:
    void show() const {
        std::cout << publicValue << '\n';
        std::cout << protectedValue << '\n';

        // std::cout << privateValue << '\n';
        // エラー:基底クラスのprivateメンバーへ直接アクセスできない
    }
};

public継承は、派生クラスが基底クラスの一種として扱える場合に使用します。

たとえば、次のような関係です。

  • 犬は動物の一種である
  • 学生は人の一種である
  • 自動車は乗り物の一種である

このような関係を、一般にis-a関係と呼びます。

ただし、日本語として「一種である」と言えるだけでは十分ではありません。

派生クラスを基底クラスとして扱っても、基底クラスが約束している動作や意味が維持されることが重要です。

protected継承

protected継承では、基底クラスのpublicメンバーとprotectedメンバーが、派生クラスではprotectedとして扱われます。

class Derived : protected Base {
};
基底クラス側派生クラス側での扱い
publicprotected
protectedprotected
private直接アクセス不可
class Base {
public:
    void publicFunction() {
    }

protected:
    void protectedFunction() {
    }
};

class Derived : protected Base {
public:
    void execute() {
        publicFunction();
        protectedFunction();
    }
};

この場合、Derivedの外部からpublicFunction()を直接呼び出すことはできません。

int main() {
    Derived object;

    // object.publicFunction();
    // エラー
}

ただし、Derivedをさらに継承したクラスからは利用できます。

class MoreDerived : public Derived {
public:
    void run() {
        publicFunction();
    }
};

private継承

private継承では、基底クラスのpublicメンバーとprotectedメンバーが、派生クラスではprivateとして扱われます。

class Derived : private Base {
};
基底クラス側派生クラス側での扱い
publicprivate
protectedprivate
private直接アクセス不可
class Base {
public:
    void start() {
        std::cout << "開始します\n";
    }
};

class Derived : private Base {
public:
    void run() {
        start();
    }
};

Derivedの内部ではstart()を呼び出せますが、外部からは呼び出せません。

int main() {
    Derived object;

    object.run();

    // object.start();
    // エラー
}

また、Derivedをさらに継承したクラスからも、原則としてstart()を利用できません。

private継承は、基底クラスの実装を内部的に利用したいものの、派生クラスを基底クラスの一種として公開したくない場合に使用します。

ただし、このような設計では、継承よりもコンポジションのほうが適切な場合があります。

オーバーライドとは

C++では、派生クラスが基底クラスの仮想関数を再定義することを、オーバーライドと呼びます。

class Animal {
public:
    virtual void speak() const {
        std::cout << "動物が鳴きます\n";
    }
};

class Dog : public Animal {
public:
    void speak() const override {
        std::cout << "ワンワン\n";
    }
};

overrideは、この関数が基底クラスの仮想関数を正しくオーバーライドしていることを、コンパイラに確認させるための指定子です。

int main() {
    Dog dog;
    dog.speak();
}

実行結果は次のようになります。

ワンワン

非仮想関数は厳密にはオーバーライドではない

基底クラスの関数がvirtualではない場合、派生クラスに同名の関数を定義しても、厳密にはオーバーライドではありません。

class Animal {
public:
    void speak() const {
        std::cout << "動物が鳴きます\n";
    }
};

class Dog : public Animal {
public:
    void speak() const {
        std::cout << "ワンワン\n";
    }
};

この場合、Dog::speak()Animal::speak()とは別の関数です。

int main() {
    Dog dog;
    Animal& animal = dog;

    dog.speak();
    animal.speak();
}

実行結果は次のとおりです。

ワンワン
動物が鳴きます

animalの静的な型はAnimal&であり、Animal::speak()は仮想関数ではないため、Animal::speak()が呼ばれます。

overrideを付ける理由

overrideは必須ではありませんが、仮想関数をオーバーライドするときは付けることが推奨されます。

たとえば、基底クラスが次のようになっているとします。

class Base {
public:
    virtual void show(int value) const {
    }
};

派生クラスで誤って引数を省略した場合、overrideがなければ別の関数として定義される可能性があります。

class Derived : public Base {
public:
    void show() const {
    }
};

overrideを付けると、シグネチャが一致していないことをコンパイラが検出します。

class Derived : public Base {
public:
    void show() const override {
        // コンパイルエラー
    }
};

正しくは次のように記述します。

class Derived : public Base {
public:
    void show(int value) const override {
    }
};

なお、基底クラスで一度virtualになった関数は、派生クラス側でvirtualを繰り返し記述しなくても仮想関数のままです。

void show(int value) const override;

この書き方で問題ありません。

仮想関数とポリモーフィズム

ポリモーフィズムとは

ポリモーフィズムとは、基底クラスのポインタや参照を通じて、実際のオブジェクトに対応した処理を呼び分ける仕組みです。

日本語では「多態性」とも呼ばれます。

class Animal {
public:
    virtual ~Animal() = default;

    virtual void speak() const {
        std::cout << "動物が鳴きます\n";
    }
};

class Dog : public Animal {
public:
    void speak() const override {
        std::cout << "ワンワン\n";
    }
};

class Cat : public Animal {
public:
    void speak() const override {
        std::cout << "ニャー\n";
    }
};

基底クラスの参照を受け取る関数を定義します。

void makeSpeak(const Animal& animal) {
    animal.speak();
}
int main() {
    Dog dog;
    Cat cat;

    makeSpeak(dog);
    makeSpeak(cat);
}

実行結果は次のとおりです。

ワンワン
ニャー

makeSpeak()Animal型としてオブジェクトを受け取っていますが、実際に渡されたオブジェクトがDogならDog::speak()CatならCat::speak()が呼ばれます。

仮想関数がない場合

基底クラスの関数にvirtualがない場合、呼び出される関数は、呼び出しに使用した変数や参照の型によって決まります。

class Animal {
public:
    void speak() const {
        std::cout << "動物が鳴きます\n";
    }
};

class Dog : public Animal {
public:
    void speak() const {
        std::cout << "ワンワン\n";
    }
};
int main() {
    Dog dog;
    Animal& animal = dog;

    animal.speak();
}

実行結果は次のようになります。

動物が鳴きます

実行時ポリモーフィズムを利用する場合は、基底クラス側の関数をvirtualにする必要があります。

純粋仮想関数と抽象クラス

基底クラスが共通のインターフェースだけを定義し、具体的な処理を派生クラスに任せたい場合は、純粋仮想関数を使用します。

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

末尾の= 0が、純粋仮想関数であることを表します。

未実装の純粋仮想関数を持つクラスは、抽象クラスになります。

抽象クラスのオブジェクトは直接作成できません。

int main() {
    // Shape shape;
    // エラー:抽象クラスはインスタンス化できない
}

ただし、抽象クラス型のポインタや参照は使用できます。

Shape* pointer = nullptr;

派生クラスで純粋仮想関数を実装する

class Rectangle : public Shape {
private:
    double width;
    double height;

public:
    Rectangle(double width, double height)
        : width(width),
          height(height) {
    }

    double area() const override {
        return width * height;
    }
};
class Circle : public Shape {
private:
    double radius;

public:
    explicit Circle(double radius)
        : radius(radius) {
    }

    double area() const override {
        constexpr double pi = 3.141592653589793;
        return pi * radius * radius;
    }
};

使用例は次のとおりです。

void printArea(const Shape& shape) {
    std::cout << shape.area() << '\n';
}

int main() {
    Rectangle rectangle(4.0, 5.0);
    Circle circle(3.0);

    printArea(rectangle);
    printArea(circle);
}

Shapeは具体的な図形ではなく、「面積を計算できるもの」という共通の仕様を表しています。

純粋仮想関数にも定義を持たせられる

純粋仮想関数は、必ずしも関数本体を持てないわけではありません。

class Base {
public:
    virtual void execute() = 0;
};

void Base::execute() {
    std::cout << "Baseの共通処理\n";
}

派生クラスから、基底クラスの定義を明示的に呼び出せます。

class Derived : public Base {
public:
    void execute() override {
        Base::execute();
        std::cout << "Derivedの処理\n";
    }
};

ただし、基底クラスに定義があっても、純粋仮想関数であることに変わりはありません。

派生クラス側でオーバーライドしなければ、その派生クラスも抽象クラスになります。

純粋仮想デストラクタ

デストラクタを純粋仮想関数にすることもできます。

class Base {
public:
    virtual ~Base() = 0;
};

ただし、純粋仮想デストラクタには定義が必要です。

Base::~Base() = default;

派生クラスのオブジェクトを破棄するときは、最後に基底クラスのデストラクタも必ず呼び出されるためです。

仮想デストラクタ

基底クラスのポインタを通じて派生クラスのオブジェクトを削除する可能性がある場合、基底クラスのデストラクタをvirtualにする必要があります。

class Base {
public:
    virtual ~Base() {
        std::cout << "Baseのデストラクタ\n";
    }
};

class Derived : public Base {
public:
    ~Derived() override {
        std::cout << "Derivedのデストラクタ\n";
    }
};
int main() {
    Base* object = new Derived();
    delete object;
}

実行結果は次のとおりです。

Derivedのデストラクタ
Baseのデストラクタ

派生クラスのデストラクタが先に呼ばれ、その後に基底クラスのデストラクタが呼ばれます。

基底クラスのデストラクタが仮想でない状態で、基底クラスのポインタから派生オブジェクトを削除すると、未定義動作につながります。

スマートポインタを使う場合

現代的なC++では、生のnewdeleteよりもスマートポインタを使用するのが一般的です。

#include <memory>

int main() {
    std::unique_ptr<Base> object =
        std::make_unique<Derived>();
}

ただし、スマートポインタを使用しても、基底クラス経由で派生オブジェクトを破棄する場合は、基底クラスのデストラクタを仮想にする必要があります。

ポリモーフィズムを目的とする基底クラスでは、一般的に次のように記述します。

class Base {
public:
    virtual ~Base() = default;
};

すべての基底クラスに仮想デストラクタが必要とは限らない

継承されるクラスだからといって、必ずデストラクタを仮想にする必要があるわけではありません。

基底クラスのポインタから削除させない設計であれば、デストラクタをprotectedかつ非仮想にする方法もあります。

class Base {
protected:
    ~Base() = default;
};

この場合、外部から基底クラス型のポインタを削除できません。

一般的には、次のように使い分けます。

class PolymorphicBase {
public:
    virtual ~PolymorphicBase() = default;
};
class NonPolymorphicBase {
protected:
    ~NonPolymorphicBase() = default;
};

コンストラクタとデストラクタの実行順序

派生クラスのオブジェクトを作成すると、先に基底クラスのコンストラクタが呼ばれ、その後に派生クラスのコンストラクタが呼ばれます。

class Base {
public:
    Base() {
        std::cout << "Baseコンストラクタ\n";
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derivedコンストラクタ\n";
    }
};
int main() {
    Derived object;
}

実行結果は次のとおりです。

Baseコンストラクタ
Derivedコンストラクタ

破棄されるときは逆順です。

Derivedデストラクタ
Baseデストラクタ

つまり、基本的な順序は次のようになります。

構築:基底クラス → 派生クラス
破棄:派生クラス → 基底クラス

より厳密な構築順序

メンバー変数や仮想基底クラスも含めると、構築順序は次のようになります。

  1. 仮想基底クラス
  2. 直接基底クラス
  3. 非静的データメンバー
  4. コンストラクタ本体

破棄は、基本的にこの逆順です。

class Base {
public:
    Base() {
        std::cout << "Base\n";
    }
};

class Member {
public:
    Member() {
        std::cout << "Member\n";
    }
};

class Derived : public Base {
private:
    Member member;

public:
    Derived() {
        std::cout << "Derived\n";
    }
};

実行結果は次の順になります。

Base
Member
Derived

初期化リストの記述順には注意する

メンバー変数は、初期化リストに記述した順番ではなく、クラス内で宣言された順番に初期化されます。

class Example {
private:
    int first;
    int second;

public:
    Example()
        : second(20),
          first(10) {
    }
};

この場合も、実際にはfirstが先に初期化され、その後にsecondが初期化されます。

初期化リストも宣言順に記述すると、コードが理解しやすくなります。

Example()
    : first(10),
      second(20) {
}

基底クラスのコンストラクタに引数を渡す

基底クラスにデフォルトコンストラクタがない場合、派生クラスの初期化リストから、基底クラスのコンストラクタを明示的に呼び出します。

class Person {
private:
    std::string name;

public:
    explicit Person(const std::string& name)
        : name(name) {
    }

    const std::string& getName() const {
        return name;
    }
};

class Employee : public Person {
private:
    int employeeId;

public:
    Employee(const std::string& name, int employeeId)
        : Person(name),
          employeeId(employeeId) {
    }
};

Person(name)によって、基底クラスのコンストラクタへ引数を渡しています。

基底クラスのコンストラクタは、派生クラスのコンストラクタ本体が実行される前に呼び出されます。

基底クラスの関数を明示的に呼び出す

派生クラスで基底クラスの仮想関数をオーバーライドした場合でも、スコープ解決演算子::を使えば、基底クラス側の関数を明示的に呼び出せます。

class Base {
public:
    virtual void show() const {
        std::cout << "Base::show\n";
    }
};

class Derived : public Base {
public:
    void show() const override {
        Base::show();
        std::cout << "Derived::show\n";
    }
};
int main() {
    Derived object;
    object.show();
}

実行結果は次のとおりです。

Base::show
Derived::show

基底クラスの共通処理を再利用し、その後に派生クラス独自の処理を追加したい場合に使います。

名前の隠蔽

C++では、派生クラスに基底クラスと同じ名前の関数を定義すると、引数が異なっていても、基底クラス側の同名関数が隠されることがあります。

class Base {
public:
    void show() const {
        std::cout << "引数なし\n";
    }

    void show(int value) const {
        std::cout << value << '\n';
    }
};

class Derived : public Base {
public:
    void show(const std::string& text) const {
        std::cout << text << '\n';
    }
};

この場合、次のコードはそのままではエラーになります。

int main() {
    Derived object;

    object.show("Hello");

    // object.show();
    // エラー

    // object.show(10);
    // エラー
}

基底クラスの同名関数も利用したい場合は、using宣言を使用します。

class Derived : public Base {
public:
    using Base::show;

    void show(const std::string& text) const {
        std::cout << text << '\n';
    }
};

これにより、すべてのshow()を使用できます。

int main() {
    Derived object;

    object.show();
    object.show(10);
    object.show("Hello");
}

隠蔽・オーバーライド・オーバーロードの違い

これらは似ていますが、異なる仕組みです。

  • オーバーライド:基底クラスの仮想関数を派生クラスで再定義する
  • オーバーロード:同じスコープ内で、異なる引数を持つ同名関数を複数定義する
  • 名前の隠蔽:派生クラスの同名関数によって、基底クラスの関数が名前探索の対象から隠れる

違いを意識すると、継承を使った関数呼び出しを理解しやすくなります。

アップキャスト

派生クラスのオブジェクトを基底クラス型として扱うことを、アップキャストと呼びます。

class Animal {
public:
    virtual ~Animal() = default;

    virtual void speak() const {
        std::cout << "Animal\n";
    }
};

class Dog : public Animal {
public:
    void speak() const override {
        std::cout << "Dog\n";
    }
};
int main() {
    Dog dog;

    Animal* animalPointer = &dog;
    Animal& animalReference = dog;

    animalPointer->speak();
    animalReference.speak();
}

public継承された、アクセス可能で一意な基底クラスへのアップキャストは、通常、暗黙的に行えます。

ただし、多重継承によって同じ基底クラスが複数存在する場合などは、変換先が曖昧になることがあります。

ダウンキャスト

基底クラス型から派生クラス型へ変換することを、ダウンキャストと呼びます。

ダウンキャストでは、実際のオブジェクトが目的の派生クラスであるとは限らないため、注意が必要です。

安全性を確認しながら変換するには、dynamic_castを使用します。

class Animal {
public:
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void bark() const {
        std::cout << "ワンワン\n";
    }
};

class Cat : public Animal {
};
void tryBark(Animal* animal) {
    Dog* dog = dynamic_cast<Dog*>(animal);

    if (dog != nullptr) {
        dog->bark();
    } else {
        std::cout << "Dogではありません\n";
    }
}
int main() {
    Dog dog;
    Cat cat;

    tryBark(&dog);
    tryBark(&cat);
}

実行結果は次のとおりです。

ワンワン
Dogではありません

ポインタに対するdynamic_castが失敗すると、nullptrが返されます。

参照に対するdynamic_castが失敗すると、std::bad_cast例外が発生します。

dynamic_castに必要な条件

dynamic_castを使ってダウンキャストする場合、変換元の基底クラスは、少なくとも1つの仮想関数を持つポリモーフィッククラスである必要があります。

class Animal {
public:
    virtual ~Animal() = default;
};

仮想デストラクタも仮想関数の一つなので、このAnimalはポリモーフィッククラスです。

dynamic_castの多用には注意する

型ごとにdynamic_castを繰り返すコードが増えた場合は、基底クラスに仮想関数を追加して、ポリモーフィズムで処理できないか検討します。

class Animal {
public:
    virtual ~Animal() = default;
    virtual void speak() const = 0;
};
void process(const Animal& animal) {
    animal.speak();
}

この設計であれば、新しい派生クラスを追加しても、process()を変更せずに済みます。

オブジェクトスライシング

派生クラスのオブジェクトを基底クラスの変数へ値として代入すると、派生クラス固有の部分が失われます。

この現象を、オブジェクトスライシングと呼びます。

class Animal {
public:
    virtual void speak() const {
        std::cout << "Animal\n";
    }
};

class Dog : public Animal {
public:
    int age = 5;

    void speak() const override {
        std::cout << "Dog\n";
    }
};
int main() {
    Dog dog;

    Animal animal = dog;
    animal.speak();
}

実行結果は次のようになります。

Animal

animalは新しく作られた独立したAnimalオブジェクトであり、Dog固有の部分は含まれていません。

ポリモーフィズムを利用する場合は、値ではなく参照やポインタを使用します。

Animal& animal = dog;
animal.speak();

関数の値渡しにも注意する

次のような関数も、オブジェクトスライシングを起こします。

void process(Animal animal) {
}

ポリモーフィズムを利用する場合は、参照で受け取ります。

void process(const Animal& animal) {
}

コンテナへ保存する場合

次のように基底クラス型のコンテナへ値として保存すると、派生クラス部分が失われます。

std::vector<Animal> animals;

Dog dog;
animals.push_back(dog);

複数の派生オブジェクトを統一的に保存する場合は、スマートポインタを使用できます。

#include <memory>
#include <vector>

std::vector<std::unique_ptr<Animal>> animals;

animals.push_back(std::make_unique<Dog>());

多重継承

C++では、一つの派生クラスが複数の基底クラスを継承できます。

これを多重継承と呼びます。

class Printer {
public:
    void print() const {
        std::cout << "印刷します\n";
    }
};

class Scanner {
public:
    void scan() const {
        std::cout << "読み取ります\n";
    }
};

class MultifunctionMachine
    : public Printer,
      public Scanner {
};
int main() {
    MultifunctionMachine machine;

    machine.print();
    machine.scan();
}

この例では、複合機が印刷機能とスキャン機能の両方を持つことを表現しています。

多重継承における名前の衝突

複数の基底クラスに同名の関数があると、どの関数を呼び出すべきか曖昧になります。

class A {
public:
    void show() const {
        std::cout << "A\n";
    }
};

class B {
public:
    void show() const {
        std::cout << "B\n";
    }
};

class C : public A, public B {
};
int main() {
    C object;

    // object.show();
    // エラー:どちらのshow()か判断できない
}

スコープを明示すれば呼び出せます。

object.A::show();
object.B::show();

派生クラス側で呼び出し先を統一することもできます。

class C : public A, public B {
public:
    void show() const {
        A::show();
    }
};

ダイヤモンド継承

ダイヤモンド継承とは、複数の継承経路を通じて、同じ基底クラスを重複して継承する構造です。

class Person {
public:
    std::string name;
};

class Student : public Person {
};

class Employee : public Person {
};

class WorkingStudent
    : public Student,
      public Employee {
};

継承関係は次のようになります。

        Person
       /      \
  Student   Employee
       \      /
    WorkingStudent

この構造では、WorkingStudentの内部に、Student経由のPersonと、Employee経由のPersonが存在します。

そのため、次のコードは曖昧になります。

int main() {
    WorkingStudent person;

    // person.name = "田中";
    // エラー:どちらのPerson::nameか判断できない
}

仮想継承

ダイヤモンド継承で共通の基底クラスを一つだけ共有したい場合は、仮想継承を使用します。

class Person {
public:
    std::string name;
};

class Student : virtual public Person {
};

class Employee : virtual public Person {
};

class WorkingStudent
    : public Student,
      public Employee {
};

これにより、WorkingStudentの内部にあるPerson部分は一つだけになります。

int main() {
    WorkingStudent person;
    person.name = "田中";
}

ここで使われるvirtualは、仮想関数を表すvirtualとは役割が異なります。

仮想基底クラスの初期化

仮想基底クラスは、最終的な最派生クラスが初期化します。

class Person {
public:
    explicit Person(const std::string& name) {
        std::cout << name << '\n';
    }
};

class Student : virtual public Person {
public:
    Student()
        : Person("Student") {
    }
};

class Employee : virtual public Person {
public:
    Employee()
        : Person("Employee") {
    }
};

class WorkingStudent
    : public Student,
      public Employee {
public:
    WorkingStudent()
        : Person("WorkingStudent") {
    }
};

WorkingStudentオブジェクトを作成した場合、共有されるPerson部分を初期化するのはWorkingStudentです。

コンストラクタやデストラクタから仮想関数を呼ぶ場合

基底クラスのコンストラクタ実行中に仮想関数を呼び出しても、まだ構築されていない派生クラス側のオーバーライドは呼ばれません。

class Base {
public:
    Base() {
        initialize();
    }

    virtual void initialize() {
        std::cout << "Base\n";
    }
};

class Derived : public Base {
public:
    void initialize() override {
        std::cout << "Derived\n";
    }
};
int main() {
    Derived object;
}

実行結果は次のようになります。

Base

Baseのコンストラクタ実行中は、Derived部分の構築がまだ完了していないため、Base::initialize()が呼ばれます。

デストラクタでも同様です。破棄が始まった派生クラス部分へは仮想ディスパッチされません。

そのため、コンストラクタやデストラクタから仮想関数を呼び出す設計は、原則として避けたほうが安全です。

継承とコンポジションの違い

クラス同士の関係を表現する方法には、継承だけでなくコンポジションもあります。

コンポジションとは、別のクラスのオブジェクトをメンバーとして保持する設計です。

継承の例

class Vehicle {
public:
    void move() {
    }
};

class Car : public Vehicle {
};

これは、「自動車は乗り物の一種である」というis-a関係です。

コンポジションの例

class Engine {
public:
    void start() {
        std::cout << "エンジンを始動します\n";
    }
};

class Car {
private:
    Engine engine;

public:
    void start() {
        engine.start();
    }
};

これは、「自動車はエンジンを持っている」という関係です。

このような関係を、has-a関係と呼びます。

継承が適している場面

継承は、主に次のような場合に適しています。

  • 派生クラスを基底クラスの一種として扱える
  • 共通のインターフェースを定義したい
  • 基底クラスの参照やポインタを通じて統一的に処理したい
  • 派生クラスごとに異なる実装を提供したい
  • 派生クラスが基底クラスの契約を維持できる

コンポジションが適している場面

コンポジションは、主に次のような場合に適しています。

  • 別のクラスの機能を部品として利用したい
  • 単にコードを再利用したい
  • 実行時に機能や部品を差し替えたい
  • 継承による強い結合を避けたい
  • クラス同士がis-a関係ではない

たとえば、次の継承は不自然です。

class Car : public Engine {
};

自動車はエンジンの一種ではありません。

正しくは、自動車がエンジンをメンバーとして持つ設計です。

class Car {
private:
    Engine engine;
};

一般的には、単なるコード再利用が目的であれば、継承よりもコンポジションを優先して検討します。

継承を使う際の注意点

オーバーライドにはoverrideを付ける

仮想関数をオーバーライドするときは、overrideを付けることで、関数シグネチャの間違いをコンパイラに検出させられます。

void execute() override {
}

ポリモーフィック基底クラスには仮想デストラクタを定義する

基底クラスのポインタや参照を通じて派生クラスを扱う設計では、基底クラスに仮想デストラクタを定義します。

class Base {
public:
    virtual ~Base() = default;
};

privateメンバーを安易にprotectedへ変更しない

派生クラスからアクセスしたいという理由だけで、基底クラスのprivateメンバーを安易にprotectedへ変更すると、カプセル化が弱くなります。

必要な操作をメンバー関数として提供するほうが安全です。

class Person {
private:
    std::string name;

protected:
    const std::string& getName() const {
        return name;
    }
};

オブジェクトスライシングに注意する

ポリモーフィズムを利用する場合は、基底クラス型へ値としてコピーせず、参照やポインタを使用します。

void process(const Animal& animal) {
}

深すぎる継承階層を避ける

継承階層が深くなると、処理の流れや責任範囲が分かりにくくなります。

Base
  ↓
DerivedA
  ↓
DerivedB
  ↓
DerivedC
  ↓
DerivedD

継承が何段階にもなる場合は、クラスの責任を見直したり、コンポジションへ変更したりすることを検討します。

dynamic_castに依存しすぎない

型判定のためにdynamic_castを多用すると、派生クラスを追加するたびに条件分岐を修正する必要が生じます。

可能であれば、仮想関数を利用して処理を派生クラス側へ分散させるほうが、拡張しやすい設計になります。

実践的な継承の例

図形を表す抽象クラスと、長方形・円を表す派生クラスを作成します。

#include <iostream>
#include <memory>
#include <numbers>
#include <stdexcept>
#include <vector>

class Shape {
public:
    virtual ~Shape() = default;

    virtual double area() const = 0;
    virtual void printName() const = 0;
};

class Rectangle final : public Shape {
private:
    double width;
    double height;

public:
    Rectangle(double width, double height)
        : width(width),
          height(height) {
        if (width < 0.0 || height < 0.0) {
            throw std::invalid_argument(
                "幅と高さは0以上である必要があります"
            );
        }
    }

    double area() const override {
        return width * height;
    }

    void printName() const override {
        std::cout << "長方形";
    }
};

class Circle final : public Shape {
private:
    double radius;

public:
    explicit Circle(double radius)
        : radius(radius) {
        if (radius < 0.0) {
            throw std::invalid_argument(
                "半径は0以上である必要があります"
            );
        }
    }

    double area() const override {
        return std::numbers::pi * radius * radius;
    }

    void printName() const override {
        std::cout << "円";
    }
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;

    shapes.push_back(
        std::make_unique<Rectangle>(4.0, 5.0)
    );

    shapes.push_back(
        std::make_unique<Circle>(3.0)
    );

    for (const auto& shape : shapes) {
        shape->printName();
        std::cout << "の面積:"
                  << shape->area()
                  << '\n';
    }
}

このコードには、継承に関する重要な要素が含まれています。

  • Shapeが抽象基底クラス
  • RectangleCircleが派生クラス
  • 純粋仮想関数で共通インターフェースを定義
  • 派生クラス側でoverrideを使用
  • 基底クラスに仮想デストラクタを定義
  • std::unique_ptrでオブジェクトを管理
  • スマートポインタを使ってオブジェクトスライシングを回避
  • 基底クラス型を通じて異なる派生クラスを統一的に処理
  • finalでそれ以上の継承を禁止
  • コンストラクタで不正な値を検証

finalは、そのクラスからさらに継承できないことを表します。

class Rectangle final : public Shape {
};

次のような継承はできません。

// class SpecialRectangle : public Rectangle {
// };
// エラー

なお、std::numbers::piはC++20以降で使用できます。C++17以前を使用する場合は、円周率をconstexprで定義する方法があります。

constexpr double pi = 3.141592653589793;

C++のクラス継承のまとめ

C++のクラス継承では、次の点が特に重要です。

  • 継承元を基底クラス、継承先を派生クラスと呼ぶ
  • 一般的なis-a関係にはpublic継承を使用する
  • classで継承方法を省略するとprivate継承になる
  • structで継承方法を省略するとpublic継承になる
  • 基底クラスのprivateメンバーには派生クラスから直接アクセスできない
  • 実行時ポリモーフィズムには仮想関数を使用する
  • 基底クラスの仮想関数を再定義することをオーバーライドと呼ぶ
  • オーバーライドする関数にはoverrideを付ける
  • ポリモーフィック基底クラスには仮想デストラクタを定義する
  • 純粋仮想関数を持つクラスは抽象クラスになる
  • 純粋仮想関数にも定義を持たせることができる
  • 基底クラス型へ値としてコピーするとオブジェクトスライシングが発生する
  • ダウンキャストにはdynamic_castを利用できる
  • 多重継承では名前の衝突やダイヤモンド継承に注意する
  • 仮想基底クラスは最派生クラスが初期化する
  • 単なるコード再利用であれば、継承よりコンポジションを優先して検討する
  • 深すぎる継承階層やdynamic_castへの過度な依存を避ける

継承の目的は、単に基底クラスのコードを再利用することではありません。

複数の異なるクラスに共通のインターフェースを与え、基底クラスのポインタや参照を通じて、安全かつ統一的に扱えるようにすることが、継承とポリモーフィズムの大きな目的です。

一方で、継承はクラス同士の結び付きを強くする仕組みでもあります。

派生クラスが本当に基底クラスの一種として扱えるか、基底クラスの契約を維持できるかを確認したうえで使用することが重要です。

以上、C++のクラス継承についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次