新社会人第一歩としてプログラミング研修を受け、数年ぶりに C 言語を触ったら、とある練習問題の思わぬところでつまづきました。私だけつまづいてるのかなと思ったら一緒に研修を受けていた全員がつまづいていてちょっと面白かったのでまとめて報告します。タイトルでオチが読めた人もよければ最後まで読んでいってください。
問題編
事件発生
練習問題の大まかな内容はこうです。権利的な問題でそのままではないことはご了承ください。
文字型配列を用意し、それらの要素に標準入力から 16 進表記の ASCII コードで 10 文字分入力を受け付けよ。そののち、その文字型配列を文字列として標準出力せよ。
この問題に対して私が書いたコードは以下です。たぶん同じ研修を受けていた全員が同じようなコードを書いていたと思います。
#include <stdio.h>
#define STRING_SIZE 10
int main(void)
{
char string[STRING_SIZE + 1] = {0};
int index = 0;
for (index = 0; index < STRING_SIZE; index++) {
printf("%d文字目の入力> 0x", index);
scanf("%X", &string[index]);
}
printf("出力> %s\n", string);
return 0;
}
そしてこれを走らせてみると…1
PS C:\Training> .\practice
0文字目の入力> 0x47
1文字目の入力> 0x6F
2文字目の入力> 0x6F
3文字目の入力> 0x64
4文字目の入力> 0x20
5文字目の入力> 0x6C
6文字目の入力> 0x75
7文字目の入力> 0x63
8文字目の入力> 0x6B
9文字目の入力> 0x2E
1文字目の入力> 0x3F
2文字目の入力> 0x3F
3文字目の入力> 0x3F
4文字目の入力> 0x57
5文字目の入力> 0x54
6文字目の入力> 0x46
...
…そう、入力が完了しなくなったのです。なんどもお。
捜査開始
無限ループといえば条件式のミスや更新のミス。というわけで見直してみるも…うーん、間違っているようには見えません。
そうこうするうちにほかの参加者も騒ぎはじめ、同じ問題に直面しているらしきことが判明。なんなら講師(問題を作った人とは別)も巻き込んでの大騒ぎに。
そして捜査線上にいろいろな情報が上がってきました。配列を必要以上に大きくしたらちゃんと停止したとか、scanf 後の時点でなぜか index が書き換わっているとか…。
やがて、原因が判明しました。
解決編
ここが じけんの あったまの やすひこ です。
問題は整数フォーマットとして scanf で読み込んだことでした。
整数フォーマットで読み込むということは整数として解釈するというだけではありません。どうやら整数型(おそらく int 型)として読み込んでいたようです。
ご存知のとおり、int 型は char 型よりもデータ長が長いです(どれくらい長いかはコンパイラ依存)。int 型のデータ長として読み込まれた ASCII コードが char 型の要素に直接ぶち込まれた結果、大量の 0 がリトルエンディアン2で読み込まれ配列の「次の」要素に流入3。最初は再び入力によって上書きされていたものの、最後には近くに確保されていた index の領域まで侵食し、index を勝手に 0 にしていたのでした。
ループをかちぬくぞ!
これを防ぐにはちゃんと char 型にキャストしてやればいいわけです。
ですが、scanf で読み込むときにキャストはできない…いや、できるかもしれませんが私は知りませんし、できたとして煩雑になるだけなので、int 型の一時保存用変数を用意し、そこからキャストすることにしました。
#include <stdio.h>
#define STRING_SIZE 10
int main(void)
{
char string[STRING_SIZE + 1] = {0};
int index = 0;
int temp = 0;
for (index = 0; index < STRING_SIZE; index++) {
printf("%d文字目の入力> 0x", index);
scanf("%X", &temp);
string[index] = (char)temp;
}
printf("出力> %s\n", string);
return 0;
}
さあ、どうでしょう。
PS C:\Training> .\practiceKai
0文字目の入力> 0x47
1文字目の入力> 0x6F
2文字目の入力> 0x6F
3文字目の入力> 0x64
4文字目の入力> 0x20
5文字目の入力> 0x6C
6文字目の入力> 0x75
7文字目の入力> 0x63
8文字目の入力> 0x6B
9文字目の入力> 0x2E
出力> Good luck.
結果、めでたくきちんと動作しました。
思ったこと
一応、長年(?)のあいだ DX ライブラリでクソゲーを作っていた身ではありますから、char 型が int 型より小さいだの、型キャストの重要性だのは知っていたつもりです。しかし、char 型は実態は整数型なんだよーとか文字には ASCII コードで整数が割り振られてるんだよーとかの知識もあった結果、「scanf でフォーマット読み込みしてやればよしなにやってくれるだろう」と勝手に思い込んでいました。よくよく考えると整数型に実数フォーマットで読み込んだからといってキャストしてくれるわけでもなし、そんな保証はどこにもない……。
今回のような処理をする機会もほとんどなく(というかあっても気づくことなく通り過ぎていたでしょう。今回のこれも STRING_SIZE を少しいじると見た目問題なく動作します)、たぶんそうだろう、という「脳内仕様」にしたがっていままでアマグラマーしてきたのが怖いな、と思ったのでした。
脳内補完された穴は厄介です。穴を探そうにも探す脳が勝手に穴を見逃すからです。今回のケースは、ちゃんとしたプログラマーにとっては鼻で笑われるような事案でしょう。しかし、根底にあるのは人間だれしもが嵌りうる意識の盲点のような気がします。そうでもないのならば素人の妄言と笑いとばしてやってください。
2018/04/20 追記
コメントで指摘いただいたあと、ためしに警告オプション付けてコンパイルしてみたんですが警告発生しませんでした。えぇ…。
以下みたいなコードでも警告出ないのやばすぎて耶馬渓になった。
#include <stdio.h>
int main(void)
{
int array[5] = {0};
array[5] = 10;
return 0;
}
/* Compile Result
PS C:\cpro> cl /Wall sample.c
Microsoft(R) 32-bit C/C++ Standard Compiler Version 13.00.9466 for 80x86
Copyright (C) Microsoft Corporation 1984-2001. All rights reserved.
sample.c
Microsoft (R) Incremental Linker Version 7.00.9466
Copyright (C) Microsoft Corporation. All rights reserved.
/out:sample.exe
sample.obj
PS C:\cpro>
*/
環境何とかして