特集 Cプログラミングの秘訣

最終更新: 2006-03-28

このテキストはC MAGAZINE 1992年4月号に掲載された原稿のオリジナルテキストを元にしてHTMLに変換したものです。掲載文章と細部が異なっていると思われます。また、気付いた個所をいくつか修正してあります。

当時はまだWindows 95もないような時代で、現在の状況から見ると違和感のある内容も結構あるかもしれませんが、時代背景を想像しながら補正しつつ読んでいただければ幸いです。

※2006年3月28日追記: 何が原因か知りませんがこのページのアクセスが増えているそうなので、 HTML のおかしなところを修正しました。 文章の変更はありません。 なお、このサイト(表ページ)は現在休眠状態ですが、 裏ページ裏の裏ページ の方を、細々と更新していたりします。


目次


Part1 よいプログラムを書く条件

今や、質はともあれ、C言語に関する書籍は数多く、入門書を自称したものだけでも十冊を越えるほどです。しかし残念ながら、これらの本を読んでC使いになろうと挑戦したにもかかわらず、挫折してしまった人や、数十行程度のプログラムなら書けるようになったが、そこから先、ちっとも上達しない、という方が多いのではないかと想像します。なぜC言語に挫折してしまうのか考えてみますと、一つは、世の中に「これだ」と言える程の入門書は本当に少ないのが一つの原因となっているようです。多くの入門書を読んでみましたが、どれも、ある程度のプログラミングの知識や、処理系、例えばMSDOSの知識が前提とされていて、本当に何もわかっていない人が読んでも何も分からないようなものが大半です。さらに悪いことには、これは私だけの意見かもしれませんが、入門をうたっているものほど、いいかげんな内容が多いようです。例えば式と文をごちゃまぜで説明したような本があります。何もしらない人がこれを読んで式とは何か、理解しろというのは無理な相談です。

そういう本に当たってしまった人は、単に運が悪かったのですから、よい本を読み直せばC使いになれる可能性が残っています。本当にくだらない理由ではありますが、よい教科書というのは実に重要なポイントだといえましょう。

さて、内容がしっかりした入門書や解説書を幸運にも買うことができたら、それでC使いになれるかといいますと、そう甘くはありません。先程書いたように、これらの書籍には、真の「わからない」人には、わからないことがいくつも前提にされています。これを身に付けているかどうかが、勝敗の分け目となるのです。

一言でそれを表現すると、「よいプログラムを書きなさい」ということです。よい文章と悪い文章があるように、よいプログラムと悪いプログラムがあるのです。また、うまい絵と下手な絵があるように、うまいプログラムと下手なプログラムがあるのです。誰でも、悪いプログラムよりは、よいプログラムを書こうと思うのが当り前です。わざわざ悪いプログラムを書こうと努力する人はいないはずです。

ところが、どのようなプログラムがよいプログラムなのか、これを説明した本はあまりありません。もちろん、「よい」という意味は総合的であり、非常に多くの内容に対して検討する必要がありますから、例えば「変数名は内容のわかるように付けよう」だとか、「不必要なコメントは省きましょう」というように、部分的に「よいプログラム」を作るための条件を指摘するという意味では、たいていの本にはそのような記述があると思います。しかし、総合的に考えて、どのようなプログラムがよいのかは、なかなか書かれていません。なぜかというと、これは簡単に書けるような問題ではないからです。それぞれの「よいプログラム」のための条件の中には、場合によっては両立しないこともあります。全体的には、バランス感覚が極めて重要であることは言えます。

そして、ここが重要なので、じっくり考えてください。「よいプログラム」を書くには、その人との相性があることが、必要不可欠だということを、事実として認めるべきです。あるいはセンス、素質の問題と言ってもいいでしょう。

この記事をご覧になっている皆さんの中には、おそらく、C言語とはどのようなものか、おおむねの知識があり、これからC言語を学んでマスターしようと意気込んでいる人や、あるいは今までC言語の入門書を何冊か読んで、Cコンパイラも買った。多少は例を見てプログラムを真似して、Hello World! くらいは画面に表示した経験がある人もいるかもしれません。また、まだ何にも知らない文字通りビギナーの人もいるでしょう。

特に、今まで何となくCのプログラムらしきものは書けるようになったか、なかなか上達せずに、行き詰まってしまった人。その理由はわからないが、今度こそはわかるかもしれないと思って期待している人もいるでしょう。

そこで、今度こそうまくいくかもしれない、と思った皆さんへは酷かもしれませんが、ここでとどめの一撃を受けてもよい時期にさしかかったのだと思ってください。人生は短いものです。無駄な時間を費やすことの方が、ずっとむごいことかもしれません。そこで、一つ考え直してみて欲しいのです。もしかして、あなたはプログラム作成には向いていないのではないでしょうか。あるいは、C言語には向いていないのではないでしょうか。今まで頑張った成果がいま一つと実感している方は、今やそれを疑ってみるべきです。

相性の悪い人がいくら頑張ってもよいCのプログラムを書くことは難しい

もしかすると、世間には「誰でもCプログラムが書ける」という迷信が広まっているかもしれません。プログラムの入門書を買ってきて読めば、誰にでもプログラムは書けるものでしょうか。ある意味では書けるといえるし、ある意味では書けないといえます。

例えば、誰かにペンを持たせて絵を描いてみなさいといったとき、全く何も描けない人は滅多にいません。とにかく絵を描くというのは、誰にでもできるのです。しかし、同じ人に、本の挿絵に使いたいからイラストを描いてくれと頼むと、ちょっと待ってくれと言われるかもしれない。「いや、私は絵は駄目なもので…」というわけです。絵が描けるというのと、「うまい絵が描ける」というのは別の問題なのです。

同じ様に、歌のうまい人と下手な人がいるし、ピアノがうまい人も下手な人もいます。。ピアノといえば、猫ふんじゃっただけは弾けるぞ、という人がたまにいるようです。そういう人にピアノを弾いてくれと頼むと、いや、実は弾けないのだと断わられるでしょう。Hello World! を画面に表示するというのは、ピアノで言えば猫ふんじゃったを弾けるかどうかのレベルの問題であり、早い話がC言語でプログラムが書けるかどうかという問題とは殆ど関係がないのです。絵の具を買ってきても誰でも絵が描けるわけではないのと同じように、コンパイラを買ってきてインストールして、よい入門書を読めば、だれでもうまいプログラムが書けるようになるとは限らないのです。

つまり、プログラムを書く人には、それなりの素質というか、相性が必要です。素質のない人が、死ぬ思いで修練したら、人並みのプログラムを書けるようにはなるかもしれませんが、生まれつきプログラマーに向いている人がセンスよく書いたようなプログラムを作るのは、多分無理でしょう。それよりはもっと自分に向いた何かを見つけて熱中した方がずっと有意義だと思います。

さて、ここで諦めた人は、もうこの後は読まないでしょうから、ここを読んでいる皆さんは、まだ頑張れるんじゃないかと希望を捨てなかった人だと思います。「相性が悪い人にはCのプログラムは書けません」で終わってしまうと、さすがに編集者に文句を言われるかもしれませんので、ここで、再度「よいプログラム」とは何か、考えてみることにしましょう。この後は、あくまで持論となります。一般論としては認められていないと考えた上で考察していただければ幸いです。

いきなり結論を書きましょう。「よいプログラム」とは何か。ずばり一言で。「そんなものは言葉で表現できない」というのが私の意見です。もちろん、各論として、よいプログラムの条件を個別に考察することは可能ですが、全体としてプログラムを捉える時、これがよいか悪いかは、結局プログラマの感性による所が大きいと思うからです。しかし、よいプログラムか悪いプログラムかは、ある程度の素質がある人が、いくつもプログラムを書いているうちには、体で判断できるようになってきます。ここが重要なのです。つまり、言葉で説明できないといっても、よい、悪いの差は歴然として存在するということです。

これについて、故松下幸之助が次のような意味のことを言ったそうです。商売の道というのは、言葉で説明しても理解できるものではなく、体験して初めて身に付くのである。例えば、塩がからい、砂糖が甘いというのは、塩や砂糖を味わった人だからわかるのであって、塩も砂糖もなめたことがない人に、それを説明することはできない。至言であります。プログラムのよい、悪いも、このように、それを体験して味わった人でなければ、真に理解することはできない、と思うのです。よい、悪いというのはそれほど感覚的な問題なのだ、という解釈です。

従って、いいプログラムの書けるC使いになるには、とにかくプログラムを作る、そして、他の人のプログラムもどんどん読む。これは重要です。自分の悪い所というのは、なかなか気付かないものです。しかし他の人と比べてみるとき、自分よりも巧くやっているな、と気付くのは簡単です。従って、自分の力だけで悪い所を直すのは、極めて非能率的であり、他のものと比較してみる、これは結構効きます。

他の人が書いたプログラムを読もう

さて、結論が出てしまいましたので、この記事はこれで終わりになる所ですが、もう一歩踏み込んでみたいと思います。よいプログラムを言葉で説明することはできないと書きました。しかし、各論として、よいプログラムの条件を個別に考察することはできるとも書きました。ならば、それぞれ細かい所でよい、悪いの評価を行う方法は残っています。これは全体的なよい、悪いの判断とは必ずしも一致しませんが、その一部分がよいプログラムは、全体としても、それ程悪くないことが多いと思います。

そこで、ありきたりのようですが、C言語に多少は関係あると思われるいくつかの条件に対して、一つのガイドラインとなるものを書くことにします。この後に書くことが、あくまでガイドラインに過ぎないことを念頭に置いてください。実際、これらのガイドラインを無視しても、Cのプログラムを書くことはできるはずです。場合によっては、あえて逆らってみる方がよいかもしれません。それが判断できる方なら、こんなガイドラインは不要ですから、自分の感性を信じて修行するべきです。

ただ、初心者の名乗る方の中には、本当に何をやってよいかわからないこともあります。C言語は、空白は読み飛ばすから、インデントはどのように付けても実行結果には関係ないだとか、好きなように書いて構いません、と言われても、それはそうかもしれませんが、何をやってよいやら全く訳のわからない人に、「好きにやってください」と言ってもかえって何もできなくなってしまうことの方が多いでしょう。よし、自分で考えて好きにやってやろう、という人は、それで結構です。考えるというのは重要なことですから。しかし、どうでもいいと言われても困る、何か参考になるものはないか、と思う人も多いはずです。既にあるものを真似して腕を磨く、というのは非常に多くの道において成り立つ手法ですが、プログラミングにおいてもそれは成り立ちます。そのための指針となるものとして、お役に立てば幸いです。

さて、各論を書く前に、宣言しておきたいことがあります。

まず、これから書くことは、私が現在こうしようと考え、実行していることです。フィンローダ流とでも言いましょうか。これはあくまで一つの流儀であって、唯一正しい解釈であるということではありません。他の解釈の方が合理的であると考える人は大勢いると思います。

もう一つは、私自身、これからもずっとこれが正しいと考えるかどうかは、保証しかねるということです。プログラミングにおいて、これが最善と断言することは、なかなか難しいものです。たかが一人がプログラムを書いて身に付いたことを書くのですから、これからさらに経験を積むことにより、他の方法がよいと思い直したり、考え方が変わることは十分考えられます。日々是修行です。私が保証できるのは、これから書くことは、今の時点で最善だと思い込んでいることに過ぎないのです。

従って、これから書くことに対して、何か反論のある方もいると思いますが、反論の方に分があると思えば、私はころころと自説を変えると思います。その時に正しいと思った判断に従うという一貫性が最も重要であって、何か一度いったらあくまで覆してはならない、といった考えは、まるで持っていません。結構無責任といえば無責任な話です。

そこで、次のことを強くおすすめします。これから書くことは、あくまであなたがどうすればよいか、あなた自身で考えるための参考に留めてください。書いてあることを鵜呑みにしないで、自分自身で考えて、それに従って判断してください。考えるということは重要です。考える習慣を付けることが、よいプログラムを作る近道であることは確かです。


Part2 明解プログラミングのすすめ

□なぜ、分かりやすくかくべきなのか

フィンローダ流のプログラミング書法の核心は、ただ一点に集約できます。プログラムを分かりやすく書く、ということ、これだけです。しかし、実際に書いてみればわかりますが、プログラムを分かりやすく書くということは、まず不可能に近いような神業に感じることが多いようです。

一般論としても、よく「プログラムは分かりやすく書こう」と言われます。中には、どうせ自分は趣味のプログラマーだから、他の人が読みやすいように努力してもしょうがない、と勝手に思い込んでいる人もいるようですが、とんでもない話です。分かりやすく書こうというのは、もちろん、自分が分かりやすくなるために書くのです。

ある程度の規模のプログラムを作ったことのある人なら誰でも、次のことを経験しているでしょう。

「プログラミングに集中している時には一時的に脳の力が高まり、普段以上の精神活動を行うことができる」

しかし、恐るべきことに、この能力は時間が経過すると通常の状態に戻っていきます。その速度は、脳の力が高まる度合に比例します。その結果として、自分が書いたものなのに、見たこともない文字の羅列となり、解釈すらできないことがよくあるのです。従って、分かりやすくプログラムを書く心構えは、まず他人たる自分のためにそうする必然性があることを、十分理解しなければなりません。

※アルジャーノン・ゴードン効果という。

しかし、はっきりいって、分かりやすいか分かりにくいかは、主観に大きく依存する問題です。これが絶対真理だ、という法則はなかなか見つかりません。しかし、大雑把な範囲で、「こうすれば分かりやすいと感じる人が多いのではないか」という程度の判断は、多くの人が感覚的に知っています。ただ、なかなかそれを主張する人がいないのは、ある人が分かりやすいと思ったとしても、果たして他の人にとっても同様に分かりやすいだろうか、という決め手に欠けることが多いからでしょう。

非常にポピュラーな定石ですが、ファイルをオープンする処理の書き方に、list 1のようなものがあります。

list 1
    if ((fp = fopen(filename, "r")) != NULL) {
        ...
    }

もちろん、...の部分には、何か処理が書かれるのであり、実際に...と書くのではありません。さて、この記法がポピュラーなのは何故か考えてみましたが、おそらくK&Rにごろごろ出てくるからだと思います。昔はCを勉強する人はK&Rを読むしかないような時代もあったので、このような書き方をして分からないという人はいませんでした。もちろん、Cが使える人の中に、という意味です。そして、今も、この書き方でわからないという人は、C使いの中にはいないでしょう。

処理は一見それほど難解ではないと思いますが、初心者なら何のことやらわからないかもしれません。例えば、これは次のように読みます。

「ふむふむ、if か、もし()の中が真なら{}の処理をするんだな、なになに、fpにfopenの値を代入してと、fpというのはファイルポインタだな、fopenの中はファイル名とオープンする時のモードがここでは"r"だから読みだしか、ほんでもって、結果がNULLでなければ値が真だから、{}の中の...を処理してと…。」

まさか本当にこう読む人はいるわけがありませんが。Cに慣れたプログラマーなら、この一行を一瞬見ただけで即座に処理を理解(連想?)して、次の行の解釈に移っています。定石とはそうでなければ意味がありません。まあ、定石だから、こう書いても何も悪くはないと思います。誰も責める人もいないだろう。私も責める気は毛頭ありません。ただし、私なら次のように書きます。

list 2
    fp = fopen(filename, "r");
    if (fp  != NULL) {
        ...
    }

何故か? 実は某所で、プロならこう書くのだがと書いた所、間髪入れずに「どうしてなんだ」と質問が出たそうです。※

※実は、とあるパソコン通信サービスの話。ここは世界でも珍しい異次元空間で、はっ、と気付いた時にはばっさり情報が消滅しているので、答える暇もなかったわけだ。ちなみに、某所ってどこだというと、「パソコン通信サービス」なんて書くと苛められそうな所だ、というと、分かる人には、なんだアソコのことか、とすぐ分かる。

パソコン通信での秘訣を一つ紹介しますと、質問されそうなことがあらかじめ分かっていたら、前もって予想して回答を考えておくという秘伝があります。あまり書きたくはないのですが、念のため書いておきますと、上の場合、もしその質問があったら書くつもりで用意した回答は、「こっちの方が味がいいから」というものでした。余計わからない? だから書きたくないって言っているではないですか。

味がいいというのを理解してもらうのは大変です。私自身がこの味をなんとなく分かるようになるまでに、Cでプログラムを作り初めてから、少なくとも5年はかかったと思います。しかも、私がいい味だと感じても他の人もそう感じるかといえば定かではありません。そこで、もう少し科学的に説明できないか考えてみました。その結果、この味が、どうも思考の順序に関係しているのではないか、ということに気が付きました。そこで次のような仮説を立ててみます。

「順序が逆転した処理は、思考を妨げる」

これは、後者のリストを、前者と同じ様に考えながら読んでみると分かります。どのように読めるでしょうか。

「さてと、fpに、fopenの値を代入するわけだ、fopenの中はと、ファイル名とオープンする時のモードがここでは"r"だから読みだしだな、次はifか、fpというのは今代入した値だな、これがNULLでなければ値が真だから、{}の中の...を処理してと…。」

なんだ、あまり変わらないではないか。その通り。でも、あまり変わらないなりにも変わった所を見逃してはなりません。最も顕著な違いは、ifが現われた時の後処理です。先程の例では、まずifを見た時点で、一旦ifの処理中であることを、頭の中のスタックに積まなければなりません。最初の記法では、

「ふむふむ、if か、もし()の中が真なら{}の処理をするんだな、なになに…」

と、

「…結果がNULLでなければ値が真だから、{}の中の...を処理してと…。」

の間に処理が割り込んでいるため、ifという文字を見てから、条件判断までの間が長くなっているのが一つの特徴です。そんな無茶苦茶長いわけではないのだから、この程度ならいいではないか、と考える人もいるかもしれません。それも確かにもっともではあります。

もう一つの特徴は、前者の括弧の数です。一般に、括弧の重なりが深いと、分かりにくくなります。かといって、むやみに括弧を外せばいい、ということではありません。次のように書く人は、余程の初心者がうっかり間違える場合でもなければ滅多にいません。

list 3
    if (fp = fopen(filename, "r") != NULL) {
        ...
    }

C言語では、演算子の優先順位がはっきり階級化しています。まず優先度の高いものから処理され、左右の順序は、優先度が同じ場合にのみ結果的に意味を持ちます。従って、このように書くと、優先順位の高い演算子である!=がまず評価され、その後にfpに続く代入演算子=が処理されることになります。すなわち、次のように括弧を付けた場合と等しく解釈されます。

list 4
    if (fp = (fopen(filename, "r") != NULL)) {
        ...
    }

これはおそらく期待した処理の流れとは全然違うはずです。

一般論としては、優先順位について、絶対に死んでも間違いないという自身があるという程の場合でなければ、たとえ余計といえども括弧を付けておいた方が意表を突かれることは少なくなります※。

※間違えれば死ねばいいという意味ではない。

括弧についてもう一つ述べておきますと、括弧によって思考そのものも確実に入れ子になりますから、場合によっては、不要な括弧があることが、かえって分かりやすいこともあります。

list 5
    if ((c >= 0x20) && (c < 0x7f)) {
        ...
    }

実は、上の例は、次のように書いても問題ありません。

list 6
    if (c >= 0x20 && c < 0x7f) {
        ...
    }

なぜなら、比較の演算子である >= や < は、&& よりも優先順位が高いからです。しかし、後者のリストを見た時、頭の中ではどのような思考が行われているでしょうか。

「まずifだな、cが0x20以上かどうかをまず見て、ふむ、次は&&か、論理アンドか、ということは、次の条件と両方成立しなければならないのか。で、cと7fを比較するのか。」

別に難解でもなさそうに見えますが、一つ大きな罠を忘れているようです。というのは、上の思考には、優先順位が暗黙の了解として使われているからです。もし、本当に前から順に読めば、次のようになるはずです。

「まずifだな、cが0x20以上かどうかをまず見て、ふむ、次は&&か、論理アンドか、ということは、次の条件と両方成立しなければならないのか。で、cか。cとの論理アンドなんてとるかな? まてまて、次は<だな。ということは、こっちの方が優先順位が上だな、…」

さて、現実にこんなことを頭で考えているかというと、この程度の記述であれば、C使いになって間もない人ならともかく、多分、慣れた人なら瞬時に何を行おうとしているのか判断するのではないでしょうか※。というのは、この種の条件判断は、しばしば行われるので、この表現自体に馴れているということも見逃せない事実であります。

※認知心理学という学問の分野がある。ユーザーインターフェースを設計しようとする人は、必ずかじっておくべきだろう。これによれば、人間が文字の並びを認識する場合には、あるまとまった単位を先にグループ化してから解釈を試みることが知られている。その意味では、

    c >= 0x20 && c < 0x7f

という文字列が与えられた場合、これを、

    c >= 0x20    &&    c < 0x7f

と解釈するのは自然だが、

    c >= 0x20 && c    <0x7f

と解釈するのは不自然に感じられるかもしれない。そのような潜在的な欲求がある場合、本文で説明したような順序で解釈を行うというのは、いかにも不自然である。

では、括弧を付けた方がよいか、付けない方がよいか、はっきりしたルールを書いておくことにしましょう。ただし、もちろん、括弧を付けなくても付けなくても評価の順序が変化しないことを前提とします。

□ 括弧がなくても心理的に間違った解釈をする可能性が全くないと思われる場合には、括弧を省略しても構わない。

□ 括弧を省略すると、心理的に間違った解釈をする可能性が極めて高いと思われる場合には、括弧を省略してはならない。

□ これ以外のケースは、各自の判断による。

なるほど、言いたいことは分かった。しかし、心理的に間違った解釈をしそうかどうかは、どのようにして判断するのかね? という質問が当然あるでしょう。それは修行して身に付けるべく、経験を積むしかないのです。

