C++でのクラスの継承階層と設計のベストプラクティス

C++のクラス継承は、ソフトウェア開発においてコードの再利用性を高め、システムの設計を簡潔かつ明確にするための重要な手法です。本記事では、C++におけるクラス継承の基本概念から応用例、さらには設計のベストプラクティスまでを具体的な例を用いて詳しく解説します。これにより、継承を用いた効率的なプログラムの設計とその最適化方法を学び、実際のプロジェクトに応用できるスキルを習得しましょう。

目次

クラス継承の基本概念

C++におけるクラス継承は、既存のクラス(親クラスまたは基底クラス)の機能を拡張し、新たなクラス(子クラスまたは派生クラス)を作成するためのメカニズムです。これにより、コードの再利用が促進され、開発効率が向上します。

クラス継承のメリット

クラス継承の主なメリットは以下の通りです:

コードの再利用性向上

既存のクラスの機能を再利用できるため、新しいクラスをゼロから作成する手間を省けます。

メンテナンス性の向上

共通の機能を親クラスに集約することで、メンテナンスが容易になり、一箇所の修正で済む場合が多くなります。

拡張性の確保

既存のクラスを基にして新たな機能を追加できるため、システムの拡張が容易です。

基本的な継承の例

以下に基本的な継承の例を示します。

// 親クラス
class Animal {
public:
    void eat() {
        std::cout << "This animal is eating." << std::endl;
    }
};

// 子クラス
class Dog : public Animal {
public:
    void bark() {
        std::cout << "The dog is barking." << std::endl;
    }
};

int main() {
    Dog myDog;
    myDog.eat();  // 親クラスのメソッドを使用
    myDog.bark(); // 子クラスのメソッドを使用
    return 0;
}

この例では、Animalクラスが親クラスであり、その機能を継承したDogクラスが子クラスです。DogクラスはAnimalクラスのメソッドeatを利用でき、さらに独自のメソッドbarkを持っています。

次の項目では、シンプルな継承階層の設計方法について具体的な例を交えて説明します。

シンプルな継承階層の設計

シンプルな継承階層の設計は、クラスの関係性を明確にし、コードの可読性と保守性を高めるために重要です。ここでは、単一継承を用いたシンプルな設計方法を紹介します。

シンプルな継承階層の基本原則

単一責任の原則

各クラスは一つの責任を持つように設計します。これにより、クラスの役割が明確になり、変更の影響範囲が限定されます。

親クラスと子クラスの関係

親クラスは共通の基本機能を提供し、子クラスはその機能を拡張します。この関係性を明確にすることで、継承階層が理解しやすくなります。

シンプルな継承の設計例

次に、シンプルな継承階層を用いた具体的な例を示します。

// 親クラス
class Vehicle {
public:
    void startEngine() {
        std::cout << "Engine started." << std::endl;
    }
};

// 子クラス
class Car : public Vehicle {
public:
    void honkHorn() {
        std::cout << "Horn honked." << std::endl;
    }
};

// さらに別の子クラス
class Bicycle : public Vehicle {
public:
    void ringBell() {
        std::cout << "Bell rang." << std::endl;
    }
};

int main() {
    Car myCar;
    myCar.startEngine();  // 親クラスのメソッドを使用
    myCar.honkHorn();     // Carクラスのメソッドを使用

    Bicycle myBike;
    myBike.startEngine(); // 親クラスのメソッドを使用
    myBike.ringBell();    // Bicycleクラスのメソッドを使用

    return 0;
}

この例では、Vehicleクラスが基本的な乗り物の機能を提供する親クラスであり、それを継承するCarクラスとBicycleクラスがそれぞれ車と自転車の機能を追加しています。

継承階層の設計における注意点

深すぎる継承階層の回避

継承階層が深すぎると、理解が難しくなり、デバッグやメンテナンスが困難になります。可能な限り浅い階層を保つように設計しましょう。

適切な抽象化レベルの設定

