C++の抽象クラスと純粋仮想関数を徹底解説:基本から応用まで

C++の抽象クラスと純粋仮想関数は、オブジェクト指向プログラミングにおいて重要な役割を果たします。本記事では、これらの概念を基本から応用まで詳しく解説し、実際の使用例や設計パターンを通じて理解を深めます。

目次

抽象クラスの基本概念

抽象クラスは、インスタンス化できないクラスであり、他のクラスに基本的な機能やインターフェースを提供します。抽象クラスを使うことで、共通の機能を子クラスに継承させることができます。以下に基本的な定義例を示します。

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

抽象クラスは少なくとも一つの純粋仮想関数を持つ必要があります。この純粋仮想関数は、子クラスで実装されなければなりません。

純粋仮想関数の基本概念

純粋仮想関数は、抽象クラスの中で定義される仮想関数であり、具体的な実装を持たないことが特徴です。純粋仮想関数は、派生クラスで必ずオーバーライドされることを期待されます。純粋仮想関数を持つクラスは抽象クラスとなり、直接インスタンス化することはできません。

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

上記のコードでは、pureVirtualFunctionが純粋仮想関数として定義されています。この関数を持つ抽象クラスを基に派生クラスを作成し、その派生クラスで関数の実装を行います。

class DerivedClass : public AbstractClass {
public:
    void pureVirtualFunction() override {
        // 関数の具体的な実装
    }
};

このようにして、純粋仮想関数は派生クラスで具体的な動作を定義させるための仕組みとして利用されます。

抽象クラスと純粋仮想関数の実装例

抽象クラスと純粋仮想関数の具体的な実装例を以下に示します。これにより、理論的な理解を実際のコードに落とし込む方法を学びます。

抽象クラスの定義

まず、抽象クラスを定義します。このクラスには純粋仮想関数が含まれています。

#include <iostream>

// 抽象クラスの定義
class Animal {
public:
    // 純粋仮想関数の宣言
    virtual void makeSound() = 0;

    void sleep() {
        std::cout << "Sleeping..." << std::endl;
    }
};

この例では、Animalクラスが抽象クラスであり、makeSoundが純粋仮想関数として宣言されています。

派生クラスの実装

次に、抽象クラスを継承する派生クラスを実装します。派生クラスでは、純粋仮想関数を具体的に実装する必要があります。

// 派生クラス Dog の定義
class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Woof!" << std::endl;
    }
};

// 派生クラス Cat の定義
class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Meow!" << std::endl;
    }
};

ここで、DogクラスとCatクラスはそれぞれAnimalクラスを継承し、makeSound関数を実装しています。

抽象クラスの利用

最後に、抽象クラスを利用してオブジェクトを操作します。

int main() {
    Dog dog;
    Cat cat;

    dog.makeSound(); // "Woof!"
    cat.makeSound(); // "Meow!"

    dog.sleep(); // "Sleeping..."
    cat.sleep(); // "Sleeping..."

    return 0;
}

この例では、DogCatのオブジェクトが生成され、それぞれのmakeSound関数が呼び出されます。抽象クラスのsleep関数も利用されています。

このようにして、抽象クラスと純粋仮想関数を使うことで、共通のインターフェースを持つ異なる具体的な実装を提供することができます。

抽象クラスとインターフェースの違い

抽象クラスとインターフェースは、どちらも多態性を実現するための手段ですが、それぞれ異なる役割と用途を持ちます。ここでは、その違いを詳しく解説します。

抽象クラスの特徴

抽象クラスは、共通の基本的な機能やデフォルトの動作を提供するクラスです。抽象クラスは、他のクラスに対して基本的なインターフェースを提供しつつ、一部の機能は具体的に実装することができます。

class AbstractClass {
public:
    virtual void pureVirtualFunction() = 0; // 純粋仮想関数
    void concreteFunction() {
        // 具体的な実装
    }
};

抽象クラスは以下のような特徴を持ちます:

  • 少なくとも一つの純粋仮想関数を持つ
  • 具体的なメンバ関数の実装を持つことができる
  • メンバ変数を持つことができる

