C/C++ · 5 min read · Oct 10, 2025

C/C++を段階的に学ぶ - ページ 16

16. 段階的C/C++ — C++プログラミング - ポリモーフィズム

ポリモーフィズム

| | 1. 関数のオーバーロード

  1. ポリモーフィズム
  2. ポリモーフィズムの種類
  3. ポインタでアクセスされる通常のメンバー関数
  4. 仮想関数
  5. 純粋関数
  6. 代入とコピー初期化
  7. コピーコンストラクタ
  8. ‘this’ポインタ |

1. 関数のオーバーロード

引数の動作が異なる同名の関数は、関数のポリモーフィズムまたは関数のオーバーロードと呼ばれます。

| | // 関数のオーバーロードの使用を示す例プログラム
#include
using namespace std;
void printline()
{
for(int i=0;i<=80; i++) cout << “-“;
}

void printline(int n)
{
for(int i =0 ;i<=n;i++) cout << “-“;
}

void printline(int n,char ch)
{
for(int i=0;i<=n; i++) cout << ch;
}

int main()
{
printline();
printline(5);
printline(10, ‘*’);
return 0;
}
|

ポリモーフィズム

ポリモーフィズムはOOPの重要な特徴の1つです。それは単に1つの名前、複数の形を意味します。ポリモーフィズムの概念がオーバーロードされた関数や演算子を使用してどのように実装されるかをすでに見てきました。オーバーロードされたメンバー関数は、引数の型と数を一致させることで呼び出されます。コンパイラはこの情報をコンパイル時に知っているため、特定の呼び出しに対して適切な関数をコンパイル時に選択することができます。これを早期バインディングまたは静的バインディングまたは静的リンクと呼びます。また、コンパイル時ポリモーフィズムとしても知られています。早期バインディングは、オブジェクトがコンパイル時にその関数呼び出しにバインドされることを意味します。

次に、基底クラスと派生クラスの両方で関数名とプロトタイプが同じである状況を考えてみましょう。例えば、次のクラス定義を考えます。

| | #include
using namespace std;
class A
{
int x;
public : void show();
};

class B : public A
{
int y;
public : void show();
};

int main()
{
B b;
b.show();
return 0;
} |

メンバー関数show( )を使用して、クラスAとBの両方のオブジェクトの値を印刷するにはどうすればよいでしょうか?show( )のプロトタイプは両方の場所で同じであるため、関数はオーバーロードされず、したがって静的バインディングは適用されません。実際、コンパイラは何をすべきかを知らず、決定を遅らせます。

プログラムが実行中に適切なメンバー関数を選択できれば良いでしょう。これをランタイムポリモーフィズムと呼びます。これはどのように起こるのでしょうか?C++はランタイムポリモーフィズムを達成するために仮想関数と呼ばれるメカニズムをサポートしています。ランタイム中に、どのクラスのオブジェクトが考慮されているかがわかると、適切なバージョンの関数が呼び出されます。

関数がコンパイル後に特定のクラスにリンクされるため、このプロセスは遅延バインディングと呼ばれます。また、適切な関数の選択がランタイム中に動的に行われるため、動的バインディングまたは動的リンクとも呼ばれます。

3. ポリモーフィズムの種類

ポリモーフィズムには2つのタイプがあります。

| 1 | コンパイル時ポリモーフィズム
または早期バインディング
または静的バインディング
または静的リンクポリモーフィズム。オブジェクトはコンパイル時にその関数呼び出しにバインドされます。 |

| 2 | ランタイムポリモーフィズム
または遅延バインディング
または動的バインディング
または動的リンクポリモーフィズム。選択と適切な関数はランタイム中に動的に行われます。 |

| | |

動的バインディングはC++の強力な特徴の1つです。これにはオブジェクトへのポインタを使用する必要があります。オブジェクトポインタと仮想関数が動的バインディングを実装するためにどのように使用されるかを詳しく説明します。

4. ポインタでアクセスされる通常のメンバー関数

以下のプログラムは基底クラスで構成されています。

| | / 通常の 関数がポインタからアクセスされる /
/ クラスを使用したポリモーフィズム(仮想ポリモーフィズムを使用しない) /
#include
using namespace std;

class BASE
{
public :
void disp() { cout << “\nあなたはBASEクラスにいます”; }
};

class DERIVED1 : public BASE
{
public :
void disp() { cout << “\nあなたはDERIVED1クラスにいます”; }
};

class DERIVED2 : public BASE
{
public :
void disp() { cout << “\nあなたはDERIVED2クラスにいます”; }
};

