Menu
Coddy logo textTech

純粋仮想関数

CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 59/104。

純粋仮想関数とは、基底クラスに実装を持たない仮想関数のことです。これは、関数の宣言に = 0 を代入することで宣言されます。これにより、派生クラスが独自の実装を必ず提供しなければならないことをコンパイラに伝えます。

class Shape {
public:
    virtual double area() = 0;  // 純粋仮想関数
    virtual ~Shape() = default;
};

デフォルトの動作を提供する通常の仮想関数とは異なり、純粋仮想関数はコントラクト(契約)を定義します。具体的な派生クラスをインスタンス化できるようにするには、そのクラスでこの関数を実装しなければなりません。基底クラスは単にをすべきかを宣言するだけで、どのように行うかは宣言しません。

class Circle : public Shape {
    double radius;
public:
    Circle(double r) : radius(r) {}
    
    double area() override {
        return 3.14159 * radius * radius;
    }
};

class Rectangle : public Shape {
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    double area() override {
        return width * height;
    }
};

CircleRectangle の両方は、Shape において area() が純粋仮想関数であるため、これを実装しなければなりません。派生クラスがすべての純粋仮想関数を実装しなかった場合、そのクラスも抽象クラスとなり、インスタンス化できなくなります。

純粋仮想関数は、基底クラスが意味のあるデフォルトの実装を提供できない場合に不可欠です。すべての図形には面積がありますが、具体的な図形の種類を知ることなく「一般的な図形の面積」を計算する合理的な方法はありません。

challenge icon

チャレンジ

簡単

純粋仮想関数がどのように異なる支払い方法間で規約(コントラクト)を強制するかを実証する、支払い処理システムを構築しましょう。すべての支払いプロセッサが実行すべき内容を定義する抽象基底クラスを作成し、その規約を満たす具体的な支払いタイプを実装します。

コードは3つのファイルに分けて構成します:

  • PaymentProcessor.h: すべての支払い方法の設計図となる抽象クラス PaymentProcessor を定義します。このクラスには以下を含める必要があります:
    • protected メンバ std::string accountId
    • アカウントIDを初期化するコンストラクタ
    • 純粋仮想関数 processPayment(double amount) メソッド — すべての支払いタイプはこれを異なる方法で実装する必要があります
    • std::string を返す純粋仮想関数 getProcessorName() メソッド
    • 仮想デストラクタ
  • PaymentMethods.h: PaymentProcessor を継承する2つの具体的な支払いプロセッサを実装します:

    CreditCardProcessor:

    • private メンバ double feePercentage (取引手数料率)
    • アカウントIDと手数料率を受け取るコンストラクタ
    • processPayment() を実装して手数料 (amount * feePercentage / 100) を計算し、次のように出力します:
      Credit Card [<accountId>]: Charged Credit Card [<accountId>]: Charged $<amount> (Fee: $<fee>)lt;amount> (Fee: Credit Card [<accountId>]: Charged $<amount> (Fee: $<fee>)lt;fee>)
    • getProcessorName() を実装して "CreditCard" を返します

    BankTransferProcessor:

    • private メンバ std::string bankName
    • アカウントIDと銀行名を受け取るコンストラクタ
    • processPayment() を実装して次のように出力します:
      Bank Transfer [<accountId>] via <bankName>: Transferred Bank Transfer [<accountId>] via <bankName>: Transferred $<amount>lt;amount>
    • getProcessorName() を実装して "BankTransfer" を返します
  • main.cpp: 4つの入力(それぞれ別々の行)を読み取ります:
    1. クレジットカードのアカウントID
    2. クレジットカードの手数料率 (double)
    3. 銀行口座のアカウントID
    4. 銀行名

    両方の支払いプロセッサを作成し、それらを PaymentProcessor* ポインタの配列に格納します。各プロセッサを通じて 100.0 の支払いを処理し、各取引の前にプロセッサ名を表示します:

    Processing with <processorName>:
    <processPayment output>

    プロセッサの間に空行を出力してください。終了時には、動的に割り当てたオブジェクトをクリーンアップしてください。

例えば、入力が CC-45212.5BA-7890National Bank の場合:

Processing with CreditCard:
Credit Card [CC-4521]: Charged $100 (Fee: $2.5)

Processing with BankTransfer:
Bank Transfer [BA-7890] via National Bank: Transferred $100

PaymentProcessor は純粋仮想関数を持っているため、直接インスタンス化することはできません。規約を満たす具体的な実装のみを作成できます。これにより、すべての支払い方法が独自の特定の処理ロジックを提供することが保証されます。

チートシート

純粋仮想関数は、仮想関数に = 0 を代入することで宣言され、基底クラスに実装がないことを示します。

class Shape {
public:
    virtual double area() = 0;  // 純粋仮想関数
    virtual ~Shape() = default;
};

純粋仮想関数は、派生クラスが必ず実装しなければならないという契約を作成します。基底クラスは「何」をすべきかを定義し、「どのように」するかは定義しません。

派生クラスをインスタンス化できるようにするには、すべての純粋仮想関数をオーバーライドする必要があります。

class Circle : public Shape {
    double radius;
public:
    Circle(double r) : radius(r) {}
    
    double area() override {
        return 3.14159 * radius * radius;
    }
};

派生クラスがすべての純粋仮想関数を実装していない場合、そのクラスも抽象クラスとなり、インスタンス化することはできません。

基底クラスが意味のあるデフォルトの実装を提供できない場合に、純粋仮想関数を使用します。

自分で試してみよう

#include <iostream>
#include <string>
#include "PaymentMethods.h"

using namespace std;

int main() {
    // 入力を読み込む
    string ccAccountId;
    double feePercentage;
    string bankAccountId;
    string bankName;
    
    getline(cin, ccAccountId);
    cin >> feePercentage;
    cin.ignore();
    getline(cin, bankAccountId);
    getline(cin, bankName);
    
    // TODO: 2つの要素を持つ PaymentProcessor* ポインタの配列を作成する
    
    // TODO: CreditCardProcessor と BankTransferProcessor オブジェクトを作成する
    // そして、それらを配列に格納する
    
    // TODO: 配列をループし、各プロセッサに対して以下を行う:
    // 1. "Processing with <processorName>:" を出力する
    // 2. 金額 100.0 で processPayment を呼び出す
    // 3. プロセッサの間に空行を出力する(最後のプロセッサの後には出力しない)
    
    // TODO: 動的に割り当てられたオブジェクトをクリーンアップする
    
    return 0;
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン