Cygwinでデバッグ

習慣の今昔

コンピュータを取り巻く環境は、短い時間に大きく変化してきています。C言語が生まれて50年ほど、コンピュータの進歩のスピードからすると長大な時間が流れました。この間にC言語も変化してきましたし、プログラミングスタイルというか、思想・哲学も変化しています。ここでは、まずC言語の歴史から知っておきましょう。

C言語の規格4世代

C言語は、UNIX というオペレーティングシステム(OS)を記述するために生まれました。1972年前後、アメリカのベル研究所でのことです。考案者はデニス・リッチー (Dennis MacAlistair Ritchie) です。その後、彼はブライアン・カーニハン (Brian Wilson Kernighan) とC言語の教科書を出版しました。これが『プログラミング言語C』という書籍で、著者2人の頭文字から「K&R」と呼ばれています。この書籍は、言語の教科書として優れていることで有名であるだけでなく、付録の言語仕様の解説が「言語の規格書」の役割を果たしたことでも有名です。この頃のC言語が第1世代で、紛らわしいことに、言語規格も「K&R」と呼ばれています。書籍の「K&R」は(改定されて)今でもバイブルですが、言語規格の「K&R」は今ではさすがに色あせています。

第2世代の言語規格は、当初の考案者の手を離れ、国際標準化機構 (ISO) と米国国内規格協会 (ANSI) が手がけました。1989年から1990年のことです。これが ANSI C、あるいは C89 とも C90 とも呼ばれるものです。ANSI C は K&R 規格を注意深く拡張しながら、C言語から派生した C++ の関数プロトタイプなどの重要な機能を逆輸入し、一度は死にかけていたC言語の息を吹き返すことに成功しました。書籍の K&R もこの規格を元に第2版に改訂され、前述のように今でもバイブルとして読み続けられています。

バイブルと呼ばれる書籍は、一般に難解で初学者を寄せ付けません。書籍の K&R もご多分に漏れず、「K&R だけで C 言語を理解した」というのは自慢にしかなりませんので、理解できなくても安心して下さい。ただし、アセンブラのようなコンピュータ言語に親しんだ人にとっては、理解しやすい構成になっています。

C89 から10年が経ち、C++ も成熟して Java も登場した頃のことです。C 言語にも新規格への機運が高まり、その中で策定されたのが第3世代のC言語規格 C99 です。名前からわかるように 1999年のことで、やはり ISO/ANSI が策定し、C89 と同じく日本工業規格 (JIS) にも取り込まれました。登場当時には gcc を始めとする新機能の取り込みに意欲的なコンパイラと、Visual Studio のようにそうでないコンパイラの両方が存在しましたが、20年が経った現在ではメジャーなコンパイラはどれも対応しています。書籍ではメインに取り上げられることは少ないかもしれませんが、Webの情報で補えますから、避ける理由はどこにもないでしょう。

更に2011年には C11 と呼ばれる4世代目のC言語規格が策定されています。C99 からの変化はそれほど大きくはありませんが、脆弱性への対処が多くなされています。コンパイラの対応も早かったので、利用を検討する価値は大いにあります。

C言語の4世代の特徴を列挙すると以下のようになります。

