第5章 配列

テキスト『新・明解C言語 入門編』第5章の内容に関する補足説明を提供します.

5-1 配列

配列

List 5-1 は,配列を使わないときの不便さを示すためのプログラムなので, 実際に書いてみる必要はありません. 読んで理解できればいいです. 配列を使って書き直したプログラムは List 5-9(P.118)です.

テキストの説明にも書かれているように(P.111), 配列の宣言において, 区切り子 [ ] の中に与える要素数は定数でなければなりません. つまり,具体的に自然数を与える必要があるということです. 変数にしておいて値を代入するということはできません. ただし,この章(p.118)で学習する オブジェクト形式マクロ(object-like macro)を使えば, 変数のようなものを与えることができます. プログラム中で配列の要素にアクセスするときには, もちろん変数を使用できます. List 5-3 で学習します. このときの [ ] は,配列の宣言での区切り子とは異なり, 添字演算子(subscript operator)という演算子です. 配列名と添字をオペランド(テキスト p.23)として,配列の要素を返します.

配列内の個々の要素にアクセスするときに使われる 添字(subscript)は, 0 番から始まっていることに注意してください. 間違いやすいところです.

配列の走査

List 5-2 は,List 5-3 を説明するための材料なので, 実際に書いてみる必要はありません. List 5-3 を学習してください.

配列の初期化

このセクションについての補足説明は現在のところありません.

配列のコピー

List 5-6 での結果の出力では, 空白がうまく入っています. 出力の1行目では,a の前に半角スペースが2つ, b の前に半角スペースが4つ入っています. このように空白が入っているのは, puts 関数の引数として与える文字列の中で, 半角スペースを入れているからです. a の前に2つ,b の前に4つの半角スペースを入れて,


puts("  a    b");

としています. 出力の3行目以降, 配列の要素が適切な空白とともに出力されているのは, printf 関数での変換指定を適切に行った結果です.


printf("%4d%4d\n", a[i], b[i]);

としています.ここで %4d という変換指定は, 整数を少なくとも4桁の10進数で出力するという意味ですね(p.36 参照). この指定により,2桁の数字(たとえば,17)では左側に2つ, 1桁の数字(たとえば,0)では左側に3つの空白が入ります.

配列の要素に値を読み込む

このセクションについての補足説明は現在のところありません.

配列の全要素の並びを反転する

List 5-8 での,要素の並びを反転する方法は理解できたでしょうか? 交換する2つの値の一方を一時的に保持しておく変数を用意しておくことが, ここでの重要なアイデアです. 変数の値を更新するけれども,更新前の値を後で使いたいことはよくあります. そのような場合には,この例題での変数 temp のように, 更新前の値を保持しておく変数を用意します.

List 5-8 での, 配列の要素の値を一時的に保持しておくための変数 temp は, 2つの値を交換するための for 文のブロック中でしか用いられていません. このような,ブロック(複合文)の中でのみ利用する変数は, そのブロックの先頭で宣言することが原則でした(テキスト p.58, p.73).

2つの値を交換するための for 文のブロック中で, 配列の添字 i は 0 からいくつまで変化させればよいのでしょうか? List 5-8 では 2 までですね. i = 2 のときは交換処理(x[2] と x[4] の値交換)が実行されます. その交換処理を終えて,i の値を 3 に更新すると, ループの継続条件 i < 3 は満たされないので, 交換処理は実行されません. 一般に,List 5-8 のように配列 x の要素数が奇数 2n + 1 の場合, x[0] から x[2n] の値を反転するときのループの継続条件は, i < n となります. 最後の交換は n 番目の要素と n + 2 番目の要素, つまり,x[n - 1] と x[n + 1] で行われます. 配列 x の要素数が偶数 2n の場合, x[0] から x[2n - 1] の値を反転するときのループの継続条件は, やはり i < n となります. 最後の交換は n 番目の要素と n + 1 番目の要素, つまり,x[n - 1] と x[n] で行われます. 具体例で確認してみてください. この問題は演習 5-5(テキスト p.121)でもう一度扱います. ここで考えたループの継続条件の一般化は,そのときに役立ちます.

配列による成績処理

List 5-9 において, 合計点は整数値しかとらないので int 型の変数 sum の値をそのまま表示します. ただし,表示桁数を5桁としています(%5d と指定). 実行例での変数 sum の値は 415 なので,左側2桁は空白となっています. 平均点は整数値になるとは限りません. そのため, 変数 sum の値を double 型にキャスト(テキスト p.35)してから学生の人数で割っています. そうしないと,あまりが切り捨てられてしまいます. 結果は,小数点以下第1位までを, 小数点も含めて5桁で表示するよう指定しています(%5.1f と指定).

オブジェクト形式マクロ

#define 指令は, ある特定の文字列を別の文字列に機械的に置換する指令です. こうした置換機能をマクロ(macro)と呼びます. 第8章で学習する関数形式マクロ(テキスト p.213)と区別して, 単純な文字列置換を行うこのマクロを オブジェクト形式マクロ(objet-like macro)と呼びます. このマクロという言葉は,いくつかのソフトウェアやプログラミング言語において, 異なった意味で用いられるので注意してください. たとえば,エクセルでの「マクロ」は, VBA(Visual Basic for Application)で書かれた, 自動化された処理を行うためのプログラムです.

#define 指令は,ソースファイルをコンパイルして実行ファイルを作成するときに, プリプロセッサ(preprocessor)というプログラムによって処理されます. ソースファイルから実行ファイルを作成する手順において, プリプロセッサによる処理は最初に実行されます.

コンパイラとして,clang あるいは gcc を使うのであれば, プリプロセッサによる処理を行ったところでコンパイルのプロセスを止めることができます. List 5-10 でこれを行い, プログラム中の NUMBER が 5 に置換されていることを確かめてみることにしましょう. このソースファイルが list0510.c という名前で授業用ディレクトリに保存されているとします. ターミナルで,授業用ディレクトリに移動し,


clang -E list0510.c > pre0510

と入力して [Return] キーを押してください. ここで -E というオプションは, プリプロセッサの処理が終わったところでコンパイルのプロセスを止めるものです. ソースファイルに対してプリプロセッサが処理を行った結果を, > pre0510 という命令によって,pre0510 という名前のファイルに出力します. 記号 > の前後には半角スペースを入れてください.

clang -E

ls コマンドで授業用ディレクトリの内容を確認すると, pre0510 というファイルができていることがわかります.

pre0510

このファイル pre0510 の内容を確認しましょう.ターミナルで,


more pre0510

と入力して [Return] キーを押してください(more の後に半角スペースが必要). 下図のように,ファイル(pre0510)の最初の部分が表示されます. この more コマンドは,テキストファイルの中身を, 1ページ(ターミナルの1画面分)ずつ表示するコマンドです.

more pre0510

上図の表示では,pre0510 というファイルの内容はまだ最後まで表示されていません. 画面の一番下で,pre0510 というファイル名が白黒反転して表示されています. ファイルの続きを表示させるには,スペース [Space] キーを押します. なお,表示を中断させたいときは [Q] キーを押します.

何度かスペースキーを押すと,ファイルの内容が最後まで表示され, 下図のようにコマンドを入力できる状態に戻ります.

more pre0510

上図に表示されている,pre0510 の最後の部分を見てください. ソースプログラムで NUMBER と書いたところ(4か所あります)が, すべて 5 に置換されていることがわかります.

#define 指令のように,プリプロセッサに対する指令はすべて # で始まります. これまでわれわれが「おまじない」として書いてきた #include <stdio.h> も, 実はプリプロセッサに対する指令です. #include 指令については第6章(テキスト p.148)で学習します.

プリプロセッサへの指令はC言語とは異なった文法に従います. よくある誤りのひとつは, #define 指令の最後にセミコロン(;)を入れてしまうものです. C言語の文法ではセミコロンで文が終了するのに対して, プリプロセッサへの指令では行端で終了します. セミコロンは不要です.

配列要素の最大値と最小値

ある2項演算子 @ を用いた式 a @ b @ c での演算順序が, (a @ b) @ c なのか,それとも a @ (b @ c) なのかは, 演算子によって異なります. この演算順序の決まりを結合性(associativity)と呼びます. 詳しくは第7章4節で学習します(テキスト p.204). 代入演算子の場合には, a = b = c という式は a = (b = c) と解釈されます.

演習 5-5 のプログラムを書くときには, 上で述べた List 5-8 についての補足説明がヒントになるでしょう.

演習 5-6 では,a および b の値が何になるか述べるだけでなく, これらの値を表示するプログラムを書いてもよいでしょう.

配列の要素数

List 5-12 のプログラムはこれまで書いてきたものより少し長いですね. こういった長いプログラムは, テキストに書かれたプログラムを全部入力してから実行するのではなく, 部分的に動作をテストすることを繰り返して完成させます その方が誤りの発見と修正が簡単だからです.

点数の入力を促すメッセージを表示するところまでプログラムを書いてみましょう. すなわち,プログラムのほぼ中央にある,