括弧の話はここまでです。では、なぜK&Rの本には、このように2行に分けずに、1行に書いたのでしょうか。当時はその方が処理系がよいコードを生成したのでしょうか。それとも、行数が減ることによって、見やすくなると判断したのでしょうか。同じ処理内容であれば、少ない行数で書いた方が、一度に目に入る密度が高くなって、理解しやすくなるという考え方もあります。

もう一の典型的な例を考えてみましょう。これは実に面白く、よくできていて、しかも初心者が引っ掛かりそうな書き方になっています。

list 7
    while ((c = getchar()) != EOF) {
        ...
    }

この処理がわからないという人は、おそらくEOFと比較しているものが一体何なのか、理解できないのだと思います。cと比較しているのではないかと、漠然と解釈して乗り切る人もいるかもしれません。while の括弧の中は、次のように読めるでしょう。

「まず、cにgetcharでもらった値を代入してだな、それがEOFでなければループの中の処理をする」

秀逸なのは、「それが」の所です。C言語は、式も値を持っていることを利用した芸当です。ですから、厳密には、cとEOFを比較しているというのは正しくないのですが、あえてそのように思い込んでいるとしても、この例ではあまり差し支えはないでしょう。

では、これを括弧を外して書くには、どうすればいいでしょうか。

list 8
    while (c = getchar() != EOF) {
        ...
    }

これは無茶です。代入演算子の優先順位より、比較の演算子の方が高いので、getcharの値は先にEOFとの比較に使われてしまい、比較して同じだったかどうかという結果の値、すなわち0か1がcに代入されてしまいます。

list 9
    while (c = getchar(), c != EOF) {
        ...
    }

処理としては間違ってはいません。問題は、whileを見てから、その条件の判断部分に到達するまでに、c = getchar() という余計な処理が一つ入ったことです。whileからこれが遠くなればなるほど、いま何の条件を考えていたのか忘れてしまうでしょう。では、whileのループの中に、処理を入れてしまうというのはどうか?

list 10
    while (1) {
        c = getchar();
        if (c == EOF)
            break;
        ...
    }

今度は、確かに括弧の数は減りました。しかし、今度はwhileを使う必然性が弱まっています。これは単なる無限ループなのですから。

結局、この場合においては、最初の書き方に比べてあまりうまい方法はなさそうです。というより、最初の書き方がうますぎる、と言ってもよいでしょう。実際、この書き方は、C使いになるには、必ずマスターしておかねばならない表現の一つだと言ってよいでしょう。

そうしますと、今度はlist 1の、

list 1 (再掲)
    if ((fp = fopen(filename, "r")) != NULL) {
        ...
    }

という書き方に対して、むしろ違和感がとれて、理解しやすいといった現象も現われるのではないでしょうか。なぜなら、この記法は、まさにwhileの書き方と同種であるからです。

さて、ここまで読んだ方が、もし相当腕利きのC使いならともかく、初心者だったなら、一体どう書けというのだ、とイライラしてきたかもしれません。そこで、どっちがいいんだ、という方だけへのアドバイスだと思ってください。自分の意見のある方は、もちろんそれを優先させるのがよいと思います。

結論

思考の順序を妨げないようにプログラムを書こう

小手先の技のように見えるかもしれませんが、これがフィンローダ流の奥義です。

list 2 (再掲)
    fp = fopen(filename, "r");
    if (fp  != NULL) {
        ...
    }

このように書くのは、思考の順序という意味では、ifの判断の中にfopenを入れるよりは勝っています。上の奥義を指針とすれば、もう一つ、よく迷うことのある判断に決着を付けることができます。

list 11
    while (EOF != (c = getchar())) {
        ...
    }

このような書き方です。特徴は、定数を二項演算子、ここでは!=の左側に持ってくることです。なぜこのようにするかというと、list 12を見てください。

list 12
    if (i == -1) {
        ...
    }

この処理は、iが-1の場合に括弧の中を実行する、というごく単純なものです。しかし、うっかりして、list 13のように間違えるかもしれません。

list 13
    if (i = -1) {
        ...
    }

これがC言語で有名な、「==と=の間違い」というヤツで、誰でも必ずこの種の間違いをした経験があるはずです。この間違いの特徴は、処理の内容は全く異なるにも関わらず、いずれも文法的には正しいため、コンパイルが正常終了してしまうことです。実行してみて、何か動作が変だ、というあたりで初めてバグに気付くこともあります。この種のバグは、気が付かないと修正するまでに結構時間を取られるものです。

しかし、次のように書いていればどうなるでしょうか。

list 14
    if (-1 == i) {
        ...
    }

-1という値にiを代入するというのは、Cの文法としては誤りなので、コンパイルの段階で、エラーを発見することができます。従って、間違いはすぐに気付くというわけです。左辺に定数を持ってくると、代入できないということと、比較は左右を入れ替えても結果が同じである、という特徴を活かしており、その意味ではうまい方法だといえるかもしれません。

余談。比較は左右を入れ替えても結果が同じ、というのは、場合によっては成り立ちません。例えば、次のような場合、結果が異なるかもしれません。

list 15
    if ((getchar() + 0x20) == getchar())
        return;

しかし、このような記述は無謀です。なぜなら、比較の左辺と右辺のどちらが先に実行されるかは決まっていないからです。このような書き方をしたプログラムは、Cコンパイラをバージョンアップした後でコンパイルし直した後で動かなくなっても、何も文句は言えません。list 15はlist 16のように書くのが正解です。

list 16
    c1 = getchar();
    c2 = getchar();
    if ((c1 + 0x20) == c2)
        return;

あるいは、ifの中のc1とc2を入れ替えてください。※

※私は、比較の場合には右が先に実行されるという先入観がある。なぜなら、そのような処理系を使っていたからである。

話を戻しましょう。先程、左辺に定数を持ってくるのは、ある意味ではうまい方法だといえるかもしれない、と書きました。しかし、実は、フィンローダ流では、左辺に定数を書くことはありません。常に右に書きます。

定数を比較演算子の左に書かないようにしよう

なぜでしょう。フィンローダ流を理解した人なら、想像できるかもしれません。少し考えてみてください。

簡単に理由を説明しますと、右に定数を書いた方が、わかりやすいから、ということです。

どういう意味なのか、詳しく説明します。皆さんは、何かに対して何かする場合、どのように考えますか。これでは漠然として分かりにくいですか、例えば傘をさす、という場合に、「さす、傘を」と考えますか、それとも「傘をさす」と考えますか? 何を馬鹿な質問をしているのだ、「傘をさす」に決まっているじゃないか、と言われそうですが、これは重要なことです。普通、ものごとを考える順序としては、何かに対して、何かする、という方向であることが圧倒的に多いわけです。

そこで、先程の処理に関して、考えてみるべきことは、iを-1と比較したいのか、それとも-1とiを比較したいのか、ということです。この点はまず異論のないものと思いますが、殆どの人が、iという変数に何らかの値を入れて、それと定数とを比較する、という順序で処理を考えているのではないかと思います。言い換えると、ここでは主役は変数であるiではないか、ということです。

ならば、思考の順序としては、i == -1 と書いて、iと−1を比較しているのだ、と読ませる方が、はるかに無理がないのではないか、というのがフィンローダ流の根拠です。思考に無理があると、その部分の理解だけではなく、無駄なエネルギーを使うことになり、全体の理解の妨げにもなりかねません。その結果、バグを誘発するということは、十分考えられることです。小手先の技に溺れて、全体を分かりにくくしては、あまりに無駄だということです。

しかし、左辺に定数を書いたら、先程指摘したような間違いは防げるではないか、という考え方もあるでしょう。あくまでその間違いが怖いと思うなら、何も私の意見に従う必要はありません、自分の判断を尊重してください。早い話が、この程度のことは、どっちかにしないと無茶苦茶差がある、といったものではありません。※

※とは書いたが、結構見やすさに差があるような気もする。

ただ、私自身は両者比較した上で、定数は左辺に書かないと決めたのですから、左辺に書かないでも大丈夫という理由も一応書いておきましょう。

まず、私が普段使っているコンパイラはBorland C++ですが、このコンパイラは、条件判断の箇所に、代入式が現われた時には、警告を表示してくれます。したがって、その気がないのに、うっかり

    if (i == 1) ..

と書くつもりの所を、

    if (i = 1) ..

と書いてしまったとしても、コンパイルこそ正常に終了しますが、そのような記述をしてしまったことは明確に判断することができます。

次に、このような間違いは、Cプログラムに慣れれば殆どしなくなります。=と==を間違うというのは、かなり初歩的かつ初心者に多い間違いです。慣れるに従い、それよりも他のバグの方が圧倒的に多くなってきます。ならば、他のバグの原因となる、思考の妨害因子を少しでも減らした方が能率的だと判断しました。

そして最後ですが、今書いたように、この間違いは慣れれば殆どしなくなるのですが、それでもする人はします。私が最後にこの間違いをしたのは、確か数年も前のことで、なんとこのバグを取るのに殆ど1日かかってしまいました。それみろ、言わんこっちゃない、定数を左に書いておけば1日得したではないか、って? 残念ながら、それは外れです。なぜなら、私がこの時間違えたのは、比較の左右ともに変数だったからです。左右ともに変数だと、どちらにどちらを書いてもコンパイルは正常に終了してしまうのです。

□わかりやすいプログラムの条件

分かりやすいプログラムに必要な条件を大雑把に2つに分類すると、次の2点に集約されます。

たとえると、前者は、読みやすい文章に相当し、後者は、きれいな字に相当すると思えばよいでしょうか。内容と、字面との差です。ただ、考えておかなければいけないことは、それぞれは完全に独立した条件ではなく、構造がきれいなプログラムを書くには、それなりの書き方のノウハウが必要だし、きれいな体裁で書くには、ある程度構造自体まとまっていなければ難しいようです。

プログラムの構造というのは、処理の流れ、アルゴリズムなどをどのように扱うかがポイントとなります。

■ファイル(モジュール)を分割するには

C言語によるプログラムは、非常に長い一つの関数を作るのではなく、ある程度の短い関数をいくつも作って呼び出すことにし、場合に応じて関数を別のファイルに記述し、分割コンパイルすることがよいとされています。

原則は、関連ある関数、変数を一つのファイルにまとめるという単純なものです。これが実際に行うと難しいのは、プログラムの中にある関数、変数というのは、完全に関係あるものと関係ないものに分かれる場合もありますが、多少関係あるとか、殆ど関係ないが、無関係でもない、といった瞹昧なものもあるからです。

初心者であるうちは、それ程大きなプログラムを作ることもないと思いますので、あまり分割に気を使うこともないでしょう。分けることばかり考えるよりは、他のことに頭を使った方がいいと思います。

■一つの関数の長さ

C言語の特徴は、まとまった処理を関数というブラックボックスの中に閉じこめるという手法により、処理を理解しやすくするという点にあります。何千行にわたるプログラムを理解するのは、さすがに大変なのですが、数十行ならそれほど難しいことでもありません。なぜかというと、あまりに行数が多いと、最後の方の処理を考えている時には、最初の方でどんな処理をしていたのか、すっかり忘れてしまうからです。

また、行数が多過ぎると、それにつれて使う変数の数も増え、間違って一時変数を二重に使ってしまうという、あり得ないようなバグの入ることもあります。これも、どこで何をしているのか、全体的に把握するのが困難になるための副作用だと考えてよいでしょう。

従って、当り前のようですが、原則として、行数を少なくすることが、わかりやすさを増す秘訣です。ただし、ここで言っている行数というのは、処理の数を指しているのであり、単に1行にいくつも処理を書いたのでは、殆ど意味がありません。

中には、関数の長さは1画面におさまるように、従って24行以内に書きなさいという人もいます。何が「従って」なのかわかりませんが。ワークステーションなどでは40行程度を一画面に表示できるものがありますが、その場合は40行まで書いていいのか、などと悩む必要はありません。

すでに、思考の順序に従って書くという奥義を紹介しました。これに従っていれば、おおむね前後の処理の結び付きが最も強くなり、離れた行ほど疎な影響になることが多くなります。従って、密な処理ほど近い所にあるため、一つの関数が画面スクロールさせなければ読めない場合でも、さほど混乱しないはずです。

処理の内容によって、しなければならないことは違います。何でもかんでも例えば24行という一定の行数で書く努力をするよりは、自然な流れになるように関数を作る方がずっと得です。

また、あまり関数を短くすると、必然的に関数の数が増えることになります。ある関数の処理を理解するには、その関数が呼び出している関数の処理を理解しておかねばなりません。呼び出している関数が何だったか確認しているうちに、呼び出している元の関数で何をやっていたのか忘れてしまうようでは、元も子もないのです。

list 17
    void sjis_to_tad(char *fname)
    {
        FILE *fp;

        fp = open_file(fname);
	sjtotad(fp); /* コード変換した結果を画面に表示する */
        close_file(fp); /* ファイルを閉じる */
    }

この処理の内容を大雑把に把握するのは簡単です。なお、コメントのうち、最後の「ファイルを閉じる」というのは明らかに無駄であり、close_fileという名の関数がファイルを閉じることは、99%の人なら瞬時に想像できます。(たとえファイルを閉じるということが、どのような処理か理解していなくとも)。

しかし、list 17を見ても、open_fileの中でどのような処理が行なわれているか確実に把握することはできません。open_fileが失敗したらどうなるか気になるはずです。この場合は、次のように書いた方が明快であることに気付くでしょう。

list 18
    void sjis_to_tad(char *fname)
    {
        FILE *fp;

        fp = fopen(fname, "rb");
        if (fp == NULL)
            exit(1);
        sjtotad(fp); /* コード変換した結果を画面に表示する */
        fclose(fp);
    }

Part3 実効的コメント書方

極論を先に紹介しますと、コメントはいらないと豪語する人もいるし、コメントはいくらあっても差し支えないと断言する人もいるようです。これらが両極端な意見なのですが、あってもなくてもいい、というのもいいかげんすぎて納得できないでしょう。プログラムが動けばいいというものではないと解釈する人には、コメントは非常に重要な意味を持ちます。

まず、コメントはいらないと思っている人へ。コメントがあってもなくてもプログラムの実行結果は同じだ、だからいらない。絶対いらない。そこまで言うなら別に考えを改めなさいとはいいませんから好きにしてください。コメントがあってもなくてもコンパイルした結果は確かに同じです。

※特殊な場合にはそうとも限らない。これは意味上はコメントとは言えないので、本記事では想定しない。

コメントはあった方がプログラムが分かりやすくなる、だからコメントはいくらあってもいい、と思っている人へ。あなたは考えを改める必要があります。私の経験では、訳の分からないコメント以上にプログラムを難解にするものはありません。プログラムの理解を助けるためには、そのコメントが適切なものである、という条件が極めて重要なのです。そして、適切なコメントは、自ずから適切な分量というものがあって、いくらでも書けるような性質のものではないのです。もっとも、これは揚げ足取りのようなものです。「コメントはいくら書いてもいい」という主張は、明らかに「適切なものであれば」という前提を想定しているからです。にもかかわらず、わざわざ揚げ足を取らせていただいたのは、世の中に不適切なコメントというものが意外と多く、コメントはただ多ければいいと思っているのではないかと邪推することも多かったからです。

なぜ不適切なコメントがいけないかというと、誤った先入観を与えるからです。この先入観というのがくせもので、最もわかりやすい例は、間違ったコメントです。

間違ったコメントに関して一番困ることは、コメントが間違っているのか、それともコメントが正しくて処理の内容が間違っているのか、分からなくなることです。実は両方おかしいことも、よくあるのですが。

□余計なコメントは書いてはいけないのか

余計なコメントというのは、間違ったコメントに比べると罪は軽いといえます。しかし、余計なコメントもやはりプログラムをわかりにくくします。なぜなら、プログラマーがプログラムを読んでいる時には、読んでいる内容に集中しようとしますが、余計なコメントが現われると、それを読む労力が割かれて、集中力が分散するからです。

ただし、多くの「C言語の入門」をうたった記事に、「余計なコメントは書かないこと」と書いてあって、次のような例まで載っていることがありますが。

list 19
    fp = fopen(filename, "rb"); /* ファイルをバイナリモードでオープンする */

よほど変なプログラムでなければ、fopenがファイルをオープンする関数であることは間違いないので、そんなことは書かなくてもいいし、"rb" とあれば、バイナリモードであるに決まっている。だから、そんなのは書いても意味がない。それは正論です。しかし、初心者向けの本には、上のようなコメントがよく書かれています。なぜかというと、初心者向けの本の場合には、fopenがファイルをオープンする関数であることとか、"rb" というのがバイナリモードを意味することを例示するという目的で、そうであることを知らない人を想定して、例となるプログラムを書くのですから、このようなコメントがあった方が適切かもしれないのです。

ところが、これを読む人は何も分からないで読むわけですから、このようなコメントは書いておいた方がいいのだと錯覚してしまうかもしれません。あくまでこれは特殊な例であって、Cでプログラムの書ける人なら、まずこのようなコメントの内容は頭に入っていると考えて間違いありません。となると、皮肉なことに、これは「不必要なコメント」の典型的な例になってしまうのです。

かといって、

    int i;  /* i という名の変数 */

というコメントを付けるという豪傑は、流石に私は今まで一度も見た経験がない。これ程ナンセンスなコメントがあると、かえって何かあるのではないかと疑ってしまうかもしれませんが、とにかく人並みの感性を持っていれば、このコメントが無駄以外の何物でもないことは、説明しなくても分かっています。

では、多くの秘伝書に「無駄なコメントは書くな」と記されているのはなぜでしょう。これは裏の意味を考えるとわかります。つまり、重要なのは「必要なコメントを書きなさい」ということなのです。無駄なコメントがあることは、必要なコメントがないことよりは、ずっと罪が軽いといえます。しかし、必要なコメントを付けるというのが、これが結構センスが必要な作業であって、なかなか無駄なものを書いてしまうことが多いわけです。「そんなことはいいから、もっと必要なことを書いてくれ」と言いたくなります。

余計なコメントがあっても、例え誰かに見られて笑われたとしても、それは笑われただけで済むことです。余計なコメントによって、必要なコメントが目立たなくなることは問題ですが、余計なコメントは、いつでも削除することができます。しかし、逆に必要なコメントを後で思い出すことは、とても難しいのです。

結論
特にC言語を始めたばかりの人は、余計かどうか判断付きかねるものは、どんどんコメントにして書きましょう。いらないと言われたら、消せば済みます。そのうちに、何が必要なコメントで、何が不要なコメントか分かってくるようになりますから、自然にいらないコメントは書かなくなるでしょう。

□必要なコメントをなぜ書けないのか

しかし、必要なコメントだと分かっていたら、言われるまでもなく、コメントに書くのではないでしょうか。にもかかわらず、コメントに書かないというのはなぜでしょう。

1)コメントを書くのが面倒だから。

場合によっては、コメントを書くのに本当に面倒な環境もあります。例えば、コンパイルしようとしたら、メモリが足りませんと表示されて、コンパイルできなかった。仕方ないのでコンパイルするために、仮名漢字変換のプログラムを追い出してメモリを空けた。コンパイルはできるようになったが、今度はプログラムの中に日本語でコメントが書けなくなってしまった、という経験があります。もちろん、真面目に環境を整備したら、もう少しマシになるのでしょうが。そして、このような場合でも、破綻した英語でもローマ字でもいいから、本当に必要なコメントなら書いておくべきです。後で、ローマ字で書いてあるコメントを普通の日本語の文章に置き換えるのと、何も書いてないプログラムに、何をコメントしたかったのか後で思い出しながら書くのと、どっちが楽かいうまでもないことです。思い出す手間をかけてコメントを書くのは、書かない方がマシと感じるほど面倒なものです。

2)必要なコメントであることがわからない。

実はこれが問題なのです。言い換えれば、そこにコメントしておく必要であるという判断ができないのです。または必要と思わなかった、従ってコメントにしなかった、ということです。その時は頭に入っているが、後でわからなくなるかもしれないことがあれば、コメントに書くのが望ましいことです。しかし、これは将来忘れるかもしれないということは、それを覚えている間は、なかなか気付かないことがあります。

もしくは、後で見てすぐに分からなくても、多少読めばわかるだろうと安易に考えてしまうこともあります。コメントを書く手間よりも、多少読む手間の方が少ないと判断するのです。これは正解であることもありますが、大失敗となることもあります。

他にも理由はありそうですが、この2つが非常にありがちだと思います。コメントを書くのが面倒というのは、認識を新たにしてもらうとしか言い様がありません。これに対し、必要なコメントであるかどうかわからない、というのは初心者の場合どうしようもないので、先程書いたように、どんどんコメントにしてしまうというのも一つの方法ですが、ある程度はパターンというものもありますので、それを目安にしてコメントする習慣を身に付けるという手もあります。

・関数を定義する場合には、それが何をする関数で、どのような場合にどんな値を戻すのかを書く。

・変数に対するコメントは、その値がどのような場合に、どういう意味を持っているのかを書く。

・繰り返し処理(for, whileなど)に対しては、どのような条件で繰り返すか、または繰り返しを終了するのかを書く。

・条件判断(if, switchなど)に対しては、どのような条件の時に処理が実行されるかを書く。

□細かい話

関数の前に書くような、割合大きなコメントは、各種の流儀があるようです。コメントのスタイルも、千差万別といえます。私の場合は、次のようなスタイルですが、これは結構よく見かけるものです。

/* このように、1行目から平気で文章を書き始め、
 * 必ず左側にアスタリスク (*) が現われるようにします。
 */

アスタリスクを書いておけば、検索した時に、表示された行がコメントなのかどうか、一目で分かります。

プログラムの先頭に書くコメントには、自分の名前や日付を入れておいた方がいいでしょう。PDS にするつもりがないなら、Copyright の表示も書いておいた方がいいと思います。


Part4 実例に学ぶインデントと空白

