【C++】ポリモーフィズムとは?わかりやすく解説!

  • ポリモーフィズムってなに?
  • どんなメリットがあるの?
  • 仮想関数とは?

このような疑問をプログラミングが初めての方にもわかるように、わかりやすく解説しますオブジェクト指向の3つの特徴の1つのポリモーフィズムというのはどういうものなのでしょうか。見ていきましょう!

無料体験ができるプログラミングスクールの詳細記事

ポリモーフィズム(polymorphism

ポリモーフィズムとは、オブジェクトなどのデータ型に関する操作が統一的であることである。

ちょっと優しく言えば、異なるオブジェクト間の操作を一緒に処理できるというオブジェクト指向の特徴の一部。

これでもまだ難しいように言っている気がします。

ざっくり言ってしまえば、異なるオブジェクトの操作をまとめてできるよって機能です。これから詳しく解説していきます。

[box02 title=”ポリモーフィズムの定義と解釈“]

オブジェクトなどのデータ型に関する操作統一的である

異なるオブジェクト間の操作
一緒に処理できる

異なるオブジェクトの操作まとめてできる

[/box02]

これはこれから紹介するサンプルコードの一部です。最終的にこんなコードを仕上げていくのが今回の記事の目標です。

//それぞれのオブジェクトのポインタを格納する配列を用意
	Human* human[N] = 
	{ &american,&japanese,&indian,&korean,&chinese,&russian,&german };

//それぞれでHello()関数を呼び出す	
	for (int i = 0; i < N; i++) {
		human[i]->Hello();
	}

次の項目で名前の由来を書いていますが興味のない方は飛ばしてください。

名前の由来

ポリモーフィズムとは、日本語で多様性 多態性 多相性と訳すことができます。読み方は、ポリモフィズムのほかに、ポリモフィズムとも読めるそうです。[r]の発音の問題ですね。

単語の構造的には poly morphism 分けられるのかな。

poly(ポリ)には「多くの」という意味があります。poly-ethylene(ポリエチレン)がいい例で、エチレン複数個集まってできている有名な化学物質です。ポリ袋、シャンプーやリンスの容器もポリエチレンで作られているらしいですよ。

morphism(モーフィズム)は、簡単に「変化」といえばいいのかなと思います。あるものからあるものへの「変化」を表しています。数学的に言えば、射と言われたりします。あるものからあるものへの写像なんて言ったりしますね。

関係ない話なのでわからなくても大丈夫です、「変化」と思ってもらって構わないです。

以上より、ポリモーフィズム(polymorphism)とは、「多くの」「変化」と直訳することができますね。「多くの変化が可能になる」という意味で「多様性」という言葉が当てはまるのだと思います。

poly(多くの) + morphism(変化)

 多様性

ざっくりしたイメージ

ここでは導入として漠然としたイメージを書きます。

○○の髪型は□□だ。

このようなJap_langの構文があったとしましょう。これの○○に田中君や木村君を入れてみると、

田中君髪形リーゼントだ。
木村君髪形マッシュだ。

このようになったとします。
大事な要素は3つです。

  1. 呼び出し元
  2. 関数
  3. 結果

すべて、
「呼び出し元」の「関数」は「結果」
という構文になっています。

○○が変われば髪型という関数が結果□□を返します。この際、髪型という関数名はいつも同じです。「同じ関数なのに呼び出した人によって結果が変わってくる。」というのがポリモーフィズムの考え方です。

これをC++に実装することが目標です。
イメージはこんな感じ。

class 田中 {
	髪型() { return "アフロ"; }
};

class 木村 {
	髪型() { return "マッシュ"; }
};

int main() {
	クラスメイト[2] = { 田中,木村 };
	for (int i = 0; i < 2; i++) {
		クラスメイト[i]の髪型();
	}
}

実際にはもう少し付け加える部分があります。下のほうで解説していきます。

定義の解釈

私は、ポリモーフィズムをこう考えるとよいと思っています。

じ名前の関数がオブジェクトによって違う働きをすること

例えばアメリカ人のクラスと日本人のクラスを作ってそれぞれに同じ名前Hello()という挨拶を表示する関数を作ったとします。

#include <iostream>
using namespace std;

//アメリカ人のクラス
class American {
public:
//挨拶のメンバ関数
	void Hello() { cout << "Hello" << endl; }
};

//日本人のクラス
class Japanese {
public:
//挨拶のメンバ関数
	void Hello() { cout << "こんにちは" << endl; }
};

アメリカ人のクラスの挨拶の関数Hello()は”Hello”と表示されるプログラムです。

一方、日本人のクラスの挨拶の関数Hello()は”こんにちは”と表示されるプログラムです。

実際にオブジェクトを作ってHello()関数を起動してみましょう。

int main()
{
//それぞれのクラスのオブジェクトを作成
	American american;
	Japanese japanese;

//それぞれでHello()関数を呼び出す
	american.Hello();
	japanese.Hello();
}

これは当たり前ですが、

Hello
こんにちは

こう表示されます。これをもし、

○○.Hello()→□□

このように○○の部分を変えて□□が表示出来たら便利ですよね。これがポリモーフィズムです。同じHelloという名前の関数が呼び出し元によって違う働きをすること。多様性なんです。

では、もっと多くの国の方のクラスを作ってみましょう。

//アメリカ人のクラス
class American {
public:
	void Hello() { cout << "Hello" << endl; }
};

//日本人のクラス
class Japanese {
public:
	void Hello() { cout << "こんにちは" << endl; }
};

//インド人のクラス
class Indian {
public:
	void Hello() { cout << "ナマステ" << endl; }
};

//韓国人のクラス
class Korean {
public:
	void Hello() { cout << "アニョハセヨ" << endl; }
};

//中国人のクラス
class Chinese {
public:
	void Hello() { cout << "ニーハオ" << endl; }
};

//ロシア人のクラス
class Russian {
public:
	void Hello() { cout << "ドーブライ ディエン" << endl; }
};

//ドイツ人のクラス
class German {
public:
	void Hello() { cout << "グーテン ターク" << endl; }
};

これを全員分表示するにはこうします。

int main()
{
//それぞれのクラスのオブジェクトを作成
	American american;
	Japanese japanese;
	Indian indian;
	Korean korean;
	Chinese chinese;
	Russian russian;
	German german;

//それぞれでHello()関数を呼び出す
	american.Hello();
	japanese.Hello();
	indian.Hello();
	korean.Hello();
	chinese.Hello();
	russian.Hello();
	german.Hello();
}

これは大変ですよね。ちなみに実行結果はこう出ます。

Hello
こんにちは
ナマステ
アニョハセヨ
ニーハオ
ドーブライ ディエン
グーテン ターク

ここであることに気づくわけです。

メンバ関数「Hello()」って名前同じだ。

実行する人の名前だけ変えればいいのでは?

そうですよね、○○.Hello();となっているわけですし、○○だけ変えて実行できないかなあと考えると思います。

後ほど説明しますが、結果的にはこう書けばいいことになります。

#include <iostream>
#define N 7
using namespace std;

//すべての親となる基底クラス
class Human {
public:
	void virtual Hello() = 0;//純粋仮想関数
};

class American : public Human {
public:
	void Hello() { cout << "Hello" << endl; }
};

class Japanese : public Human {
public:
	void Hello() { cout << "こんにちは" << endl; }
};

class Indian : public Human {
public:
	void Hello() { cout << "ナマステ" << endl; }
};

class Korean : public Human {
public:
	void Hello() { cout << "アニョハセヨ" << endl; }
};

class Chinese : public Human {
public:
	void Hello() { cout << "ニーハオ" << endl; }
};

class Russian : public Human {
public:
	void Hello() { cout << "ドーブライ ディエン" << endl; }
};

class German : public Human {
public:
	void Hello() { cout << "グーテン ターク" << endl; }
};

main文は、

int main()
{
//それぞれのクラスのオブジェクトを作成
	American american;
	Japanese japanese;
	Indian indian;
	Korean korean;
	Chinese chinese;
	Russian russian;
	German german;

//それぞれのオブジェクトのポインタを格納する配列を用意
	Human* human[N] = 
	{ &american,&japanese,&indian,&korean,&chinese,&russian,&german };

//それぞれでHello()関数を呼び出す	
	for (int i = 0; i < N; i++) {
		human[i]->Hello();
	}
}

このようにmain文がきれいにまとまったのがわかると思います。for文で一気に回せています。付け足した部分は、#define N 7純粋仮想関数継承オブジェクト型のポインタを格納する配列for文などですね。下で解説していきます。

変更点の一番大きな点はこちらです。

//それぞれでHello()関数を呼び出す
	american.Hello();
	japanese.Hello();
	indian.Hello();
	korean.Hello();
	chinese.Hello();
	russian.Hello();
	german.Hello();

この気持ちの悪い部分が、

//それぞれのオブジェクトのポインタを格納する配列を用意
	Human* human[N] = 
	{ &american,&japanese,&indian,&korean,&chinese,&russian,&german };

//それぞれでHello()関数を呼び出す	
	for (int i = 0; i < N; i++) {
		human[i]->Hello();
	}

このように「それっぽく」なったこと。これがまさにポリモーフィズムの結晶です。

ポリモーフィズムを実現するために必要な機能や知識

上でも書いた通り、ポリモーフィズムとはある一つの機能ではありません。日本語で言えば「多様性」、言い換えれば、異なるオブジェクト間の操作を一緒に処理できるというオブジェクト指向の特徴の一部です。ですので、継承などと違って、ポリモーフィズムはこうやって使うんだとかってのが書きにくい部分でもあります。

ここでは、オブジェクト指向の特徴である、ポリモーフィズムを実現するために必要な機能や知識を紹介していきます。

継承

基底クラス-派生クラス、言い換えれば親クラス-子クラスといえます。この関係がわかる方は飛ばしていただいて大丈夫です。継承がわからない方はこちらをご覧ください。

【C++】継承とは?わかりやすく解説!

基底クラスへのポインタ

これをわかっていないと今回のポリモーフィズムの例は理解できないはずです。

基底クラスへのポインタは派生クラスのポインタも代入することができるというところさえわかっていれば大丈夫です。

こちらの記事で解説しています。

【C++】基底クラス型のポインタの使い方を解説!

仮想関数

仮想関数についてはC++のポリモーフィズムの一番大切な部分です。こちらの記事で解説しています。

https://www.jpazamu.com/virtual/

ポリモーフィズムのメリット

ポリモーフィズムを使う(使うといっていいのかわからないですが)メリットというのはズバリ、変更に強くなるという点が大きい思います。

変更に強くなる柔軟性

サンプルコードのこの場所、

//それぞれのオブジェクトのポインタを格納する配列を用意
	Human* human[N] = 
	{ &american,&japanese,&indian,&korean,&chinese,&russian,&german };

//それぞれでHello()関数を呼び出す	
	for (int i = 0; i < N; i++) {
		human[i]->Hello();
	}

ポリモーフィズムのない場合はこれ↓です。

//それぞれでHello()関数を呼び出す
	american.Hello();
	japanese.Hello();
	indian.Hello();
	korean.Hello();
	chinese.Hello();
	russian.Hello();
	german.Hello();

かっこ悪いし、こんな長々と書きたくないです。またこのような処理がしたいとき、またこのコードを書かないといけなくなります。

例えば、もう一回この関数を呼び出すときに、

//ポリモーフィズムあり
//それぞれでHello()関数を呼び出す	
	for (int i = 0; i < N; i++) {
		human[i]->Hello();
	}

こう書くのと、

//ポリモーフィズムなし
//それぞれでHello()関数を呼び出す
	american.Hello();
	japanese.Hello();
	indian.Hello();
	korean.Hello();
	chinese.Hello();
	russian.Hello();
	german.Hello();

どっちがいいですかって話。派生クラスが増えたら増えただけ、ポリモーフィズムのない下のコードのほうが厄介ですよ。

次にすべてのクラスに新たなメンバ関数Hoge()が増えたとします。

//アメリカ人のクラス
class American : public Human {
public:
	//挨拶のメンバ関数
	void Hello() { cout << "Hello" << endl; }

	//新たなメンバ関数
	int Hoge() { return 0; }
};

ちなみに0を返すだけの意味ない関数です。もちろんほかの子クラスにも作り、親クラスには純粋仮想関数を作っておきます。そしてこの関数を一緒に呼び出してみましょう。

//ポリモーフィズムなし

//それぞれでHello()関数を呼び出す
american.Hello();
japanese.Hello();
indian.Hello();
korean.Hello();
chinese.Hello();
russian.Hello();
german.Hello();

//それぞれでHoge()関数を呼び出す
american.Hoge();
japanese.Hoge();
indian.Hoge();
korean.Hoge();
chinese.Hoge();
russian.Hoge();
german.Hoge();

非常に格好悪い。ポリモーフィズムを使えばこんな書き方ができます。

//ポリモーフィズムあり

//それぞれでHello(),Hoge()関数を呼び出す	
for (int i = 0; i < N; i++) {
	human[i]->Hello();
	human[i]->Hoge();
}

なんて美しいのでしょうか!

このように記述量が減るというのも変更に強い点ですが、さらには、変更点や追加点がわかりやすいという点も変更に強い点です。

サンプルコードの「#define N 7」というのは「これから大文字のNは7という意味で定義しますよ」という構文です。

#include <iostream>
#define N 7
using namespace std;

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

class American : public Human {
public:
	void Hello() { cout << "Hello" << endl; }
};

class Japanese : public Human {
public:
	void Hello() { cout << "こんにちは" << endl; }
};

class Indian : public Human {
public:
	void Hello() { cout << "ナマステ" << endl; }
};

class Korean : public Human {
public:
	void Hello() { cout << "アニョハセヨ" << endl; }
};

class Chinese : public Human {
public:
	void Hello() { cout << "ニーハオ" << endl; }
};

class Russian : public Human {
public:
	void Hello() { cout << "ドーブライ ディエン" << endl; }
};

class German : public Human {
public:
	void Hello() { cout << "グーテン ターク" << endl; }
};

int main()
{
//それぞれのクラスのオブジェクトを作成
	American american;
	Japanese japanese;
	Indian indian;
	Korean korean;
	Chinese chinese;
	Russian russian;
	German german;

//それぞれのオブジェクトのポインタを格納する配列を用意
	Human* human[N] = 
	{ &american,&japanese,&indian,&korean,&chinese,&russian,&german };

//それぞれでHello()関数を呼び出す	
	for (int i = 0; i < N; i++) {
		human[i]->Hello();
	}
}

例えば、新しいクラスFrenchが増えたとします。その時の変更点は、基本的にクラスの個数が増えたわけですから#define N 8にします。そしてインスタンスを作り最後にポインタの配列に追加するだけです。ポリモーフィズムのないときは、様々な場所で変更、追加する必要が出てくるはずです。(今回のサンプルコードだけだと少ないですが。)

最後に

理解や解釈が難しいポリモーフィズムですが、私も最初理解に苦しみましたので、いろんな記事を見るとか、直接触れてみて感じてみるとかするととてもいいと思います。

またメリットなどは個人的な見解も含んでいますので、実際にプログラミングしてみてここは違うんじゃないと思うこともあるかもしれません。一つ参考程度に見てください。

この記事はプログラミングを始める方用に作っていますので、これからプログラミングを始める方に少しでも役に立てれば、参考にしてもらえればと思います。

オブジェクト指向のメリットとは?例に例えてわかりやすく解説!

最後まで読んでいただきありがとうございます。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です