【C++】第6回:C++でのポインタと配列の利用


応用プログラミングA(C++言語)、前回の「C++のフレンド関数」に引き続き、今週は「C++でのポインタと配列の利用」です。

C++言語はオブジェクト指向(クラス指向)ではありますが、C言語のパワフルなポインタも使えることは意外と知られていません。
(とはいえ実用上はオブジェクトの配列を使うことが多いと思います)

今回は「学生の成績を管理するクラス」を作成して、ここまでの復習を兼ねた総合問題という設計になっています。

出題された課題は3問あります。
Q1、Q2、Q3…と順に解いていかないと、最後までたどり着けませんので注意です。

またサンプル中に出てくる学生の名前は昨年のAKB48からガラッと変わって『ラブライブ!』からの引用が多いようですが、打つのが大変なのと、私自身が思い入れがないので適当なボカロあたりの名前に差し替わっております。
このあたり、まるまるコピーした場合は減点対象となりますので要注意。

まずQ1です。

ここでは学生のデータを管理するStudentListクラスを作成します。

class StudentList {
	char name[50]; //学生の氏名
	int id, grade; //学籍番号、学年
public:
	void set(char *n, int i, int g);
	void show();
};

このオブジェクトの配列に10人分のデータをコンソールから入力し、すべて入力し終わったらコンソールに出力する基本となるプログラムを作ります。

「独習C++(第4版)」の例4.2を参考にするとよいようです。
独習C++ 第4版

//配列とポインタ:クラスオブジェクトを配列でとった場合も、C言語におけるポインタと同様に扱える。
#include <iostream>
#include <cstring>
using namespace std;
class StudentList {
	char name[50];
	int id, grade;
public:
	void set(char *n, int i, int g);
	void show();
};
void StudentList::set(char *n, int i, int g) {
	strcpy(name, n);
	id = i;		grade = g;
}
void StudentList::show() {
	cout << "氏名:" << name;
	cout << "\t学籍番号:" << id;
	cout << "\t学年:" << grade << "\n";
}

int main() {
	StudentList slist[10];		 //入力用にオブジェクトの配列を宣言
	int id, grd;	char nm[50]; //入力用変数
	int max = 3;				 //最大件数
	StudentList *ptr;			 //配列の操作用にポインタ変数を宣言
	ptr = slist; //ポインタ渡し,ここで配列とポインタを接続
	for (int i=0; i<max ; i++ ) {
		cout << i+1 << "人目のデータを入力します\n";
		cout << "名前を入力してください:";  cin >> nm;
		cout << "学籍番号を入力してください:"; cin >> id;
		cout << "学年を入力してください:";  cin >> grd;
		//仮の入力用変数からポインタの先頭にデータを格納し、ポインタを進行させる.
		ptr->set(nm, id, grd); ptr++;
		//※このptr++で配列1行分、ポインタが進む
	}
	ptr = slist;  //ふたたびポインタをリセット
	for (int i = 0; i < max; i++ ) {
		cout << i+1 << "\t";
		ptr->show(); //このような形でメソッドが呼べることに注意
		ptr++;
	}
	return 0;
}

実行すると、10人分の名前と学籍番号、学年の入力を強いられるだけのマゾプログラムです。
しかも最後の return 0; にブレイクポイントを仕込んでおかないと結果が一瞬で流れて消えます。
それではあまりにご無体ですので、ここではmax=3として端折っています。レポートではmax=3としたら誤りです。

ちなみに講義中はptrのことを『ハンドル』と呼んでしまいましたが、厳密には異なります。
ここは固定のメモリアドレスをptrに渡しているので『ポインタ』と呼ぶのが適切でしょう。
『ハンドル』とは、動的にメモリ配置が変わる場合、たとえばガーベージコレクションなどを備えたJavaやC#のような環境において、実メモリアドレスが移動しても、捕まえておけるオブジェクトのことを『ハンドル』と呼ぶべきようです。

つづいてQ2です。タイトルは「総合問題1」となっています。

先ほどのQ1の続きからはじめて、成績を管理するResultListクラスを追加します。
先述のStudentListクラスをPublicに継承して、公開メンバとして100個までの科目名を格納し、その単位異数と評価を格納します。また格納されている科目数を記憶する変数を非公開メンバ変数として持ちます。関数は評価を格納するための関数Regist()と、全科目の科目名、単位数、評価を出力するshowResult()を持ちます。

以下のようなクラス設計、継承(inheritance)の関係がイメージできましたでしょうか?

Classes
(Visual Studio 2010の「クラスダイアグラム」機能で描画してみました)

継承をコードにするとこんな感じになります。

class ResultList : public StudentList {
	char	course[100][50];	//科目名
	int		credit[100];		//単位数
	char	grade[100];			//評価
	int		number;				//格納してある科目数
public:
	ResultList() { number = 0; }
	void regist(char *title, int crd, char grd);
	void showResult();
};