K&R
関数の引数の型チェックなし
ANSI C (C89)
プロトタイプ宣言による引数の型チェック, void型や列挙型(enum)の導入
C99
1行コメント(//), 変数宣言がブロックの途中でもよい, 可変長配列, 論理型(bool)や複素数型(Complex)の導入
C11
gets()の廃止などの脆弱性対応, 型による分岐(Generic)やインライン関数指示子の導入

新しい規格の中には、既に当時のコンパイラの独自拡張として広まっていた機能を追認しているものもあります。特に行末までの1行コメント (//) は、C89 の時代からかなり広まっていました。この文章でも、C89 の文脈でも気にせず1行コメントを使っているところもあります。

ANSI C と言えば、昔は紛れもなく C89 のことを指していました。しかし、C99 の策定にも ANSI が関わった結果、ANSI C という表現が C89 だけなのか C99 も含んでいるのか、見分けがつきにくくなりました。「ANSI C」という言葉はだんだん使われなくなってきているので、「C89 の頃の話」と読み替えるとよさそうです。

C言語の特徴

このように4世代に渡って進化してきたC言語です。今後もおよそ10年毎に新しい規格が登場するでしょうが、将来にわたって維持されると想像できる、一貫した特徴もあります。

つまり、当初のオペレーティングシステムを記述するという目的に見合った、ハードウェア寄りの操作が得意で、実行時の効率が良いという性質がみてとれます。その代わりに、完全なプログラムを書くためには相応のスキルが必要であるという傾向は、改善はされつつも昔から変わっていないというわけです。

例えて言えば、Javaがオートマチック車だとすれば、C言語はマニュアル車どころか、レーシングカーになるでしょう。C言語ではギアチェンジが手動なのはもちろん、急ブレーキを小刻みに踏み分けてくれる ABS のような装備もなく、一つ一つの操作がストレートに反映され、間違えた操作はすぐに事故につながります。その代わり、車体も軽く、エンジンの性能を最大限に引き出して高速に走ることができます。いわば、プロのための言語がC言語なのです。

なぜC言語を学ぶのか

このようなプロのための言語を、コンピュータ言語の初学者が習うことが多いのは、言語規模が小さいという特徴が大きく効いているからでしょう。Javaは確かにオートマチックで安全に配慮された言語ですが、オブジェクト指向やスレッドなど、自在にプログラムを書くためには多くの知識が必要とされます。このような機能をうまく隠して少ない知識ですませるように指導するのも一つの方法ですが、単純にC言語を選ぶという戦略にも納得がいきます。もっとも、教える側がC言語に慣れているという側面もそれなりに影響を与えていそうです。

昔はコンパイラ(開発環境)は非常に高価なソフトウェアで、ウン10万円しても当たり前という感覚がありました。当然ながら、大枚をはたいて購入した、1つの言語の1つのコンパイラを使い続けることになりました。今は様々な言語の開発環境が無料で手に入ります。処理内容の得意不得意に応じて言語を気軽に選べるようになりました。

今、最初にC言語を学んでいる人も、これから先はいくつもの言語を使い分けることになるでしょう。最初のとっかかりとして、C言語は悪くない選択ですから、コンピュータ言語一般の習慣を身につけることに主眼を置くことにして、言語特有の機能を深く掘り下げるのはやめておくのが賢明だと思われます。この文章では、似た機能が他の言語ではどう扱われているのか、折りに触れて紹介していきます。

省略の美徳はK&Rの時代のもの

K&R 時代には、コンピュータの能力が今とは比べ物にならないほど貧弱でした。そのため、C言語ソースにも無駄なことは書かないという習慣がありました。具体的には、符号なし整数の型として "unsigned int" とは宣言せずに "unsigned" と int を省略しましたし、今でもこの習慣は生きています。こういうものにとどまらず、今なら「5で割り切れない」と判定するのに

if (x % 5 != 0) { ... }

と書きますが、これを短くして

if (x % 5) { ... }

と、比較の != 0 は書かないという流儀があります。

if の成立条件には != 0 は書いても書かなくても、結果は一緒になります。→比較演算

Java では if (x % 5) { ... } はエラーになります。

同様に、

if (malloc(size) != NULL)

とするところを、

if (malloc(size))

と書く流儀があります。書かずにすむことは、徹底して書かない、というわけです。

しかしながら、malloc() 関数の動作説明には「エラーの場合、この関数は NULL を返す。」とあるので、NULL との比較式を書くのが素直に思えます。NULL というシンボル(マクロ定数)が0であるのは周知の事実ではありますが、だからといって積極的に省略すべき理由は(今では)思いつきません。コンピュータ資源が豊富になった現在では、少ない知識で読み書きできるスタイルを学ぶべきでしょう。

読み易さを根拠にどちらかを勧める人がいるかもしれませんが、たぶん慣れによる影響が大きいと思います。

なお、NULL がどんなコンパイラでも例外なく 0 である、と言い切るには、かなりの知識が必要です。→http://www.kouno.jp/home/c_faq/c5.html#5

i++ と ++i の使い分けは職人技

i++ と ++i は、結果として i が1増えることには変わりありませんので、単独で用いる場合は区別しなくても大丈夫です。しかし、比較などと組み合わせると差が現れます。式としての値を取り出すタイミングが、i++なら1増える前ですし、++iなら1増えた後です。

int i=0; while (i++ < 1) { /* 1回実行される */}
int j=0; while (++j < 1) { /* 実行されない */}

上記の (i++ < 1) の条件は、iが1増える前に < の比較が行われ、(0 < 1) は成立してますので while ループは実行されます。
(++j < 1) だとjが1増えた後に < の比較が行われ、(1 < 1) は不成立ですので、whileループは一度も実行されません。

もっと平易な書き方で代用すると次のようになります。

int i=0; while (i < 1) { i++; .....; }
int j=1; while (j < 1) { .....; j++; }

コンピュータの資源を無駄使いしないよう、少しでも短い表記が好まれた時代がありました。その頃には多用された表記ですが、微妙な違いが動作不良(バグ)につながるので、使いこなすには言語に精通せねばならず、今となっては「分割して書いたほうが間違いない」というように考え方が変わってきています。

(&&)||(&&)

変数名関数名の長さ制限 6文字 & 8文字


トップ   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS