[C++] クラスの継承について詳しく解説

C++におけるクラスの継承は、既存のクラスを基に新しいクラスを作成するための重要な機能です。

継承を利用することで、コードの再利用性を高め、オブジェクト指向プログラミングの基本原則である多態性を実現できます。

基底クラスから派生クラスを作成する際、派生クラスは基底クラスのメンバ関数や変数を引き継ぎます。

アクセス指定子(public、protected、private)を使用して、基底クラスのメンバの可視性を制御することが可能です。

また、仮想関数を用いることで、派生クラスでのメソッドのオーバーライドが可能になり、動的ポリモーフィズムを実現します。

この記事でわかること
  • 継承の基本概念とそのメリット
  • 基本的な継承の使い方と構文
  • ポリモーフィズムの実現方法
  • 抽象クラスとインターフェースの利用
  • 継承における注意点と多重継承の問題

目次から探す

クラスの継承とは

継承の基本概念

継承は、既存のクラス(基底クラス)から新しいクラス(派生クラス)を作成する機能です。

派生クラスは基底クラスの属性やメソッドを引き継ぎ、さらに独自の属性やメソッドを追加することができます。

これにより、コードの再利用性が向上し、プログラムの構造が整理されます。

以下は、基本的な継承の例です。

class Animal {
public:
    void speak() {
        std::cout << "動物の声" << std::endl;
    }
};
class Dog : public Animal {
public:
    void bark() {
        std::cout << "ワンワン" << std::endl;
    }
};

この例では、DogクラスAnimalクラスを継承しています。

DogクラスAnimalクラスspeakメソッドを使用できます。

継承のメリット

継承を使用することで、以下のようなメリットがあります。

スクロールできます
メリット説明
コードの再利用既存のクラスを再利用することで、重複を避けることができる。
階層的な構造クラスの階層を作成することで、プログラムの構造が明確になる。
拡張性新しい機能を追加する際に、既存のクラスを拡張するだけで済む。

継承の種類

C++における継承には、主に以下の種類があります。

スクロールできます
継承の種類説明
公開継承 (public)基底クラスのpublicメンバは派生クラスでもpublicとして扱われる。
保護継承 (protected)基底クラスのpublicおよびprotectedメンバは派生クラスでprotectedとして扱われる。
非公開継承 (private)基底クラスのpublicおよびprotectedメンバは派生クラスでprivateとして扱われる。

これらの継承の種類を使い分けることで、クラスのアクセス制御を柔軟に行うことができます。

基本的な継承の使い方

基本的な継承の構文

C++における継承の基本的な構文は以下のようになります。

派生クラスは基底クラスをコロン(:)で指定し、継承の種類を明示します。

class BaseClass {
    // 基底クラスのメンバ
};
class DerivedClass : public BaseClass {
    // 派生クラスのメンバ
};

この構文を使用することで、DerivedClassBaseClassのメンバを継承します。

継承の種類にはpublicprotectedprivateがあります。

基底クラスと派生クラス

基底クラスは他のクラスから継承されるクラスであり、派生クラスは基底クラスを拡張したクラスです。

以下の例では、Vehicleクラスが基底クラスで、CarBikeが派生クラスです。

class Vehicle {
public:
    void start() {
        std::cout << "車両がスタートしました。" << std::endl;
    }
};
class Car : public Vehicle {
public:
    void honk() {
        std::cout << "ビービー" << std::endl;
    }
};
class Bike : public Vehicle {
public:
    void ringBell() {
        std::cout << "リンリン" << std::endl;
    }
};

この例では、CarBikeVehiclestartメソッドを使用できます。

アクセス指定子(public, protected, private)

C++では、クラスのメンバに対するアクセス制御を行うために、アクセス指定子を使用します。

主なアクセス指定子は以下の通りです。

スクロールできます
アクセス指定子説明
publicどこからでもアクセス可能。派生クラスでもpublicとして扱われる。
protected同じクラスおよび派生クラスからアクセス可能。外部からはアクセスできない。
private同じクラス内からのみアクセス可能。派生クラスからはアクセスできない。

これらのアクセス指定子を適切に使用することで、クラスの設計をより安全にし、意図しないアクセスを防ぐことができます。

例えば、以下のようにアクセス指定子を使ったクラスを定義できます。

class Example {
public:
    int publicVar;          // どこからでもアクセス可能
protected:
    int protectedVar;       // 派生クラスからアクセス可能
private:
    int privateVar;         // 同じクラス内からのみアクセス可能
};

このように、アクセス指定子を使うことで、クラスのメンバに対するアクセスを制御し、カプセル化を実現します。

継承の詳細

コンストラクタとデストラクタの呼び出し順序

C++において、クラスのインスタンスが生成される際、コンストラクタは基底クラスから派生クラスの順に呼び出されます。

逆に、インスタンスが破棄される際は、派生クラスから基底クラスの順にデストラクタが呼び出されます。

以下の例で確認してみましょう。

class Base {
public:
    Base() {
        std::cout << "Baseのコンストラクタ" << std::endl;
    }
    ~Base() {
        std::cout << "Baseのデストラクタ" << std::endl;
    }
};
class Derived : public Base {
public:
    Derived() {
        std::cout << "Derivedのコンストラクタ" << std::endl;
    }
    ~Derived() {
        std::cout << "Derivedのデストラクタ" << std::endl;
    }
};
int main() {
    Derived obj;
    return 0;
}

このプログラムを実行すると、以下のような出力が得られます。

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

メンバ関数のオーバーライド

派生クラスでは、基底クラスのメンバ関数をオーバーライドすることができます。

オーバーライドとは、基底クラスで定義されたメソッドを派生クラスで再定義することを指します。

以下の例を見てみましょう。

class Animal {
public:
    virtual void speak() {
        std::cout << "動物の声" << std::endl;
    }
};
class Dog : public Animal {
public:
    void speak() override { // オーバーライド
        std::cout << "ワンワン" << std::endl;
    }
};

この場合、DogクラスspeakメソッドAnimalクラスspeakメソッドをオーバーライドしています。

Dogのインスタンスでspeakを呼び出すと、ワンワンと出力されます。

仮想関数と純粋仮想関数

仮想関数は、基底クラスで定義され、派生クラスでオーバーライドされることを意図した関数です。

仮想関数を使用することで、ポリモーフィズムを実現できます。

純粋仮想関数は、基底クラスで実装を持たず、派生クラスで必ず実装しなければならない関数です。

以下の例を見てみましょう。

class Shape {
public:
    virtual void draw() = 0; // 純粋仮想関数
};
class Circle : public Shape {
public:
    void draw() override {
        std::cout << "円を描画" << std::endl;
    }
};

この例では、Shapeクラスに純粋仮想関数drawが定義されています。

Circleクラスはこの関数をオーバーライドし、具体的な実装を提供しています。

Shapeクラスは抽象クラスとなり、直接インスタンス化することはできません。

多重継承

C++では、1つのクラスが複数の基底クラスを持つことができる多重継承が可能です。

これにより、異なるクラスからの機能を組み合わせることができます。

ただし、多重継承には注意が必要で、ダイヤモンド継承問題などの複雑さを引き起こす可能性があります。

以下は多重継承の例です。

class A {
public:
    void funcA() {
        std::cout << "Aのメソッド" << std::endl;
    }
};
class B {
public:
    void funcB() {
        std::cout << "Bのメソッド" << std::endl;
    }
};
class C : public A, public B {
public:
    void funcC() {
        std::cout << "Cのメソッド" << std::endl;
    }
};

この例では、CクラスABの両方を継承しています。

Cのインスタンスは、funcAfuncBの両方を使用することができます。

多重継承を使用する際は、基底クラスのメンバにアクセスする際に、どの基底クラスのメンバを参照するかを明示する必要があります。

継承の応用例

ポリモーフィズムの実現

ポリモーフィズムは、同じインターフェースを持つ異なるクラスのオブジェクトを同一の方法で扱うことができる特性です。

C++では、仮想関数を使用することでポリモーフィズムを実現します。

以下の例では、Animalクラスのポインタを使って、異なる動物のspeakメソッドを呼び出しています。

class Animal {
public:
    virtual void speak() {
        std::cout << "動物の声" << std::endl;
    }
};
class Dog : public Animal {
public:
    void speak() override {
        std::cout << "ワンワン" << std::endl;
    }
};
class Cat : public Animal {
public:
    void speak() override {
        std::cout << "ニャー" << std::endl;
    }
};
void makeAnimalSpeak(Animal* animal) {
    animal->speak(); // ポリモーフィズムを利用
}
int main() {
    Dog dog;
    Cat cat;
    makeAnimalSpeak(&dog); // ワンワン
    makeAnimalSpeak(&cat); // ニャー
    return 0;
}

このプログラムでは、makeAnimalSpeak関数Animal型のポインタを受け取り、実際のオブジェクトに応じたspeakメソッドが呼び出されます。

抽象クラスの利用

抽象クラスは、少なくとも1つの純粋仮想関数を持つクラスであり、直接インスタンス化することはできません。

抽象クラスを利用することで、共通のインターフェースを定義し、派生クラスに具体的な実装を強制することができます。

以下の例では、Shapeクラスが抽象クラスとして定義されています。

class Shape {
public:
    virtual void draw() = 0; // 純粋仮想関数
};
class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "長方形を描画" << std::endl;
    }
};
class Triangle : public Shape {
public:
    void draw() override {
        std::cout << "三角形を描画" << std::endl;
    }
};

このように、Shapeクラスを基底クラスとして、RectangleTriangleなどの具体的な形状を表すクラスを作成することができます。

これにより、異なる形状を同じ方法で扱うことが可能になります。

インターフェースの実装

C++では、インターフェースを抽象クラスとして実装することができます。

インターフェースは、メソッドのシグネチャのみを定義し、実装は派生クラスに任せることが特徴です。

以下の例では、IAnimalインターフェースを定義し、DogCatがそれを実装しています。