Cのプログラムを書くとき、まずとまどうのは、どのような書式で書いてよいかわからないということでしょう。C言語においては、特別な場合を除いて、文法という観点からは、あまりこだわらずに自由な書き方ができます。例えば一行にいくつも文を書いても構わないし、一つの文を2行に分けて書いても構いません。

しかし、実際にCのプログラムを見ると、殆どのものがインデントを行っていることに気付きます。インデントというのは、プログラムの内容を書くときに、左に空白を置いて見やすくする、アレのことです。昔なつかしBASICのプログラムなどは、一番左には行番号があり、それに続けてすべての行がおおむね同じ位置から始まっていたものでした。別に空白を置いてはいけないというわけではありませんが、当時は、メモリが足りなくなるだとか、こうした方が実行速度が早いだとか、結構空しい理由で、実に見にくいマルチステートメント形式の記述が行なわれたりしたものです。同じ様な非本質的な理由で、わざわざ一文字の変数名を使ったりもしました。最近はBASICといっても、コンパイラになっていたり、構造化できたりするわけで、メモリはどっさりあることだし、当時の名残りもないかもしれません。実は最近のBASICは使った事がないので、詳しくは知らないのです。

ということで、インデントのスタイルについては、とりあえず見よう見まねで始める人が多いようです。私もそうでした。まず、それで一向に構わないことを頭に入れておいてください。基本的に、インデントのスタイルによって実行結果が異なるということは、C言語ではないと考えて構いません。つまり、自分の好みによって、いいと思うように勝手にやっていいということです。

ところが、パソコン通信でたまに見掛けるのですが、あまりCに馴れていないと思われる人が、「動かないのですが (;_;) 」などと泣き付いてきて、最後の手段、恥をしのんでソースリストをそのまま掲示してしまう、という奥の手があります。世間には信じられないかもしれませんが結構親切な人もいて、また、世話をやきたくてしょうがない人が見ているものなので、しかもその人達が相当プログラムの知識があったりして、添削をしてくれることもあります。パソコン通信には、そういった使い方があることを覚えておいても損はしません。

それはいいのだが、この時に「ひどいインデントですね、これでは動かないのも当り前だ」と言われてしまうことがあるのです。

単純に考えると、インデントがどうなっていようが動くプログラムは動くはずなのですが、それは理屈だけの話です。経験的には、インデントをちゃんと書ける人と、Cが使いこなせる人との間には相関関係があるようです。というよりは、きちんとした構造を持ったプログラムを構成する能力のある人なら、自ずからインデントもきちんとできる、というのが本当の所でしょう。

実際、あまりひどいインデントのプログラムは、階層化された構造を見誤る原因になりますから、すなわちバグの原因にもなるのです。人間がプログラムを読む時には、空間的な位置関係から意味を解釈しようとします。これは視覚的なパターン認識能力のおかげで、これを活用したのがインデントだといえる訳です。しかし、コンパイラはそのような認識能力は持っていません。単に文字の出現した順序だけを見て、解釈を行います。その結果、期待した動作と現実が食い違うことになります。よくある誤りの一つは、リストのようなものです。

list 20
    for (i = 0; i < 10; i++);
        printf("%d¥n", i);

おそらく、この処理を書いた人は、0から9までの数字を表示したかったのですが、余計な所にセミコロンがあるために、コンパイラは次のように解釈するでしょう。

list 21
    for (i = 0; i < 10; i++)
        ;
    printf("%d¥n", i);

この場合、for によって繰り返し処理されるのは、;という空文であって、printfではありません。従って、結果は10という値を一度だけ表示しておしまい、という多分予期したものとは異なる結果になってしまいます。

と説明して、成程、そうであったか、と理解したのなら、多分あなたが人間である証拠です。もしあなたがコンパイラだったら、最初のリストも次のリストも、何にも違わないじゃないか、と首をかしげるはずですから。

空白の使い方ひとつで、プログラムの分かりやすさに極端な差が出るのですが、インデントについて、あまり重視する人がいないのは不思議なことです。うまくインデントを付けるということは、実は極めて重要な技術であることを認識してください。

■括弧の位置

インデントというのは、処理が深みにはまった時に、段を付けることによって、処理のまとまりを明確にするのが目的です。

list 22
    for (i = 0; i < 10; i++)
        printf("%d¥n", i);

このリストでは、printfを書く位置が、forよりも右から始まっています。こう書くことによって、printfがforのループの中にある処理であることを明確に表現しようとしているのです。このように、何かの処理の中にある処理であることを表現する場合に、外の処理よりも右から始めるというのは、まず異論のないことだと考えてください。

では、何が問題になるかというと、括弧を付ける場合にどう付けるのか、ということなのです。複雑怪奇な付け方まで含めると、えらい種類になってしまいますので、ある程度よく見掛けそうなものを紹介します。

まず、始まりの括弧と終わりの括弧の桁が同じものは、3種類あります。

(1)
    for (i = 0; i < 10; i++)
    {
        printf("%d¥n", i);
    }

(2)
    for (i = 0; i < 10; i++)
      {
        printf("%d¥n", i);
      }

(3)
    for (i = 0; i < 10; i++)
        {
        printf("%d¥n", i);
        }

説明のため、forのことを外の処理、printfのことを内の処理と呼ぶことにします。(1)は、括弧が、外の処理の桁に合わせられた記法です。(3)は、括弧が、内の処理の桁に合わせられています。(2)は、括弧が、内の処理と外の処理の中間に位置します。

この3種類に対して、派生した記法があります。すなわち、始まりの括弧を、外の処理と同じ行の最後に持ってくるのです。

