配列とマクロ定数

多数のデータを手軽に取り扱うため,配列の適切な使い方を理解しよう. ついでに,配列と一緒に利用されることの多いマクロ定数についても.

教科書の該当範囲:第6章,第11.1節
今回で初級編が完了となります. 今回の内容まで修得すれば,大抵の動作については, 「なんとかプログラミングできる」ようになっているハズです. (「うまくプログラミングできる」とは言っていない.それは次回以降で.)

基礎知識

配列の宣言・利用

複数の同型の変数の集合を一気に用意できる.

ソースコード記述方法:

型名  配列名[要素数];

具体例:

int  data[100];

これで,100個の int型の要素変数 data[0],data[1],data[2],...,data[99] から成る配列 data が用意される.

なんて便利なんだ. 普通の変数 100個分の宣言文 int data00, data01, data02, data03, ..., data99; なんて,書きたくありませんよね?

なお,要素数 100 と要素番号 0〜99 との違いに注意しよう.

配列 a[N] の場合...

存在しない要素変数へのアクセスでは, 実行時にエラーとなり,プログラムが強制終了とか挙動不審となったりする. 要素番号の範囲には特に注意が必要である.

これはコンパイラでは発見できない種類のエラーなので厄介. プログラマの技量・注意力が試される.
配列だけが原因とは限りませんが... エラーであるにも関わらず,短期的には発現せず, うまく動いているように見えてしまう場合もあるんです. しかし,長期的には必ず破綻の時が来る. 現実社会でも,時々,銀行とか空港とかのインフラシステムが突然停止して, パニックが発生したりしていますよね?
マクロ定数の定義・利用

ソースコード内の数値に名前を付けておける.

ソースコード記述方法:

#define  マクロ名  数値

具体例:

#define  N  100		// マクロ名 N として数値 100 を定義

int  data[N];		// コンパイル時に int data[100]; へ書き換えられる

コンパイル時にソースコード中のマクロ名 N が 定義内容の数値 100 へ自動的に書き換えられる. マクロは,変数と似ているように見えるが,まったく異なり, プログラム実行時に内容(数値)の変更はできない. プログラム実行中に不変であるべき数値を ソースコード全体で一貫的に矛盾なく記述したい場合に利用する.

変数とマクロとを簡単に見分ける(間違いを少なくする)ための慣例に従い, 変数名は小文字で,マクロ名は大文字で表記すると良いだろう.

ありがちな間違い:

#define  n  100	

n = 0;		// NG.コンパイル時に「100 = 0;」へ書き換えられ,エラーとなる
クドいですが...マクロの値は,変数とは異なり,実行時には変更できません.
ソースコードの整形

ソースコードを書く時,レイアウトを適切に整えておけば,間違いに気付き易くなる. これからは,インデントの作法に従おう.

今後,レポートのソースコードについて, 不適切なインデントは減点対象となります.
>/メールアプリの設定によっては,送信時に勝手に, インデントが書き換えられてしまう場合もあります. 不利益を被らないように,各自で注意してください.

本日の作業

タスク1:データ編集プログラム

(本日の課題1です.)

複数データの入力・修正が可能なプログラム record.c を作成してみよう.

前回の平均計算プログラムでは,入力データについて, 最新のもの1個以外を捨てていました. 今回は入力データをすべて残しておくようにしますよ.
「実習の準備」等については,もう省略でいいよね. 各自,適切に準備しよう.
Step 1. 配列データを表示してみる.

まずは, データ入力については後回しとして, データ表示の機能だけ作り, とりあえず, 配列の基本的な使い方だけ確認してみよう.

record.c

#include <stdio.h>

int main(void)
{
	int	data[3];	// データ記録用の配列(この行を色々と差し換えて実験してね)
	int	i;		// 要素番号のカウンタ

	printf("データ:");
	for (i = 0; i < 3; i++) {
		printf("%d  ", data[i]);	// 配列要素の数値を空白で区切って表示
	}
	printf("\n");		// 最後に改行を表示

	return (0);
}
$ cc  record.c  -o  record
$ ./record
...		# 3個のゴミ(意味不明な数値)が表示される
ゴミがすべてゼロの場合もあるかもしれないが, いつでも必ずゼロになるとは限らない.

普通の変数と同様,配列要素にもゴミが入っている. 大抵の場合,初期化が必要になる. 配列の様々な初期化方法について, ソースコードの配列宣言の部分を 次のそれぞれの行と差し替えて試してみよう:

//	int	data[3];		// 初期値指定なし.各要素にはゴミが入っている
	int	data[3] = {1, 2, 3};	// 初期値を一気に指定できる