インターフェースの特徴

インターフェースは、純粋にメソッドのシグネチャのみを定義し、実装を持たないクラスです。C++では、すべてのメソッドが純粋仮想関数であり、メンバ変数を持たない抽象クラスをインターフェースとして使用します。

class Interface {
public:
    virtual void method1() = 0;
    virtual void method2() = 0;
};

インターフェースの特徴は以下の通りです:

  • すべてのメソッドが純粋仮想関数
  • 具体的な実装を一切持たない
  • メンバ変数を持たない

使用シナリオの違い

  • 抽象クラス: ある程度の共通機能を提供しつつ、派生クラスでの具体的な動作を指定したい場合に使用します。例えば、異なる種類の動物クラスが共通の動作を持ちつつ、それぞれ特有の動作を実装する場合です。
  • インターフェース: 共通のインターフェースのみを提供し、実装を完全に派生クラスに任せたい場合に使用します。例えば、異なる種類のデータベース接続クラスが共通の操作方法を提供するが、接続の詳細はそれぞれ異なる場合です。

コード例

// インターフェース
class IDatabase {
public:
    virtual void connect() = 0;
    virtual void disconnect() = 0;
};

// 抽象クラス
class DatabaseBase {
public:
    virtual void connect() = 0;
    virtual void disconnect() = 0;
    void log(const std::string& message) {
        std::cout << "Log: " << message << std::endl;
    }
};

// 具体的な実装
class MySQLDatabase : public DatabaseBase {
public:
    void connect() override {
        std::cout << "Connecting to MySQL..." << std::endl;
    }
    void disconnect() override {
        std::cout << "Disconnecting from MySQL..." << std::endl;
    }
};

この例では、IDatabaseがインターフェースとして定義され、DatabaseBaseが抽象クラスとして定義されています。それぞれ異なる目的で使用されることが分かります。

抽象クラスの応用例

抽象クラスは、多くの実際のプロジェクトで利用されており、その応用範囲は広範です。ここでは、いくつかの具体的な応用例を紹介します。

応用例1: GUIフレームワーク

GUIフレームワークでは、抽象クラスを用いて基本的なウィジェットの動作を定義し、具体的なウィジェットクラスでその動作を実装します。

// 抽象クラス Widget
class Widget {
public:
    virtual void draw() = 0; // 純粋仮想関数
    virtual void resize(int width, int height) = 0; // 純粋仮想関数
};

// 具体的なウィジェットクラス Button
class Button : public Widget {
public:
    void draw() override {
        std::cout << "Drawing a button." << std::endl;
    }
    void resize(int width, int height) override {
        std::cout << "Resizing button to " << width << "x" << height << "." << std::endl;
    }
};

// 具体的なウィジェットクラス TextBox
class TextBox : public Widget {
public:
    void draw() override {
        std::cout << "Drawing a text box." << std::endl;
    }
    void resize(int width, int height) override {
        std::cout << "Resizing text box to " << width << "x" << height << "." << std::endl;
    }
};

この例では、Widgetが抽象クラスとして基本的なウィジェットのインターフェースを定義し、ButtonTextBoxがその具体的な実装を提供しています。

応用例2: ゲーム開発

ゲーム開発においても、抽象クラスは重要な役割を果たします。例えば、さまざまな種類のゲームキャラクターを定義するために使用されます。

// 抽象クラス Character
class Character {
public:
    virtual void attack() = 0; // 純粋仮想関数
    virtual void move(int x, int y) = 0; // 純粋仮想関数
};

// 具体的なキャラクタークラス Warrior
class Warrior : public Character {
public:
    void attack() override {
        std::cout << "Warrior attacks with a sword!" << std::endl;
    }
    void move(int x, int y) override {
        std::cout << "Warrior moves to (" << x << ", " << y << ")." << std::endl;
    }
};

