第5章 配列

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

5-1 配列

配列

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

配列の宣言において区切り子 [ ] の中に与える要素数は定数式とすることが原則です. つまり,具体的に自然数を与えます. テキストの第1版では「定数でなければなりません」と書かれていました. 絶対のルールだったのが「原則」に変更された事情は第2版の Column 5-2 を読んでください. 要素数は定数で指定する習慣をお勧めします(著者の柴田先生も Column 5-2 でこのように推奨しています). ただし,この章で学習する オブジェクト形式マクロ(object-like macro)を使えば, 変数のようなものを与えることができます.

プログラム中で配列の要素にアクセスするときには, もちろん変数を使用できます. List 5-3 で学習します. このときの [ ] は,配列の宣言での区切り子とは異なり, 添字演算子(subscript operator)という演算子です. 配列名と添字をオペランド(第1版 p.22,第2版 p.24)として,配列の要素を返します.

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

配列の走査

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

配列の初期化

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

配列のコピー

このセクションはテキストの第2版では 5-1 節の最後にされました. このウェブページでも順序を変更しましたので, 5-1 節の最後を見てください. 第1版での List 5-6 は第2版での List 5-13 です.

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

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

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

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

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

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-4(第1版 演習 5-5)でもう一度扱います. ここで考えたループの継続条件の一般化は,そのときに役立ちます.

配列による成績処理

第1版でのこのセクションは次の「オブジェクト形式マクロ」のセクションと統合されました.

オブジェクト形式マクロ

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

#define 指令は, ある特定の文字列を別の文字列に置換する指令です. こうした置換機能をマクロ(macro)と呼びます. 第8章で学習する関数形式マクロ(第1版 p.213,第2版 p.228)と区別して, 単純な文字列置換を行うマクロを オブジェクト形式マクロ(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章(第1版 p.148,第2版 p.158)で学習します.

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

List 5-10 でのマクロは定数の定義を行っていると言えます. 本来であれば定数が入る場所に NUMBER という文字列を入れておき, #define 指令でこの文字列を定数に置換しています. 配列の宣言において, 区切り子 [ ] の中に与える要素数は定数である必要がありました. マクロを使うと, あたかも要素数を変数としたかのように書くことができます.

List 5-10 で生徒の数を意味する NUMBER のように, 意味をよく表現する名前をつけた定数のことを 記号定数(symbolic constant)と呼びます. 記号定数には一般に大文字が使われます. 記号定数に大文字を使うのは,関数名や変数名との意図しない一致を避けるためです. 関数名や変数名を小文字にしておけば. うっかり記号定数と同じ名前を使ってしまっても, プリプロセッサによる置換は行われません.

#define 指令で定数を定義することは避けるべきだという意見があります (たとえば,『日経ソフトウェア』2013年7月号特集「そのコードは古い」). 理由のひとつは定数に置換される文字列が型の情報を持たないことです. List 5-10 での NUMBER という文字列は整数に置換されるべきなので, int 型の変数のようなものです. しかし,実際には NUMBER は単なる文字列なので, 型の情報を持ちません. 型の情報を持つ変数はコンパイル時に型のチェックがされます. たとえば,int 型の変数に実数(小数点以下あり)が代入されていれば, 警告が出るでしょう(第3章を参照). マクロではこうしたチェックがなされません.

第6章(第1版 p.152,第2版 p.162)で学習する const 型修飾子を使うと, 値を変更できない変数,すなわち定数を定義できます. #define 指令で


#define NUMBER 5

と書くのではなく,変数の宣言で


const int NUMBER = 5;

とします.ここで NUMBER は int 型の変数ですが, 値を5から変更できません.

しかし,const 型修飾子を使うこの方法では,NUMBER はもちろん変数ですので, List 5-10 のように配列の大きさを NUMBER と指定することは, 本節の最初に学習した原則に反します. この例のように配列の大きさを柔軟に変更したい場合には,マクロを使うことは許されるでしょう.

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

ある2項演算子 @ を用いた式 a @ b @ c での演算順序が, (a @ b) @ c なのか,それとも a @ (b @ c) なのかは, 演算子によって異なります. この演算順序の決まりを結合性(associativity)と呼びます. 詳しくは第7章4節で学習します(第1版 p.204,第2版 p.220). 代入演算子の場合には, 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

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

testing List 5-12 partially completed

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

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


printf("人数を入力せよ:");
scanf("%d", &num);

printf("%d人の点数を入力せよ。\n", num);

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

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


num < 1 || num > 120

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


num < 1 || num > 80

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

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

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

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


printf("      100;");

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

演習5-8(第1版 演習5-9)はやや難しいです.

配列のコピー

List 5-13(第1版 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進数で出力するという意味ですね(第1版 p.36,第2版 p.38 参照). この指定により,2桁の数字(たとえば,17)では左側に2つ, 1桁の数字(たとえば,0)では左側に3つの空白が入ります.

5-2 多次元配列

多次元配列

テキストでも強調されているように, 多次元配列(multidimensional array)とは,配列の配列です. テキスト p.133 の Fig. 5-10(第1版では p.125 の Fig.5-9)を見てください. 2次元配列の正体は,要素が配列である1次元配列です. これまでに学習した int 型の配列(1次元配列)は, 要素が int 型の整数でした. 整数を並べるかわりに配列を並べれば,2次元配列となります.

多次元配列の宣言では,先頭の添字(要素数)を省略することができます. たとえば,List 5-15(第1版 List 5-13)での配列 tensu1 の宣言では,


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

と書くことができます. 多次元配列を1次元配列として理解すると, この1次元配列の添字が省略可能となっていることがわかります. これは int 型の配列(もちろん1次元)の宣言で添字を省略できたことと同じです.

添字を省略できるのは先頭だけです. たとえば,List 5-15(第1版 List 5-13)での配列(tensu1,tensu2,sum)では, 末尾の添字である 3 は省略できません. これまで使ってきた1次元配列の宣言では,要素数は指定しなくても, 要素が何であるかは明示していました. たとえば,int a[] と宣言すれば, 要素が int 型の整数であることは明示されています. 同様に,2次元配列において int a[][3] と宣言すれば, 要素が int[3] 型の配列であることはわかります.

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


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

とします.

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

List 5-15

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