//	int	data[] = {1, 2, 3};	// 初期値を指定したら要素数は省略できる
//	int	data[3] = {1, 2, 3, 4, 5};	// 初期値が多すぎ...これはNG
//	int	data[3] = {1, 2};	// 初期値は少ないが...これはOK(指定なしの data[2] の値はどうなる?)
//	int	data[3] = {};		// これもOK.初期値を一気にゼロにできる
Step 2. 配列データを入力・修正してみる.

データを順番に入力できるようにしてみよう;

...
	int	data[3] = {};
...
	for (i = 0; i < 3; i++) {
		printf("%d番目の整数データ > ", i);
		scanf("%d", &data[i]);	// 要素 data[i] へデータを入力
	}

	printf("データ:");
...

はい,これで入力はできるようになった. しかし,修正はできない.

データ入力時に要素番号も指定して, データを自由に入力・修正できるようにしてみよう:

...
//	for (i = 0; i < 3; i++) {
	while (1) {
		printf("要素番号(-1 で終了)> ");
		scanf("%d", &i);	// 要素番号を入力
		if (i < 0) break;

		printf("%d番目の整数データ > ", i);
		scanf("%d", &data[i]);
//	}

		printf("データ:");
		...
	}
	return (0)
...

ついでに,インデントも適切に整えておこう. データ表示部分を while ループ内に入れたので, この部分のインデントを1段右へ移動することになる.

これで,データの書き換えも自由にできるようになった. しかし,要素番号として大きすぎる数値を入力してしまうと... 実行時エラーによって強制終了させられたり, 異常動作に陥ることもある. (いろいろな数値で試すこと.)

原因は,配列用のメモリ領域の範囲外に データが書き込まれてしまったことである. これはバッファオーバラン(buffer overrun)と呼ばれ, C言語プログラムに(本来あってはならないが)よくある重大な欠陥である.

バッファオーバランの無茶な実験のひとつとして, 要素番号にはマイナス値を指定してもよいだろう. ソースコードの if-break 行をコメント化し, 負の要素番号を入力してみよう.
Step 3. 安全対策を追加してみる.

バッファオーバランを防止してみよう:

...
		printf("要素番号(-1 で終了)> ");
		scanf("%d", &i);
		if (i < 0) break;
		if (i >= 3) continue;	// 範囲外の要素番号を無視
...

これで,とりあえずオーバランは回避できただろう. しかし,次の通り,これだけでは不充分だ.

データ配列の要素数について,3 では少なすぎるので,増やしてみたい. この場合,ソースコード中の複数箇所にある定数 3 をすべて書き換える必要がある. しかし,この時,間違いが発生しやすい. たとえば,if (i >= 3) の 3 だけを 10 に書き換え, 配列宣言 data[3] の 3 をそのまま書き換え忘れてしまうと? バッファオーバランが簡単に発生してしまう.

Step 4. 更なる安全対策を追加してみる.

プログラミング時の間違いの予防策を取り入れてみよう. 配列要素数をマクロ定義しておき, 即値(定数 3 そのまま)を ソースコード内に何度も書かないようにしておけばよい:

#include ...
#define	N_DATA	3		// 配列要素数のマクロ定義

