第4回 C++・クラスの継承


クラスの継承

オブジェクト指向プログラミングのありがたさは、 単にクラスを作りメンバー変数やメンバー関数に対してアクセス制御ができるだけでなく、 クラスの継承 (class inheritance) ができることにその利便性があります。 継承の部分をプログラム言語を例にとって見ていきます。

プログラム言語間の継承関係例

以下の図は、授業でもよく用いるプログラム言語の歴史的な関連性をグラフで表したものです。 たとえば、C++言語はC言語にクラスの概念を含むオブジェクト指向言語の特徴を導入することで誕生した経緯があります。一方、Java言語は、Xeroxの研究所で生まれたSmalltalk-80のほか、 C言語やC++言語の影響を受けて誕生したといわれています。 注意しなければならないのは、この矢印にある継承関係は、誕生時に影響を受けているという意味で、元となった言語の機能をすべて包括するわけではありません。 また、関数型言語の典型的な機能のひとつであるラムダ式 (lambda expression)に関しても、最新のバージョンであれば、C++(C++11以降), Java(バージョン8以降), Pythonなど、いずれの言語でも使えるような改訂がなされています。



車クラスでの階層と継承事例

以前Carクラスを示しました。「自動車」という言葉 の下位概念として、たとえば代表的な燃料を元に、 「ガソリン車」「ディーゼル車」「ハイブリッドカー」「電気自動車」「燃料電池自動車」などに分類することが可能です。 これを図で表すと以下のようになります。



また、「映画」というクラスを作ることを考えるとき、 大雑把にジャンルで分けて「アクション」「コメディ」「アドベンチャー」「アニメ」「歴史」「ミステリ」「SF」「ファンタジー」「戦争」など、少し列挙しただけでも、非常に細分化した分類を考えることができます。 これらの例で言いたいことは、通常、何か、特定の概念をさす(主として)名詞を使うと、しばしば、その上位概念や下位概念がある、ということに気づくと思います。 この上位概念、下位概念をオブジェクト指向言語では、 基底クラス (base class, super class) (上位概念)と派生クラス (derived class, sub class) (下位概念)で表現することができます。 今、「電気自動車」のクラスとしてEVcarクラスを考えます。 EVも自動車の一種であることは間違いないですからCarクラスの属性は自然にEVクラスにもあてはまります。 一方、電気自動車だからこそありうる「充電時間」「充電方法」「充電用プラグタイプ」「バッテリの種類」「電源ボルテージ」 などの属性は、電気自動車にユニークなものと考えられます。 このEVcarクラスを定義する際、Carクラスの派生クラスとして定義するには、C++言語では、たとえば以下のように記述します。


class EVcar: public Car {
 public:
	EVcar(){}/* コンストラクタ */
	EVcar(string batteryType, string plugType);/* コンストラクタ */
	~EVcar(){}/* デストラクタ */
	void setChargeTime(double chargeTime);/* 充電時間の設定 */
	double getChargeTime();/* 充電時間の問い合わせ */
	void setVoltage(double voltage);/* ボルテージの設定 */
	double getVoltage();/*ボルテージの問い合わせ */
 private:
	double chargeTime; /* 充電時間 */
	string batteryType; /* バッテリ種類 */
	string plugType; /* 充電プラグタイプ */
	double voltage; /* ボルテージ (V) */
};

この例でわかるように、クラスの定義で、

class EVcar:   public Car

のように、記述しています。これは、
        EVcarはCarクラスを基底クラスとして継承する派生クラスである
という意味になります。 派生クラスでは、基底クラスで定義されたメンバー変数やメンバ関数を引き継ぎます。 ただし、派生クラスから自由にアクセスできるのは、 基底クラスのpublicで宣言されたメンバー変数、メンバ関数と、 これまでは、例示していなかった、 protected アクセス制御子で指定されるメンバー変数とメンバ関数となります。 逆に、privateで指定されたメンバー変数やメンバ関数は 派生クラスからは、アクセスできませんので注意してください。 以下の表に、public, protected, privateに対するアクセス制御をまとめています。

アクセス指定子(制御子) public protected private
クラス内から(又はfriendクラスやfriend関数)
派生クラスから ×
他のクラスから(又はmain関数) × ×


以下に自動車クラス(基底クラス)と電気自動車クラス(派生クラス)の定義例、ならびに、 それぞれのメンバー関数定義例と電気自動車に対するサンプルプログラムを示します。 なお、Car.hは、前回の資料のプログラム3-11とは、内容が変わっています。 特に、派生クラスを意識した仮想関数やprotectedアクセス識別子のラベルが加わっています。
Car.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* プログラム4-1: 自動車クラス */
#include <iostream>
using namespace std;

class Car {
	public:
		Car(){}/* コンストラクタ */
		Car(int year, double distance);/*コンストラクタ */
		Car(const Car &car);/* コピーコンストラクタ */
		virtual ~Car(){}/* デストラクタ */
		virtual void print();/* 仮想関数:購入価格をプリント */
		double calculateValue();/* 現在の価格を計算 */
		void setPrice( double price );/* 販売価格の設定 */
		double getPrice(); /* 販売価格の入手 */
	private:
		string plateNumber; /* プレートナンバー */
		double kph; /* 時速(KPH) kilometer per hour */
		double horsePower; /* 馬力 */
		int year; /* 製造年 */
		string color; /* 色 */
		double price; /* 販売価格 */
		double length; /* サイズ:前後 */
		double width; /* サイズ:左右 */
		double height; /* サイズ:高さ */
	protected: /* 派生クラスからアクセス可、他クラスやmainからは不可 */
		string maker; /* メーカー */
		string type; /* 車種 */
		enum { diesel, gas, HV, FCV, EV } energy;/* 燃料 */
		double distance;/* 走行距離 */
};
自動車クラスの派生クラスである電気自動車クラスは 以下のように定義するとします。
EVcar.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* プログラム4-2: 電気自動車クラス */
#include "Car.h"
class EVcar: public Car {
 public:
	EVcar(){}/* コンストラクタ */
	EVcar(string batteryType, string plugType)/* コンストラクタ */
	 : Car(2016, 0.0), batteryType(batteryType), plugType(plugType)/* 初期化リスト */
	 { energy = EV; } /* Carクラスのprotected要素は派生クラスからアクセス可能 */
	~EVcar(){}/* デストラクタ */
	void setChargeTime(double chargeTime);/* 充電時間の設定 */
	double getChargeTime();/* 充電時間の問い合わせ */
	void setVoltage(double voltage);/* ボルテージの設定 */
	double getVoltage();/*ボルテージの問い合わせ */
	void print(); /* 仮想関数に対応するprintメソッド */
 private:
	double chargeTime; /* 充電時間 */
	string batteryType; /* バッテリ種類 */
	string plugType; /* 充電プラグタイプ */
	double voltage; /* ボルテージ (V) */
};
さて、プログラム4-1とプログラム4-2を見比べて気づくと 思いますが、クラスの継承がされている点以外に、

virtual void print()

