『プログラミング言語C』とは何であったか?

言語ではなく本の話である。ついでに言えば第2版の方。

最近、新人向けのC言語の本として『プログラミング言語C 第2版』を推そうとする声にストップをかけたのだが、その辺を含めて、この本についての2020年時点の個人的所感を書き残しておこうと思う。

本書を無理やり一文で表現すると、かつては「コンピュータ科学の基礎を学んだプログラミング経験のあるUnix-like環境ユーザ向けの『C89/C90によるプログラミング』の入門書」だった。

時代背景を考えると、想定読者は「コンピュータにどっぷり肩まで浸った、FortranPascalなどで本格的なソフトウェアを書いた経験のある人」あたりだったのではないかと思う。

ややこしいことに、本書はCプログラミングの入門書であると同時に、Cプログラミングの奥義書でもある。今となっては入門書としての側面は古びているのだが、奥義書としては十二分に通用する。

だから本書を最も評価するのは、Cプログラミングの酸いも甘いもかみ分けたベテラン勢であり――彼らは本書のダメな部分を排除した上で、残りの部分を是としている。守破離で言うならば、本書にたいして破や離のスタンスで接するようにならないと適切に扱うことができない、という難しさがあるように思う。

K&R 2nd. はコンピュータ科学の基礎を学んだプログラミング経験者向け

本書はプログラミング未経験者向けではない。他の言語でそこそこ本格的にコードを書いた経験のある人向けである。また、単なるプログラミング経験だけでなく、コンピュータ科学の基礎部分である離散数学アルゴリズムとデータ構造・コンピュータシステムあたりの知識も暗に要求される。

少なくとも第1章「やさしい入門」について、第0章「はじめに」P.4に次のように書かれている。

ここでは,読者はプログラミングの基本的な要素に関する実際的な知識をもっているものと想定している。コンピュータ,コンパイル過程,n = n + 1といった式の意味などについては説明しない。

第2章から第6章は言語機能の各論であるが、それらに関してP.5に以下のように書かれている。

第2章から第6章では,Cのいろいろな側面を第1章よりもさらに詳しく,もっときちんと論じている。ただそうはいっても断片的なプログラムというよりは,完全なプログラムの実例に重きを置いている。

実際に第2章から第6章の内容は、C言語の言語仕様について事細かに解説している、とは言い難い。コードを書くのに必要な程度には細かく書かれているものの、同時にそこそこ本格的なサンプルコードも出現する。読者は本文の内容と同時にサンプルコードも読み、言語仕様の理解と並行して「実際にコードを書く際に、その言語機能をどう扱うか」という点を学ぶことになる。

「そこそこ本格的なサンプルコード」と書いたように、hello, worldの次のサンプルが「華氏・摂氏の温度対応表を出力する」であるし、第2章「データ型・演算子・式」では関数の解説もまだなのにatoi / rand / srand / strcat / strlenのサンプル実装例が出てきたりする。

一般的な入門書のサンプルコードが「紹介した言語機能の動作を紹介する」という点に注力した内容であるのにたいして、本書のサンプルコードは「現実のプログラミングにおいて、『紹介した言語機能』はどのように使われるか」という点を示したものだ。

このような構成は、他言語でのプログラミング経験がそれなりにある人がC言語を学ぶ際には、丁度よい塩梅だ。C言語に限らず、一般的な「○○言語入門」的な本は想定読者にプログラミング初心者を含むことが多く、プログラミング経験者からすると冗長で読み進めにくい内容となっている。本書にはその冗長な部分が排されている。だから「簡潔で明瞭な内容」と言われる。

留意すべき点として、サンプルコードの「完全なプログラムの実例に重きを置い」た内容には、コンピュータ科学ネタが多く含まれる。例えばシェルソートクイックソート、二分探索、逆ポーランド電卓、二次元座標、二分木などだ。これらのネタについて、本書では事細かな解説はなされていない。なので読者には予備知識が求められる。

だから本書は、どちらかと言えば他人のコードを読み慣れている玄人向けであるし、コンピュータ科学の基礎として学ぶような内容に慣れ親しんでいる必要もある。裏返すと、本書はプログラミング未経験者に読ませるには早すぎる。

K&R 2nd. は少しだけUnix-like環境ユーザ向け

本書の別の特徴として、多少なりともUnix-like環境の経験があると読み進めやすい部分がある。

そもそも最後の章である第8章が「UNIXシステム・インタフェース」なのだが……それを除いたとしても、以下のような指摘ができる。

  • P.7やP86のコンパイル実行例でcc(1)を使用している。
  • 第7章「入出力」にてパイプとリダイレクトに触れている。
  • cat(1)、echo(1)、wc(1)を題材としたサンプルコードが出現する。

これらの要素についても、やはり本書内では事細かな説明は無い。事前にUnix-likeなユーザランド(シェルによるCLI環境)に慣れ親しみ、予備知識を蓄えておくと安全だろう。

K&R 2nd. は「C言語」ではなく「Cプログラミング」の入門書

先にも書いたが、本書の内容はC言語の言語仕様について事細かに解説したものではない。プログラミングに必要な程度には細かく解説されているものの、主眼は「その言語機能を、現実のプログラミングにおいてどう用いるか」という点に置かれている。

つまり「C言語」の入門書というよりは、「Cプログラミング」の入門書という側面が大きいと考えた方がよい。

プログラミング経験者を想定読者とした上で、読者が短期間でC言語を使えるようになること、それもうまく扱えるようになること(UNIX環境で培われた「効率的かつ慣用的」なコーディングができるようになること)を目指した本だといえる。

この辺は、第1章「やさしい入門」の冒頭P.6に書かれている以下の一文が物語っている。

読者には,できるだけ早く役に立つプログラムが書けるようになってもらいたい。それには,変数と定数,算術,制御の流れ,関数,基本的な入出力といった基本事項に注意を集中する必要がある。

比較的最近の本で例えるなら『プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)』のノリに近い。こちらもプログラミング未経験者向けではないし、サンプルコードも実用寄りの題材を元ネタとした内容だ(少なくとも「機能説明のためのサンプルコード」ではない)。著者の片一方が同一人物だから、似たようなノリなのだろうか?

K&R 2nd. を読むうえで注意すべき点

本書を読むうえで注意すべきなのは、「早合点せずに章や節の最後まで読み進める」ということを心掛けないと誤読してしまう、という点だ。正確を期すためにアレコレ書いているためか、本文の最後に書かれた補足で「ちゃぶ台返しかよ!」という感想を抱くことが時々ある。

例えば1.10「外部変数と通用範囲」では、外部変数についてP.39に次のような記述がある。

これらの変数は,それを使用する関数の中でも宣言しなければならない。これは,はっきりexternと宣言するか,あるいは前後関係から暗黙のうちに宣言すればよい。

その上で、例えば外部変数char line[MAXLINE]について、各関数の中でextern char line[];と宣言したサンプルコードを掲載している。

/* copy:  特別版 */
void copy(void)
{
    int i;
    extern char line[], longest[];

    i = 0;
    while ((longest[i] = line[i]) != '\0')
        ++i;
}

ここまでの内容だと、あたかもTclでビルトインコマンドglobalを使うのと同じように、外部変数使う際には関数内で宣言しなくてはならないかのように見える。

しかし、ここまでやっておいて、P.40で次のようにちゃぶ台返ししている。

ある状況の下ではextern宣言は省略可能である。すなわち,外部変数の定義が,特定の関数で使われる以前にそのソース・ファイルの中でなされていれば,関数内でのextern宣言は不要となる。つまりmain, getline, copyにおけるextern宣言は余分である。実際,普通のやり方は,ソース・ファイルの最初ですべての外部変数を定義してしまい、extern宣言は全て省くという形である。

まあ、確かに書かれた内容の通りなのだが、正直なところ「なんで『例外則→通常例』の順番で説明してるんだ! 紛らわしいぞ。というか『extern宣言は余分である』というのなら、なんでextern宣言付きのコードを載せたのよ」というのが個人的な感想である。

そしてextern宣言が省かれたサンプルコードが掲載されておらず、上記の文章のみだという点に、「これ、誤読して混乱する人が出そう」と一抹の不安を覚えてしまうのである。

経年劣化した部分

本書は原書が1988年(日本語版が1989年)に出てから30年以上経過しており、メンテナンスされなくなって久しいため、経年劣化を含めて何点か問題が表面化している。

1点目は、所々に訳が良くない部分があることだ。明らかに誤訳……という部分は無いものの、分かりにくい表現の部分があって、理解の妨げにはならないけど理解に時間がかかってしまう。第2版の日本語版は1994年ごろに訳書訂正版が出ていて、現在流通している版はその流れを汲むものだと思われるのだが、それでも翻訳の問題が指摘されている。

個々の翻訳の問題については先人たちが指摘済みなので、ここでは挙げない。それよりも重要なのは、それら指摘済みの問題点が改善されないままであることだ。

一応、翻訳版を擁護しておくと、前項に書いたように元が「早合点せずに章や節の最後まで読み進めないと誤読しかねない」という部分のある代物なので、翻訳時に色々と惑わされた可能性は否定できないのではないかと思う。

2点目は、サンプルコードのスタイルが現代の「行儀のよいC言語のコーディングスタイル」と異なることだ。

例えばmain関数について、戻り値の型を明示しない*1、引数を利用しない場合にvoidを書かない*2、return文を書かないことがある*3、などの問題がある。これらのスタイルは、現代のCプログラミングでは推奨されないだけでなく、コンパイルを行う環境*4によっては警告が出てしまい、初心者を惑わせることになる。

また、防御的プログラミングの観点で忌避されることのあるぶら下がり文(制御構文の内容が単文の場合に、大括弧を付けないスタイル)が採用されている。これなどは、C言語シンタックスとしては合法だが、Cプログラミングの先達として「初心者にはあまり真似してほしくない」と思うスタイルである(守破離の破や離の段階で触れるべきものであり、少なくとも守の段階では触れるべきではない)。

大概の新人は、教科書のサンプルコードのスタイルを真似るはずだ。そこでいきなり微妙な感じのスタイルを真似されると非常に困るのだ。同じような理由で『Cプログラミング専門課程』も新人に薦められなくて困っている。ある程度分かっているレベルの人なら問題ないのだが、新人に読ませるには躊躇する。ええ本なんやけど、なあ。

3点目は、あくまでもC89/C90の本である、ということだ。さすがに21世紀も5分の1が過ぎようとしている昨今では、特段の事情がない限り新規開発ではC99対応のコンパイラが使える訳で、やはり入門書も最低でもC99の仕様(の中でもC11以降でオプショナルにならなかったもの)には追随してほしい。

ただ、職業プログラマとしてC言語を扱う場合、時に「C89/C90/C95」と「C99」と「C11/C18」の3グループぐらいは区別をつける必要がある。そう考えると、本の構成として例えば「いきなりC99」とするか「『C89/C90』のちC99(もしくはその逆順)」とするのか、判断が難しいと言えるだろう。

なお私は最近その「特段の事情」で面倒な思いをしている。誰だよ、 Microsoft C/C++ Compilerをサポートしようと言い出した奴は*5

蛇足として、本書の第8章「UNIXシステム・インタフェース」は、例えるなら現代のLinuxユーザが『UNIXプログラミング環境』を読み進めるのと同じ程度には、内容を鵜呑みせずに読解すべき章だろう。

……ここの記述って、多分POSIX1よりも前のものだよね? POSIX1のリリースは1988年だから、おそらく原書の執筆時点ではPOSIX準拠の環境は存在しなくて、ベル研内のUNIX上でサンプルコードの実装と動作確認を行ったのだと思うけど、どうなのだろう?

色褪せぬ部分:C99もC11も所詮はC言語

色々と経年劣化した部分は見られるものの、それでも本書を評価するベテランは依然としてそれなりにいると思う。

それは本書が「Cプログラミング」の本であり、C99やC11に進化しようともC言語C言語のままであるため、依然として内容の本質部分の多くが通用するからだろう。

例えばC++でのプログラミングは、STL登場の前と後とで結構変わったし、C++03とC++11の間でも大きな変化が生じたように思う。

一方で、C99やC11での言語仕様や標準ライブラリの変化は比較的「痒い所に手が届く」ものだ。確かに今まで不便だと思っていた部分は改善されたが、しかしCプログラミングの本質部分が大きく変わるような変化ではない。

だから本書は、細かい諸々こそ古びているものの、本質部分は現代でも十分に通用する。

問題は、読者に「古びている細かい諸々」をフィルタリングして読み進める能力が求められることだ。例えるなら『ソフトウェア作法』や『UNIXプログラミング環境』を今から読むようなもので、どちらも良書だけど中身を鵜呑みできない(RatforもFortranも使っていないし、UNIXではなくLinuxユーザである――という読者が大多数のはずだ)。必要に応じて「今、自分が置かれた環境ではどうなるか」という視点で読み替えを行うことになる。だから、新人に薦めたいけど薦められず、問題なく読める読者が中堅やベテランに限られてしまう傾向が生じる。

フィルタリング能力が求められる時点で、本書の読者にはC言語やCプログラミングの知識・経験が必要となるのだ。

まとめ

本書は「コンピュータ科学の基礎を学んだプログラミング経験のあるUnix-like環境ユーザ向けの『C89/C90によるプログラミング』の入門書」である。つまり元より、読者にそれ相応の知識や経験が求められる代物である。

同時に本書は「C言語」ではなく「Cプログラミング」の本である。C99やC11ではCプログラミングの本質が変わるような変化はなかったため、本書の本質部分は現在でも十分に通用する。その一方で、コードの細かい部分では変化が生じている。そのため読者には、過去のコンピュータ系名著を読むような「内容を鵜呑みせず、時代変化に応じて読み替えをおこなう」という技術が要求される。

だから今となっては、本書はCプログラミングの中堅やベテランには薦められるものの、Cプログラミングの未経験者には推薦できない。ましてプログラミング未経験者にはもってのほかである。

*1:暗黙の戻り値の型であるint型を期待している、C95以前の記法。C99ではNGである。

*2:関数の定義において、引数なしの時に f() と書く記法は、ANSI Cよりも前の伝統的な形式であり、C99にて廃止予定機能となった。だから使うべきではない。なお関数の宣言においては、 f() は「ANSI Cのプロトタイプ宣言」ではなく「伝統的な形式の宣言」とみなされ、「引数なし」ではなく「任意個数の引数をとりうる」と解釈される――つまりプロトタイプ宣言による引数の個数/型のチェックが行われないのである。

*3:C95以前でもC99以降でも合法的な記法ではあるが、main関数のみに許された特別扱いでもある。他の関数定義との整合性を考えると、慣れるまでは「戻り値がvoid型以外の関数では、必ずreturn文で『戻り値の型に合致する値』を返す」というスタイルで統一した方がよいだろう。

*4:コンパイラの種類・バージョン・付与するオプションなど。

*5:言い出した奴は自分なので、自業自得である。C99スタイルのインライン関数と可変長配列が未サポートなんだよね。