抽象化のレベルを適切に設定し、共通機能は親クラスに集約し、特有の機能は子クラスに実装するようにします。

次の項目では、多重継承の利点と注意点について解説します。

多重継承の利点と注意点

多重継承は、C++で複数のクラスから機能を継承できる強力な機能ですが、その反面、注意すべき点も多く存在します。ここでは、多重継承の利点とそれに伴う注意点について解説します。

多重継承の利点

多機能クラスの作成

多重継承を利用することで、複数の基底クラスから機能を継承し、豊富な機能を持つクラスを作成できます。これにより、再利用性が向上し、コードの冗長性が減少します。

class Printable {
public:
    void print() {
        std::cout << "Printable content." << std::endl;
    }
};

class Saveable {
public:
    void save() {
        std::cout << "Content saved." << std::endl;
    }
};

class Document : public Printable, public Saveable {
    // DocumentはPrintableとSaveableの両方の機能を持つ
};

この例では、DocumentクラスがPrintableSaveableの両方の機能を継承して、多機能なクラスとなっています。

多重継承の注意点

ダイヤモンド問題

多重継承の際、同じ基底クラスからの継承経路が複数存在すると、ダイヤモンド継承と呼ばれる問題が発生します。これにより、基底クラスのメンバが二重に継承されるため、予期しない動作を引き起こすことがあります。

class A {
public:
    void func() {
        std::cout << "Function from A." << std::endl;
    }
};

class B : public A {
};

class C : public A {
};

class D : public B, public C {
    // DはAを二重に継承するため、ダイヤモンド問題が発生する
};

この例では、DクラスがAクラスを二重に継承してしまい、A::funcの呼び出しが曖昧になります。

解決策:仮想継承

仮想継承を利用することで、ダイヤモンド問題を解決できます。仮想継承を指定すると、基底クラスのインスタンスが共有され、二重継承の問題が解消されます。

class A {
public:
    void func() {
        std::cout << "Function from A." << std::endl;
    }
};

class B : public virtual A {
};

class C : public virtual A {
};

class D : public B, public C {
    // 仮想継承により、Aは一度だけ継承される
};

この例では、BCAを仮想継承することで、DクラスはAを一度だけ継承します。

多重継承の適用例

多重継承を使う際は、設計を慎重に行い、各クラスの責任を明確にすることが重要です。適切に設計された多重継承は、強力で柔軟なシステムを実現できます。

次の項目では、仮想継承の役割と具体的な使用例について解説します。

仮想継承の役割と使用例

仮想継承は、C++の多重継承における問題を解決するための重要な手法です。特に、ダイヤモンド継承問題を防ぐために使用されます。ここでは、仮想継承の役割とその具体的な使用例を解説します。

仮想継承の役割

ダイヤモンド継承問題の解決

仮想継承は、基底クラスのインスタンスが共有されるため、同じ基底クラスから派生したクラスが複数存在する場合でも、基底クラスのメンバが二重に継承されることを防ぎます。

メモリ効率の向上

仮想継承を使用すると、基底クラスのインスタンスは一つだけ作成されるため、メモリの無駄遣いを防ぐことができます。

仮想継承の基本例

仮想継承を使用したクラス継承の具体例を示します。

#include <iostream>

class Person {
public:
    void speak() {
        std::cout << "Person speaks." << std::endl;
    }
};

class Employee : public virtual Person {
};

class Student : public virtual Person {
};

class WorkingStudent : public Employee, public Student {
};

int main() {
    WorkingStudent ws;
    ws.speak(); // Personクラスのメソッドが一度だけ継承される
    return 0;
}

この例では、EmployeeStudentが共にPersonを仮想継承しており、WorkingStudentクラスはPersonクラスを一度だけ継承します。

仮想継承の使用方法

クラスの宣言における仮想継承

仮想継承は、クラス宣言の際にvirtualキーワードを使って指定します。仮想継承を使用することで、基底クラスの共有が確保されます。

