多数のデータを手軽に取り扱うため,配列の適切な使い方を理解しよう. ついでに,配列と一緒に利用されることの多いマクロ定数についても.
複数の同型の変数の集合を一気に用意できる.
ソースコード記述方法:
型名 配列名[要素数];
具体例:
int data[100];
これで,100個の int型の要素変数 data[0],data[1],data[2],...,data[99] から成る配列 data が用意される.
なお,要素数 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です.)
複数データの入力・修正が可能なプログラム record.c を作成してみよう.
まずは, データ入力については後回しとして, データ表示の機能だけ作り, とりあえず, 配列の基本的な使い方だけ確認してみよう.
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.初期値を一気にゼロにできる
データを順番に入力できるようにしてみよう;
... 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言語プログラムに(本来あってはならないが)よくある重大な欠陥である.
バッファオーバランを防止してみよう:
... printf("要素番号(-1 で終了)> "); scanf("%d", &i); if (i < 0) break; if (i >= 3) continue; // 範囲外の要素番号を無視 ...
これで,とりあえずオーバランは回避できただろう. しかし,次の通り,これだけでは不充分だ.
データ配列の要素数について,3 では少なすぎるので,増やしてみたい. この場合,ソースコード中の複数箇所にある定数 3 をすべて書き換える必要がある. しかし,この時,間違いが発生しやすい. たとえば,if (i >= 3) の 3 だけを 10 に書き換え, 配列宣言 data[3] の 3 をそのまま書き換え忘れてしまうと? バッファオーバランが簡単に発生してしまう.
プログラミング時の間違いの予防策を取り入れてみよう. 配列要素数をマクロ定義しておき, 即値(定数 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です.)
試験成績の度数分布(頻度分布,ヒストグラム)を算出・表示するプログラム hist.c を作成してみよう.
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
... 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 エラー:不正な入力を無視しました... ...
#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 ...
余裕があれば,更に改良してみよう.
例:
$ ./hist ... 階級:度数:グラフ 100 : 3 : *** 90 : 10 : ********** 80 : 10 : ********** 70 : 5 : ***** 60 : 0 : ... 0 : 0 : 平均点= 83.5 最高点=100 最低点= 71
(このタスクについては,余裕ある人だけ挑戦すれば良い.)
下記の基本ソースコードを元にして, 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 に回答し,電子メールで提出せよ.
Step 4 に取り組んだ場合,改良点についても説明せよ.
メールの送信形式を必ず「テキスト形式 or プレーンテキスト」に変更せよ.