// 具体的なキャラクタークラス Mage
class Mage : public Character {
public:
    void attack() override {
        std::cout << "Mage casts a spell!" << std::endl;
    }
    void move(int x, int y) override {
        std::cout << "Mage teleports to (" << x << ", " << y << ")." << std::endl;
    }
};

この例では、Characterが抽象クラスとしてキャラクターの基本的な動作を定義し、WarriorMageがその具体的な実装を提供しています。

応用例3: ファイルシステム

ファイルシステムの設計でも、抽象クラスはファイルやディレクトリの操作を定義するために使用されます。

// 抽象クラス FileSystemObject
class FileSystemObject {
public:
    virtual void open() = 0; // 純粋仮想関数
    virtual void close() = 0; // 純粋仮想関数
};

// 具体的なファイルクラス File
class File : public FileSystemObject {
public:
    void open() override {
        std::cout << "Opening file." << std::endl;
    }
    void close() override {
        std::cout << "Closing file." << std::endl;
    }
};

// 具体的なディレクトリクラス Directory
class Directory : public FileSystemObject {
public:
    void open() override {
        std::cout << "Opening directory." << std::endl;
    }
    void close() override {
        std::cout << "Closing directory." << std::endl;
    }
};

この例では、FileSystemObjectが抽象クラスとしてファイルシステムオブジェクトの基本的な操作を定義し、FileDirectoryがその具体的な実装を提供しています。

これらの応用例を通じて、抽象クラスがさまざまなシステム設計においてどのように利用されるかを理解できます。

抽象クラスと継承

抽象クラスを利用した継承は、コードの再利用性を高め、共通のインターフェースを提供することで多様な実装を可能にします。ここでは、抽象クラスと継承の具体的な利用方法とその利点について説明します。

基本的な継承の仕組み

抽象クラスを基にした継承の基本的な仕組みを以下のコードで示します。

#include <iostream>

// 抽象クラスの定義
class Animal {
public:
    virtual void makeSound() = 0; // 純粋仮想関数
    void eat() {
        std::cout << "Eating..." << std::endl;
    }
};

// 派生クラス Dog の定義
class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Woof!" << std::endl;
    }
};

// 派生クラス Cat の定義
class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Meow!" << std::endl;
    }
};

この例では、Animalクラスが抽象クラスとして定義されており、DogクラスとCatクラスがそれを継承し、それぞれのmakeSound関数を実装しています。

継承を利用するメリット

継承を利用することで、以下のようなメリットが得られます:

  1. コードの再利用性向上
    共通の機能(例えばeat関数)を基底クラスにまとめることで、コードの重複を避け、再利用性を高めます。
  2. 拡張性
    新しい派生クラスを追加するだけで、既存のコードを変更することなく機能を拡張できます。
  3. 一貫性の確保
    共通のインターフェースを提供することで、異なるオブジェクトが一貫した方法で操作できるようになります。

抽象クラスを用いた多態性の実現

継承を利用して多態性(ポリモーフィズム)を実現する例を以下に示します。

void playSound(Animal* animal) {
    animal->makeSound();
}

int main() {
    Dog dog;
    Cat cat;

    playSound(&dog); // "Woof!"
    playSound(&cat); // "Meow!"

    return 0;
}

この例では、playSound関数がAnimal型のポインタを引数に取ることで、多態性を実現しています。DogオブジェクトとCatオブジェクトを渡しても、それぞれの具体的なmakeSound関数が呼び出されます。

複数の継承と仮想関数のオーバーライド

複数の抽象クラスを継承し、各クラスの純粋仮想関数をオーバーライドすることも可能です。

class FlyingAnimal {
public:
    virtual void fly() = 0;
};

class Bird : public Animal, public FlyingAnimal {
public:
    void makeSound() override {
        std::cout << "Chirp!" << std::endl;
    }
    void fly() override {
        std::cout << "Flying..." << std::endl;
    }
};

この例では、BirdクラスがAnimalクラスとFlyingAnimalクラスの両方を継承し、それぞれの純粋仮想関数であるmakeSoundflyを実装しています。