class Base {
public:
    void baseFunction() {
        std::cout << "Function in Base class." << std::endl;
    }
};

class Derived1 : public virtual Base {
};

class Derived2 : public virtual Base {
};

class FinalDerived : public Derived1, public Derived2 {
public:
    void useBaseFunction() {
        baseFunction(); // Baseクラスのメソッドが一度だけ継承される
    }
};

この例では、Derived1Derived2Baseを仮想継承し、FinalDerivedクラスがBaseの機能を一度だけ継承します。

仮想継承の注意点

コンストラクタの呼び出し

仮想継承を使用する場合、基底クラスのコンストラクタは最も派生したクラスで一度だけ呼び出されます。そのため、コンストラクタの呼び出し順序に注意が必要です。

class Base {
public:
    Base() {
        std::cout << "Base constructor" << std::endl;
    }
};

class Derived1 : public virtual Base {
public:
    Derived1() {
        std::cout << "Derived1 constructor" << std::endl;
    }
};

class Derived2 : public virtual Base {
public:
    Derived2() {
        std::cout << "Derived2 constructor" << std::endl;
    }
};

class FinalDerived : public Derived1, public Derived2 {
public:
    FinalDerived() {
        std::cout << "FinalDerived constructor" << std::endl;
    }
};

この例では、BaseのコンストラクタはFinalDerivedクラスのインスタンス化時に一度だけ呼び出されます。

次の項目では、継承階層のデザインパターンについて紹介します。

継承階層のデザインパターン

C++における継承階層の設計には、特定のデザインパターンを用いることで、コードの再利用性や保守性を高めることができます。ここでは、代表的な継承階層のデザインパターンを紹介します。

テンプレートメソッドパターン

テンプレートメソッドパターンは、親クラスで処理の枠組みを定義し、その詳細な実装を子クラスに任せるパターンです。これにより、共通の処理手順を親クラスに集約し、詳細な処理は子クラスで個別に実装することができます。

class AbstractClass {
public:
    void templateMethod() {
        baseOperation1();
        requiredOperation1();
        baseOperation2();
        requiredOperation2();
        baseOperation3();
    }

protected:
    void baseOperation1() {
        std::cout << "Base Operation 1" << std::endl;
    }

    void baseOperation2() {
        std::cout << "Base Operation 2" << std::endl;
    }

    void baseOperation3() {
        std::cout << "Base Operation 3" << std::endl;
    }

    virtual void requiredOperation1() = 0;
    virtual void requiredOperation2() = 0;
};

class ConcreteClass1 : public AbstractClass {
protected:
    void requiredOperation1() override {
        std::cout << "ConcreteClass1 Operation 1" << std::endl;
    }

    void requiredOperation2() override {
        std::cout << "ConcreteClass1 Operation 2" << std::endl;
    }
};

class ConcreteClass2 : public AbstractClass {
protected:
    void requiredOperation1() override {
        std::cout << "ConcreteClass2 Operation 1" << std::endl;
    }

    void requiredOperation2() override {
        std::cout << "ConcreteClass2 Operation 2" << std::endl;
    }
};

この例では、AbstractClassが共通の処理手順を定義し、ConcreteClass1ConcreteClass2がその詳細を実装しています。

ファクトリーメソッドパターン

ファクトリーメソッドパターンは、オブジェクトの生成をサブクラスに任せることで、生成するオブジェクトの種類を動的に変更できるようにするパターンです。これにより、柔軟で拡張性の高いコードを実現できます。

class Product {
public:
    virtual void use() = 0;
};

class ConcreteProductA : public Product {
public:
    void use() override {
        std::cout << "Using Product A" << std::endl;
    }
};

class ConcreteProductB : public Product {
public:
    void use() override {
        std::cout << "Using Product B" << std::endl;
    }
};

class Creator {
public:
    virtual Product* factoryMethod() = 0;

