C言語超入門⑥ 配列と文字列を使おう
複数の値をまとめる配列、添字、文字列、fgets、strlenの基本を短いコードで学びます。
5人分の点数をscore1、score2、score3……と保存すると、人数が増えるたびに変数と計算を書き足すことになります。それなら、同じ種類の値を順番に並べて扱いたいですよね。
そのための仕組みが配列です。今回は配列で点数を集計し、その考え方を文字列にも広げます。C言語の文字列には「終わりの目印」があることが重要です。
短いコード欄は、stdio.hを読み込んだプログラムのmain内へ書く部分です。前の例に続けると説明した箇所以外は、例ごとに分けて試してください。
1. 配列には0番から順番に値が入る
int scores[5] = {72, 88, 95, 64, 81};
scoresは、int型の値を5個保存する配列です。1個ずつの値を要素、位置の番号を添字(そえじ)と呼びます。
| 添字 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 値 | 72 | 88 | 95 | 64 | 81 |
scores[0]が最初、scores[2]が3番目です。5個あるからといってscores[5]が最後ではありません。
printf("最初: %d\n", scores[0]);
scores[1] = 90;
printf("2番目: %d\n", scores[1]);
出力は72と90です。配列全体ではなく、[1]の場所だけを書き換えています。
範囲外アクセスは、必ずエラーになるとは限らない
scores[5]やscores[-1]を読むと未定義動作になります。これは、結果が言語で保証されないという意味です。
たまたま数字が出ても、正しく動いている証拠にはなりません。自動で範囲を確認してくれる配列ではないため、0以上、要素数未満を自分で守ります。
2. for文で合計と平均を求める
#include <stdio.h>
int main(void) {
int scores[5] = {72, 88, 95, 64, 81};
int total = 0;
for (int i = 0; i < 5; i++) {
total += scores[i];
}
printf("合計: %d\n", total);
printf("平均: %.1f\n", total / 5.0);
return 0;
}
合計: 400
平均: 80.0
ループでiが0から4へ変化し、それに合わせてscores[i]が72、88、95、64、81へ切り替わります。
途中の合計は72 → 160 → 255 → 319 → 400です。total += scores[i]は、total = total + scores[i]の短い形でしたね。
total / 5.0では、小数の5.0を使うことで小数まで計算します。今回はちょうど割り切れますが、整数の5だけで割ると、別の点数では端数を失います。
sizeofは配列の大きさをバイトで調べる
配列がその場にあるなら、sizeof(scores) / sizeof(scores[0])で要素数を求められます。全体のサイズを、1要素のサイズで割るためです。
ただし、関数の引数に書いたint scores[]は配列そのものを丸ごと受け取る宣言ではなく、ポインタとして扱われます。その関数内のsizeof(scores)では元の配列の要素数は分かりません。関数へ配列を渡すときは、要素数も別に渡すことを覚えておきましょう。
3. C言語の文字列はcharの配列
char name[] = "Sora";
printf("%s\n", name);
C言語には、C++のstd::stringのような標準の文字列クラスはありません。終端文字を含むcharの並びを文字列として扱います。
"Sora"から作った配列は、次の5要素です。
| 添字 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 内容 | 'S' |
'o' |
'r' |
'a' |
'\0' |
'\0'は文字列の終わりを示す、値0の文字です。数字の文字'0'とは別物です。
printfの%sは、この終端までを表示します。だから「4文字なのに5個分の場所」が必要なんですね。終端のない配列を%sへ渡してはいけません。
宣言後は配列全体へ=で文字列を入れ直せない
char name[20] = "Sora";は初期化です。その後のname = "Ren";は書けません。
内容を変更するにはコピー用の関数などを使い、保存先に十分な容量があることを確認します。ポインタと配列を同じものとして考えると、ここで混乱しやすくなります。
4. fgetsで空白を含む1行を受け取る
#include <stdio.h>
#include <string.h>
int main(void) {
char name[50] = "";
printf("名前: ");
if (fgets(name, sizeof(name), stdin) == NULL) {
printf("入力を読み取れませんでした。\n");
return 1;
}
name[strcspn(name, "\n")] = '\0';
printf("こんにちは、%sさん。\n", name);
return 0;
}
Sora Aoiと入力すれば、空白も含めて受け取れます。
fgetsの引数を分けて読みましょう。
name:文字を保存する配列sizeof(name):配列の容量。今回は50バイトstdin:標準入力。通常はキーボードからの入力
最大で容量より1つ少ない文字単位を読み、最後に'\0'を置きます。入力が途中で終わるなどして失敗するとNULLを返すため、先に成否を確認しています。Microsoftのfgetsの説明でも、改行と終端の扱いを確認できます。
改行を取り除くのはなぜ?
fgetsは読み取れた改行も配列に保存します。そのまま%sさんと表示すると、「さん」が次の行へ落ちることがあります。
strcspn(name, "\n")は、先頭から改行までの位置を求めます。そこを'\0'へ変えると、改行の前で文字列が終わります。改行がなければ元の終端位置を返すので、その位置を再び終端にしています。
この説明だけで少し複雑なら、まずこの1行を「末尾の改行を消す処理」として使い、あとでstring.hの関数を調べれば大丈夫です。
長すぎる入力は、全部読めない
name[50]に50バイト以上を入力すると、残りが入力側に残る場合があります。fgetsを1回呼べば常に1行全部が入る、という意味ではありません。
今回の例では短い名前を入力してください。長い入力を断り、残りを取り除いて再入力させる方法は、最終回で扱います。
また、数字用のscanfの後にfgetsを使うと、残っていた改行を先に読むことがあります。入力のたびに別の方法へ切り替えるより、1行ずつ読む方式へそろえると整理しやすくなります。
5. strlenは見た目の文字数とは限らない
#include <stdio.h>
#include <string.h>
int main(void) {
char word[] = "code";
printf("長さ: %zu\n", strlen(word));
printf("配列の容量: %zu\n", sizeof(word));
return 0;
}
長さ: 4
配列の容量: 5
strlenは最初の'\0'より前のバイト数を数え、終端は含めません。sizeofは配列全体なので終端分も含みます。結果の型size_tに対応する表示指定が%zuです。
英数字なら見た目の文字数と一致しやすいですが、UTF-8の"種"は通常3バイトです。「日本語何文字まで」をstrlenだけで判定することはできません。
配列・文字列の定義はC11公開委員会草案の6.7.6.2と7.24が一次資料です。
6. 練習:配列から最大値を探そう
5個の整数{12, 7, 24, 9, 18}から、最も大きい値を探してください。最大値を固定で24と書かず、ループで求めます。
模範解答
#include <stdio.h>
int main(void) {
int numbers[5] = {12, 7, 24, 9, 18};
int maximum = numbers[0];
for (int i = 1; i < 5; i++) {
if (numbers[i] > maximum) {
maximum = numbers[i];
}
}
printf("最大値: %d\n", maximum);
return 0;
}
解説
最初の12を暫定1位にします。7は小さいので変更せず、24で更新。9と18は24を超えないため、最後に24が残ります。
最初の要素はすでに使ったので、ループをi = 1から始めています。
maximum = 0にしないのは、全要素が負の場合に0を誤って返してしまうからです。{-8, -3, -10, -6, -7}へ変えた場合の答えは-3です。配列が空の場合は最初の要素を読めませんが、この例は5個あると決めています。
次の一歩
配列では「何個あるか」と「何番を読むか」、文字列ではそれに「どこで終わるか」が加わります。この3つを意識すると、Cのデータの置き方が少しずつ見えてきます。