これにより、Birdオブジェクトは動物としての振る舞いと、飛ぶ動作の両方を持つことができます。このように、抽象クラスを利用した継承は、柔軟かつ強力なコード設計を可能にします。

抽象クラスと多態性(ポリモーフィズム)

抽象クラスは、多態性(ポリモーフィズム)を実現するための重要な手段です。多態性とは、異なるクラスのオブジェクトを同じインターフェースで操作できるようにする特性を指します。ここでは、抽象クラスを用いた多態性の実現方法について詳しく説明します。

多態性の基本概念

多態性を利用すると、基底クラスのポインタや参照を通じて派生クラスのオブジェクトを操作できます。これにより、異なる型のオブジェクトを同一の方法で扱うことができます。

#include <iostream>

// 抽象クラスの定義
class Animal {
public:
    virtual void makeSound() = 0; // 純粋仮想関数
};

// 派生クラス Dog の定義
class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Woof!" << std::endl;
    }
};

// 派生クラス Cat の定義
class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Meow!" << std::endl;
    }
};

この例では、Animalクラスが抽象クラスとして定義され、DogクラスとCatクラスがそれを継承しています。

多態性の実現

以下の例では、Animal型のポインタを使用して、DogオブジェクトとCatオブジェクトを操作しています。

void playSound(Animal* animal) {
    animal->makeSound();
}

int main() {
    Dog dog;
    Cat cat;

    playSound(&dog); // "Woof!"
    playSound(&cat); // "Meow!"

    return 0;
}

このコードでは、playSound関数がAnimal型のポインタを引数に取ることで、DogオブジェクトとCatオブジェクトの両方を同じ関数で処理しています。これが多態性の実現です。

多態性の利点

多態性を利用することで得られる利点は以下の通りです:

  1. コードの柔軟性向上
    異なるクラスのオブジェクトを同一のインターフェースで操作できるため、コードの柔軟性が向上します。
  2. 拡張性の向上
    新しい派生クラスを追加する際に、既存のコードを変更せずに機能を拡張できます。
  3. メンテナンス性の向上
    共通のインターフェースを通じてオブジェクトを操作するため、コードのメンテナンスが容易になります。

実際の応用例

実際のプロジェクトでは、多態性を利用してさまざまなシナリオを実現できます。例えば、ゲーム開発において異なる種類のキャラクターを同一の方法で操作する場合や、異なるデータベース接続を共通のインターフェースで管理する場合などです。

// ゲームキャラクターの例
class GameCharacter {
public:
    virtual void attack() = 0;
};

class Warrior : public GameCharacter {
public:
    void attack() override {
        std::cout << "Warrior attacks with sword!" << std::endl;
    }
};

class Mage : public GameCharacter {
public:
    void attack() override {
        std::cout << "Mage casts a spell!" << std::endl;
    }
};

void performAttack(GameCharacter* character) {
    character->attack();
}

int main() {
    Warrior warrior;
    Mage mage;

    performAttack(&warrior); // "Warrior attacks with sword!"
    performAttack(&mage);    // "Mage casts a spell!"

    return 0;
}

この例では、WarriorMageの異なる攻撃方法を同一のperformAttack関数で操作しています。これにより、多態性の利点を活かした柔軟なコード設計が可能になります。

純粋仮想関数を用いた設計パターン

純粋仮想関数は、設計パターンにおいて重要な役割を果たします。ここでは、いくつかの主要な設計パターンにおける純粋仮想関数の役割と実例について紹介します。

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

ファクトリメソッドパターンは、オブジェクトの生成をサブクラスに委ねるデザインパターンです。このパターンでは、基底クラスに純粋仮想関数を定義し、派生クラスでその関数を実装します。

#include <iostream>
#include <memory>

// 抽象クラス Product
class Product {
public:
    virtual void use() = 0; // 純粋仮想関数
};

// 具体的な製品クラス ConcreteProductA
class ConcreteProductA : public Product {
public:
    void use() override {
        std::cout << "Using Product A" << std::endl;
    }
};