class IAnimal {
public:
    virtual void speak() = 0; // 純粋仮想関数
    virtual ~IAnimal() {} // 仮想デストラクタ
};
class Dog : public IAnimal {
public:
    void speak() override {
        std::cout << "ワンワン" << std::endl;
    }
};
class Cat : public IAnimal {
public:
    void speak() override {
        std::cout << "ニャー" << std::endl;
    }
};

このように、IAnimalインターフェースを使用することで、異なる動物のクラスが同じメソッドspeakを持つことを保証できます。

これにより、クライアントコードは具体的なクラスに依存せず、インターフェースを通じて動物を扱うことができます。

継承における注意点

継承のデメリット

継承は強力な機能ですが、いくつかのデメリットも存在します。

主なデメリットは以下の通りです。

スクロールできます
デメリット説明
高い結合度基底クラスと派生クラスの間に強い依存関係が生まれ、変更が難しくなる。
再利用性の低下基底クラスの変更が派生クラスに影響を与えるため、再利用が難しくなることがある。
複雑性の増加多重継承や複雑な継承関係があると、コードの理解が難しくなる。

これらのデメリットを考慮し、継承を使用する際は慎重に設計する必要があります。

ダイヤモンド継承問題

ダイヤモンド継承問題は、C++における多重継承の特有の問題です。

これは、同じ基底クラスを持つ複数の派生クラスがあり、それらの派生クラスからさらに派生したクラスが存在する場合に発生します。

以下の図を参照してください。

  A
 / \
B   C
 \ /
  D

この場合、クラスDはクラスAを2回継承することになります。

これにより、DのインスタンスがAのメンバを2つ持つことになり、どちらのAのメンバを参照するかが不明確になります。

この問題を解決するために、C++では仮想継承を使用します。

以下の例を見てみましょう。

class A {
public:
    void display() {
        std::cout << "Aのメソッド" << std::endl;
    }
};
class B : virtual public A {
};
class C : virtual public A {
};
class D : public B, public C {
};

このように、BCAを仮想継承することで、DAのインスタンスを1つだけ持つことになります。

これにより、ダイヤモンド継承問題を回避できます。

継承とコンポジションの使い分け

継承とコンポジションは、オブジェクト指向プログラミングにおける2つの主要な設計手法です。

継承は is-a 関係を表現するのに対し、コンポジションは has-a 関係を表現します。

以下のポイントを考慮して使い分けることが重要です。

スクロールできます
手法説明使用例
継承基底クラスの特性を引き継ぐ。DogAnimalの一種である。
コンポジション他のクラスをメンバとして持つ。CarEngineを持つ。

一般的に、継承はクラス間の強い関係がある場合に使用し、コンポジションは柔軟性が求められる場合に使用します。

コンポジションを使用することで、クラスの再利用性が向上し、変更に対する耐性が強化されます。

例えば、以下のようにコンポジションを使用したクラスを定義できます。

class Engine {
public:
    void start() {
        std::cout << "エンジンがスタートしました。" << std::endl;
    }
};
class Car {
private:
    Engine engine; // コンポジション
public:
    void start() {
        engine.start(); // Engineのメソッドを呼び出す
        std::cout << "車がスタートしました。" << std::endl;
    }
};

このように、コンポジションを使用することで、CarクラスEngineクラスの機能を持ちながら、より柔軟な設計が可能になります。

よくある質問

継承とポリモーフィズムの違いは何ですか?

継承は、既存のクラスから新しいクラスを作成する手法であり、クラス間の関係を定義します。

一方、ポリモーフィズムは、同じインターフェースを持つ異なるクラスのオブジェクトを同一の方法で扱うことができる特性です。

ポリモーフィズムは、主に仮想関数を通じて継承の結果として実現されます。

つまり、継承は構造を提供し、ポリモーフィズムはその構造を利用して柔軟性を持たせるものです。

多重継承は避けるべきですか?

多重継承は強力な機能ですが、ダイヤモンド継承問題などの複雑さを引き起こす可能性があるため、注意が必要です。

多重継承を使用する場合は、仮想継承を利用して問題を回避することができますが、設計が複雑になることを考慮し、可能であればコンポジションを使用することを検討するのが良いでしょう。

一般的には、シンプルな継承関係を維持することが推奨されます。

継承を使うべきタイミングは?

継承を使用するべきタイミングは、クラス間に is-a 関係が明確に存在する場合です。

例えば、DogAnimalの一種であるため、継承を使用するのが適切です。

また、共通の機能を持つクラス群を作成する際にも、継承を利用することでコードの再利用性を高めることができます。

ただし、継承を使う際は、クラスの設計が適切であることを確認し、過度な依存関係を避けるようにしましょう。

まとめ

この記事では、C++におけるクラスの継承について詳しく解説しました。

継承の基本概念から、ポリモーフィズム、抽象クラス、インターフェースの実装、さらには注意点まで幅広くカバーしました。

継承を適切に利用することで、コードの再利用性や柔軟性を高めることができますので、ぜひ実際のプログラミングに活かしてみてください。

当サイトはリンクフリーです。出典元を明記していただければ、ご自由に引用していただいて構いません。

関連カテゴリーから探す

  • URLをコピーしました!
目次から探す