なお、今回のプログラムでは入力チェックなどは一切行いません。削除や変更もしませんし、科目名の重複もチェックしません。

//Q2:総合問題1 成績管理(入力)
#include <iostream>
#include <cstring>
using namespace std;
class StudentList {
	char name[50];
	int id, grade;
public:
	void set(char *n, int i, int g);
	void show();
};
void StudentList::set(char *n, int i, int g) {
	strcpy(name, n);
	id = i;		grade = g;
}
void StudentList::show() {
	cout << "氏名:" << name;
	cout << "\t学籍番号:" << id;
	cout << "\t学年:" << grade << "\n";
}
//ここから上はQ1と同じ
//ここから下はテキストにある通り、StudentListを継承したResultListを宣言
class ResultList : public StudentList {
	char	course[100][50];	//科目名
	int		credit[100];		//単位数
	char	grade[100];			//評価
	int		number;				//格納してある科目数
public:
	ResultList() { number = 0; }
	void regist(char *title, int crd, char grd);
	void showResult();
};
void ResultList::regist(char *title, int crd, char grd) {
	strcpy(course[number], title);
	credit[number] = crd;
	grade[number]  = grd;
	number++;
}
void ResultList::showResult() {
	//格納してある科目数すべてについてリストする
	for (int i = 0; i < number; i++ ) {
		cout << "科目名:" << course[i];
		cout << "\t単位数:" << credit[i];
		cout << "\t評価:" << grade[i] << "\n";
	}
}

int main() {
	ResultList rlist[10];
	int i,n; char str[50]; char c;
	//名前学籍学年データ , 継承したResultListの配列にセットします
	rlist[0].set("高坂穂乃果",1123005, 3);
	rlist[1].set("絢瀬絵里", 1023003, 4);
	rlist[2].set("南ことり", 1123008, 3);
	rlist[3].set("園田海未", 1123039, 3);
	rlist[4].set("初音ミク", 1023010, 3);
	rlist[5].set("ルカ",	 1223007, 2);
	rlist[6].set("リン",	 1023007, 4);
	rlist[7].set("レン",	 1223008, 2);
	rlist[8].set("かいと",   1023015, 4);
	rlist[9].set("らぴす",   1323013, 1);

	while (1) {
		cout << "学生一覧\n";
		for (i = 0; i < 10 ; i++ ) {
			cout << i+1 << "\t";
			rlist[i].show(); //普通はこう呼ぶほうが多いですよね。継承元のStudentList::show()です
		}
		cout << "何番の学生の成績を入れますか?\n";
		cin >> i;
		cout << "成績を入力してください\n入力終了はendを科目名に入れて下さい\n";
		while(1) {
			cout << "科目名:"; cin >> str;
			if (strcmp(str, "end") == 0) break;
			cout << "単位数:"; cin >> n;
			cout << "評価:";   cin >> c;
			rlist[i-1].regist(str, n, c );
		}
		rlist[i-1].show();
		cout << "この学生のデータを入力しました。\n終了するなら1を続けるなら0を:";
		cin >> n;
		if (n == 1) break;
	}
	//結果表示
	for (i = 0; i < 10; i++ ) {  //個々の学生についてループ
		rlist[i].show();		//個々の学生の情報について表示
		rlist[i].showResult();	//個々の科目の成績について表示
		cout << "\n";
	}
	return 0;
}

【実行結果】
cpp0521q2

9番目の「かいと」(KAIT!?)と10番目の「らぴす」の成績を入力しています。
「応用プログラミングA」が同じ名前なのですが、見た目には動作しています、が、デバッガ仕掛けるとわかりますが、別の科目になっています。
重複チェックを入れるのはさほど難しいことではないので考察としてやってみてもよいのでは。
また結果を表示するshow()ももう一工夫してもいいように思います(Q3にもありますので…)。
それに「end」と入れずに[0]を入れると終わるなど、工夫のしようはありそうです。
cinとifを使ったコマンドプロンプトによる対話は、インタラクティブなプログラムの基本中の基本なので しれっと 書けるようになってください。

さて最後のQ3「総合問題2」です。

前述Q2のResultListクラスに総合成績評価をあらわすGPAを追加します。
GPAは成績評価のSを4、Aを3、Bを2、Cを1、それ以外を0とした取得講義の平均で求めます。
また先ほどのRegist()を拡張してすでに同盟の科目の成績が入力されている場合には評価を上書き修正するようにします。
さらに科目と成績を削除するdel()も実装します。

0521q3class

以下、サンプルソースです。
main()がないととっかかりがつかみづらいと思いますので。
★レポート締切前なので、ResultListクラスの個々のメソッド実装は未公開です。

//0521 課題3 総合問題2
#include <iostream>
#include <cstring>
using namespace std;