// 具体的な製品クラス ConcreteProductB
class ConcreteProductB : public Product {
public:
    void use() override {
        std::cout << "Using Product B" << std::endl;
    }
};

// 抽象ファクトリクラス Creator
class Creator {
public:
    virtual std::unique_ptr<Product> createProduct() = 0; // 純粋仮想関数
};

// 具体的なファクトリクラス ConcreteCreatorA
class ConcreteCreatorA : public Creator {
public:
    std::unique_ptr<Product> createProduct() override {
        return std::make_unique<ConcreteProductA>();
    }
};

// 具体的なファクトリクラス ConcreteCreatorB
class ConcreteCreatorB : public Creator {
public:
    std::unique_ptr<Product> createProduct() override {
        return std::make_unique<ConcreteProductB>();
    }
};

int main() {
    std::unique_ptr<Creator> creatorA = std::make_unique<ConcreteCreatorA>();
    std::unique_ptr<Product> productA = creatorA->createProduct();
    productA->use(); // "Using Product A"

    std::unique_ptr<Creator> creatorB = std::make_unique<ConcreteCreatorB>();
    std::unique_ptr<Product> productB = creatorB->createProduct();
    productB->use(); // "Using Product B"

    return 0;
}

この例では、Creatorクラスが純粋仮想関数createProductを定義し、ConcreteCreatorAConcreteCreatorBがそれぞれ具体的な製品を生成しています。

2. ストラテジーパターン

ストラテジーパターンは、アルゴリズムをクラスにカプセル化し、動的にアルゴリズムを変更できるようにするデザインパターンです。純粋仮想関数を使用して異なるアルゴリズムを定義します。

#include <iostream>

// 抽象戦略クラス Strategy
class Strategy {
public:
    virtual void execute() = 0; // 純粋仮想関数
};

// 具体的な戦略クラス ConcreteStrategyA
class ConcreteStrategyA : public Strategy {
public:
    void execute() override {
        std::cout << "Executing Strategy A" << std::endl;
    }
};

// 具体的な戦略クラス ConcreteStrategyB
class ConcreteStrategyB : public Strategy {
public:
    void execute() override {
        std::cout << "Executing Strategy B" << std::endl;
    }
};

// コンテクストクラス Context
class Context {
private:
    Strategy* strategy;
public:
    Context(Strategy* strategy) : strategy(strategy) {}
    void setStrategy(Strategy* newStrategy) {
        strategy = newStrategy;
    }
    void executeStrategy() {
        strategy->execute();
    }
};

int main() {
    ConcreteStrategyA strategyA;
    ConcreteStrategyB strategyB;

    Context context(&strategyA);
    context.executeStrategy(); // "Executing Strategy A"

    context.setStrategy(&strategyB);
    context.executeStrategy(); // "Executing Strategy B"

    return 0;
}

この例では、Strategyクラスが純粋仮想関数executeを定義し、ConcreteStrategyAConcreteStrategyBがそれぞれ異なるアルゴリズムを実装しています。

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

テンプレートメソッドパターンは、アルゴリズムの骨組みを定義し、具体的なステップをサブクラスに委ねるデザインパターンです。基底クラスにテンプレートメソッドを定義し、その中で純粋仮想関数を呼び出します。

#include <iostream>

// 抽象クラス AbstractClass
class AbstractClass {
public:
    void templateMethod() {
        baseOperation();
        requiredOperation1();
        requiredOperation2();
        hook();
    }

    void baseOperation() {
        std::cout << "Base operation" << std::endl;
    }

    virtual void requiredOperation1() = 0; // 純粋仮想関数
    virtual void requiredOperation2() = 0; // 純粋仮想関数

    virtual void hook() {} // 任意のフック
};

// 具体クラス ConcreteClassA
class ConcreteClassA : public AbstractClass {
public:
    void requiredOperation1() override {
        std::cout << "ConcreteClassA: Operation1" << std::endl;
    }
    void requiredOperation2() override {
        std::cout << "ConcreteClassA: Operation2" << std::endl;
    }
};

// 具体クラス ConcreteClassB
class ConcreteClassB : public AbstractClass {
public:
    void requiredOperation1() override {
        std::cout << "ConcreteClassB: Operation1" << std::endl;
    }
    void requiredOperation2() override {
        std::cout << "ConcreteClassB: Operation2" << std::endl;
    }
    void hook() override {
        std::cout << "ConcreteClassB: Custom Hook" << std::endl;
    }
};

int main() {
    ConcreteClassA classA;
    classA.templateMethod();
    // Output:
    // Base operation
    // ConcreteClassA: Operation1
    // ConcreteClassA: Operation2

    ConcreteClassB classB;
    classB.templateMethod();
    // Output:
    // Base operation
    // ConcreteClassB: Operation1
    // ConcreteClassB: Operation2
    // ConcreteClassB: Custom Hook

    return 0;
}

この例では、AbstractClassがテンプレートメソッドtemplateMethodを定義し、その中で純粋仮想関数requiredOperation1requiredOperation2を呼び出しています。派生クラスConcreteClassAConcreteClassBは、これらの関数を具体的に実装しています。

これらの設計パターンを通じて、純粋仮想関数がどのように利用され、柔軟で拡張性の高いソフトウェア設計を実現するかが理解できます。

抽象クラスと純粋仮想関数に関するよくある質問

抽象クラスと純粋仮想関数については、初学者から経験者まで多くの疑問が寄せられます。ここでは、よくある質問とその回答をまとめ、理解を深めるためのサポートを提供します。

Q1: 抽象クラスとインターフェースの違いは何ですか?

A: 抽象クラスは、一部のメソッドに具体的な実装を持つことができ、メンバ変数を持つことができます。一方、インターフェースは純粋にメソッドのシグネチャのみを定義し、実装を持ちません。C++ではインターフェースという専用の概念はなく、すべてのメソッドが純粋仮想関数であり、メンバ変数を持たない抽象クラスがインターフェースとして機能します。

Q2: 抽象クラスのオブジェクトを直接作成することはできますか?

A: いいえ、抽象クラスのオブジェクトを直接作成することはできません。抽象クラスはインスタンス化できないため、その派生クラスのオブジェクトを作成しなければなりません。派生クラスは、抽象クラスの純粋仮想関数をすべて実装する必要があります。

Q3: 純粋仮想関数を持つクラスをインスタンス化するためにはどうすればよいですか?

A: 純粋仮想関数を持つクラスをインスタンス化するには、そのクラスを継承する派生クラスを定義し、すべての純粋仮想関数をオーバーライドする必要があります。派生クラスがすべての純粋仮想関数を実装すれば、その派生クラスのオブジェクトをインスタンス化できます。

Q4: 抽象クラスと純粋仮想関数はどのような場面で使用されますか?

A: 抽象クラスと純粋仮想関数は、以下のような場面で使用されます:

  • 共通のインターフェースを提供する場合:異なるクラスが共通のメソッドを持ち、それぞれ異なる実装を持つ場合に使用します。
  • 多態性を実現する場合:基底クラスのポインタや参照を使って異なる派生クラスのオブジェクトを操作する場合に有効です。
  • 設計パターンの実現:ファクトリメソッドパターンやストラテジーパターンなど、多くの設計パターンで利用されます。

Q5: 純粋仮想関数は何のために使用されるのですか?

A: 純粋仮想関数は、基底クラスで共通のインターフェースを定義し、具体的な実装は派生クラスに任せるために使用されます。これにより、異なるクラスが共通のメソッドを持ちながら、各クラスで異なる実装を提供することができます。これは、コードの柔軟性と拡張性を高めるために非常に有効です。

これらの質問と回答を通じて、抽象クラスと純粋仮想関数の基本概念や応用方法についての理解が深まることを願っています。

演習問題

ここでは、抽象クラスと純粋仮想関数の理解を深めるための演習問題を提供します。これらの問題を通じて、実際に手を動かしながら学習を進めてください。