    void anOperation() {
        Product* product = factoryMethod();
        product->use();
        delete product;
    }
};

class ConcreteCreatorA : public Creator {
public:
    Product* factoryMethod() override {
        return new ConcreteProductA();
    }
};

class ConcreteCreatorB : public Creator {
public:
    Product* factoryMethod() override {
        return new ConcreteProductB();
    }
};

この例では、Creatorクラスがオブジェクト生成の枠組みを提供し、ConcreteCreatorAConcreteCreatorBが具体的な生成方法を提供しています。

デコレータパターン

デコレータパターンは、オブジェクトに動的に新しい機能を追加するパターンです。基本クラスに新しい機能を追加するのではなく、ラッピングすることで機能を拡張します。

class Component {
public:
    virtual void operation() = 0;
};

class ConcreteComponent : public Component {
public:
    void operation() override {
        std::cout << "Concrete Component Operation" << std::endl;
    }
};

class Decorator : public Component {
protected:
    Component* component;
public:
    Decorator(Component* c) : component(c) {}

    void operation() override {
        component->operation();
    }
};

class ConcreteDecoratorA : public Decorator {
public:
    ConcreteDecoratorA(Component* c) : Decorator(c) {}

    void operation() override {
        Decorator::operation();
        std::cout << "Concrete Decorator A Operation" << std::endl;
    }
};

class ConcreteDecoratorB : public Decorator {
public:
    ConcreteDecoratorB(Component* c) : Decorator(c) {}

    void operation() override {
        Decorator::operation();
        std::cout << "Concrete Decorator B Operation" << std::endl;
    }
};

この例では、DecoratorクラスがComponentクラスをラッピングし、ConcreteDecoratorAConcreteDecoratorBが追加の機能を提供しています。

次の項目では、継承とコンポジションの使い分けについて説明します。

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

C++におけるオブジェクト指向設計では、継承とコンポジションの使い分けが重要です。これらの手法は、コードの再利用性と保守性を向上させるために用いられますが、それぞれの適用シナリオを理解することが大切です。

継承の使い所

「is-a」関係の確立

継承は、クラス間に「is-a」関係(○○は△△の一種である)を確立する場合に使用します。例えば、CarVehicleの一種であるため、CarVehicleを継承します。

class Vehicle {
public:
    void startEngine() {
        std::cout << "Engine started." << std::endl;
    }
};

class Car : public Vehicle {
public:
    void honkHorn() {
        std::cout << "Horn honked." << std::endl;
    }
};

この例では、CarクラスがVehicleクラスを継承し、Vehicleの機能を持ちながらも独自の機能を追加しています。

基本機能の再利用

共通の基本機能を親クラスに集約し、その機能を複数の子クラスで再利用する場合に継承を使用します。

コンポジションの使い所

「has-a」関係の確立

コンポジションは、クラス間に「has-a」関係(○○は△△を持っている)を確立する場合に使用します。例えば、CarEngineを持っているため、CarEngineをコンポジションとして持ちます。

class Engine {
public:
    void start() {
        std::cout << "Engine started." << std::endl;
    }
};

class Car {
private:
    Engine engine;
public:
    void startEngine() {
        engine.start();
    }
};

この例では、CarクラスがEngineオブジェクトをメンバとして持ち、Engineの機能を利用しています。

柔軟な設計

コンポジションは、クラスの柔軟性を高めるために使用します。異なるオブジェクトを組み合わせて新しい機能を実現することができます。

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

継承を使う場合

動物クラスのような抽象的な概念を基にして、具体的な動物クラスを作成する場合に継承を使用します。

class Animal {
public:
    void eat() {
        std::cout << "Animal is eating." << std::endl;
    }
};

class Dog : public Animal {
public:
    void bark() {
        std::cout << "Dog is barking." << std::endl;
    }
};

コンポジションを使う場合

特定の機能を持つクラスを組み合わせて新しいクラスを作成する場合にコンポジションを使用します。