という関数がCar.hに登場しています。 これは仮想関数(virtual function と呼ばれるもので、基底クラスと派生クラスで名前も、引数も、戻り値も全く同じ関数をC++言語では、 このように呼びます。 なお、派生クラスでvirtualという修飾子をつけても問題ありません。 また、基底クラスの仮想関数は、 派生クラスで再定義することで、 オーバーライド(overrideされている、といいます。 関数(や演算子)の多重定義であるオーバーロード(overload) と合わせて、ポリモルフィズム(多態性、多相性) と呼ばれるオブジェクト指向言語の独特なプログラミング手法のひとつと位置付けられます。 ちなみに、Java言語には仮想関数はありませんが、 抽象関数(C++では、純粋仮想関数に相当)により関数をオーバーライドすることが可能です。 なお、上例のprint関数のように、 (少なくとも)基底関数の仮想関数にvirtualという修飾子をつけていますが、 これがないとオーバーライドになりませんので注意してください。

また、Carクラスにしかなく、EVcarクラスからは隠蔽されているprivateな変数があることにも注意してください。たとえばプレートナンバーや馬力などの変数はEVcarクラスからはアクセスできません。これは、最初にプログラミング言語の継承関係で示したC言語とC++言語の関係にもあてはまります。たとえば、以下の関数はC言語では定義可能ですが、C++言語では定義できません。これは可変長配列引数という機能がC言語だけにprivateなモードで実装されていると考えることで説明できます。

/* C言語の可変長配列引数を持つ関数例 (C++言語では不可) */
void matmul(int m, int n, int r, double a[m][r], double b[r][n], double c[m][n]){
    int i, j, k;
    for (i=0; i < m ;i++)  for (j=0; j < n ;j++) c[i][j] = 0.0; /* 初期化 */
    for (i=0; i < m ;i++)  for (j=0; j < n ;j++) for (k=0;k < r;k++)  c[i][j]+=a[i][k]*b[k][j];
    return;
}


メンバー関数は以下のCar.cppで定義しておきます。
Car.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* プログラム4-3: 自動車クラスのメンバー関数 */
#include "Car.h"
#include <iomanip>

/* Carクラスのメンバー関数の定義 */
/* コンストラクタ */
Car:: Car(int year, double distance) : year(year), distance(distance){/* コンストラクタ */
	this->energy = gas; /* デフォルトの燃料:ガソリン */
}

void
Car::print(){/* 仮想関数(基底クラス内)の実装例 */
	cout << "【基底クラス(Carクラス)printメソッド】" << endl;
	cout << "購入価格 = \\" << fixed << setprecision(1) << setw(8) << price << endl;
	string label;
	switch (energy){/* 燃料をプリント */
		case diesel: label = "ディーゼル"; break;
		case gas: label = "ガソリン"; break;
		case HV: label = "ハイブリッド"; break;
		case FCV:label = "燃料電池自動車"; break;
		case EV: label = "電気自動車" ; break;
		default: label = "未定義燃料" ; break;
	}
	cout << "燃料:" << label << endl;
	cout << "走行距離:" << distance << endl;

}

/* 価格見積もり関数 (実際の中古車価格モデルとは異なる想像モデル)*/
const float THIS_YEAR = 2016.0;
const float YEAR_COEF  = 20.0; /* 製造年から見た中古度合係数 */
const float MILEAGE_COEF = 0.085; /* 走行距離での減額係数 */
const float DOWN_COEF = 0.55; /* 全体係数 */
double
Car::calculateValue(){/* 中古車見積もり例 */
	double t = ::YEAR_COEF * (year - ::THIS_YEAR) ;
	double d = ::MILEAGE_COEF * distance;
	double estimate = ::DOWN_COEF * (price  +  t * d);
	return estimate;
}
double
Car::getPrice(){ return price;}/* 価格を返す関数 */

void
Car:: setPrice(double price){/* 価格を設定 */
	this->price = price;
}
電気自動車に関するメンバー関数の実装例は以下のようです。 printメソッドの中で基底クラスのprotected変数にアクセスしている点に注意してください。
EVcar.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* プログラム4-4: 電気自動車のメンバー関数例 */
#include "EVcar.h"

void
EVcar::setChargeTime(double chargeTime){/* 充電時間の設定 */
	this->chargeTime = chargeTime;
}

void
EVcar::setVoltage(double voltage){/* ボルテージの設定 */
	this->voltage = voltage;
}

void 
EVcar::print(){ 
	cout << "【派生クラス(EVクラス)】" << endl;
	cout << "バッテリ:" << batteryType << endl;
	cout << "プラグ:" << plugType << endl;
	cout << "チャージ時間:" << chargeTime << endl;
	cout << "ボルテージ:" << voltage << endl;
	if (energy != EV)/* Carクラスのprotected要素にアクセス */
		cout << "電気自動車に" << energy << "が誤って設定されています" << endl;
	Car::print();/* 基底クラスのprintメソッドを明示的に呼び出す */
}
基底クラスも派生クラスも仮想関数(ここではprint関数)は同じ型で同じ引数で 定義されており、内容だけが異なる点に注目してください。 また、派生クラス内の再定義された関数から基底クラスの関数を明示的に呼び出すこともできます。 その際、基底クラスのクラス名::を頭につけてprint関数を呼び出します。 一般に、基底クラスでは、そのクラスのprivateやprotectedメンバー変数をプリントします。 一方、派生クラスでは、派生クラス独自のメンバー変数をプリントするのが通例です。 ただし、protected変数にもアクセスできますので、必要に応じてアクセスして使うことがあります。 実際EVcarクラスでは、電気自動車に関連するメンバー変数だけをプリントしています。 これを以下のようなメインプログラムを含むソースコードで実行してみます。
EVcarMain.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* プログラム4-5: 電気自動車に関するmain関数での実施例*/
#include "EVcar.h"
int main(){
	Car *base = new Car(2004, 54000.0);/* 基底クラスのオブジェクト */
	base->setPrice(1500000.0);/* 価格の設定 */
	EVcar ev1("Litium", "CHAdeMO");/* 電気自動車クラス(派生クラス)のインスタンス生成 */
	ev1.setChargeTime(240.0);/* 4時間の充電時間を設定 */
	ev1.setVoltage(200.0);/* 200Vに設定 */
	ev1.setPrice(2000000);/* 基底クラスの価格を設定 */
	ev1.print();/* 派生クラスのprint */
	Car *car = base;/* 基底クラスのオブジェクトのprintメソッドを呼出したい場合 */
	car->print();/* 基底クラスのprintメソッドの呼出し */
	EVcar* ev2 = new EVcar("Lead-acid", "Combo");/* 派生クラスのインスタンス生成 */
	ev2->setChargeTime(120.0);/* 2時間の充電時間を設定 */
	ev2->setVoltage(240.0);/* 200Vに設定 */
	ev2->setPrice(4000000);/* 基底クラスの価格を設定 */
	ev2->print();

	//EVcar *ev3 = &car2; //Error!(派生クラスのポインタに基底クラスのポインタは代入できない)
	
	Car *car2 = ev2;//基底クラスのポインタに派生クラスのポインタを代入するのはOK
	car2->print();
	return 0;
}
プログラム4-5の15行目に示すように、 基底クラスのポインタ(または参照変数)に派生クラスのアドレス(ポインタ)を代入することができます。 しかし、注意すべきは、 この逆はできません!
派生クラスを複数用意して、それらのアドレスを基底クラスのポインタ(または参照)変数に代入して仮想関数を利用して、基底クラスの関数をアクセスするだけで、派生クラスの対応する仮想関数にアクセスする手法は、非常に有用であり、クラスの継承の真髄ともいえますので、いろいろな例で修得してください。(あとで、食べ物クラスとその派生クラスの例を述べます。)

main関数が定義できたので、これまでのように、以上のファイルを用意し、以下のようなMakefileでmakeしてから実行してみます。

Makefile
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#
# Makefile ソフトウェア演習(アドバンスコース)
#
# $ make  コンパイルして(ライブラリなしで)実行モジュールを作成
# $ make clean オブジェクトファイルを削除
# 
#
# for C++ define  CC = g++
C++ = g++
CFLAGS  = -Wall -std=c++14
OFILES = Car.o EVcar.o EVcarMain.o
default: EVcarMain

EVcarMain:$(OFILES) 
	$(C++) $(CFLAGS) -o EVcarMain $(OFILES)

# 依存関係
Car.o:  Car.h Car.cpp
	$(C++) $(CFLAGS) -c Car.cpp
EVcar.o: EVcar.cpp EVcar.h Car.h
	$(C++) $(CFLAGS) -c EVcar.cpp
EVcarMain.o: EVcarMain.cpp Car.h EVcar.h
	$(C++) $(CFLAGS) -c EVcarMain.cpp
#
clean: 
	$(RM) *.o

実行結果は以下のようです。 基底クラスの変数に、派生クラスのポインタを代入したあと、 print関数を呼び出すと、基底クラスの変数であっても、 派生クラス側のprint関数が呼び出されているのがわかります。

$ make
g++ -Wall -std=c++14 -c Car.cpp
g++ -Wall -std=c++14 -c EVcar.cpp
g++ -Wall -std=c++14 -c EVcarMain.cpp
g++ -Wall -std=c++14 -o EVcarMain Car.o EVcar.o EVcarMain.o
$ ./EVcarMain
【派生クラス(EVクラス)】
バッテリ:Litium
プラグ:CHAdeMO
チャージ時間:240
ボルテージ:200
【基底クラス(Carクラス)printメソッド】
購入価格 = \2000000.0
燃料:電気自動車
走行距離:0.0
【基底クラス(Carクラス)printメソッド】
購入価格 = \1500000.0
燃料:ガソリン
走行距離:54000.0

仮想関数で基底クラスのポインタに派生クラスを代入しprintを呼出すと?
【派生クラス(EVクラス)】
バッテリ:Litium
プラグ:CHAdeMO
チャージ時間:240.0
ボルテージ:200.0
【基底クラス(Carクラス)printメソッド】
購入価格 = \2000000.0
燃料:電気自動車
走行距離:0.0
【派生クラス(EVクラス)】
バッテリ:Lead-acid
プラグ:Combo
チャージ時間:120.0
ボルテージ:240.0
【基底クラス(Carクラス)printメソッド】
購入価格 = \4000000.0
燃料:電気自動車
走行距離:0.0
【派生クラス(EVクラス)】
バッテリ:Lead-acid
プラグ:Combo
チャージ時間:120.0
ボルテージ:240.0
【基底クラス(Carクラス)printメソッド】
購入価格 = \4000000.0
燃料:電気自動車
走行距離:0.0

もし、Carクラスの定義でprintメソッドを仮想関数にしないと?

(後半部分のprintの)結果は、以下のようになります。 この場合、基底クラスの変数に一旦派生クラスの変数(のポインタ)が代入されていますが、 print関数は基底クラスのほうが呼び出されます。 しかし、10行目で派生クラスの変数で価格が200万円に設定されている部分は反映されています。



これらを比較してわかるように、基底クラスのポインタに派生クラスのオブジェクトのアドレスを代入した場合、 printメソッドを呼出すと、仮想関数でない場合は、基底クラスのprintメソッドが呼出されますが、 仮想関数にしておくと、 たとえ元が基底クラスのポインタであったとしても派生クラスのオブジェクトのアドレスを代入した時点で、 オーバーライドされた派生クラスのprintメソッドが呼出されることになります。

プリント関数などでは、「仮想関数のありがたみ」はあまり伝わりませんが、 たとえば1kmあたりの燃費を計算する関数を仮想関数にしておけば、 電気自動車のこのタイプならいくらで、ガソリン自動車やディーゼル自動車だといくらで、 ハイブリッドや燃料電池ではいくら、 というように「燃費計算」という概念は同じでも、 計算方法が派生クラスごと変化するような場合、 オーバーライドという概念は非常に有効な方法といえます。 なお、上のプログラムに出てきた

Car *car = &ev1;

基底クラスのポインタに派生クラスのポインタ(アドレス)を代入しています。 これが許されるのは、派生クラスが基底クラスを継承しているからにほかなりません。 同様に、

Car *car = new EVcar("リチウム", "SAE J1772");

のような「基底クラスのポインタ=派生クラスのアドレス」という代入文は、 プログラム4-5のコメントで述べたとおり、許されます。 なお、仮想関数はクラス内の通常の関数で定義できます。 また、デストラクタを仮想関数にすることができます。 実は、これはクラスの継承+仮想関数環境下で非常にしばしば利用されます。 理由は、「基底クラスのポインタ=派生クラスのアドレス」という代入文を行ってデータを(vectorやlistやmap等で)管理する場合、 基底クラス側で、delete文で呼び出すだけで、 派生クラスのデストラクタが呼び出されるからです。なお、 コンストラクタは仮想関数にはできません。

オブジェクト・スラインシング問題

仮想関数(Javaなら抽象関数)とクラスの継承をうまく利用すると、 基底クラス(親クラス、スーパークラスとも呼ぶ) の仮想関数から派生クラスの対応する関数を自然に呼び出すことができます。 しかし、便利である一方で、注意すべき点もあります。 先ほどのプログラムに生成したオブジェクトのサイズをプリントしてみます。
EVcarMainSize.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
29
20
21
22
23
24
25
/* プログラム4-6: 電気自動車に関するmain関数での実施例*/
#include "EVcar.h"
int main(){
	Car *base = new Car(2004, 54000.0);/* オブジェクト1 */
	base->setPrice(1500000.0);/* 価格の設定 */
	cout << "sizeof(Car *base) = " << sizeof(*base) << endl;
	cout << "★派生クラス★ ";
	EVcar ev1("Litium", "CHAdeMO");/* 電気自動車クラスのインスタンス生成 */
	ev1.setChargeTime(240.0);/* 4時間の充電時間を設定 */
	ev1.setVoltage(200.0);/* 200Vに設定 */
	ev1.setPrice(2000000);/* 基底クラスの価格を設定 */
	ev1.print();/* 派生クラスのprint */
	cout << "sizeof(EVcar ev1) = " << sizeof(ev1) << endl;
	cout << "★基底クラス★ ";
	Car *car = base;
	car->print();/* 基底クラスのprintメソッドの呼出し */
	cout << "sizeof(Car *car) = " << sizeof(*car) << endl;
	cout << endl <<
		"仮想関数で、基底クラスのポインタに派生クラスを代入しprint関数を呼出すと?" << endl;
	car = &ev1;/* 基底クラスのポインタ変数に派生クラスのアドレスを代入 	*/
	cout << "after car = &ev1; sizeof(Car *car) = " << sizeof(*car) << endl;
	car->print();/* 基底クラスのポインタだが派生クラスのprintメソッドの呼出し */
	delete base;//メモリ開放
	return 0;
}
実行結果は以下のようです。なお、Car.hは、仮想関数を持ってい方を使ったプログラムを実行しています。



実行結果を見て分かるように、ベースクラスのオブジェクトのサイズよりも、派生クラスのオブジェクトのサイズのほうが大きいことがわかります。 (赤色でマークした部分) 一方、基底クラスの変数に派生クラスの変数を代入することができるわけですが、その場合、仮想関数のような便利な機能があるかわりに、基底クラスのオブジェクトのサイズが小さいため、派生クラスが持っていた独自の変数領域は切り取られてしまいます。 このような現象をオブジェクト・スライシングと呼びます。 ある意味では当たり前ですが、派生クラス全体をアクセスしたい場合と、基底クラスから仮想関数を通して継承するクラスを一括してアクセスしたい場合とで、見える領域が違うことを知っていることは重要です。

Shopクラスでのクラス継承例

クラスの継承として別の事例を考えます。 いろいろな商品の販売を行う仮想商店を考えます。 クラスとして、商品クラス、商品在庫管理クラス、店クラスの3つを導入します。 商品クラス(Goodsクラス)には、商品の名前(string)と定価(int)をクラス変数として持たせ、 これらのアクセス法をpublicな関数で持たせます。特に、コンストラクタとして、
	Goods(name, price)
を用意するとします。 商品在庫管理クラス(GoodsStockクラス)は、Goodsクラスの変数と在庫数(int)をクラス変数として 持たせ、これらのアクセス法をpublicな関数で持たせます。 また、整数を返すbuyGoods(num)関数を用意し、「商品をnum個、買いたい」 という購入要求を出し、在庫数をチェックし、購入可能な数を返す関数を作成するとします。 店クラス(Shop class)には、店の名前(string)、電話番号、商品在庫リスト(vector 型)をもたせるとします。 店クラス、商品クラスを題材として、クラスの継承の別の事例を紹介します。まず、商品クラスですが、これを継承するクラスをあとで定義するため、printメソッドをvirtual【仮想】関数にします。また、クラスの継承時のメモリの解放に便利なデストラクタも仮想関数にしておきます。それ以外は、通常のクラスと同様です。 具体例として、店に関しては、
	東京店:TEL 03-1234-5678
	豊橋店:TEL 0532-44-9876
を与え、東京店に商品として、以下の商品を用意します。
	Goods("海洋深層水", 1200), 在庫数N1 (N1は1~30の整数)
	Goods("ビタミンC", 350), 在庫数N2 (N2は1~30の整数)
	Goods("アガリクス",2000),  在庫数N3 (N3は1~30の整数)
豊橋店には商品として、以下の商品を用意します。
	Goods("天然黒酢",890),     在庫数N4 (N4は1~20の整数)
	Goods("ロイヤル葉緑素",1600),在庫数N5 (N5は1~20の整数)
	Goods("アロエはちみつ",650), 在庫数N6 (N6は1~20の整数)
今、以下の問題を考えます。 自分が顧客と仮定し、東京店の全商品からランダムに1つ商品を選び、それをN7個買うとします。 一方、豊橋店の全商品からランダムに商品を1つ選び、それをN8個買うと仮定します。 ただし、N7,N8は1~10の間のランダムな個数(整数値)とします。 このとき、2つの店での買い物を合計し、必要金額をプリントする問題を考えます。 ただし、すべての商品の定価に8%の消費税がかかるとします。 万一、在庫数より、多く買おうとしたときは、在庫までの数までの販売にとどめ、 「在庫はxxだけで、在庫限り販売します。」と書き出すこととします。
Goods.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* プログラム4-7: 商品クラス */
#ifndef GOODS_H
#define GOODS_H
#include <iostream>
using namespace std;

class Goods {
	private:
		string name; // 商品名
		int price; //定価
	public:
		Goods() : name(""), price(0) {}//無引数コンストラクタ
		Goods(string name, int price): name(name), price(price) {}//コンストラクタ
		virtual ~Goods(){} //デストラクタは仮想関数
		string getGoodsName(){//商品名を返す
			return name;
		}
		int getPrice(){//定価を返す
			return price;
		}
		virtual void print(){//商品のプリント
			cout << "Goods: " << name << " " << price << "円" << endl;
		}
};
#endif
次に、商品クラスを 継承するMedGoodsクラス(薬クラス)を作成してみます。 privateなメンバー変数として、処方箋の必要な薬かどうかのbool変数を加えておきます。 コンストラクタの初期化は電気自動車の場合と似ていますが、基底クラスであるGoodsのコンストラクタを初期化リストで呼び出します。 なお、Java言語のクラスの継承では、初期化リストがないので、基底クラスのコンストラクタを呼び出すには、superというキーワードを使った別のメカニズムを利用します。
MedGoods.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* プログラム4-8: 薬クラス */
#ifndef MEDGOODS_H
#define MEDGOODS_H
#include <iostream>
using namespace std;
#include "Goods.h"

class MedGoods: public Goods {
	private:
		bool prescription;//処方箋の有無
	public:
		MedGoods(string name, int price, bool prescription): Goods(name,price), 
			prescription(prescription){}
		bool getPrescription(){//処方箋の有無を返す
			return prescription;
		}
		void print(){//薬品のプリント
			string s = (prescription == true) ? "必要" : "不要";
			cout << "MedGoods: " << getGoodsName() << " " << getPrice() << "円" << 
				" 処方箋:" << s << endl;
		}
};
#endif
FlowerGoods.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* プログラム4-8-2: 花クラス */
#ifndef FLOWERS_H
#define FLOWERS_H
#include <iostream>
using namespace std;
#include "Goods.h"

class FlowerGoods: public Goods {
	private:
		bool gifts;//贈答品の有無
		string bestBefore;// 賞味期限
	public:
		FlowerGoods(string name, int price, bool gifts, string bestBefore): 
			Goods(name,price), gifts(gifts), bestBefore(bestBefore) {}
		bool getGifts(){//贈答品の有無を返す
			return gifts;
		}
		string getBestBefore(){//賞味期限を返す
			return bestBefore;
		}
		void print(){//花のプリント
			string s = (gifts == true) ? "はい" : "いいえ";
			cout << "FlowerGoods: " << getGoodsName() << " " << getPrice() << "円" << 
				" 贈答品:" << s << " 賞味期限:" << bestBefore << endl;
		}
};
#endif
GoodsStockクラスは、Goodsクラスへのポインタと在庫数のリストをもたせることにします。ポインタにすることで、(main関数などから)派生クラスのポインタを代入できるからです。なお、assert関数をprintメソッドで使用してみました。 これは、エラーチェックにとても便利です。 使用する場合は、#include <cassert>を忘れないでください。
GoodsStock.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/* プログラム4-9: 商品在庫クラス */
#ifndef GOODS_STOCK_H
#define GOODS_STOCK_H
#include <iostream>
using namespace std;
#include <cassert>
#include "Goods.h"

class GoodsStock {
	private:
		Goods *goods; // 商品 
		int stock; //在庫数
	public:
		GoodsStock(Goods *goods , int stock): goods(goods), stock(stock){}//コンストラクタ
		Goods* getGoods() {//商品を返す
			return goods;
		}
		int getStock(){//在庫数を返す
			return stock;
		}
		int buyGoods(int number){//number個だけ商品を買う
			if (stock>=number){/* 在庫が十分にある場合 */
				stock -= number;
				return number;
			}
			else if (stock == 0){
				cout << "GoodsStock: 在庫がありません" << endl;
				return 0;
			}
			else {
				cout << "GoodsStock: 在庫は" << stock << 
					"だけで、在庫限り販売します" << endl;
				int saveStock = stock;
				stock = 0; 
				return saveStock;
			}
		}
		void print(){//商品ストックのプリント(商品のプリントを呼び出す)
			assert(goods != nullptr);
			cout << "GoodsStock (商品): " << endl;
			goods->print();
			cout << "GoodsStock (在庫数):" << stock << endl;
		}
};
#endif
店のクラス(Shopクラス)は、別のクラスに継承されるため、Goodsクラスと同様に、 デストラクタとprintメソッドを仮想関数としています。
Shop.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/* プログラム4-10: 店クラス */
#ifndef SHOP_H
#define SHOP_H
#include <iostream>
#include <vector>
using namespace std;
#include <cassert>
#include "GoodsStock.h"

class Shop {
	private:
		string name; // 店の名前
		string telephone; //電話番号
		vector <GoodsStock> goodsStockList;//商品在庫リスト
	public:
		Shop(string name, string telephone): name(name), telephone(telephone) {}//コンストラクタ
		virtual ~Shop(){}//仮想デストラクタ
		void setGoodsStockList(vector <GoodsStock>& goodsStockList){//商品リストをセット
			this->goodsStockList = goodsStockList;
		}
		string getShopName(){//店名を返す
			return name;
		}
		string getTelephone(){//電話番号を返す
			return telephone;
		}
		vector <GoodsStock>&  getGoodsStockList(){//在庫数を返す
			return goodsStockList;
		}
		int buyRequest(Goods *goods, int num){//購入要求処理
			assert(goods != nullptr);
			for (auto i=0; i < (int)goodsStockList.size(); i++){
				GoodsStock x = goodsStockList[i];
				Goods *y = x.getGoods();
				assert(y != nullptr);
				if (goods->getGoodsName() == y->getGoodsName()){//match
					return(x.buyGoods(num));
				}
			}
			cout << name <<"には" << goods->getGoodsName() <<"はありません"<< endl;
			return(0);
		}
			
		virtual void print(){//店のプリント(商品はイタレータでプリント)
			cout << "Shop: " << name << " " << telephone << endl;
			for (auto i=0; i < (int)goodsStockList.size(); i++){
				GoodsStock x = goodsStockList[i];
				x.print();
			}
			cout << endl;
		}
};
#endif
Shopクラスを継承するクラスとして、花屋(FlowerShop)クラスを定義した例です。 コンストラクタの初期化では、基底クラスのコンストラクタを呼び出します。 private変数としては、FlowerGoodsクラスのvector(可変長リスト)をもたせます。
FlowerShop.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* プログラム4-11: 花屋クラス */
#ifndef FLOWERSHOP_H
#define FLOWERSHOP_H
#include <iostream>
#include <vector>
using namespace std;
#include "Flowers.h"
#include "Shop.h"

class FlowerShop: public Shop { /* 花屋クラス */
	private:
		vector <FlowerGoods> flowers;//花のリスト
	public:
		FlowerShop(string name, string telephone, vector <FlowerGoods>flowers):
			Shop(name, telephone), flowers(flowers) {}//コンストラクタ
		vector <FlowerGoods> getFlowers(){//花のリストを返す
			return flowers;
		}
		void print(){//店のプリント(花はイタレータでプリント)
			cout << "FlowerShop: " << endl;
			for (auto i=0; i < (int)flowers.size(); i++){
				Goods goods = flowers[i];
				goods.print();
			}
			cout << "--------------------------------------" << endl;
		}
};
#endif
main関数を含むShopMain.cppでは、整数[min,max]の間の乱数発生器や ヘッダー生成を外部関数とし、2つの店(東京店と豊橋店)を定義します。 東京店は薬屋(ドラグストア)とし、豊橋店は花屋としています。 それぞれ3種類の商品を扱っているとしています。 その後、それぞれの商品の取扱い数を乱数で決め、GoodsStockクラスのオブジェクトを商品種類だけ生成します。 生成された商品を(setGoodsStockListメソッドで)お店にリンクさせ、 仮想的なショッピングを実行しています。
ShopMain.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/* プログラム4-12: 
	File: ShopMain.cpp
	メイン関数を含む店クラスを用いた仮想買い物
	作者:foo.bar
	Date: blah blah blah
*/
#include <iostream>
using namespace std;
#include <cstdlib>
#include <ctime>

int getRand(int min, int max){// [min,max]の間の乱数を発生
	int range = max - min + 1;
	int value = (rand() % range) + min;
	return value;
}

auto myHeader(){/* 課題に限らず、できるだけ作者と日付を残す */
    time_t now = time(nullptr);
    struct tm* local = localtime(&now);
    cout << "**************************************" << endl;
    cout << "作成者:青野雅樹, 01162069" << endl;
  	cout << "日付:" << local->tm_year+1900 << "年" << local->tm_mon+1 << "月" <<
  		local->tm_mday << "日" << local->tm_hour << "時" <<
  		local->tm_min << "分" << local->tm_sec << "秒" << endl;
    cout << "内容:クラスの継承例" << endl;
    cout << "**************************************" << endl;
    return now;
}

#include <vector>
#include "Shop.h"
#include "Flowers.h"
#include "MedGoods.h"

int main(int ac, char **av){
	int N1, N2, N3, N4, N5, N6, N7, N8;

	time_t now = ::myHeader();
	srand ( now );/* 現在時刻で乱数を初期化 */
	
	Shop tokyo("東京店","03-1234-5678");
	Shop toyohashi("豊橋店","0532-44-9876");

	MedGoods mg1("海洋深層水", 1200, false);
	MedGoods mg2("アガリクス", 2000, false);
	MedGoods mg3("抗インフルエンザ抗生物質",2800, true);

	FlowerGoods fg1("バラ", 3000, true, "2019-7-11");
	FlowerGoods fg2("カーネーション", 1800, false, "2019-7-20");
	FlowerGoods fg3("胡蝶蘭", 25000, true, "2019-7-31");
	
	const int MAXR1 = 30;
	const int MAXR2 = 20;
	N1 = ::getRand(1,MAXR1); GoodsStock gs1(&mg1,N1);//海洋深層水をN1個
	N2 = ::getRand(1,MAXR1); GoodsStock gs2(&mg2,N2);//ビタミンCをN2個
	N3 = ::getRand(1,MAXR1); GoodsStock gs3(&mg3,N3);//アガリクスをN3個
	N4 = ::getRand(1,MAXR2); GoodsStock gs4(&fg1,N4);//天然黒酢をN4個
	N5 = ::getRand(1,MAXR2); GoodsStock gs5(&fg2,N5);//ロイヤル葉緑素をN5個
	N6 = ::getRand(1,MAXR2); GoodsStock gs6(&fg3,N6);//アロエはちみつをN6個
	cout << "N1 = " << N1 << ", " << "N2 = " << N2 << ", "
			<< "N3 = " << N3 << ", " << "N4 = " << N4 << ", "
			<< "N5 = " << N5 << ", " << "N6 = " << N6 << endl;

	cout << "==== お店情報========================" << endl;
	vector <GoodsStock> v1 = {gs1, gs2, gs3};//東京店用商品ストック
	tokyo.setGoodsStockList(v1);//東京店にセット
	tokyo.print();//東京店の商品をプリント

	vector <GoodsStock> v2 = {gs4, gs5, gs6};//豊橋店用商品ストック
	toyohashi.setGoodsStockList(v2);//豊橋店にセット
	toyohashi.print();//豊橋店の商品をプリント

	// mg1とfg1を代表でチョイス
	const float salesTax = 1.08;
	Goods *gA,*gB;
	int choice1 = ::getRand(1,3);
	switch (choice1){//東京店の商品からランダムに選択
		case 1: gA = &mg1; break;
		case 2: gA = &mg2; break;
		case 3: gA = &mg3; break;
	}	
	int choice2 = ::getRand(1,3);
	switch (choice2){//豊橋店の商品からランダムに選択
		case 1: gB = &fg1; break;
		case 2: gB = &fg2; break;
		case 3: gB = &fg3; break;
	}	

	cout << "================================" << endl;

	// 消費税等の計算
	int price1 = gA->getPrice() * salesTax;
	N7 = ::getRand(1,10);
	cout << "N7 = " << N7 << endl;
	cout << "商品:" << gA->getGoodsName() << "を"<< tokyo.getShopName() << "から"
		 << N7<< "個、購入希望します" << endl;
	cout << "商品:" << gA->getGoodsName() << "は税込みで1個当たり"<< price1<< "円です" << endl;
	int R7 = tokyo.buyRequest(gA,N7);
	cout << "商品:" << gA->getGoodsName() << "を"<< R7 << "個、販売します" << endl;
	int taxedPrice1 = price1 * R7;

	int price2 = gB->getPrice() * salesTax;
	N8 = ::getRand(1,10);
	cout << "N8 = " << N8 << endl;
	cout << "商品:" << gB->getGoodsName() << "を"<< toyohashi.getShopName() << "から"
		 << N8<< "個、購入希望します" << endl;
	cout << "商品:" << gB->getGoodsName() << "は税込みで1個当たり"<< price2<< "円です" << endl;
	int R8 = toyohashi.buyRequest(gB,N8);
	cout << "商品:" << gB->getGoodsName() << "を" << R8 << "個、販売します" << endl;
	int taxedPrice2 = price2 * R8;
	
	// 合計金額の計算
	cout << "================================" << endl;
	cout << "合計金額:" << taxedPrice1+taxedPrice2 << "円" << endl;

	return 0;
}
実行結果例は以下のようです。
$ ./ShopMain
**************************************
作成者:青野雅樹, 01162069
日付:2019年7月1日20時4分23秒
内容:クラスの継承例
**************************************
N1 = 30, N2 = 3, N3 = 18, N4 = 9, N5 = 13, N6 = 8
==== お店情報========================
Shop: 東京店 03-1234-5678
GoodsStock (商品):
MedGoods: 海洋深層水 1200円 処方箋:不要
GoodsStock (在庫数):30
GoodsStock (商品):
MedGoods: アガリクス 2000円 処方箋:不要
GoodsStock (在庫数):3
GoodsStock (商品):
MedGoods: 抗インフルエンザ抗生物質 2800円 処方箋:必要
GoodsStock (在庫数):18

Shop: 豊橋店 0532-44-9876
GoodsStock (商品):
FlowerGoods: バラ 3000円 贈答品:はい 賞味期限:2019-7-11
GoodsStock (在庫数):9
GoodsStock (商品):
FlowerGoods: カーネーション 1800円 贈答品:いいえ 賞味期限:2019-7-20
GoodsStock (在庫数):13
GoodsStock (商品):
FlowerGoods: 胡蝶蘭 25000円 贈答品:はい 賞味期限:2019-7-31
GoodsStock (在庫数):8

================================
N7 = 5
商品:抗インフルエンザ抗生物質を東京店から5個、購入希望します
商品:抗インフルエンザ抗生物質は税込みで1個当たり3024円です
商品:抗インフルエンザ抗生物質を5個、販売します
N8 = 9
商品:胡蝶蘭を豊橋店から9個、購入希望します
商品:胡蝶蘭は税込みで1個当たり27000円です
GoodsStock: 在庫は8だけで、在庫限り販売します
商品:胡蝶蘭を8個、販売します
================================
合計金額:231120円

純粋仮想関数

純粋仮想関数(Pure Virtual Functions)は、仮想関数の定義において、 最後に  =0   とつけることにより、実現できます。純粋仮想関数を1個でも含むクラスは、 抽象クラスと呼ばれます。 抽象クラスのオブジェクトは生成することができません。 継承される派生クラスでは、その関数の実体を必ず定義する必要があります。 もし、全部定義しない場合は、その派生クラスも抽象クラスと呼ばれ、やはりオブジェクトを生成できません。ただし、あとの例で出てきますが、ポインタ変数等は定義できます。 オブジェクトを生成できるのは、すべての純粋仮想関数の実体が定義された派生クラスだけです

以下は、今回の課題と似ていますが、2次元図形(Shape2D)クラス という抽象的な基底クラスに対して、円クラスと正方形クラスとを派生クラスとして定義している例です。 この中で、周辺長(perimeter())関数を仮想関数として定義しています。 特に、Shape2Dクラスで=0として、純粋仮想関数としています。 このため、派生クラスでは、必ず実体を定義しなければなりません。 3つのヘッダーファイル(クラス)とmain関数を含むプログラムを まとめて例示します。
Shape2D.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* プログラム4-13: 純粋仮想関数例(2次元図形クラス:基底クラス) */
#ifndef _SHAPE2D
#define _SHAPE2D
#include <iostream>
using namespace std;

class Shape2D {
 private:
	double R, G, B;
 public:
 	Shape2D(){}/* コンストラクタ */
	Shape2D(double R, double G, double B) : R(R), G(G), B(B) {}
	virtual double perimeter()=0;/* 純粋仮想関数 */
};
#endif
Circle.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* プログラム4-14: 純粋仮想関数例(円クラス:派生クラス) */
#ifndef _CIRCLE
#define _CIRCLE
#include "Shape2D.h"
#include <cmath>
using Coord2 = double[2];
const double M_PI = 4.0*atan(1.0);

class Circle: public Shape2D {/* 円 */
 private:
	Coord2 center; /* 円の中心座標 */
	double r; /* 半径 */
 public:
	Circle(Coord2 center, double r): r(r) {
		this->center[0] = center[0];
		this->center[1] = center[1];
	}
	virtual double perimeter() { return 2*::M_PI*r; }/* 円周:仮想関数の実体 */
};
#endif
なお、C言語の<math.h>で普通に使えた円周率の定数M_PIですが、 C++言語の処理系では使えないことがあります。 そのような場合は、たとえば、const double myM_PI = 4.0*atan(1.0); のような定数を定義して使うといいかと思います。
Square.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
/* プログラム4-15: 純粋仮想関数例(正方形クラス:派生クラス) */
#ifndef _SQUARE
#define _SQUARE
#include "Shape2D.h"

class Square: public Shape2D {/* 正方形 */
 private:
	double x0, y0;/* 中心座標 */
	double length;/* 一辺の長さ */
 public:
	Square(double x0, double y0, double length):
		x0(x0), y0(y0), length(length) {}
	virtual double perimeter () { return 4 * length; }/* 正方形の周囲長:仮想関数の実体 */
};
#endif
Shape2DMain.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
/* プログラム4-16: 純粋仮想関数例(main関数) */
#include "Circle.h"
#include "Square.h"
int main(){
	Circle *circle1= new Circle(10.0, 40.0, 5.0);/* 円を作り基底クラス変数に代入 */
	Square *square1 = new Square(50.0, 30.0, 15.0);/* 正方形を作り基底クラスに代入 */
	Shape2D *s[2] = { circle1, square1 };/* 基底クラスに代入 */
	double perimeter = 0.0;
	for (int i = 0 ; i < 2 ; i++)
		perimeter += s[i]->perimeter();/* 形を意識せずに周囲長を合計(派生クラスの関数)*/
	cout << "周囲長の総長 = " << perimeter << endl;
	return 0;
}

クラスの階層ハッシュマップ(Map)

もうひとつ例を示します。ここでは、 前回の課題と似た食べ物の階層を考えます。 Foodクラスの派生クラスとして、Drinkクラス、Vegetableクラス、 Fruitクラス、Sweetsクラスを考えます。 ファイルでは、それぞれの食べ物が、この4つのクラスのいずれかであるかを明示的に示しておきます。 このようなデータは、統計的機械学習を行う場合のラベルともなり有用です。

食べ物のCSVファイルを読み込み、 基底クラスのポインタ(Foodクラス)をハッシュマップの要素としてもつようにしてみます。 ただし、ハッシュマップにおけるキーは、食べ物の名前(string)とします。 その後、CSV内に現れる適当な食べ物データに基づき、 そのカロリー合計を求めてプリントするものとします。 カロリー計算において、純粋仮想関数を使います。 純粋仮想関数の場合、派生クラス個々で、その関数を定義する必要があり、 この例では、print()とgetCalorie()という2つの関数ともに、 純粋仮想関数とします。 特にprint()関数では、派生クラス独自に保持するメンバー変数の値を書かせるものとします。 データの中身は以下のようです。


名前, 種類, GI, 炭水化物, タンパク質, 脂質, カロリー
ミルク, Drink, 25.0, 4.8, 3.3, 3.8, 67.0
コーヒー, Drink, 16.0, 0.7, 0.2, 0.0, 4.0
紅茶, Drink, 10.0, 0.1, 0.1, 0.0, 1.0
コーラ, Drink, 25.0, 11.4, 0.1, 0.0, 46.0
ココア, Drink, 47.0, 80.4, 7.4, 6.8, 271.0
日本酒, Drink, 35.0, 4.5, 0.4, 0.0, 103.0
ビール, Drink, 34.0, 3.1, 0.3, 0.0, 40.0
ワイン, Drink, 32.0, 2.0, 0.1, 0.0, 73.0
梅酒, Drink, 53.0, 20.7, 0.1, 0.0, 271.0
オレンジジュース, Drink, 42.0, 11.0, 0.8, 0.0, 42.0
アボガド, Fruit, 27.0, 6.2, 2.5, 18.7, 187.0
りんご, Fruit, 36.0, 14.58, 0.196, 0.117, 54.11
キウイ, Fruit, 35.0, 13.5, 1.0, 0.1, 53.0
イチゴ, Fruit, 29.0, 8.46, 0.897, 0.128, 34.62
パイナップル, Fruit, 65.0, 13.4, 0.3, 0.1, 51.0
バナナ, Fruit, 55.0, 22.5, 1.11, 0.2, 86.0
じゃがいも, Vegetable, 90.0, 17.6, 1.6, 0.133, 76.0
かぼちゃ, Vegetable, 65.0, 13.3, 1.9, 0.1, 60.0
にんじん, Vegetable, 80.0, 9.1, 0.6, 0.1, 37.0
キャベツ, Vegetable, 26.0, 5.2, 1.3, 0.2, 23.0
ほうれん草, Vegetable, 15.0, 4.0, 2.6, 0.5, 25.0
レタス, Vegetable, 23.0, 2.8, 0.6, 0.1, 12.0
トマト, Vegetable, 30.0, 4.7, 0.7, 0.1, 19.0
ショートケーキ, Sweets, 80.0, 47.1, 7.4, 1.4, 344.0
アイスクリーム, Sweets, 65.0, 23.2, 3.9, 8.0, 180.0
チョコレート, Sweets, 91.0, 55.4, 7.4, 34.0, 557.0
ここで用いるクラスの継承関係は以下のようにイラストで書くことが出来ます。



Nutrient.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* プログラム4-17: 栄養物(Nutrient)クラス */
#ifndef _NUTRIENT
#define _NUTRIENT
#include <iostream>
using namespace std;
#include <string>

class Nutrient {
 public:
	string name; /* 名前 */
 protected:
	double calorie;/* カロリー値:100gあたり */
	double GI;/* GI値(glycemic index) 血糖の上がりやすさ指数  */
	double carbon;/* 炭水化物:100gあたり */
	double protein;/* タンパク質:100gあたり */
	double fat;/* 脂質:100gあたり */
 public:
 	Nutrient(string name) : name(name) { calorie = 0.0; }
	Nutrient(string name, double calorie, double GI, double carbon, double protein, double fat):
		name(name),calorie(calorie), GI(GI), carbon(carbon), protein(protein), fat(fat) {}
	string getName(){ return name; }
};
#endif
Food.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* プログラム4-18: 食べ物クラス(派生クラス) */
#ifndef _FOOD
#define _FOOD
#include "Nutrient.h"

class Food : public Nutrient {
	int numTotal; /* 全体の食べ物総数 */
 public:
 	Food(string name) : Nutrient(name) {};
	Food(string name, double GI, double carbon, double protein, double fat, double calorie)
		: Nutrient(name, GI, carbon, protein, fat, calorie) {}	
	virtual ~Food(){}/* 仮想デストラクタ*/
	virtual double getCalorie()=0;/* 純粋仮想関数 */
	virtual void print()=0;/* 純粋仮想関数 */
	void printFood(){
		cout << "(name,GI,carbon,protein,fat,calorie)=(" <<
			name << "," <<	GI << "," << carbon << "," << protein << "," <<
			fat << "," << calorie << ")" << endl;
	}
};
#endif
Drink.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* プログラム4-19: 飲み物クラス(派生クラス) */
#ifndef _DRINK
#define _DRINK
#include "Food.h"

class Drink : public Food {
	bool isAlcoholic;/* アルコール飲料かどうか */
 public:
 	Drink(string name): Food(name),isAlcoholic(false) {}
	Drink(string name, double GI, double carbon, double protein, double fat, double calorie):	
	Food(name, GI, carbon, protein, fat, calorie),isAlcoholic(false){}
	bool getAlcoholic(){ return isAlcoholic; }
	void setAlcoholic(bool isAlcoholic){ this->isAlcoholic = isAlcoholic;}
	double getCalorie(){/* 純粋仮想関数の実体 */
		return calorie;/* protected領域なので派生クラスからアクセス可能 */
	}
	void print(){/* ドリンクのプリント関数 */
		cout << "【Drink】" << isAlcoholic << endl;
		Food::printFood();
	}
};
#endif
Sweets.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* プログラム4-20: お菓子クラス(派生クラス) */
#ifndef _SWEETS
#define _SWEETS
#include "Food.h"

class Sweets: public Food {
	bool isFrozen; /* 冷凍庫に入れるものか? */
 public:
 	Sweets(string name) : Food(name), isFrozen(false) {}
	Sweets(string name, double GI, double carbon, double protein, double fat, double calorie):
	Food(name, GI, carbon, protein, fat, calorie), isFrozen(false){}
 	double getCalorie(){/* 純粋仮想関数の実体 */
		return calorie;/* protected領域なので派生クラスからアクセス可能 */
	}
	void print(){/* スイーツのプリント関数*/
		cout << "【Sweets】" << isFrozen << endl;
		Food::printFood();
	}
};
#endif
Vegetable.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* プログラム4-21: 野菜クラス(派生クラス) */
#ifndef _VEGETABLE
#define _VEGETABLE
#include "Food.h"

class Vegetable: public Food {
	bool isRooted; /* 根菜類? */
	bool isGreen;/* 緑色野菜? */
 public:
 	Vegetable(string name) : Food(name), isRooted(false), isGreen(false) {}
	Vegetable(string name, double GI, double carbon, double protein, double fat, double calorie):
	Food(name, GI, carbon, protein, fat, calorie), isRooted(false), isGreen(false){}
	double getCalorie(){/* 純粋仮想関数の実体 */
		return calorie;/* protected領域なので派生クラスからアクセス可能 */
	}
	void print(){/* 野菜のプリント関数 */
		cout << "【Vegetable】" << isRooted << "," << isGreen << endl;
		Food::printFood();
	}
};
#endif
Fruit.h
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* プログラム4-22: フルーツクラス(派生クラス) */
#ifndef _FRUIT
#define _FRUIT
#include "Food.h"

class Fruit: public Food {
	double vitaminC; /* ビタミンCの含有量 */
	bool isTropical; /* 熱帯のフルーツかどうか */
 public:
 	Fruit(string name) : Food(name),vitaminC(0),isTropical(false) {}
	Fruit(string name, double GI, double carbon, double protein, double fat, double calorie):
	Food(name, GI, carbon, protein, fat, calorie),vitaminC(0),isTropical(false){
		if (name == "レモン") vitaminC = 100; 
		else if (name == "いちご") vitaminC = 62;
		else if (name == "キウイ") vitaminC = 69;
	}
 	double getCalorie(){/* 純粋仮想関数の実体 */
		return calorie;/* protected領域なので派生クラスからアクセス可能 */
	}
	void print(){/* フルーツのプリント関数 */
		cout << "【Fruit】" << vitaminC << "mg," << isTropical << endl;
		Food::printFood();
	}
};
#endif
FoodMap.cpp
1 
2 
3 
4 
5 
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/* プログラム4-23: main関数用のヘッダーと入力補助関数 */
using namespace std;
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <cstdlib>
#include <vector>
#include <map>
#include "Food.h"
#include "Drink.h"
#include "Sweets.h"
#include "Vegetable.h"
#include "Fruit.h"

void trim(string& str){/* 文字列の両端のspaceを削除 */
	string::size_type pos = str.find_last_not_of(' ');
	if (pos != string::npos ) {
		str.erase(pos + 1);
		pos = str.find_first_not_of(' ');
		if ( pos != string::npos ) str.erase(0, pos);
	}
	else str.erase( str.begin(), str.end() );
}

vector  split(string& input, char delimiter)
{
    istringstream stream(input);/* 入力文字列をistringstreamとして処理 */
    string field;
    vector  result;/* 結果の文字列リストを保持 */
    while (getline(stream, field, delimiter)) {
        ::trim(field);/* 両端のスペースを削除 */
        result.push_back(field);/* データをvectorにpush */
    }
    return result;
}

int main(int ac, char **av){
    char *fn = av[1];/* csvファイル */
    ifstream fin(fn, ios::in );
    if (!fin){
        cerr << "ファイル" << av[1] << "がオープンできません" << endl;
        return 1;
    }
    string name, kind;
    double GI, carbon, calorie, fat, protein;
    string line;

    /* skip the first line */
    getline(fin, line);/* 1行目をスキップ */
    vector <string> result;
    map <string, Food*> data;//ハッシュマップ(キーは名前、値はFood*)
    map <string, Food*>::iterator it;//イタレータ

    while (fin.good()){
        if (!getline(fin, line)) break;/* 行単位に読み、EOFならbreak */
        result = ::split(line,',');
        name = result[0];/* 名前を読み込む */
        kind = result[1];/* 種類を読み込む */
        GI = stof(result[2]);/* GI値を読み込む(stod関数でも可) */
        carbon = stof(result[3]);/* 炭水化物を読み込む */
        protein = stof(result[4]);/* タンパク質を読み込む */
        fat = stof(result[5]);/* 脂質を読み込む */
        calorie = stof(result[6]);/* カロリーを読み込む */
        cout << name << " " << kind << " " << GI << " " <<
            carbon << " " << protein << " " << fat << " " << 
            calorie << endl;
        if (kind == "Drink"){/* ドリンク用のクラスのポインタ変数 */
            Drink *drink = new Drink(name, GI, carbon, protein, fat, calorie);
            data[name] = drink;/* 基底クラスのポインタに派生クラスを代入 */
        }
        else if (kind == "Sweets"){/* スイーツ用のクラスのポインタ変数 */
            Sweets *sweets = new Sweets(name, GI, carbon, protein, fat, calorie);
            data[name] = sweets;/* 基底クラスのポインタに派生クラスを代入 */
        }
        else if (kind == "Vegetable"){/* 野菜用のクラスのポインタ変数 */
            Vegetable *vegetable = new Vegetable(name, GI, carbon, protein, fat, calorie);
            data[name] = vegetable;/* 基底クラスのポインタに派生クラスを代入 */
        }
        else if (kind == "Fruit"){/* フルーツ用のクラスのポインタ変数 */
            Fruit *fruit = new Fruit(name, GI, carbon, protein, fat, calorie);
            data[name] = fruit;/* 基底クラスのポインタに派生クラスを代入 */
        }
    }
    fin.close();/* ファイルのクローズ */

    /* 特定の食べ物のカロリーを計算:実行時タイプチェック */ 
    cout << "\n" << "あなたが食べたもののカロリー等を調べます..." << endl;
    vector <string> myFood = {"ミルク", "ショートケーキ", "トマト", "キウイ" };//C++11以降
    double totalCalorie = 0.0;
    int size = myFood.size();
    for (int i = 0 ; i < size ; i++){//基底クラスの関数を呼び出すと、派生クラスの関数が呼び出される
        it = data.find(myFood[i]);
        if (it != data.end()){//data中に見つかった場合
            Food *food = it->second;// mapのvalueであるFood*をゲット
            totalCalorie += food->getCalorie();//派生クラスのgetCalorie関数を呼ぶ
            food->print();//派生関数のprint関数
        }
    }
    cout << "【総カロリー数】:" << totalCalorie << endl;
    /* memory cleanup */
    for (it=data.begin(); it != data.end(); it++){
        Food *food = it->second;
        delete food;// Foodのデストラクタが仮想関数なので派生クラスのデストラクタが呼ばれる
    }
    return 0;
}
実行した結果は以下のようです。

$ make
g++ -Wall -std=c++14 -c FoodMap.cpp
g++ -Wall -std=c++14 -o FoodMap FoodMap.o

$ ./FoodMap foodData.csv
ミルク Drink 25 4.8 3.3 3.8 67
コーヒー Drink 16 0.7 0.2 0 4
紅茶 Drink 10 0.1 0.1 0 1
コーラ Drink 25 11.4 0.1 0 46
ココア Drink 47 80.4 7.4 6.8 271
日本酒 Drink 35 4.5 0.4 0 103
ビール Drink 34 3.1 0.3 0 40
ワイン Drink 32 2 0.1 0 73
梅酒 Drink 53 20.7 0.1 0 271
オレンジジュース Drink 42 11 0.8 0 42
アボガド Fruit 27 6.2 2.5 18.7 187
りんご Fruit 36 14.58 0.196 0.117 54.11
キウイ Fruit 35 13.5 1 0.1 53
イチゴ Fruit 29 8.46 0.897 0.128 34.62
パイナップル Fruit 65 13.4 0.3 0.1 51
バナナ Fruit 55 22.5 1.11 0.2 86
じゃがいも Vegetable 90 17.6 1.6 0.133 76
かぼちゃ Vegetable 65 13.3 1.9 0.1 60
にんじん Vegetable 80 9.1 0.6 0.1 37
キャベツ Vegetable 26 5.2 1.3 0.2 23
ほうれん草 Vegetable 15 4 2.6 0.5 25
レタス Vegetable 23 2.8 0.6 0.1 12
トマト Vegetable 30 4.7 0.7 0.1 19
ショートケーキ Sweets 80 47.1 7.4 1.4 344
アイスクリーム Sweets 65 23.2 3.9 8 180
チョコレート Sweets 91 55.4 7.4 34 557

あなたが食べたもののカロリー等を調べます...
【Drink】0
(name,GI,carbon,protein,fat,calorie)=(ミルク,4.8,3.3,3.8,67,25)
【Sweets】0
(name,GI,carbon,protein,fat,calorie)=(ショートケーキ,47.1,7.4,1.4,344,80)
【Vegetable】0,0
(name,GI,carbon,protein,fat,calorie)=(トマト,4.7,0.7,0.1,19,30)
【Fruit】69mg,0
(name,GI,carbon,protein,fat,calorie)=(キウイ,13.5,1,0.1,53,35)
【総カロリー数】:170