2016年11月29日
C言語、知ってるつもり?
本記事は、原著者の許諾のもとに翻訳・掲載しております。
「Cならわかるよ」というプログラマーは大勢います。確かにCの文法はよく知られているし、44年の歴史を誇るわけだし、あいまいな機能に悩まされることもありません。簡単ですよね!
あ、「Cならわかるよ」と 言うだけなら 簡単ですよねっていう意味ですよ。学校で習った人もいるだろうしお仕事で使ったことがある人もいるでしょう。覚えることもそんなに多くないし、完璧だという人もいるかもしれません。いいでしょう。でも、Cって実は、そんなにシンプルではないのです。
嘘だと思うなら、今から挙げる問題を解いてみましょう。たった5問です。基本的にはどれも同じで「戻り値は何ですか?」という問題ばかりです。四択方式で、どの問題も正解はひとつだけです。さあどうぞ。
第1問
struct S{
int i;
char c;
} s;
main(){
return sizeof(*(&s));
}
A. 4 B. 5 C. 8 D. わかりません。
第2問
main(){
char a = 0;
short int b = 0;
return sizeof(b) == sizeof(a+b);
}
A. 0 B. 1 C. 2 D. わかりません。
第3問
main(){
char a = ' ' * 13;
return a;
}
A. 416 B. 160 C. -96 D. わかりません。
第4問
main()
{
int i = 16;
return (((((i >= i) << i) >> i) <= i));
}
A. 0 B. 1 C. 16 D. わかりません。
第5問
main(){
int i = 0;
return i++ + ++i;
}
A. 1 B. 2 C. 3 D. わかりません。
はい、ここまで。鉛筆を置いてください。正解はこの曲のあとで。
それでは正解です。
A B C D
1 v
2 v
3 v
4 v
5 v
正解は、ぜんぶ「わかりません」でした。
それでは、ひとつずつ解説していきましょう。
第1問 は、構造体のパディングを扱ったものです。Cコンパイラは、データをRAMに格納するときに、隙間なくびっしり詰めると効率が悪くなることを知っています。そこでコンパイラは、隙間を入れてデータの配置が揃うようにしています。構造体のデータが5バイトだったとしても、実際にはおそらく8バイトぶん確保していることでしょう。16バイトかもしれませんし、6バイトかもしれません。それ以外になることだってあるでしょう。GCCの aligned
属性や packed
属性のような拡張を使えば、このあたりをある程度は思いどおりに制御できます。ただ、それは標準規格からは外れています。標準Cにはパディングを扱う属性が定義されていないので、正解は「わかりません」となります。
第2問 は汎整数拡張に関する問題です。結果が short int
の最大値になる式の型が short int
になるのは妥当でしょう。でも、妥当だからといって、必ずしもそれがCとして正しいとは限りません。すべての整数の式は、 int
として扱うというルールがあります。実際のところはもう少し入り組んだ話なのですが、そのあたりは規格書を読んで楽しんでください。
それはそれとして、ここで比較しているのは型ではなく、そのサイズです。 short int
と int
のサイズについて標準規格が定めていることといえば、前者が後者より大きくなってはいけないということだけです。両者が同じサイズになることだって十分あり得ます。なので、正解は「わかりません」です。
第3問 は、標準規格がカバーしていない部分に関する問題です。整数のオーバーフローについても、 char
型の符号の有無についても、標準規格では定められていません。オーバーフロー時の振る舞いは不定だし、後者については実装依存です。そもそも、 char
型のサイズが何ビットであるかさえも定められていないのです。このサイズが6ビットなプラットフォームもありましたし( トライグラフ 、覚えてますか?)、5種類の整数型のサイズをすべて32ビットでそろえているプラットフォームもあります。これらがすべて決まらない限り、いくら結果を予測してもそれは無意味です。したがって、正解は「わかりません」です。
第4問 は少しトリッキーですが、ここまでくればそんなに難問でもないでしょう。みなさんは既に、 int
型のサイズが標準規格で定められていないことを知っているからです。サイズが16ビットの場合だってあるでしょう。そんな場合は、演算の最初のほうでビットがあふれてしまい、振る舞いは不定になります。これはCのせいではありません。プラットフォームによっては、そもそもアセンブリのレベルでこの挙動が不定な場合もあります。Cコンパイラが演算結果の妥当性を保証するには、大量の処理が必要になってしまうのです。
というわけで、正解はまた「わかりません」となります。
第5問 は、まあ古典的なやつですね。 +
の両オペランドの評価順序についても、前置インクリメント演算子と後置インクリメント演算子との間の優先順位についても、標準規格では定められていません。つまり、 i++
と ++i
を含む演算には落とし穴があります。これらはオペランドを変更してしまうからです。あるプラットフォームでは期待どおりに動いたとしても、別のプラットフォームでは違う結果になることだってあり得ます。これが、定義されていない挙動に関する問題です。こんな場面に遭遇したら、答えはいつだって「わかりません」です。
ここでひとこと謝っておかないといけませんね。どの問題も挑発的なものばかりでした。気を悪くなさったならごめんなさい。
私がCを覚えたのは1998年ごろで、それから15年くらいはずっと「自分はCができる」と思いこんでいました。大学時代に学んだ言語だったし、最初に入った職場でもいくつかのプロジェクトを成功に導きました。C++を主に使うようになってからも、C++は要するに肥大化したCだと考えていました。
そんな考えを改めることになったのが2013年のこと。安全性が最重要視されるPLCプログラミングにかかわるようになったときでした。それは原発のオートメーションに関する研究プロジェクトで、仕様化されていない挙動は絶対に受け入れられませんでした。Cのプログラミングについてはよく知っているつもりでしたが、改めて学びなおす必要がありました。そして、知ってるつもりだったことの大半が、間違いだったのです。学びなおすのは、それはもうつらいことでした。
言い伝えに頼るのではなく標準規格を参照すること、推測を信じるのではなく計測すること、そして「とりあえず動く」は疑ってかかること。エンジニアとしての姿勢を学びなおすことになりました。ここがポイント。人ごとだと思わないでくださいね。
今回のちょっとしたテストが、過去の私のような人たちの考えを(15年もかけずに15分で)改めさせる助けになることを願います。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa