C++ポリモーフィズム徹底解説:実践例とキー概念
C++におけるポリモーフィズムとは
C++において、ポリモーフィズムとは、それを呼び出す/呼び出されるオブジェクトに基づいてメンバ関数が異なる振る舞いをすることを指します。ポリモーフィズムは「多くの形を持つ」を意味するギリシャ語に由来します。継承を通じて関連するクラスの階層があるときに発生します。
例えば、makeSound()関数があるとします。猫がこの関数を呼ぶと「ニャー」と鳴きます。牛が同じ関数を呼ぶと「モー」と鳴きます。
同じ関数でも状況に応じて異なる振る舞いをします。関数が多くの形を持つため、ポリモーフィズムが達成されています。
このC++チュートリアルで学ぶ内容:
- ポリモーフィズムとは
- ポリモーフィズムの種類
- コンパイル時ポリモーフィズム
- 関数オーバーロード
- 演算子オーバーロード
- 実行時ポリモーフィズム
- 関数オーバーライド
- C++仮想関数
- コンパイル時vs実行時ポリモーフィズムの比較
ポリモーフィズムの種類
C++は2種類のポリモーフィズムをサポートします:
- コンパイル時ポリモーフィズム
- 実行時ポリモーフィズム
コンパイル時ポリモーフィズム
オーバーロードされた関数を、引数の数と型をマッチングして呼び出します。この情報はコンパイル時に存在します。つまり、C++コンパイラはコンパイル時に適切な関数を選択します。
コンパイル時ポリモーフィズムは関数オーバーロードと演算子オーバーロードによって達成されます。
関数オーバーロード
関数オーバーロードは、同じ名前だが異なる引数を持つ複数の関数があるときに発生します。引数は数や型で異なります。
例1:
#include <iostream>
using namespace std;
void test(int i) {
cout << " The int is " << i << endl;
}
void test(double f) {
cout << " The float is " << f << endl;
}
void test(char const *ch) {
cout << " The char* is " << ch << endl;
}
int main() {
test(5);
test(5.5);
test("five");
return 0;
}
出力:
コードのスクリーンショット:
コード解説:
- iostreamヘッダをインクルード
- std名前空間をインクルード
- 整数パラメータiを取るtest関数を作成
- 上記test関数が呼び出されたときに実行される文
- 上記test関数の本体終了
- floatパラメータfを取るtest関数を作成
- 上記test関数が呼び出されたときに実行される文
- 上記test関数の本体終了
- 文字パラメータchを取るtest関数を作成
- 上記test関数が呼び出されたときに実行される文
- 上記test関数の本体終了
- main()関数を呼び出し
- test関数を呼び出し、5を引数として渡す。最初のtest関数(整数受け取り)が呼び出される
- test関数を呼び出し、5.5を引数として渡す。2番目のtest関数(float受け取り)が呼び出される
- test関数を呼び出し、"five"を引数として渡す。3番目のtest関数(文字受け取り)が呼び出される
- プログラムは正常実行時に値を返す
- main()関数の本体終了
同じ名前だが異なる型の引数を持つ3つの関数があります。ポリモーフィズムを達成しました。
演算子オーバーロード
演算子オーバーロードでは、C++演算子に新しい意味を定義します。演算子の動作も変更されます。例えば、+演算子を2つの文字列の連結に定義できます。元々は数値加算の演算子として知られています。定義後、整数の間に置かれれば加算し、文字列の間に置かれれば連結します。
例2:
#include<iostream>
using namespace std;
class ComplexNum {
private:
int real, over;
public:
ComplexNum(int rl = 0, int ov = 0) {
real = rl;
over = ov;
}
ComplexNum operator + (ComplexNum const &obj) {
ComplexNum result;
result.real = real + obj.real;
result.over = over + obj.over;
return result;
}
void print() {
cout << real << " + i" << over << endl;
}
};
int main() {
ComplexNum c1(10, 2), c2(3, 7);
ComplexNum c3 = c1+c2;
c3.print();
}
出力:
コードのスクリーンショット:
コード解説:
- iostreamヘッダをインクルード
- std名前空間をインクルード
- ComplexNumクラスを作成
- privateアクセス修飾子で変数をプライベートに
- realとoverという2つの整数変数を定義
- publicアクセス修飾子でコンストラクタをパブリックに
- クラスコンストラクタを作成し変数を初期化
- real変数の値を初期化
- over変数の値を初期化
- コンストラクタ本体終了
+演算子の意味をオーバーライドする必要がある- ComplexNum型のresultデータ型を作成
+演算子を複素数で使用。ある数の実部を別の数の実部に加算+演算子を複素数で使用。ある数の虚部を別の数の虚部に加算- プログラムは正常実行時にresult変数の値を返す
+演算子の新しい意味の定義、つまりオーバーロードの終了- print()メソッドを呼び出し
- 加算後の新しい複素数をコンソールに出力
- print()関数の本体終了
- ComplexNumクラスの本体終了
- main()関数を呼び出し
- 実部と虚部の値を渡して加算。c1の第1部はc2の第1部に加算(10+3)。c1の第2部はc2の第2部に加算(2+7)
- オーバーロードされた
+演算子を使って演算し、結果を変数c3に格納 - 変数c3の値をコンソールに出力
- main()関数の本体終了
実行時ポリモーフィズム
これはオブジェクトのメソッドがコンパイル時ではなく実行時に呼び出される/コールされるときに発生します。実行時ポリモーフィズムは関数オーバーライドによって達成されます。呼び出される関数は実行時に確立されます。
関数オーバーライド
基底クラスの関数が派生クラスで新しい定義を与えられたときに関数オーバーライドが発生します。その時、基底関数はオーバーライドされたと言えます。
例:
#include <iostream>
using namespace std;
class Mammal {
public:
void eat() {
cout << "Mammals eat...";
}
};
class Cow: public Mammal {
public:
void eat() {
cout << "Cows eat grass...";
}
};
int main(void) {
Cow c = Cow();
c.eat();
return 0;
}
出力:
コードのスクリーンショット:
コード解説:
- iostreamヘッダをインポート
- std名前空間をインクルード
- Mammalクラスを作成
- publicアクセス修飾子でこれから作る関数をパブリックに。クラス外部からもアクセス可能
- eatというパブリック関数を作成
- eat()関数が呼び出されたときにcout関数に追加された文を出力
- eat()関数の本体終了
- Mammalクラスの本体終了
- Mammalクラスを継承するCowクラスを作成。Cowが派生クラス、Mammalが基底クラス
- publicアクセス修飾子でこれから作る関数をパブリックに
- 基底クラスで定義されたeat()関数をオーバーライド
- この関数が呼び出されたときにコンソールに出力する文
- eat()関数の本体終了
- Cowクラスの本体終了
- main()関数を呼び出し
- Cowクラスのインスタンスを作成し、cという名前を付ける
- Cowクラスで定義されたeat()関数を呼び出し
- プログラムは正常完了時に値を返す
- main()関数の終了
C++仮想関数
仮想関数はC++で実行時ポリモーフィズムを実装するもう1つの方法です。基底クラスで定義され、派生クラスで再定義される特殊な関数です。仮想関数を宣言するには、virtualキーワードを使用します。キーワードは基底クラスでの関数宣言の前に置く必要があります。
仮想関数クラスが継承されると、派生クラスは必要に応じて仮想関数を再定義します。例:
#include <iostream>
using namespace std;
class ClassA {
public:
virtual void show() {
cout << "The show() function in base class invoked..." << endl;
}
};
class ClassB :public ClassA {
public:
void show() {
cout << "The show() function in derived class invoked...";
}
};
int main() {
ClassA* a;
ClassB b;
a = &b;
a->show();
}
出力:
コードのスクリーンショット:
コード解説:
- iostreamヘッダをインクルード
- std名前空間をインクルード
- ClassAというクラスを作成
- publicアクセス修飾子でクラスメンバをパブリックに
- show()という仮想関数を作成。パブリック関数
- show()が呼び出されたときに出力するテキスト。endlは改行を意味するC++キーワード
- 仮想関数show()の本体終了
- ClassAクラスの本体終了
- ClassAを継承するClassBという新しいクラスを作成。ClassAが基底クラス、ClassBが派生クラス
- publicアクセス修飾子でクラスメンバをパブリックに
- 基底クラスで定義された仮想関数show()を再定義
- 派生クラスで定義されたshow()関数が呼び出されたときにコンソールに出力するテキスト
- show()関数の本体終了
- 派生クラスClassBの本体終了
- main()関数を呼び出し。プログラムロジックは本体内に記述
- aというポインタ変数を作成。ClassAクラスを指す
- ClassBクラスのインスタンスを作成。インスタンス名はb
- bに格納されているアドレスの値を変数aに代入
- 派生クラスで定義されたshow()関数を呼び出し。遅延バインディングが実装される
- main()関数の本体終了
コンパイル時ポリモーフィズム vs 実行時ポリモーフィズム
主な違いは以下の通り:
| コンパイル時ポリモーフィズム | 実行時ポリモーフィズム |
|---|---|
| 早期バインディングまたは静的ポリモーフィズムとも呼ばれる | 後期/動的バインディングまたは動的ポリモーフィズムとも呼ばれる |
| メソッドはコンパイル時に呼び出される | メソッドは実行時に呼び出される |
| 関数オーバーロードと演算子オーバーロードで実装 | メソッドオーバーライドと仮想関数で実装 |
| 例:メソッドオーバーロード。同じ名前だが引数の数や型が異なる複数のメソッド | 例:メソッドオーバーライド。同じ名前と同じプロトタイプの複数のメソッド |
| メソッド発見がコンパイル時に行われるため実行が高速 | メソッド発見が実行時に行われるため実行が遅い |
| コンパイル時にすべてが既知のため問題解決の柔軟性は低い | 実行時にメソッドが発見されるため複雑な問題解決に高い柔軟性を提供 |
まとめ:
- ポリモーフィズムは「多くの形を持つ」を意味する
- 継承を通じて関連するクラスの階層があるときに発生
- ポリモーフィズムにより、関数はそれを呼び出すオブジェクトに基づいて異なる振る舞いができる
- コンパイル時ポリモーフィズムでは、呼び出される関数がコンパイル時に確立される
- 実行時ポリモーフィズムでは、呼び出される関数が実行時に確立される
- コンパイル時ポリモーフィズムは関数オーバーロードと演算子オーバーロードで決定される
- 関数オーバーロードでは、同じ名前だが異なる引数を持つ複数の関数がある
- パラメータは数や型で異なる
- 演算子オーバーロードでは、C++演算子に新しい意味を定義する
- 実行時ポリモーフィズムは関数オーバーライドで達成される
- 関数オーバーライドでは、派生クラスが基底クラスで定義された関数に新しい定義を与える
C言語
- C++配列を関数に渡す完全ガイド:1次元・多次元配列の実例付き
- C++演算子を徹底解説:種類、例、サンプルプログラム付き
- C++ switch文をマスター:構文、使い方、実践例
- C++動的配列の確保をマスター:コード例付き実践ガイド
- C++演算子オーバーロード:コード例付き実践ガイド
- C++構造体を実例で徹底解説
- C++のstd::listをマスターする:構文、関数、実践例
- C++関数を実例で徹底解説:基本から応用まで
- C++ structとclassの違い:構文、特徴、使い分けのベストプラクティス
- C言語のcalloc():ゼロ初期化メモリ割り当てと実践例
- C言語free()関数完全マスター:実践ガイドと例