int main(void)
{
	int	data[N_DATA];
	...
	while (1) {
		...
		if (i >= N_DATA) continue;
		...
		for (...; i < N_DATA; ...) {
			...
		...
	}
	...
}

こうしておけば,1箇所(#define の 3)だけ変更すれば, 関連するすべての部分を漏れなく自動的に書き換えてもらえる. 最初から,こんな書き方をしておくべきだったんだ.


タスク2:度数分布表示プログラム

(本日の課題2です.)

試験成績の度数分布(頻度分布,ヒストグラム)を算出・表示するプログラム hist.c を作成してみよう.

これまでの学習の集大成となりそうな問題です.
Step 1. とりあえず入力データ全体の個数をカウントしてみる.
特に目新しい部分はありませんが...

hist.c

#include <stdio.h>

int main(void)
{
	int	x;	// 成績(入力データ)が入るよ
		// 人数を無制限とするため,成績を記録するための配列は使わない
		// ...というか,配列の要素数を無限にはできない
	int	n = 0;	// 受験者数(入力データの個数)

	printf("成績を入力してください(0 〜 100,最後に -1)\n");
	while (1) {
		scanf("%d", &x);
		if (x < 0) break;

		n++;	// 入力データ数をカウント
	}
	printf("人数:%d\n", n);

	return (0);
}
$ cc  hist.c  -o  hist
$ ./hist
成績を入力してください(0 〜 100,最後に -1)
85
90
75
-1
人数: 3
Step 2. 入力のエラー処理を追加してみる.
これも前回までの学習内容の確認...
...
int main(void)
{
	...
LOOP:
	while (1) {
		...
		if (x > 100) goto ERROR;	// エラー処理

		n++;
	}
RESULT:	// ←gotoして来ないので余計だけど...構成を見通し良くするために付けてみた
	printf("人数:%d\n", n);

	return (0);

ERROR:
	printf("エラー:不正な入力を無視しました.100以内な!!\n");
	goto LOOP;
}

実行例:

$ cc  hist.c  -o  hist
$ ./hist
成績を入力してください(0 〜 100,最後に -1)
500
エラー:不正な入力を無視しました...
...
Step 3. 階級毎の度数をカウントしてみる.
ここから新しい知識も採り入れますよ...
なお,階級の度数とは,例えば 70点台(70〜79点)が何人いるのさ?ということね. (多分,他の科目「数学」等の「統計学」とかの単元で学習すると思われます. しらんけど.)
#include ...

#define	WC	10	// 階級の幅
#define	NC	11	// 階級の個数
	// 0点台,10点台,20点台,...,90点台,100点の 10点刻みに 11階級の度数

int main(void)
{
	...
//	int	n = 0;
	int	c;		// 階級の番号(x/WC)が入るよ
		// 例:成績x が 75点なら,階級値c は 7
	int	f[NC] = {};	// 各階級の度数の配列
		// 例:f[7] は 70点台(70〜79点)のデータの個数
	...
LOOP:
	while (1) {
		...
//		n++;
		c = x/WC;	// 成績の階級化
		f[c]++;	// 階級の度数をカウント
			// 実は,変数c なんて余計.f[x/WC]++; とすれば済むじゃん?
			// でも,結果表示では使うし,処理の意味も明確になるし,ここでも使っておくかー
	}
RESULT:
//	printf("人数:%d\n", n);
	printf("階級:度数\n");	// 度数分布の表示
	for (c = 0; c < NC; c++) {
		printf("%3d : %3d\n", c*WC, f[c]);
	}

	return (0);

ERROR:
	...
}

実行例:

$ cc  hist.c  -o  hist
$ ./hist
成績を入力...
70
75
70
85
60
-1
階級:度数
  0 :   0
 10 :   0
...
 60 :   1
 70 :   3
 80 :   1
...
Step 4. 色々と工夫してみる.

余裕があれば,更に改良してみよう.

例:

$ ./hist
...

階級:度数:グラフ
100 :  3 : ***
 90 : 10 : **********
 80 : 10 : **********
 70 :  5 : *****
 60 :  0 :
...
  0 :  0 :

平均点= 83.5
最高点=100
最低点= 71
棒グラフ表示のヒント: コード「printf("*");」で1文字「*」を表示できる. これを反復すれば棒になる. 最後に改行表示「printf("\n");」も必要.

タスク3:BINGO カード生成プログラム

(このタスクについては,余裕ある人だけ挑戦すれば良い.)

下記の基本ソースコードを元にして, BINGO カードの B の1列だけを生成するプログラム bingo.c を作成せよ. 番号 1〜15 からランダムに選ばれた重複しない5個を昇順に表示すること.

bingo.c の基本ソースコード:

#include <stdio.h>
#include <stdlib.h>	// srand() と rand() に必要
#include <time.h>	// time() に必要

#define	NF	16	// 既出フラグの要素数

int main(void)
{
	int	r;		// 番号(乱数,1〜15)
	int	f[NF] = {};	// 既出フラグ
		// f[r] が 0 なら,番号r は未出,1 以上なら既出とする.
		// 実際の BINGO に番号0 は無いので,f[0] は不使用.
		// 用意しといて不使用は,モッタイナイけど,単純化のための犠牲.
	int	n = 5;		// 生成すべき番号の個数

	srand(time(NULL));	// 乱数列を現在時刻に応じてシャッフル
		// 時刻を使うのは,要するに,乱数の再現性を失くすため.

	// 重複しない n個の番号をランダムに生成
	while (n > 0) {
		r = rand()%(NF-1) + 1;	// 1〜15の乱数
		if (...) ...;		// 重複してたらやり直し!!
		...;			// 既出フラグをセット
		n--;			// 個数をカウントダウン
	}

	// 番号を昇順に並べ替えて表示
	printf("B\n");
	for (r = 1; r < NF; r++) {	// 昇順に...
		if (...) continue;	// 未出なら無視
		printf("%d\n", r);	// 番号を表示
	}

	return (0);
}

実行例:

$ ./bingo
B
3
7
9
10
12

$ ./bingo
B
1
3
4
8
15
上級者向け

上級者は,BINGO カードの全ての列を表示してみては?


本日の課題

レポートを提出せよ.

質問 Q1〜Q3 に回答し,電子メールで提出せよ.