(1')
    for (i = 0; i < 10; i++) {
        printf("%d¥n", i);
    }

(2')
    for (i = 0; i < 10; i++) {
        printf("%d¥n", i);
      }

(3')
    for (i = 0; i < 10; i++) {
        printf("%d¥n", i);
        }

(1)の書き方は、内の処理が括弧よりも右に現われるという特徴を持っているため、内の処理の終わりを見分けるのが最も簡単であると思われます。これに比べると、(3)の書き方の場合、次の処理の先頭を捜して、その直前の、内の処理の終わりを見ることになりますが、(3)の場合は、内の処理と括弧の位置が揃っているので、一見見やすそうな感じがするかもしれません。ただし、これに関しては、疑問点があります。

    oooooooooo A
        {
        oooooooo
        ooo
        ...
        ooooo
        }

    oooooooo  B
    ....

上のように書かれている場合、最初のまとまりを認識するのは、括弧を見てではなく、むしろ次の処理Bをみて判断しているのではないか、という点です。次の例は実際には括弧がないため不可ですが、

    oooooooooo A
        oooooooo
        ooo
        ...
        ooooo

    oooooooo  B
    ....

もしこれで動作するなら、こちらの方がすっきりするのではないか。括弧は単に文法上の必要から付けているだけであり、プログラマーが目で見て確認するためには、あまり役に立っていないのではないか、という疑問です。

これに対して、(1)の書き方だと、外の処理の次の括弧からまっすぐ下ろして、最初にぶつかった閉じ括弧までの間が内の処理であることが明確に判断できます。なぜ明確かというと、その間の処理は、括弧の位置よりも右から記述が始まっているため、途中で見落としたりする危険が避けられるからです。特に、(1)'のかきかだと、外の処理の始まりからまっすぐ下ろす間に文字がないため、極めて簡単に見つけることができます。

(1)'
    oooooooooooo {
    |  oooooooooo
    |  ooooo
    |    ...
    ↓  ooooo
    }
  (ここまでが内の処理)
おなじように閉じ括弧で(3)の場合の内の処理の終わりを捜そうとすると、内の処理の記述と同じ位置に括弧があるので、他の記述に埋もれてしまって、捜すのが大変だし、見落としやすくなるのです。

とはいっても、ほとんどの場合、内の処理が終わった時点で、次の外の処理がインデント幅だけ左から記述されていますから、捜すのが大変、と大袈裟に言う程には、見誤ることは少ないのですが、あまりないケースとして、独立したブロックがある時、非常に混乱することがあります。まず、(1)の書き方から。

(1)
    while ((c = fgetc(fp)) != EOF)
    {
        putchar(c);
    }

    {
        int i;

        i = ferror(fp);
        if (i)
            printf("エラーが発生しました。¥n");
    }

    fclose(fp);

このように、(1)の書き方だと、2つのまとまりを混乱することはありません。(1)'の場合も、whileの後に開き括弧が移動するだけで、同様に混乱は起きません。さて、(3)の書き方だと、どうなるでしょうか。

    while ((c = fgetc(fp)) != EOF)
        {
        putchar(c);
        }

        {
        int i;

        i = ferror(fp);
        if (i)
            printf("エラーが発生しました。¥n");
        }

    fclose(fp);

printfの所までがwhileのループの中に見えないでしょうか。え、見えないですか? それなら別に構いませんが…。

この他に、開き括弧と閉じ括弧の位置をずらす書き方もあります。ただ、私には、これらが何を意図しているのかよくわかりません。誰か教えてください。

(4)
    for (i = 0; i < 10; i++)
    {
        printf("%d¥n", i);
        }

(5)
    for (i = 0; i < 10; i++)
        {
        printf("%d¥n", i);
    }

インデントのための文字

まず、空白にするかタブにするかという問題があります。これに関しては、4文字の間隔のタブに設定して、タブを使うことをおすすめします。

ただし、ソースプログラムを外部に発表する場合には注意してください。この場合は検討の余地があります。他の人もタブが4文字間隔であるとは限らないからです。最悪の場合、タブを認識しない環境もあるかもしれません。

殆どC言語とは無関係の話ですが、ニフティサーブの端末設定には、コントロール文字を扱う/扱わないという選択があります。これを「扱わない」にしておくと、タブも無視されてしまいます。即ち、タブでインデントされたソースリストが電子会議室に掲示されると、左にべったりくっついて表示されてしまうのです。それだけならよいのですが、変数名などタブで区切ってあるものが、全部くっつくので、全く訳がわからなくなってしまいます。そこで、かなり昔の話ですが、タブをコントロール文字として無視するのならCRもLFも無視すべきだと提案したことがありますが、流石にこの提案の方が無視されました(もちろん、タブもCRやLFと同様に扱って欲しいという意味です、CRやLFが無視されたらやってられません)。

※コントロール文字を「扱う」にすると、希とはいえ、変なエスケープ文字列が入った発言に出くわしてひどい目に会うことが予想される。

というのは極端なケースですが、タブの間隔が個人によって異なる設定になっていると何が不都合か、もう少し具体的に考えてみましょう。一番ありがちなのは次のようなケースです。経験的に、タブの間隔の設定としては、4文字とする人と、8文字とする人が大半であることがわかっています。

list 23
    for (i = 0; i < 10; i++) {
        for (j = 0; j < 10; j++) {
            printf("i = %d, j=%d¥n", i, j);
        }
    }

リストの中の空白が、どのような文字で書かれているか、紙に印刷されたものを読んでもわかる訳がない。空白は単に空白に見えるだけです。これはパソコンのディスプレイ上でも、しばしば似たような状況となります。エディタによっては、「ここにタブがあるぞ」ということを見分けるための、特別な表示を行う機能がある場合もあります。しかし、全く空白と区別の付かないこともあります。このリストが、タブが4文字間隔になるという仮定で書かれたソースだとしましょう。タブが8文字間隔になるような表示でこれを見ると、次のようになるでしょう。

list 24
        for (i = 0; i < 10; i++) {
                for (j = 0; j < 10; j++) {
                        printf(quot;i = %d, j=%d¥n", i, j);
                }
        }

これは、単にインデント幅が4から8になっただけだが、これならあまり気にならないかもしれません。しかし、常にこううまくいくとは限りません。よく見かけるのが、変数の定義の箇所で、変数名を揃えたくなる人です。これは多いようです。場合によってはコメントも揃えたくなるようです。

    char    *s;
    int     pos = 0;

タブを4文字間隔のつもりで書いている人がこのように書いた場合、char の後にあるのはタブが1個、intの後にあるのはタブが2個となります。intというのは3文字なので、タブが1個では次のようになってしまいます。

    char    *s;
    int pos = 0;

同じリストをタブが8文字間隔の人が見ると、次のリストのようになりますが、これはちょっと見にくい。

    char    *s;
    int         pos = 0;

それでは、タブが文字何個に相当するかにかかわらず、あまり見苦しくなく表示されるように書くにはどうすればよいでしょうか。簡単です。タブを使わなければいいのです。リストの空白を、タブではなく空白で表現すれば解決します。

ところが、この場合も、いったい空白をいくつ程度置けばよいのかが問題になります。たとえば、次のような1行を追加したくなったら、どうしましょうか。

    static int  lcount = 0;

変数名の書き始める位置にこだわっている人なら、多分次のように書くのは見苦しいと思うでしょう。

    char    *s;
    int     pos = 0;
    static int  lcount = 0;

多分、次のように書きたくなるはずです。

    char        *s;
    int         pos = 0;
    static int  lcount = 0;

では、さらに、static unsigned long の変数を追加したくなった場合、他の変数もわざわざずらせて合わせるべきでしょうか。

    char                    *s;
    int                     pos = 0;
    static int              lcount = 0;
    static unsigned long    wcount = 0;

この後で、やっぱりlcountやwcountは、外部変数にして、ソースプログラムの頭に書こうと思ったりしたりすると、2行削った後が、

    char                    *s;
    int                     pos = 0;

というのは、流石にみっともないので、また左につめるのでしょうか。こういう作業はやってられない、というのが正直な所だと思いますが、ぶつぶついいながら、ちゃんと揃える人も多いかもしれません。あるいは、この程度なら後で揃えるためのソフトがあるかもしれないし、そういう機能を備えたり、マクロで処理できるようにしたエディタがあるかもしれません。

しかし、私はこういう手間に神経質になるのは不本意ではないかと考え、長年の研究の結果、ついに次のように書くことを結論するに至りました。空白は1つで十分。べつに見にくくもなんともない(と思い込むのが秘訣)。

    char *s;
    int pos = 0;
    static int lcount = 0;
    static unsigned long wcount = 0

と書いたが、改めてこうして見てみると、そんなに見やすくないような気もします。ちなみに、別に長年研究しなくても、K&Rの本の書き方を真似していたら、最初からこのように書いてありますから、自然にこのように書くことになります。という余談はともかく、なぜ上の表現があまり見やすい気がしないのか考えてみますと、どうも

    static unsigend long

という前書きがあまりに長すぎて圧倒されてしまい、肝心の変数名があまり目立たないからのようです。このような場合には、

    typedef unsigned long ULONG;

とでもしておけば、次のようになります。

    char *s;
    int pos = 0;
    static int lcount = 0;
    static ULONG wcount = 0;

この程度なら、あまり変に見えません。ただ、このあたりの感覚は、かなり個人差によって意見の分かれる所ですから、異論のある人も多いでしょう。実は私自身、このように書くようになったのがつい最近のことで、かなり長い間、変数の始まりが揃うようにタブを使って書いていました。

またまた話題を元に戻して、タブに対する文字数が異なることによって、インデントがもっとひどくなる例を考えてみましょう。ふたたび、list 23の例を使います。

list 23 (再掲)
    for (i = 0; i < 10; i++) {
        for (j = 0; j < 10; j++) {
            printf("i = %d, j=%d¥n", i, j);
        }
    }

ただし、今度は、これがタブを8文字とみなして書かれているものとします。実は、MSDOS の type コマンドを使った時など、タブが8文字と見なされることは結構多いので、自然とタブを8文字と考えてコーディングする人か多くなりがちなのですが、それでも、Cのプログラムのインデントは、4文字の空白分の間隔としたい、という人が多いようです。これは、8文字を1段としてインデントを行うと、すぐに奥が深くなってしまい、目が左右に振られてかえって見にくくなってしまうからだと思われます。インデントの幅は、ぱっと見てわかる程度に、しかし目をあちこち動かさずにすむように、多過ぎない程度に、というのが理想と言えるでしょう。

すると、先程のリストなどは、実際は次のように書くことになります。最初のforの前には4個の空白を、次のforの前には1つのタブを、printfの前には、1つのタブと4個の空白を、という具合です。これを、もしタブを4文字とカウントする人が見ると、次のように見えてしまうのです。

list 25
    for (i = 0; i < 10; i++) {
    for (j = 0; j < 10; j++) {
        printf("i = %d, j=%d¥n", i, j);
    }
    }

これは、見にくいというレベルをもはや越えており、処理の流れを見誤る危険がかなり大きいのです。

■K&R流インデント、空白の付け方

C言語は初心者で、どう書いていいのかわからない、という方のために、K&R流のインデント、空白の使い方を紹介します。自分なりのスタイルを持っている方は、別にこれにこだわる必然性はありません。K&R スタイルを模倣することの最も大きなメリットは、誰かに「なぜそのように書いたのか」と尋ねられた時に、「K&R にこう書いてあるから」と答えれば、大部分の人が納得するということです。※

※もしあなたがK&R ではない自己流の書き方を支持している人と対決したら、別の書き方を示して「このように書いた方がよい」と頑固に主張されることになるかもしれない。が、私の経験では、「K&Rにもこう書いてある」と言えば、大抵の人は諦めるようである。もっとも、相手がCの熟練者でなければ、この限りではない。:-) もう一つの可能性は、相手が GNU の熱狂的な支持者であり、GNU Emacs 流のインデントを強硬に主張することである。The C Users Journal Japan の7号を見よ。

なお、文中の○×は、これが K&R スタイルに合致しているかどうかを表現するものであって、×のような書き方が間違っているという意味ではありません。

□□□ 規則 □□□

・インデントの幅は4文字分にする。

    while ((c = getchar()) != EOF) {
        if (c < ' ') {
            putchar('^');
            c += '@';
        }
        putchar(c);
    }

インデント幅については、多くの流儀がありますが、大部分は2〜8文字です。K&R 1st. では空白5つでした。空白2つにする人も多いようです。特に GNU のソースに目立つときがあります。Borland C++ のリファレンスマニュアルのプログラム例は3文字になっています。TAB 幅を指定できるエディタが使えるなら、TAB=4 に設定して編集するとよいでしょう。ただし、ソースリストを公開する場合は注意が必要です。

・予約語if、for、while、switch 等の後ろに必要な「(」の前には空白を置く。「)」の後に「{」を書く場合には、間に空白を置く。

        ×  if(c == EOF) { .. /* if の直後の空白がない */
             ^^
        ×  if (c == EOF){ .. /* { の直前の空白がない */
                        ^^
        ○  if (c == EOF) { ..

・関数名やマクロの後ろに続く「(」の前には空白を置かない。

        ×  printf ("hello world¥n");
                 ^^^
        ○  printf("hello world¥n");

このように、if、for、…等と関数との書き方を区別しておくと、区別が楽になり、エディタやツールで検索する時に作業が楽になることがあります。

・「(」と、そのの直後の文字との間には、空白を置かない。「)」とその直前の文字との間にも空白を置かない。

        ×  if ( c == EOF ) { ..
                ^        ^
        ○  if (c == EOF) { ..

空白を置いてもよさそうなものですが、あまり空白ばかりだとかえって見にくくなるということでしょうか。少なくとも、括弧が続く時に、次のように書くと、結構間延びしてしまいます。

    while ( ( c = getchar() ) != EOF )

・カンマ(,)、セミコロン(;)の後には、原則として、空白を1つ置く。

        ×  printf("i = %d¥n",i);
                              ^^
        ○  printf("i = %d¥n", i);

これはCの書き方というより、英文を書く場合には普通に行われていることです。ただし、英文タイプの場合、セミコロンの後には、2個の空白をタイプするのが普通のようです。

・単項演算子とそれを作用させる式などの間には空白をあけない。

        ×  i ++;
             ^
        ○  i++;

単項演算子の優先順位はかなり高いので、強いつながりを持っているという意味を含ませて、つなげて書くのだと思います。

・2項演算子の両側には、原則として空白を1つずつ置く。ただし、特に理由のある場合にはその限りではない。

        △  sum=a+b;
              ^^^^^
        ○  sum = a + b;

2項演算子は、単項演算子よりも優先順位が低いので、特に単項演算子と混じった場合には、このように書くと読みやすくなるからだと思います。

K&Rのリスト中には2項演算子の両側に空白を置かない例は、散在している。おそらく、演算の結果に重点を置いているような意味を持たせていると思われる場合に、このような例が多い。
            sum = a++ + -b;

例えば、

            sum = a-b;

は、まだいいのですが、

            sum = a--b;

だとか、

            sum = a---b;

となると、訳がわからなくなります。

・構造体のメンバをアクセスするためのピリオド(.)や -> の両側には空白を置かない。

        ×  cursor . x = newptr -> h;
                  ^ ^          ^  ^
        ○  cursor.x = newptr->h;

"."、"->"に関しては、優先順位が高いため、空白を置かない方が、かえってつながりを明確に表現できるためと思われます。

・3項演算子 ?: の、「?」、「:」の両側には、空白をおく。

        ×  abs = (a >= 0)?a:-a;
                         ^^^^^
        ○  abs = (a >= 0) ? a : -a;

これも優先順位の関係だと思えば納得できるでしょう。

・キャスト演算子に用いる括弧「)」の直後には空白をおく。

        ×  i = (int)c;
                    ^^
        ○  i = (int) c;

・return の後に不必要な () は付けない。

        ×  return (0);
                   ^ ^
        ○  return 0;

K&R の第1版では、return (式); のように表記されていました。なぜ括弧が付いていたのか、よくわかりません。よく聞く理由としては、括弧があった方がわかりやすいから、というものがありますが、私にはなぜ括弧があった方がわかりやすいのか、全く理解できません。かえって分かりにくくなるだけだと思うのですが。しかし、第2版になると、return の後の括弧はなくなってしまいました。

・for 文の書き方('{' '}' の位置)

    for (expression1; expression2; expression3) {
        ..
    }

    for (;;) { /* 無限ループの場合 */
        ..
    }

for の中の式を区切るセミコロンの後には、空白を開けますが、無限ループの場合に限り、空白なしで詰めて書いて構いません。これは慣用句のようなものです。

次のように、for 文の本体が複文でない場合、および空文の時は、それを同じ行に書かずに、必ず次の行に書きます。特に、空文を意味するセミコロンを同じ行に書いてはいけません。混乱の元になります。

    for (expression1; expression2; expression3)
        ;

・while 文の書き方('{' '}' の位置)

    while (expression) {
        ...
    }

for 文と同様、次のように、while 文の本体が複文でない場合、および空文の時は、それを同じ行に書かずに、必ず次の行に書きます。

    while (expression)
        ;

・do while 文の書き方('{' '}' の位置)

    do {
        ..
    } while (expression);

・switch 文の書き方('{' '}' の位置)

    switch (expression) {
    case constant-expression:
        ..
        break;

    default:
        ..
        break;
    }

これが結構異論のある所だと思います。switch 文に対して、case ラベルを、同じ位置に揃えて書きます。default も同様です。これ以外の書き方としては、case を switch より少し後に書く人もいますが、K&R では switch と case は同じ高さです。ラベルを書く時には、インデントを一つ解除して、目立つようにすると覚えておけばよいでしょう。

・if 文の書き方('{' '}' の位置)

    if (expression) {
        ...
    } else if {
        ...
    } else {
        ...
    }

すでに述べたように、これらの括弧の書き方の基本は、キーワード (for, while, do, switch, if)の先頭の文字から、まっすぐ下に向かってながめた時に、最初に現れた '}' の所までが、そのキーワードに対する有効範囲になっている、という点にあります。有効範囲の内部は、一段深くインデントを付けて、区別されているため、簡単に識別できます。

・関数の定義の例。('{' '}' の位置)

    unsigned int foo()
    {
        ...
    }

実際は、左に空白を置きません。最も左から書き始めます。また、関数の型は、関数名と同じ行に書きます。こうしておくと、grep のようなツールで関数名で行単位の検索を実行した時に、型が付いたまま表示されて便利です。

・変数定義部分と、それ以後の部分は、空白行によって分離する。

    unsigned int foo(int count)
    {
        int total = 0;

        while ((count & 1) == 0) {
            total++;
            count >>= 1;
        }
        return total;
    }
※もちろん、これ以外の個所でも意味の区切りとして適切な個所で空白行を入れることが望ましい。なお、上の例では、int total = 0; の行の次に空白行がある。

・配列の [] の前後には空白をあけない。

    ×  array [10]
             ^
    ○  array[10]

・式文を表す ; の直前には空白をあけない。

    ×  char keyword[10] ;
                        ^
    ○  char keyword[10];

・変数の宣言の仕方の例。

    FILE *fp;
    char *str;
    int i, j;

型名の後に、単に空白を1つあけます。TAB で揃えたりはしません。

経験的に、TAB を使ったりして、変数の先頭を揃える人は、非常に多い。

ポインタは、前述のように書きます。次のように書いてはいけません。

    char* str;

K&R流の書き方の紹介は、以上です。

GNU indent の -kr オプションを使えば、一気に整形できるが、これではソースを読まないで済むので面白くない、と思いつつ、最近はこれを使うことが、よくある。

最近は、パソコン通信に参加したり、雑誌の付録のディスクを手に入れることによって、Cで書かれた多くのフリーソフトウェアのソースが簡単に入手できるようになりました。これらの多くは、上の規則とは全く異なった、独自の表現によって書かれています。このようなソースを単に読むのは退屈な作業かもしれませんが、インデントなどを自分のスタイルに直しながら読むと、理解するのが容易になります。このような手作業を行うには、ソースプログラムをくまなく読まなければならないからです。


Part5 変数・関数のネーミング法

最後に、名前の付け方について考えてみます。名前というのは、具体的には変数名、関数名などです。これらの名前は、予約語を使うことができないとか、コンパイラによって一定の長さ以内にしなければならない、といった絶対的な制限もありますが、これは流石に守らないわけにはいかないので、悩むこともないと思います。長さといっても、最近の処理系だと、実用的に十分な長さですから、昔、先頭の2文字しか判別に使われない言語があって、a01とa02が同じ変数とされてしまうので難儀したといったような苦労は、もはやあまり経験することもないと思います。昔話をもう一つしておくと、昔は、変数名が短い方がメモリやディスクの使用量が少なくて済むので、といった苦労もありました。C言語は基本的にコンパイラの環境が提供されているため、変数名とメモリは直接関係ないように思えるかもしれませんが、メモリが64KBもあれば大容量といっていた時代には、変数が長すぎるとコンパイルの途中でメモリが足りなくなってしまったり、ソースファイルが長くなるためにディスクの空き容量が不足してコンパイルに失敗するという、今では想像もできない世界があったのです。※

※ところが今でもあるらしい。メモリがあればあるだけごちゃごちゃとソフトを常駐させ、その上で巨大なコンパイラを走らせるからである。昔、某雑誌のパロディ版に載っていた超高機能エディタ(機能が多過ぎるためにメモリを食いつくし、1頁の文章しか編集できない)の記事を思い出す。

もはやメモリを気にしながらコンパイルする時代は終わったと考えておきましょう。もちろん、コンパイルの時の話です。実行形式のプログラムが消費するメモリ量は、ますます少なくなることを要求されています。というのは、今後はパソコンの上でマルチタスクの環境を実現する方向へ世界が向かっているからです。個々のプログラムが小さくても、プログラムを10個同時に走らせようとすると、10倍のメモリが必要です。640KBのメモリが必要なソフトを10個同時に実行するには、単純に考えても約6MBのメモリが必要です。もちろん、実際は、あの手この手のメモリ管理を行いますから、メモリが足りなくてもディスクに一時退避するなどして、何とか少ないメモリをやりくりするのですが、それぞれのプログラムがメモリの使い方に無頓着かどうかで、かなり全体の使い勝手に影響することは間違いないでしょう。

という話は今回は全然関係ないので、とりあえず変数名、関数名の話に戻ります。

念のため書いておきますが、C言語には予約されている語がいくつかあります。これらは用途が決まっていますので、プログラマーが勝手に使うことはできません。もちろん、それを無視するとコンパイルする時点で多分エラーになるので、予約語は変数名に使えないということさえ頭に入れておけば、うっかりしても大変な混乱に至ることは希だと思います。

ここからがポイントになってきますので、注意してください。まず、C言語に非常に近い言語に、C++という言語があります。C++には、C言語では予約語とされていないいくつかの語が、新たに予約語として追加されています。これらの予約語は、Cのプログラムであっても使わないことをおすすめします。同様に、他の言語の予約語は、できれば使わないことをおすすめします。

例えば、学校の成績処理プログラムを書きたい人は、次のような構造体を作りたくなるかもしれません。

    struct student {
        int grade;
        int class;
        int number;
        char *name;
    };

この構造体の欠点は、classという名前を使っていることです。将来C++に移植しようとした人が、びっくりするかもしれません。悲惨なことにならないことを祈ります。C言語から他の言語へのプログラムを移植する機会は、神経質になる程のものではないかもしれませんが、一応、C++への移植というのは、最もありそうなことですから、C++の予約語だけは使うのを避けた方がよいでしょう。

ここまでが、使ってはいけない、という指針です。一般に「〜してはいけない」というのは簡単ですが、「〜するのがよい」というのは説明困難になりがちです。以後、あえて、どのような名前を付けるべきかという指針について述べようと思います。

一言で書けば、「わかりやすい名前を付ける」とよろしい。これが大原則です。これだけを理解していれば、あとは感性に正直に名前を付けても、相当変な人でないかぎり、何も問題になりません。ただ、驚いたことに、わかりやすい名前にしようとさえ、夢にも思わない人が結構いるように思えるのですが。名前がわかりにくいケースはもう一つあります。 どうすればわかりやすい名前になるのか、理解できない場合です。これは驚くには値しません。 経験を積むに従って、自然に身についてくるものだからです。

■伝統的に使われる名前について

次のリストを見てください。少しCでプログラムを書いたことのある人にこれを見せると、「間違っているぞ」と指摘されるかもしれません。

list 26
    void main(int argv, char *argc[])
    {
        while (--argv > 0) {
            puts(*argc++);
        }
    }

しかし、このプログラムは文法的には何も間違ってはいないので、コンパイルすればちゃんと予期した通りに動作します。間違っているのは、プログラムではなく、main の引数に argv、argc という順序で名前を付けるという感性です。この種の名前は、伝統的に命名されていて、プログラマーが勝手に変えることは望ましくありません。そういう意味ではこのプログラムは確かに間違っているわけです。

伝統的に使われる名前は、できるだけ先達に従うのが得策です。argc、argv はそれぞれ関数 main に渡される引数で、コマンドラインのパラメータの数と、各パラメータの文字列へのポインタを順に格納した配列を意味するのが伝統ですから、上のように書いたら目茶苦茶というわけです。

他に、よく使われる伝統的な意味を持つ変数を示しておきましょう。


  int c;

文字を格納する使い捨ての変数です。これを char とする人がよくいますが、伝統的に int です。また、int で受けないとおかしくなることもあります。getc などはマニュアルを見ればわかるように、int の値を戻しますから、当然 int で受けなければなりません。


  char *s;

作業用の、文字列へのポインタとして使います。string の頭文字を連想させるためです。


  int i, j, k;

これらは、使い捨ての変数として使われます。for でループさせる場合のカウンタによく使われます。これらに限らず、1文字の変数は、主に使い捨ての一時変数として使われるものと思えばたいてい当っています。


  int n;

同じく使い捨ての変数ですが、number を連想させるので、個数を意味することが多いようです。


  long l;

使い捨ての変数でも、l は long の変数として使われることが多いようです。


  int len;

長さでしょう。long のことがあるかもしれません。long なら llen と書きたくなる所ですが。


  char buf[];

作業用の文字列を格納するためのバッファとして使われます。buffer の後半分を略したものでしょう。


  FILE *fp;

ファイルポインタとして使われます。file pointer だから、fp です。ただし、しばしばファイルを扱うプログラムを書く場合には、二つ以上のファイルを扱いたくなりますから、fp だけを使っていては混乱するかもしれません。fp_in、fp_out のような書き方はよく見かけます。

ファイルポインタという表現は、特にUNIXの文脈で意味するものと、C言語の文脈が意味するもので、混乱を招くことがある。

  int fd;

ファイルディスクリプタの意味でしょう。


  int *p;

使い捨てのポインタとして pointer を連想させる p を使います。これが例えば long へのポインタだと、lp となることが多いようです。


  size_t size;

文字通りサイズです。何のサイズかというと、主に malloc のようにメモリを獲得する関数への引数として使われますが、それ以外の用途にもよく使われるようです。


  int status;

ステータス、すなわち状態ですが、何の状態かというと、しばしば関数の戻す値、もしくは exit の引き数として使われます。


  int temp;

これは明らかに一時変数でしょう。

この他に、Cの関数マニュアルを見ると、引数の説明として使われている名前がありますので、参考にするとよいと思います。

■名前の書き方について

現状では、日本語の名前が使えるCコンパイラは、あまり見掛けないし、ANSI の規格通りなら、当然日本語の変数名など考慮されている訳がないから、いわゆるメジャーなコンパイラについては、英数のみ使えると考えておきます。

□省略するか、しないか

まず、英単語を名前に使うとして、省略するか、省略しないかという選択があります。例を見るのが手っ取り早いでしょう。

    char *file_name; /* 省略しない */
    char *fn; /* 省略する */

省略しないことのメリットは、意味を誤解する確率が小さくなることです。fnと書くと関数名のことかと誤解する人がいるかもしれませんが、file_nameを見て関数名かと思う人はまずいませんし、これを見た人は必ずファイル名であることを理解できます。

では、省略するメリットは何でしょうか。一つは、あまりにもしばしば使われる変数に長い名前を付けてしまうと、入力するのが大変だ、ということがあります。ある程度長い名前になると、開発環境によってはcut&pasteのような方法で、文字列コピーすることができますから、その方が間違いが少なくなります。長い名前はタイプする時に間違いがちなので嫌だという人がいますが、これはある意味で危険です。タイプする時に間違う確率は、何文字あたり一回、というものであって、短い変数でも間違える時には間違うものです。間違えた時にどの程度の影響があるかということも検討しておくべきです。短い変数をいくつも使っていると、タイプし間違えた時に、偶然別の変数になってしまうことがあります。この原因によるバグは、一度コンパイルに通ってしまうと、なかなか発見できません。長い名前の場合、間違えてタイプしたら、そんな変数はないとコンパイル時にはじかれる確率が高くなり、その意味では安全かもしれません。

しかし、長い名前にもデメリットがあります。あまり長い名前だと、肝心の処理の流れを理解するための思考を妨げる危険もあります。名前を追いかけて最後まで読んで、ああこれはあの変数だな、と思った時には、今whileのループ条件を調べていたのか、ifの判断の途中だったのか忘れてしまうかもしれません。という例は流石に大袈裟すぎますが、名前が長すぎると、かえって全体が見にくくなる可能性があるのは確かです。

そこで、妥協案として、あまり長い場合には省略する、という方法が考えられます。これも悪くはありません。

    char *fname; /* ちょっとだけ省略する */

この方法の場合、長すぎもなく、全然わからない程省略するのでもない、という加減に成功すれば、かなり効果があると思います。ただ、一つ気になるのは、経験的に、どの程度省略したのか、しばしば忘れてしまうということです。例えば、一時ファイルの名称を保存したい変数の名前を、tempfile としたのか、tmpfile としたのか、プログラムを書いている途中ですら忘れてしまうことがよくあります。

フィンローダ流は、慣習として省略した書き方が固定したものでなければ、できる限り省略しない、というものです。

ただし、以上の議論は、継続的に意味を持ち得る変数の場合のことであり、使い捨ての変数はこの限りではありません。その中にも、ある程度は慣習的に決まった名前がありますので、それらが使える範囲においては、ありがちな名前を使っておくのが無難な選択だと言えましょう。

□名前のスタイル

流儀としては、大きく分けて2つに分かれます。ここでは、英単語を組み合わせることによって名前を付けることにします。

1 SendMessage

このスタイルは、単語の先頭の文字だけを大文字にして、単語をぴったりと付けたものです。

2 send_message

こちらは、小文字で書いた単語を、アンダースコアでくっつけるというものです。

私の経験では、1を好む人が多いような気がします。理由は、2の方が、名前が長くなってしまうからではないかと思います。長い文字をタイプする方が面倒といえばそうかもしれません。

しかし、フィンローダ流は2です。2の長所は2つあります。

まず、すべて小文字で書かれていることです。1の場合、どこを大文字にしたか間違えることがあるかもしれませんが、2ではそれはありません。

ただし、FileName と書いたのか Filename と書いたのかわからなくなるように、filename と書いたのか file_name と書いたのかわからなくなることはありますので、2のように書いた方が混乱しないと一概には言えないかもしれません。

私が2の書き方を気に入っている理由は、単語と単語の間が、アンダースコアという、見た目がかなり空白に近い文字で区切られているため、ぱっと見た時に読みやすいと思われることです。1の書き方は、確かに大文字を認識して単語を分解することが可能ですが、ぱっと見た時に大文字小文字を区別する方が、アンダースコアよりも一瞬手間がかかるように感じます。

特に、後者は、わかりやすさに影響してくると思いますので、我流としては2を選択することにしました。

ただ、場合によっては2の短所はもう一つあります。アンダースコアが比較的入力しにくいキーに割り当てられているキーボードがあることです。

□名前の長さについて

変数には、固定した意味が与えられ、プログラム、または関数の処理が終わるまでその意味を持ち続けるものと、いわゆる使い捨ての一時的な変数とがあります。一時的変数としては、関数の戻り値をとりあえず入れるとか、ループのカウントに使われるものがあります。

list 7 (再掲)
    while ((c = getchar()) != EOF) {
        ...
    }

ここに出てくるcという非常に短い名前を持った変数は、伝統的にgetcharのような一文字入力の結果を保存するために使われます。cという文字が、characterを連想させるので、この場合cという名前が使われるのでしょう。ただし、cの型はintであるのが普通なので、注意してください。初心者の中には、これをうっかりcharにしてしまうことが多いようです。この変数は、伝統的なので、短いながらも全く無意味であるわけではありませんが、この種の使い捨て変数は、その場限りで処理が完結するという意味で、変数名に意味を持たせなくても、理解の妨げにはあまりならないのです。

これに対して、複雑な処理を行う関数の名前や、大局的に使われている変数の名前は、できるかぎりその意味を表すような名前を付ける、というのがフィンローダ流です。要するに、長さにはまるで無頓着なのです。

さて、できる限り省略しないとなりますと、場合によっては異様に長い名前になってしまうことがあります。この名前が、頻繁に使われるものであると、かえって名前を読むのに労力を使ってしまうことも考えられます。また、プログラムの見栄えも超高層ビルが乱立したかのごとくなってしまい、あまり面白くないかもしれません。

長い関数名を嫌った意見としては、1990年8月号のCマガジンの記事、岩谷宏氏の「恥ずかしながらドジりました」に興味あることが書かれていたので、これについて意見を書いておきたいと思います。手元にバックナンバーがない方のために、岩谷氏の主張を簡単に説明しておきますと、「GUIを扱うプログラマーは、ものすごく長い名前の関数を大量に覚えなければならない。例えば、AppJugemJugemuGokoNoSurikirePaipoPaipo() が何をするのか空で言えるようになったころは、プログラマーは老人と化しているだろう」というのが氏の指摘です。もちろん、この関数名は単なる冗談のようですが。

なお、私にもこの関数が何をするのか全くわかりません。ただし、この関数名の4文字目以降は、全く見たこともないわけではありません。つまり、関数名が全く無意味ではなさそうだという見当だけは付きます。ということは、この関数の意味がわからないのは、名前が長いからではなく、名前から意味が連想できない所にあります。

氏の指摘に対して私が楽観的に考えたのは、そんな長い名前をわざわざ暗記しなくても、プログラムは書けるのではないか、と思ったからです。

つまり、GUIシステムに使われている関数名は、確かにやたら長いのですが、名前に関数の意味が含まれているので、初めて見た関数であっても、およその意味は連想できます。また、仮に何か処理をしたくなったが関数名を覚えていない場合は、手元に置いてあるマニュアルを見ればいいだけのことです。このことは、昔NIFTY-Serveで発言したことがありました。その時に、試しに、X Window ハンドブックを適当にエイっと開いて、ぱっと目に付いた関数を選んでいるのですが、出てきた関数が何とこれです。

   XSetWindowBackgroundPixmap(display, window, background_pixmap);

これはかなり運が悪い部類に属するのではないかと思うのですが、実は私は残念ながら X Window に関して多少の知識があるため、上の関数の内容が理解できても当然なのかもしれませんが、仮に全くX Window に関する知識がないと仮定してみても、この関数が、「ウインドウのバックグラウンドパターンをセットするのではないか」ということは容易に想像できると思います※。

※これに対して、いや、全く想像できなかった!、というプログラマーがいたらコメントしてくれと書いたが、誰もコメントを書かなかったのは、想像できなかったというより、私の説があまりに陳腐なので無視されてしまったのではないかと思われる。なお、岩谷氏の指摘は、長い関数が覚えられないので困ると言っているのではなく、むしろ各種雑多なGUIシステムに対し、同じような処理をするための関数名が全て異なっており互換性も保たれていないことを問題にしているのであって、早い話が私の意見はいいがかりである。にも関わらずそれを書いたのは、氏の言うところの、「ものすごく長い名前」という表現に対しては、何か書いておかなければいけないような強迫観念を感じたからである。

ちなみに、同発言の私の主張は、「丸暗記の技術は過去の遺物にし、小学校からカンニングの方法をちゃんと授業で教え、迅速な検索能力を取得した人材を育ててほしい、そうすれば、適当なイマジネーションのあるプログラマーなら、ものすごく長い名前の関数をちゃんと覚える必要は全くない」という無茶苦茶なものだった。これではコメントがないのも当り前か。

この話題は COMPUTING AT CHAOS RUINS -127-「XなんとかBackgroundほれほれ」 にも使われている。

もし名前が短かったら、覚えるのも早いでしょうか。そのためには、2つの危険を乗り越えなければなりません。一つは、名前を短くしようとすればするほど、省略しなければならない単語が増え、ぱっと見ただけで何をする関数かわからなくなる確率が高くなることです。例えばitofという私が先日書いた関数の処理を、一目で想像できる人は百人に一人もいないでしょう※。省略の仕方によっては、逆に何だかなかなか思い出せなくなるかもしれません。もう一つの危険は、名前を短くしすぎたために、思わぬ所で衝突してしまうことです。

※「イスカンダルのトーフ屋」の主処理を行う関数。

□細かい注意

処理系に依存して、特別な意味を持った名前が使われていることがあります。このような名前はあらかじめ確認しておき、うっかり他の意味に使わないようにします。

名前は英語である必要はありませんが、英語で書いて理解できれば最も効率的だと思います。ただし、英語の綴りを間違えるのは、プログラムの動作に差し支えないとはいえ、かなり恥ずかしいものですから、あまり自信がなければ、まだローマ字の方がましだと思います。

アンダースコアで始まる名前は、特別な意味を持つことがあるので、通常は使わない方が安全です。


参考文献

プログラミング言語C(第1版)
B.W.カーニハン/D.M.リッチー著、石田晴久訳、共立出版、1981
プログラミング言語C第2版
B.W.カーニハン/D.M.リッチー著、石田晴久訳、共立出版、1989
別冊インターフェース、実践ソフトウェア作法
CQ出版社、1988
Cプログラム正書法
近野重夫、インターフェース’90−5 pp.174−203
NIFTY−Serve、C言語フォーラム(FC)電子会議

(C) 1992,1998 Phinloda, All rights reserved
無断でこのページへのリンクを貼ることを承諾します。問い合わせは不要です。
内容は予告なく変更することがあります。

[Home]