int main()
{
DERIVED1 d1; // 派生クラス1のオブジェクト
DERIVED2 d2; // 派生クラス2のオブジェクト
BASE *b; // 基底クラスへのポインタ

b=&d1; // ポインタbにd1のアドレスを割り当てる
b->disp(); // disp()の呼び出し
b=&d2; // ポインタbにd2のアドレスを割り当てる
b->disp(); // disp()の呼び出し
return 0;
} |

上記のプログラムは次のことを示しています:

| | • BASEクラス
• BASEから派生したDERIVED1、DERIVED2クラス
• 派生クラスのオブジェクト(d1,d2)
• BASEクラスのポインタb | | | 出力 あなたはBASEクラスにいます
あなたはBASEクラスにいます |

5. 仮想関数

仮想とは実際には存在しないが、効果的に存在することを意味します。メンバー関数は、メンバー関数の前にキーワードvirtualを付けることで仮想関数にすることができます。

| | / クラスを使用したポリモーフィズム(仮想ポリモーフィズム) /
#include
using namespace std;
class B
{
public :
void show(){ cout << “\nclass BメソッドShow() “; }
virtual void disp() { cout << “\nclass Bメソッドdisp()”; }
};

class D : public B
{
public :
void show(){cout << “\nclass DメソッドShow() “; }
void disp(){ cout << “\nclass Dメソッドdisp()”; }
};

int main()
{
D d1;
d1.show();
d1.disp(); // 基底クラスメンバー

B b;
D d;
B *Bptr;
Bptr = &b;
Bptr->show();
Bptr->disp(); // 基底クラスメンバー

Bptr=&d;
Bptr->show(); // 派生クラスメンバー
Bptr->disp(); // 基底クラスメンバー
return 0;
} | | | 出力 class DメソッドShow()
class Dメソッドdisp() |

6. 純粋関数

基底クラスで定義され、派生クラスに関連する定義がない関数は純粋関数と呼ばれます。簡単に言えば、純粋関数は本体を持たない仮想関数です。

| | #include
using namespace std;
class B
{
public :
void show(){ cout << “\nclass BメソッドShow() “; }
virtual void disp() = 0; // 純粋仮想関数
};

class D : public B
{
public :
void show(){cout << “\nclass DメソッドShow() “; }
void disp(){ cout << “\nclass Dメソッドdisp()”; }
};

int main()
{
D d1;
d1.show(); // O/P : Class Dメソッドshow()
d1.disp(); // O/P : Class Dメソッドdisp()

D d;
B *Bptr;

Bptr=&d;
Bptr->show(); // O/P : Class Bメソッドshow()
Bptr->disp(); // O/P : Class Dメソッドdisp()
return 0;
} |

Bptr -> show() *は基底からのデフォルト実行可能関数です Bptr -> disp()は基底からのデフォルト実行可能関数ですが、仮想純粋関数として宣言されているため、ランタイムで派生クラスのdisp()*が呼び出されます。

| | / 純粋仮想関数の利点を示すプログラム /
#include
using namespace std;
enum boolean { false, true };
class NAME
{
protected : char name[20];
public :
void getname()
{ cout << “名前を入力してください :”; cin >> name; }

void showname()
{ cout << “\n名前は” << name; }

boolean virtual isGradeA() = 0; // 純粋仮想関数
};

class student : public NAME
{
private : float avg;
public :
void getavg()
{
cout << “\n学生の平均を入力してください :”;
cin >> avg;
}

boolean isGradeA()
{ return (avg>=80) ? true : false ; }
};

class employee : public NAME
{
private : int sal;
public :
void getsal()
{ cout << “\n給与を入力してください “; cin >> sal; }

boolean isGradeA()
{ return (sal>=10000) ? true : false ; }
};

int main()
{
NAME names[20]; // 名前へのポインタの数
student
s; // 学生へのポインタ
employee *e; // 従業員へのポインタ
int n = 0; // リスト上の名前の数
char choice;
do{
cout << “学生または従業員を入力してください (s/e) “;
cin >> choice;
if(choice==’s’)
{
s = new student; // 新しい学生を作成
s->getname();
s->getavg();
names[n++]=s;
}
else
{
e = new employee; // 新しい従業員を作成
e->getname();
e->getsal();
names[n++]=e;
}
cout << “別の名前を入力しますか (y/n) ?”; // もう一度
cin >> choice;
} while(choice==’y’);
for(int j=0; j{
names[j]->showname( );
if(names[j]->isGradeA( )==true)
cout << “彼はグレード1の人です”;
}
return 0;
} |

7. 代入とコピー初期化

C++コンパイラは常にあなたのために忙しく、あなたが面倒だと思うことを行っています。あなたが責任を持つと、コンパイラはあなたの判断に従います。そうでなければ、コンパイラは自分の方法で物事を行います。このプロセスの2つの重要な例が代入演算子とコピーコンストラクタです。

あなたは代入演算子を何度も使用してきたでしょう。おそらくあまり考えずに。a1a2がオブジェクトだとしましょう。コンパイラに別の指示をしない限り、次の文は

a2 = a1; // a2をa1の値に設定

コンパイラがa1からa2にデータをメンバーごとにコピーすることを引き起こします。これは代入演算子のデフォルトの動作です。

変数を初期化すること、別のオブジェクトでオブジェクトを初期化することにも慣れているでしょう。

alpha a2(a1); // a2をa1の値に初期化

これも同様の動作を引き起こします。コンパイラは新しいオブジェクトa2を作成し、a1からデータをメンバーごとにコピーします。これはコピーコンストラクタのデフォルトの動作です。

これらのデフォルトの活動は、コンパイラによって無償で提供されます。メンバーごとのコピーが必要な場合は、これ以上のアクションを取る必要はありません。ただし、代入または初期化がより複雑なことを行う必要がある場合は、デフォルト関数をオーバーライドできます。代入演算子とコピーコンストラクタのオーバーロードの技術については、別々に説明します。

代入演算子のオーバーロード

| | // 代入演算子( = )のオーバーロード
#include
using namespace std;
class alpha
{
private:
int data;
public:
alpha() { } // 引数なしコンストラクタ
alpha( int d )
{ data = d; } // 引数1つのコンストラクタ
void display()
{ cout << data; } // データを表示
alpha operator =(alpha & a) // オーバーロードされた=演算子
{
data = a.data; // 自動的には行われない
cout << “\n 代入演算子が呼び出されました “;
return alpha(data);
}
};

int main()
{
alpha a1(37);
alpha a2;
a2 = a1; // オーバーロードされた=を呼び出す
cout << “\n a2 = “; a2.display(); // a2を表示

alpha a3 = a2; // =を呼び出さない
cout << “\n a3 = “; a3.display(); // a3を表示
return 0;
}
| | | 出力: a2 = 37
a3 = 37 |

8. コピーコンストラクタ

前述のように、オブジェクトを別のオブジェクトの値で定義し、同時に初期化することができます。

alpha a3(a2); // コピー初期化
alpha a3 = a2; // コピー初期化、代替構文

どちらの定義スタイルもコピーコンストラクタを呼び出します。つまり、引数を新しいオブジェクトにコピーするコンストラクタです。コンパイラが自動的に提供するデフォルトのコピーコンストラクタは、メンバーごとのコピーを実行します。これは代入演算子が行うことに似ていますが、コピーコンストラクタは新しいオブジェクトも作成します。

次の例はコピーコンストラクタを示しています。

| | #include
using namespace std;
class alpha
{
private :
int data;
public:
alpha( ) { } // 引数なしコンストラクタ
alpha(int d) { data = d; } // 引数1つのコンストラクタ
alpha(alpha& a) // コピーコンストラクタ
{
data = a.data;
cout << “\nコピーコンストラクタが呼び出されました”;
}
void display( )
{ cout << data; }
void operator = (alpha& a) // オーバーロードされた=演算子
{
data = a.data;
cout << “\n代入演算子が呼び出されました”;
}
};

int main()
{
alpha a1( 37 );
alpha a2;

a2 = a1; // オーバーロードされた=を呼び出す
cout << “ a2 = “; a2.display(); // a2を表示

alpha a3( a1 ); // コピーコンストラクタを呼び出す

// alpha a3 = a1; // a3の同等の定義

cout << “ a3 = “; a3.display(); // a3を表示
return 0;
} |

上記のプログラムは、代入演算子とコピーコンストラクタの両方をオーバーロードします。
オーバーロードされた代入演算子は、前の例のものと似ています。

9. ‘this’ポインタ

C++は、メンバー関数を呼び出すオブジェクトを表すためにthisというユニークなキーワードを使用します。thisは、this関数が呼び出されたオブジェクトを指すポインタです。

このポインタは、オブジェクト自身を返すというタスクを単純に実行します。

次のプログラムはi, jオブジェクトを定義し、iに5の値を割り当て、iの全体のオブジェクトをそのメンバー関数によってjに割り当てます。

| | #include
using namespace std;
class A
{
int a;
public:
A() { }
A(int x)
{ a = x; }
void display()
{
cout << a;
}
A get()
{

return *this; // 自身を返す

}
};
int main()
{
A i(5);
A j;

j = i.get();
j.display();
return 0;
} |

参考: Turbo C++におけるオブジェクト指向プログラミング: ロバート・ラフォール

Share: X/Twitter LinkedIn

新しい投稿を受信箱で受け取る

スパムはありません。いつでも購読を解除できます。