class Engine {
public:
    void start() {
        std::cout << "Engine started." << std::endl;
    }
};

class Wheels {
public:
    void roll() {
        std::cout << "Wheels are rolling." << std::endl;
    }
};

class Car {
private:
    Engine engine;
    Wheels wheels;
public:
    void drive() {
        engine.start();
        wheels.roll();
    }
};

この例では、CarクラスがEngineWheelsをコンポジションとして持ち、それぞれの機能を利用しています。

次の項目では、具体的なコーディング例を通じて継承階層の設計を実践します。

実際のコーディング例

ここでは、実際のコーディング例を通じて、継承階層の設計を実践します。具体的なシナリオを設定し、そのシナリオに基づいてクラスの継承を設計します。

シナリオ:図形の描画

図形を描画するアプリケーションを設計する際に、基本的な図形クラスを作成し、それを継承して具体的な図形クラスを作成します。

基本クラス:Shape

まず、すべての図形の基本クラスとなるShapeクラスを定義します。このクラスには、描画のための基本的なメソッドを含みます。

class Shape {
public:
    virtual void draw() = 0; // 純粋仮想関数
    virtual ~Shape() = default; // 仮想デストラクタ
};

具体的な図形クラス:CircleとRectangle

次に、Shapeクラスを継承して具体的な図形クラスを作成します。

#include <iostream>
#include <vector>
#include <memory>

// Shapeクラスの定義
class Shape {
public:
    virtual void draw() = 0; // 純粋仮想関数
    virtual ~Shape() = default; // 仮想デストラクタ
};

// Circleクラスの定義
class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a Circle." << std::endl;
    }
};

// Rectangleクラスの定義
class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a Rectangle." << std::endl;
    }
};

// 描画関数
void drawShapes(const std::vector<std::shared_ptr<Shape>>& shapes) {
    for (const auto& shape : shapes) {
        shape->draw();
    }
}

int main() {
    // Shapeのポインタを保持するベクタ
    std::vector<std::shared_ptr<Shape>> shapes;
    shapes.push_back(std::make_shared<Circle>());
    shapes.push_back(std::make_shared<Rectangle>());

    // すべての図形を描画
    drawShapes(shapes);

    return 0;
}

この例では、Shapeクラスを基底クラスとして、CircleクラスとRectangleクラスを派生させています。drawメソッドをオーバーライドして、各図形に特有の描画処理を実装しています。

複雑な継承階層:3D図形の追加

次に、さらに複雑な継承階層を設計します。2D図形を基本とし、その上に3D図形を追加します。

基本クラスの拡張:Shape3D

2D図形の基本クラスShapeを拡張して、3D図形の基本クラスShape3Dを作成します。

class Shape3D : public Shape {
public:
    virtual void draw3D() = 0; // 純粋仮想関数
};

具体的な3D図形クラス:SphereとCube

Shape3Dクラスを継承して具体的な3D図形クラスを作成します。

#include <iostream>
#include <vector>
#include <memory>

// Shape3Dクラスの定義
class Shape3D : public Shape {
public:
    virtual void draw3D() = 0; // 純粋仮想関数
};

// Sphereクラスの定義
class Sphere : public Shape3D {
public:
    void draw() override {
        std::cout << "Drawing a 2D representation of a Sphere." << std::endl;
    }

    void draw3D() override {
        std::cout << "Drawing a 3D Sphere." << std::endl;
    }
};

// Cubeクラスの定義
class Cube : public Shape3D {
public:
    void draw() override {
        std::cout << "Drawing a 2D representation of a Cube." << std::endl;
    }

    void draw3D() override {
        std::cout << "Drawing a 3D Cube." << std::endl;
    }
};

// 3D描画関数
void draw3DShapes(const std::vector<std::shared_ptr<Shape3D>>& shapes3D) {
    for (const auto& shape : shapes3D) {
        shape->draw3D();
    }
}

