プログラムを作成していると、これから実装したい機能と似たようなコードを別の場所で書いていたな・・・ということが起こります。
こういった場合の対応としてビギナープログラマにありがちなのは、その似たようなコードをコピーしてきて、必要な部分を修正して使うという方法です。このような方法を繰り返していると、やがてプログラムが似たようなコードで埋め尽くされて、コード量が増加してきます。 それだけならばまだ良いのですが、ここでもしもコピー元のコードにバグが含まれていたとしましょう。これを修正するのに、プログラム中にちりばめられたコピーコードのすべてを修正する必要が出てきます。
こういった状況に懲りたビギナーを卒業したプログラマは、もとのコードをコピーして使用するのはよくないことに気がつきます。決まった操作は一元化して記述しておけば、修正も容易です。元のコードをコピーして修正するのではなく、一元化した関数にパラメータを与えて挙動を変化させればいいのです。なかなかうまくいきそうな方法です。しかし、気がつくと元のコードには if 文やswitch文でちりばめられて、結局読みづらく保守性の悪いコードとなってしまうことが多々あります。
このような状況をスマートに解決する方法として、オブジェクト指向言語には継承と呼ばれる機能があります。継承とは、すでにあるなにかを引き継ぐことです。
継承の機能を使うことにより、すでに定義済みのオブジェクトに
・機能を追加
・変数を追加
・機能の一部を変更
などをエレガントに記述することができるようになり、オブジェクト(コード)の再利用性が向上します。すでに実装済みの機能に、自分が実装したい機能として足りない部分だけを追加してプログラムを作成できるようになりますので、これを差分プログラミングと呼んだりします。
C++の場合には、クラスを基盤としてプログラムを作成していきます。すでにあるクラスに継承の機能を使って新しいクラスを派生させます。もとになるクラスを基本クラスや基底クラスと呼び、派生して作成したクラスを派生クラスと呼びます。

base という名前のクラス定義がすでにあるとします。base には ひとつのメンバ変数とひとつのメンバ関数があります。
この、baseクラスの定義をもとにして、新しいクラスを書きたいときに「継承」という機能を使うことができます。
新しく定義する derived という名前のクラス定義を上図のように書くと、base クラスのメンバに加えて、さらにひとつづつのメンバ変数とメンバ関数を加えたクラスを定義することができます。 新しく作成したクラスは、二つのメンバ変数(var1, var2) と、二つのメンバ関数 (func1とfunc2)を持つことになります。class Student {
public:
char name[20]; //氏名
int kadai1; //課題1の点数
int kadai2; //課題2の点数
int ave; //平均点
void SetName(const char *n) { strcpy(name, n); }
void SetKadai1(int k1) { kadai1 = k1; }
void SetKadai2(int k2) { kadai2 = k2; }
char *Name() { return(name); }
int GetKadai1() { return(kadai1); }
int GetKadai2() { return(kadai2); }
int Average(){ return(ave); }
int GetSum(); //合計点を求める
void CalcAve(); //平均点を求める
};
課題1で使用した Studentクラスを派生して、以下のメンバを追加した派生クラス StudentEx を作成しなさい。
(Studentクラスはこの課題の為に合計点を返す関数が追加してあります。また、この課題では継承機能の基礎に集中して学習したいので、すべてのメンバは
public にしてあります。)
#define _CRT_SECURE_NO_WARNINGS
#include <string.h>
#include <iostream>
using namespace std;
class Student {
public:
char name[20]; //氏名
int kadai1; //課題1の点数
int kadai2; //課題2の点数
int ave; //平均点
void SetName(const char *n) { strcpy(name, n); }
void SetKadai1(int k1) { kadai1 = k1; }
void SetKadai2(int k2) { kadai2 = k2; }
char *Name() { return name; }
int GetKadai1() { return kadai1; }
int GetKadai2() { return kadai2; }
int Average(){ return ave; }
int GetSum(); //合計点を求める
void CalcAve(); //平均点を求める
};
int Student::GetSum()
{
return kadai1 + kadai2;
}
void Student::CalcAve()
{
ave = (kadai1 + kadai2)/2;
}
//ここに StudentEx クラスを実装
void main()
{
Student taro;
taro.SetKadai1(100);
taro.SetKadai2(80);
taro.CalcAve();
cout << "taro" << endl;
cout << "kadai1:" << taro.GetKadai1()
<< " kadai2:" << taro.GetKadai2() << endl;
cout << "ave:" << taro.Average()
<< " sum:" << taro.GetSum() << endl;
StudentEx jiro;
jiro.SetKadai1(90);
jiro.SetKadai2(70);
jiro.SetKadai3(50);
jiro.CalcAve();
cout << "jiro" << endl;
cout << "kadai1:" << jiro.GetKadai1()
<< " kadai2:" << jiro.GetKadai2()
<< " kadai3:" << jiro.GetKadai3() << endl;
cout << "ave:" << jiro.Average()
<< " sum:" << jiro.GetSum() << endl;
}
ヒント:main関数内では、Student型のオブジェクト taro と、StudentEx型のオブジェクト jiro を作成して、それぞれの課題の得点の設定と表示を行っています。
taro
kadai1:100 kadai2:80
ave:90 sum:180
jiro
kadai1:90 kadai2:70 kadai3:50
ave:80 sum:160
続行するには何かキーを押してください . . .
練習問題1-1の実行結果をみると、StudentEx クラスには三つめの課題の得点が追加されていて、その設定や取得が出来ていることがわかります。また、Studentクラスで実装されていた他の関数(SetKadai1やCalcAve、Average関数)もそのまま使用できています。このように、継承を使うことによりすでに実装済みの機能はそのままに、新しい機能を追加することができます。
ところが、実行結果を見ると、平均点と合計点の計算結果が間違っています。課題の数が増えたのに対して、平均点を計算するCalcAve関数では課題1と課題2の平均しか計算していないからです。
そこで StudentEx クラスでは、StudentExクラス用に CalcAve関数を書きなおす必要があります。
派生クラスでは、基底クラスにあったメンバ関数を同名・同引数で再定義することができます。
StudentExクラスで CalcAve関数を再定義し、正しく3科目の平均を計算するようにしなさい。
また、GetSum関数を再定義して、合計点を正しく表示するようにしなさい。
taro
kadai1:100 kadai2:80
ave:90 sum:180
jiro
kadai1:90 kadai2:70 kadai3:50
ave:70 sum:210
続行するには何かキーを押してください . . .
あるクラスのメンバ関数内から、同じクラス内のメンバ関数を呼び出すことができるのと同様に、派生クラスのメンバ関数内から基底クラスのメンバ関数を呼び出すことができます。
ところが、次の例を見てください。
class base {
void function();
};
void base::function()
{
....
}
class derived : public base {
void function(); // function を再定義
};
void derived::function()
{
....
ここから、基底クラスのfunction関数を呼びたい。。
}
derivedクラスで定義した derived::function関数の中から、基底クラスのfunction関数を呼び出したい場合があります。基底クラスの処理にちょっとだけ改変を加えたい場合などに必要になる処理です。ここで下のように記述してしまうと自分自身を再帰的に呼び出してしまいます。
void derived::function()
{
....
function();
}
このような場合には、スコープ解決演算子(::)を使って、基底クラスのメンバを明示的に呼び出します。
void derived::function()
{
....
base::function();
}
GetSum関数の実装を、kadai1、kadai2、kadai3 を加算して求めるのではなく、StudentクラスのGetSum関数の戻り値(kadai1とkadai2の合計)にkadai3の値を加算して求めるように書き直しなさい。
Studentオブジェクト、StudentExオブジェクトそれぞれのサイズを求めなさい。(ヒント:デバッガやsizeof演算子を使用する。)そして、それぞれがなぜそのような大きさになったかを考察しなさい。
C++ではオブジェクトの初期化にコンストラクタと呼ばれるメンバ関数を使うことができました。コンストラクタはオブジェクトがインスタンス化されたとき(メモリ上に配置された時)に自動的に呼び出されます。(もう少し別の言い方をすると、オブジェクトをメモリに割り当てた後に、コンストラクタの呼び出しコードがコンパイラによって自動で生成されます。)
継承関係にある複数のオブジェクトがコンストラクタを持つ場合、その呼び出し順序は基底クラスのコンストラクタから順に呼び出されます。
StudentクラスとStudentExクラスにそれぞれ引数をとらないコンストラクタを追加し、その呼び出し順序を確認しなさい。
ヒント:以下のようなコンストラクタを追加して、その表示順序を確認する。StudentExクラスにも同様に追加する。
class Student {
public:
Student() { cout << "Studentクラスのコンストラクタ" << endl; }
...
};
引数を持つコンストラクタをオーバーロードして作成し、オブジェクトの構築時にパラメータによって初期化することができました。基本クラスの Studentに、以下の名前、課題1と課題2の得点を受け取るコンストラクタを追加します。
class Student {
public:
Student() { cout << "Studentクラスのコンストラクタ" << endl; }
Student(const char* name, int k1, int k2); // 引数付きコンストラクタ
...
};
Student::Student(const char* name, int k1, int k2)
{
SetName(name);
kadai1 = k1;
kadai2 = k2;
}
Student クラスを使用する main 関数からは、以下のように初期化時に名前と得点の設定をすることができます。
Student taro("taro", 100, 80);
そこで、派生クラスであるStudentEx にも引数つきのコンストラクタを作成して、名前、課題1~3の得点をまとめて初期化するにはどうしたらよいでしょうか。まず、StudentExクラスにも、これらの4つの引数を受け取るコンストラクタを定義します。 このコンストラクタ内でそれぞれの変数に代入すると、次のようなコードになります。
class StudentEx : public Student {
public:
StudentEx(const char* name, int k1, int k2, int k3) {
SetName(name);
kadai1 = k1;
kadai2 = k2;
kadai3 = k3;
}
...
...
};
この例の場合では、上記のようなコードでも期待した動作はしてくれます。しかし、Studentクラスのコンストラクタが自動的に呼び出された後に、StudentExクラスのコンストラクタ内で代入をやり直すのは多少の無駄があります。(この例では「多少の無駄」で済んでいますが、オブジェクトの種類によっては、初期化してから代入をやりなおすのにかなりのコストがかかる場合があります。)
そこで、せっかく定義した Studentクラスの引数付きのコンストラクタを使用したいですよね。StudentExクラスのコンストラクタに、イニシャライザを指定することにより、基本クラスの初期化の方法を指定して引数を与えることができます。以下にその例を示します。
class StudentEx : public Student {
public:
StudentEx(const char* name, int k1, int k2, int k3) : Student(name, k1, k2) {
kadai3 = k3;
}
...
...
};
下線の部分がイニシャライザと呼ばれ、コンストラクタが呼び出される前に初期化を済ませたいものを記述します。この例では、StudentExに渡された引数のうち name, k1, k2 をそのままつかって 基本クラスの Student を初期化しています。
Studentクラス と StudentExクラスを、上記の引数つきのコンストラクタを使って実装しなさい。main関数も、コンストラクタでオブジェクトを初期化するように書き換えなさい。
練習問題1-6では、イニシャライザを基本クラスの初期化に使用しましたが、イニシャライザはメンバ変数の初期化にも使用できます。Studentクラスのコンストラクタで、メンバ変数の初期化にイニシャライザを使用した例を以下に示します。
class Student {
public:
...
int kadai1; //課題1の点数
int kadai2; //課題2の点数
...
Student() { cout << "Studentクラスのコンストラクタ" << endl; }
Student(const char* name, int k1, int k2);
...
};
Student::Student(const char* name, int k1, int k2) : kadai1(k1), kadai2(k2)
{
SetName(name);
}
上記の例のように、初期化したいメンバをカンマで並べて記述できます。C++では、int 型の kadai1やkadai2のような変数も、下線の部分のようにクラスの初期化と同様の書式で書くことができます。
またこの例のように、宣言部と実装部を分けて書いた場合、イニシャライザは実装部のほうに記述します。もし、どちらに書いて良いか悩んだら次の原則を思い出してください。
「宣言部はクラスを使う人に見せる場所、実装部は見せる必要が無い場所」
メンバや基本クラスの初期化にイニシャライザを使っていようがいまいが、クラスを使う人には関係ありません。ですからイニシャライザは実装部に書きます。
この原則は、関数のデフォルト引数の指定にも成り立ちます。クラスを使用する人は、デフォルト引数の値を知る必要があります。(デフォルト値を知らないと、引数を与えるかデフォルト値を使うかを決められませんからね。)ですから、デフォルト引数の指定は、宣言部に記述します。
これまで、Studentクラス、StudentExクラス、main関数をひとつのファイルに記述してきました。これらを以下の5つのファイルに分割してビルドしてください。
| Student.h | Studentクラスの定義 |
| Student.cpp | Studentクラスの実装 |
| StudentEx.h | StudentExクラスの定義 |
| StudentEx.cpp | StudentExクラスの実装 |
| main.cpp | main関数部 |
※ ヘッダファイルには、2重インクルード防止の措置を入れてください。
StudentExクラスからさらに派生クラス StudentEx2クラスを作成し、新たに課題4の得点を管理できるクラスとしなさい。StudentEx2クラスとヘッダは別ファイルで記述すること.また,main関数は、StudentEx2クラスの動作が確認できるように修正すること。