多数のデータを手軽に取り扱うため,配列の適切な使い方を理解しよう. ついでに,配列と一緒に利用されることの多いマクロ定数についても.
複数の同型の変数の集合を一気に用意できる.
ソースコード記述方法:
型名 配列名[要素数];
具体例:
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;」へ書き換えられ,エラーとなる
ソースコードを書く時,レイアウトを適切に整えておけば,間違いに気付き易くなる. これからは,インデントの作法に従おう.
複数データの入力・修正が可能なプログラム 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)だけ変更すれば, 関連するすべての部分を漏れなく自動的に書き換えてもらえる. 最初から,こんな書き方をしておくべきだったんだ.
(本日の課題1です.)
試験成績の度数分布(頻度分布,ヒストグラム)を算出・表示するプログラム 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: // ←余計だけど...構成を見通し良くするために付けてみた
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
(本日の課題2です.)
下記の基本ソースコードを元にして, 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〜Q4 に回答し,電子メールで提出せよ.
Step 4 に取り組んだ場合,改良点についても説明せよ.
メールの送信形式を必ず「テキスト形式 or プレーンテキスト」に変更せよ.