int main() {
    // Shape3Dのポインタを保持するベクタ
    std::vector<std::shared_ptr<Shape3D>> shapes3D;
    shapes3D.push_back(std::make_shared<Sphere>());
    shapes3D.push_back(std::make_shared<Cube>());

    // すべての3D図形を描画
    draw3DShapes(shapes3D);

    return 0;
}

この例では、Shape3Dクラスを基底クラスとして、SphereクラスとCubeクラスを派生させています。draw3Dメソッドをオーバーライドして、各3D図形に特有の描画処理を実装しています。

次の項目では、継承を使った設計でよくある問題とその解決策を紹介します。

よくある問題とその解決策

継承を使った設計では、さまざまな問題が発生することがあります。ここでは、よくある問題とその解決策について説明します。

よくある問題

ダイヤモンド継承問題

多重継承を使用する場合、同じ基底クラスを複数の経路で継承することで、ダイヤモンド継承問題が発生することがあります。これにより、基底クラスのメンバが二重に継承され、意図しない動作が発生する可能性があります。

class A {
public:
    void show() {
        std::cout << "Class A" << std::endl;
    }
};

class B : public A {
};

class C : public A {
};

class D : public B, public C {
};

この例では、DクラスがAクラスを二重に継承しているため、A::showメソッドの呼び出しが曖昧になります。

解決策:仮想継承

仮想継承を使用することで、ダイヤモンド継承問題を解決できます。仮想継承を指定すると、基底クラスのインスタンスが共有され、二重継承の問題が解消されます。

class A {
public:
    void show() {
        std::cout << "Class A" << std::endl;
    }
};

class B : public virtual A {
};

class C : public virtual A {
};

class D : public B, public C {
};

この例では、BCAを仮想継承することで、DクラスはAを一度だけ継承します。

基底クラスの変更による影響

基底クラスの設計変更が、すべての派生クラスに影響を与えることがあります。これにより、メンテナンスが難しくなることがあります。

class Base {
public:
    virtual void display() {
        std::cout << "Base Display" << std::endl;
    }
};

class Derived : public Base {
public:
    void display() override {
        std::cout << "Derived Display" << std::endl;
    }
};

この例では、Baseクラスのdisplayメソッドを変更すると、Derivedクラスにも影響が及びます。

解決策:インターフェースの使用

基底クラスにインターフェースを使用することで、実装の詳細を隠蔽し、変更の影響を最小限に抑えることができます。

class IDisplayable {
public:
    virtual void display() = 0; // 純粋仮想関数
};

class Base : public IDisplayable {
public:
    void display() override {
        std::cout << "Base Display" << std::endl;
    }
};

class Derived : public IDisplayable {
public:
    void display() override {
        std::cout << "Derived Display" << std::endl;
    }
};

この例では、IDisplayableインターフェースを使用して、displayメソッドを定義しています。これにより、実装の詳細が隠蔽され、変更の影響が抑えられます。

よくある問題のまとめ

オーバーライドミスの防止

メソッドのオーバーライド時に、基底クラスのメソッドと異なるシグネチャを持つメソッドを定義してしまうことがあります。

class Base {
public:
    virtual void show() {
        std::cout << "Base Show" << std::endl;
    }
};

class Derived : public Base {
public:
    void show(int value) { // オーバーライドではなく、オーバーロード
        std::cout << "Derived Show: " << value << std::endl;
    }
};

この例では、Derivedクラスのshowメソッドはオーバーライドではなく、オーバーロードされています。

解決策:`override`キーワードの使用

overrideキーワードを使用することで、オーバーライドミスを防ぐことができます。

class Base {
public:
    virtual void show() {
        std::cout << "Base Show" << std::endl;
    }
};

class Derived : public Base {
public:
    void show() override {
        std::cout << "Derived Show" << std::endl;
    }
};

この例では、Derivedクラスのshowメソッドにoverrideキーワードを付けることで、オーバーライドされていることを明示しています。