printf("%d人の点数を入力してください。\n", num);

という文まで書いたら,それ以降はまだ書かないでおきます. ただし,最後の return 0; は書かないとプログラムを終了できません. すると,以下のようなプログラムとなります.

List 5-12 partially completed

これでコンパイルしましょう. コンパイルできなかったときには, コンパイル時のエラーメッセージをよく読んで, 誤りを修正します. コンパイルできたらプログラムの動作をテストしましょう. 下図では,80人よりも多い人数や,負の人数を入力したときに, 適切な人数の入力を指示するメッセージが表示されることをテストしています. 問題なさそうなので,15人という適正な人数を入力し, この人数の点数の入力を促すメッセージが出力されることを確認しています.

testing List 5-12 partially completed

部分的に完成させたプログラムが正しく動作することを確認したら, プログラムの作成を続けましょう. 残りの部分はまだ長いですから, 点数の入力を受け付けるための for 文だけ加えるとよいでしょう. この部分を加えたらまたテストを行い.動作を確認します. 正しく動作していることを確認したら, 残されたグラフ描画の部分を書きましょう.

テキストで説明されているように, List 5-12 での最初の do 文は, 点数を入力される人数を1から NUMBER の範囲に制限するための文です. NUMBER の値は 80 としています. このような制限をしないなら,


printf("人数を入力してください:");
scanf("%d", &num);

printf("%d人の点数を入力してください。\n", num);

とするだけでいいです. しかし,想定していない入力に対しては, 一般にプログラムが適切に動作しない可能性が高いですから, List 5-12 のように再入力を促す方がいいです.

この do 文の動作はわかりますね. 最初に,入力された変数を scanf 関数で変数 num に格納します. 次に,do 文の中に埋め込まれた if 文で, 入力値が1から NUMBER の範囲であるかをチェックします. この if 文の制御式は


num < 1 || num > 80

ですね. 人数として入力された数値が1から NUMBER の範囲にない場合に, この制御式を評価した値が1となります(テキスト p.60 参照). そうすると,再入力を促す文が提示されます. 人数として入力された数値が1から NUMBER の範囲であれば, 何もしません.以上で do 文のループが1回実行されました. ここで,do 文のループを繰り返すかどうか判定が行われます. この判定に用いる制御式は while の直後に書かれている


num < 1 || num > 80

ですね.これは先ほどの if 文での制御式と同じです. すなわち,if 文の制御式の値が1となり, 再入力を促すメッセージが表示された場合に, do 文のループが再び実行されます. 再入力された値が変数 num に格納され, 適切な値であるかどうかが判定されます.

入力された点数を配列 tensu に格納するときに, もうひとつの do 文が使われています. この do 文の動作はいま説明した最初の do 文の動作と本質的に同じです. プログラムをよく読んで動作を理解してください.

配列 bunpu についてのテキスト(初版)の説明に誤植があります. テキスト p.123 での「点数の分数を格納するために」という記述は, 「点数の分布を格納するために」の誤りです.

分布グラフでの最初の行(100点の度数)を描くのに使われている,


printf("      100;");

という文では,100 の前に半角スペースを6つ入れてください.

演習5-9 はやや難しいです.

5-2 多次元配列

多次元配列

多次元配列の宣言では,先頭の要素数を省略することができます. List 5-13 の配列 tensu1 の宣言では,


int tensu1[][3] = {{91, 63, 78}, {67, 72, 46}, {89, 34, 53}, {32, 54, 34}}

と書きます.もちろん,省略された要素数がいくつなのか, 初期化子から明確にわかる必要があります. 要素数を省略できるのは先頭だけです.List 5-13 では, 末尾の要素数 3 は省略できません.

List 5-13 では配列 sum は初期化されていません. 変数を宣言するときには初期化するという原則に従うなら, これも初期化しておくとよいでしょう. 一般に,数量のデータを入れる配列では, 構成要素はすべて 0 で初期化しておけばよいでしょう. 1次元配列の初期化と同様に,


int sum[4][3] = {0};

とします.

List 5-13 での点数表示では2重の for ループを使っています. 改行を出力するための putchar 関数は外側のループにあります. わかりにくければ,以下の図に示すように, 内側のループで実行される文を区切り子 { } で囲んでください.

List 5-13

演習 5-10 は数学での行列(matrix)を扱っています. 高校数学のカリキュラムから行列が消えてしまいましたから, 知らなければこの問題はスキップしてかまいません. もし行列についての知識があっても, この問題は少し難しいです.