class StudentList {
	char name[50];
	int id;
	int grade;
public:
	void set(char *n, int i, int g);
	void show();
};
void StudentList::set(char *n, int i, int g) {
	strcpy(name, n);
	id = i;
	grade = g;
}
void StudentList::show()	{
	cout << "氏名:" << name;
	cout << "\t学籍番号:" << id;
	cout << "\t学年:" << grade << "\n";
}
class ResultList : public StudentList {
	char course[100][50];
	int credit[100];
	char grade[100];
	int number;
	double gpa;  //今回追加
public:
	ResultList() { number = 0; gpa = 0; }
	void regist(char *title, int crd, char grd);
	void showResult();
	void del(char *title);
	double get_gpa() { return gpa; }
};

void ResultList::regist(char *title, int crd, char grd) {
	int i, sumc = 0, sumg = 0;
	for (i = 0; i < number; i++)	{
		if (strcmp(course[i], title) == 0) {
			credit[i] = crd;
			grade[i] = grd;
			break;
		}
	}
	if (i == number)	{
		strcpy(course[number], title);
		credit[number] = crd;
		grade[number] = grd;
		number++;
	}
	for (i = 0; i < number; i++) {
		sumc += credit[i];
		switch (grade[i])	{
			case 'S':
				sumg += 4 * credit[i];
				break;
			case 'A':
				sumg += 3 * credit[i];
				break;
			case 'B':
				sumg += 2 * credit[i];
				break;
			case 'C':
				sumg += 1 * credit[i];
				break;
			default:
				break;
		}
	}
	gpa = (double)sumg / sumc;
}

void ResultList::showResult() {
	for (int i = 0; i < number; i++) {
		cout << "科目名:" << course[i];
		cout << "\t単位数:" << credit[i];
		cout << "\t評価:" << grade[i] << "\n";
	}
}

void ResultList::del(char *title) {
	int i, j, sumc = 0, sumg = 0;
	for (i = 0; i < number; i++) {
	   if (strcmp(course[i], title) == 0) {
// 以下は登録されている科目の順番を変えないように処理している.順番を変えてもよいならもっと簡単
		for (j = i; j < number; j++) {
			strcpy(course[j], course[j+1]);
			credit[j] = credit[j+1];
			grade[j] = grade[j+1];
		}
		number--;
		for (j = 0; j < number; j++) {
			sumc += credit[j];
			switch (grade[j]) {
				case 'S':
					sumg += 4 * credit[j];
					break;
				case 'A':
					sumg += 3 * credit[j];
					break;
				case 'B':
					sumg += 2 * credit[j];
					break;
				case 'C':
					sumg += 1 * credit[j];
					break;
				default:
					break;
			}
		}
		gpa = (double)sumg / sumc;
		break;
	   }
	}
}
//以下はmainの参考。
int main() {
	ResultList rlist[10];
	int i, j, n;
	char str[50];
	char c;
	//学生ダミーデータ:レポートでそのまま使用した場合はカンニングとみなします :-p
	rlist[0].set("高坂穂乃果", 1123005, 3);
	rlist[1].set("絢瀬絵里", 1023003, 4);
	rlist[2].set("南ことり", 1123008, 3);
	rlist[3].set("園田海未", 1123039, 3);
	rlist[4].set("星空凛", 1023010, 4);
	rlist[5].set("西木野真姫", 1223014, 2);
	rlist[6].set("東條希", 1023007, 4);
	rlist[7].set("小泉花陽", 1223008, 2);
	rlist[8].set("矢澤にこ", 1023015, 4);
	rlist[9].set("アルパカ", 1323013, 1);
	while (1) {
		cout << "学生一覧\n";
		for (i = 0; i < 10; i++) {
			cout << i+1 << "\t";
			rlist[i].show();
		}
		// 以下では入力値の検証を行っていない.本当は入力値の検証を行った方がよい
		cout << "何番の学生のデータを処理しますか?";
		cin >> i;
		cout << "成績を入力するには1を、削除するには2を入力してください:";
		cin >> j;
		cout << "入力を終了するには科目名にendと入力してください。\n";
		if (j == 1) {
			while (1) {
				cout << "科目名:";	cin >> str;
				if (strcmp(str, "end") == 0) break;
				cout << "単位数:";	cin >> n;
				cout << "評価:";	cin >> c;
				rlist[i-1].regist(str, n, c);
			}
		} else {
			while (1) {
				cout << "科目名:";	cin >> str;
				if (strcmp(str, "end") == 0) break;
				rlist[i-1].del(str);
			}
		}
		rlist[i-1].show();
		cout << "この学生のデータを入力しました。\n\n";
		cout << "入力を終了しますか?\n";
		cout << "終了するなら1を、続けるなら0を入力してください:";
		cin >> n;
		if (n == 1) break;
	}
	for (i = 0; i < 10; i++) {
		rlist[i].show();
		rlist[i].showResult();
		cout << "GPA:" << rlist[i].get_gpa() << "\n\n";
	}
	return 0;
}

実行結果のスクリーンショットはうまく、登録や削除を説明できるように複数撮りましょうね!
たとえば、このスクリーンショットだけでは何が何だかわかりませんよね?

0521q3

第7回に続きます。