次の項目では、理解を深めるための応用例と演習問題を提供します。

応用例と演習問題

継承階層の設計をより深く理解するために、いくつかの応用例と演習問題を紹介します。これらを通じて、継承の概念を実践的に学びましょう。

応用例

応用例1: 家電製品の継承

家電製品の基本クラスと、具体的な家電製品クラスを設計します。

#include <iostream>

class Appliance {
public:
    virtual void turnOn() = 0;
    virtual void turnOff() = 0;
    virtual ~Appliance() = default;
};

class WashingMachine : public Appliance {
public:
    void turnOn() override {
        std::cout << "Washing Machine is now ON." << std::endl;
    }

    void turnOff() override {
        std::cout << "Washing Machine is now OFF." << std::endl;
    }
};

class Refrigerator : public Appliance {
public:
    void turnOn() override {
        std::cout << "Refrigerator is now ON." << std::endl;
    }

    void turnOff() override {
        std::cout << "Refrigerator is now OFF." << std::endl;
    }
};

int main() {
    WashingMachine wm;
    Refrigerator fridge;

    wm.turnOn();
    wm.turnOff();

    fridge.turnOn();
    fridge.turnOff();

    return 0;
}

この例では、Applianceクラスが家電製品の基本機能を定義し、WashingMachineRefrigeratorクラスが具体的な家電製品の機能を実装しています。

応用例2: 動物の継承と多態性

動物クラスを基にして、特定の動物クラスを設計し、多態性を利用して動物の動作を実装します。

#include <iostream>
#include <vector>
#include <memory>

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

class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Woof!" << std::endl;
    }
};

class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Meow!" << std::endl;
    }
};

void animalSounds(const std::vector<std::shared_ptr<Animal>>& animals) {
    for (const auto& animal : animals) {
        animal->makeSound();
    }
}

int main() {
    std::vector<std::shared_ptr<Animal>> animals;
    animals.push_back(std::make_shared<Dog>());
    animals.push_back(std::make_shared<Cat>());

    animalSounds(animals);

    return 0;
}

この例では、Animalクラスを基底クラスとして、DogクラスとCatクラスがそれぞれの動物の鳴き声を実装しています。animalSounds関数を通じて、多態性を利用して動物の鳴き声を出力しています。

演習問題

演習問題1: 図形の継承

以下の要件を満たす図形クラスを設計しなさい。

  1. 基本クラスShapeには、面積を計算する純粋仮想関数calculateAreaを持つ。
  2. Shapeクラスを継承したCircleクラスとRectangleクラスを作成し、それぞれ面積を計算するメソッドを実装する。
  3. Circleクラスでは、半径を持ち、面積をπ * r^2で計算する。
  4. Rectangleクラスでは、幅と高さを持ち、面積を幅 * 高さで計算する。

演習問題2: 交通手段の継承

以下の要件を満たす交通手段クラスを設計しなさい。

  1. 基本クラスTransportには、移動するメソッドmoveを持つ。
  2. Transportクラスを継承したCarクラスとBicycleクラスを作成し、それぞれ異なる移動方法を実装する。
  3. Carクラスでは、moveメソッドが「車が走っています」と表示する。
  4. Bicycleクラスでは、moveメソッドが「自転車が走っています」と表示する。

これらの演習問題を通じて、継承の設計と実装を練習し、理解を深めましょう。

次の項目では、本記事のまとめを行います。

まとめ

本記事では、C++におけるクラス継承の基本概念から応用例、デザインパターン、そしてよくある問題とその解決策について詳しく解説しました。継承は、コードの再利用性と保守性を高めるための強力な手法ですが、その設計には注意が必要です。仮想継承やインターフェースの使用、コンポジションとの使い分けを理解し、適切な場面でこれらの技法を使うことで、柔軟で拡張性の高いシステムを構築できます。この記事を通じて、継承に関する知識を深め、実践的なスキルを磨いてください。

コメント

コメントする

目次