問題1: 基本的な抽象クラスの実装

以下の要件に従って抽象クラスShapeを定義し、それを継承する具体的なクラスCircleRectangleを実装してください。

  • Shapeクラスには純粋仮想関数draw()area()を含めること。
  • Circleクラスでは、半径を持ち、その面積を計算する。
  • Rectangleクラスでは、幅と高さを持ち、その面積を計算する。
#include <iostream>
#include <cmath>

// 抽象クラス Shape の定義
class Shape {
public:
    virtual void draw() = 0; // 純粋仮想関数
    virtual double area() = 0; // 純粋仮想関数
};

// 具体的なクラス Circle の定義
class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    void draw() override {
        std::cout << "Drawing a circle." << std::endl;
    }
    double area() override {
        return M_PI * radius * radius;
    }
};

// 具体的なクラス Rectangle の定義
class Rectangle : public Shape {
private:
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    void draw() override {
        std::cout << "Drawing a rectangle." << std::endl;
    }
    double area() override {
        return width * height;
    }
};

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

    circle.draw();
    std::cout << "Circle area: " << circle.area() << std::endl;

    rectangle.draw();
    std::cout << "Rectangle area: " << rectangle.area() << std::endl;

    return 0;
}

問題2: 多態性の実現

以下のコードを完成させ、多態性を利用してShapeオブジェクトのリストを操作し、それぞれの形状を描画し、その面積を出力してください。

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

// 抽象クラス Shape の定義(前述の問題1を参考)

// 具体的なクラス Circle と Rectangle の定義(前述の問題1を参考)

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(5.0));
    shapes.push_back(std::make_unique<Rectangle>(4.0, 6.0));

    for (const auto& shape : shapes) {
        shape->draw();
        std::cout << "Area: " << shape->area() << std::endl;
    }

    return 0;
}

問題3: ストラテジーパターンの実装

ストラテジーパターンを用いて、異なるアルゴリズムを動的に切り替えるプログラムを作成してください。Strategy抽象クラスを定義し、ConcreteStrategyAConcreteStrategyBを実装した後、Contextクラスでそれらを使用します。

#include <iostream>

// 抽象戦略クラス Strategy の定義
class Strategy {
public:
    virtual void execute() = 0; // 純粋仮想関数
};

// 具体的な戦略クラス ConcreteStrategyA の定義
class ConcreteStrategyA : public Strategy {
public:
    void execute() override {
        std::cout << "Executing Strategy A" << std::endl;
    }
};

// 具体的な戦略クラス ConcreteStrategyB の定義
class ConcreteStrategyB : public Strategy {
public:
    void execute() override {
        std::cout << "Executing Strategy B" << std::endl;
    }
};

// コンテクストクラス Context の定義
class Context {
private:
    Strategy* strategy;
public:
    Context(Strategy* strategy) : strategy(strategy) {}
    void setStrategy(Strategy* newStrategy) {
        strategy = newStrategy;
    }
    void executeStrategy() {
        strategy->execute();
    }
};

int main() {
    ConcreteStrategyA strategyA;
    ConcreteStrategyB strategyB;

    Context context(&strategyA);
    context.executeStrategy(); // "Executing Strategy A"

    context.setStrategy(&strategyB);
    context.executeStrategy(); // "Executing Strategy B"

    return 0;
}

これらの演習問題を解くことで、抽象クラスと純粋仮想関数の理解が深まり、実践的な応用力が身に付きます。

まとめ

本記事では、C++の抽象クラスと純粋仮想関数について基本から応用まで幅広く解説しました。抽象クラスと純粋仮想関数は、オブジェクト指向プログラミングの強力なツールであり、コードの再利用性、拡張性、保守性を高めるために重要な役割を果たします。具体的な実装例や設計パターンの応用、さらに演習問題を通じて、実際の開発に役立つ知識を提供しました。これを機に、C++プログラミングにおいて抽象クラスと純粋仮想関数を効果的に活用してみてください。

コメント

コメントする

目次