初級C言語Q&A(6)

初出: C MAGAZINE 1995年11月号
Updated: 1996-03-12

[1つ前] [1つ後] [質問一覧] [記事一覧] [ホームページ]



スタイル

 スタイルなどという言葉はC言語の世界では未定義(^^)なのですが、そのため、 逆に非常によく議論の対象になることでもあります。特に、仕様上はどちらでも 構わないという場合には、個人の好みに左右されるため、意見が分かれます。し かし、C言語も結構長い寿命の言語であり、いろいろな議論を通り抜けてきた慣 習はそれなりの経験に裏付けられた意味のあるものだと考えるべきでしょう。


コメント


Q 【//】

 次のようなコードを書いたら注意された。
    printf("hello world!¥n"); // お決まりの文句

 ANSI Cの定義に従えば、文法違反だからです。ANSI Cにおいては、「//」が現 れたらそこから行末までコメントとする、といった決まりはありません。これは 割り算を意味する「/」が二つ続けて書かれたと解釈されることになるでしょう。 そのような書き方は許されていないので、文法違反となります。

 ただ、一部の処理系には、このような書き方をコメントとみなすように勝手に 拡張されているものがあります。このため、他の処理系では使えないことをうっ かり忘れて使ってしまう人がいるようです。「私の処理系では a >>< 1 と書けば右に1ビットローテイトするから、このように書けば他の人も解釈して くれるだろう」のように考え始めると、結局どんなことを書いてもC言語になって しまいます。

 誰でも理解できる共通概念として標準的なC言語があるのですから、多くの人と の対話が必要なら、標準的な書き方から逸脱しない方が自分のためにもなります。

 C++の場合は「//」をコメントとして解釈する規格になっています。


Q 【コメントのネスト】

 プログラムに次のようなコードが出てきた。
   /* i = 1;
   /* i = foo(i); */

 C言語は、コメントの中にさらにコメントを含むことは許されていません。従っ て、「 /* 」が現れた場合、次に「 */ 」が現れるまでの全てをコメントとして解釈します。例のコードは、最初、 "i = foo(i);" だけをコメントにした状態だったが、その後 "i = 1;" もコメントにしたいとプログラマーが考えたのかもしれません。

 コンパイラの種類によっては、ネストしたコメントを書けるようにするオプシ ョンが用意されていますが、後で問題になることを避けるためには、コメントを ネストさせない方がよいでしょう。


main


Q 【void main(void)】

 main の型を void で定義したら先輩に怒られた。

 ANSI Cでは main 関数の型は int と決められています。 void とするのは間違いです。

 にも関らず、ある種の参考書には main の型を void として説明しているようです。また、初心者に限らず熟練したプログラマーがこ のように書くことが珍しくありません。このため、特に初心者のプログラマーは 何も考えずに(あるいは特に決まった値を戻す必要がないと思って) main の型を void にしてしまうことがありますが、ネットではこれは非難の対象になりがちです。


Q 【mainの戻り値】

 今作っているプログラムは、終了した時に特別な値を戻す必要はまったくない。 このような場合には、 main の型を void とした方が、むしろ自然ではないのか。

 意味のある値を戻さない関数は、 void と定義すべきではないか、という発想があります。考え方としては、これは正し いと思います。ただ、 main 関数の場合には、それが意味のある値を戻さないということ自体に、むしろ問題 があると考えられます。特別な値を戻す必要がないなら、0でもいいし、 EXIT_SUCCESS を戻してもいいはずです。何か値を return しておく方が無難です。

Q 【mainの型】

main の型が int でないといけない根拠は何か。

 仕様で int と決められているから、という答えが理解しやすいでしょう。 main 関数が値を指定しないでリターンした場合にホスト環境に戻される終了状態は、 未定義とされています。コンパイラによっては、 int の関数と void の関数とでは、呼び出しの手順が異なるかもしれません。その場合 int の関数である main を想定して書かれたスタートアップルーチンは、 void と定義された main を呼び出すか、あるいはそこから戻ってくる時に、とんでもない結果になるかも しれません。

 規格には、

 int main (void) {/*...*/}
 として定義することも、二つの仮引数をもつ関数
  int main (int argc, char *argv[]) {/*...*/}
 として定義することもできる、と書かれています。これ以外の書き方は規格に はありません。いずれかの書き方をしなければいけません。

Q 【規格を杓子定規に解釈しよう】

しかし、規格のその箇所は、引数の数が0個か2個であり、2個の場合には引数が どんな型でないといけないかを表現しているのであって、関数の型が int であるということを述べているのではないのでは。もしその表現を厳密に解釈す るのなら、 main 関数の実際の処理の内容は、 "..." という文字列が書かれたコメント一個でなければ仕様に反するのではないか。

 完璧に規格に従えば、 main 関数の中身は {/*...*/} というものでなければならない、という主張なのですが(これはcomp.std.cで実際 に議論になりました)、それはいくら何でも考え過ぎです。ここは常識で考えれば 、実際はこの箇所にいろんなコードが入るよ、という暗黙の合意があると考える べきです。

Q 【return 0;】

 main の最後に return 0; という文があった。 return EXIT_SUCCESS; とするべきではないか。

 仕様では、0または EXIT_SUCCESS という値が成功終了を意味することになっています。従って、0でも全く問題あ りません。呼び出した処理系に複数の状態を戻したいには、プログラマーが0以外 の特別な値を意図的に使うこともあります。

1996-03-12訂正


インデント等


Q 【関数の型の位置】

 次のように書かれたコードを見たことがある。なぜ関数の型を別の行に分けて 書くのか。
    static int
    foo(void)
    {
        ...
    }

 このような位置で改行しても、C言語の仕様としては全く問題ありません。 従ってこのように書くのも書かないのもプログラマーの選択次第です。一つ考え られるのは、関数を必ず左端から書くことによって、いざという時に関数定義を 見つけるのが簡単になるというメリットです。たとえばgrepというコマンドがあ れば、grep '^[a-z][a-z]*[(]' *.c を実行すれば、名前が小文字だけでできてい る関数が定義されているファイルを見つけることができます。

 しかし、良いことばかりではありません。関数名が分かっている時に、例えば grep foo *.cを実行すれば、上のように定義されたコードから foo(void) という行を抜き出してくれます。これだけでは関数 foo の型が分かりません。

    static int foo(void)
    {
        ...
    }
 このように書いてあれば、grepは static int foo(void) という行を抜き出して くれるので、この関数が static int と定義されていることが分かります。

 一つのプログラムソースの中で無意味に混在していなければ、どちらの書き方 でも殆ど問題ないでしょう。


Q 【char* p】

char* p; という書き方と、 char *p; という書き方は、どちらがよいのか。

 まず、Cの規則としては、どちらも正しい書き方であることを認めるべきです。 従って、後は好みの問題となります。

 発想としては、 char* p; というのは「 p(char *) という型である」という考え方であり、 char *p; というのは「 *p(char) という型である」ということになります。特に前者を意識している人なら、 char* p; と書きたくなると思われます。 私の場合、基本的に後者のように発想するので、 char *p; と書いた方が分かりやすいのですが。

 よく指摘されることとして、複数の変数を宣言する時に、 char* p; のような書き方をるすと失敗しやすい、というものがあります。

    char* p, q;
 という宣言は、一見、 (char *) という型の変数 pq を意味しているようですが、 実際は (char *) の変数 p と、 (char) の変数 q を宣言することになります。
    char *p, q;
 このように書けば、先程のような失敗は少なくなりそうです。

 このような失敗をするのは、一行で複数の変数を宣言するから悪いのだ、とい う人もいるようですが、他の人もすべてそのように考えるくれるとは限らないこ とも頭に入れておくべきです。また、 typedef を使うことも、失敗を避ける効果があります。

    typedef char *charp;
    charp p;
 このように定義しておけば、分かりやすいし、失敗も少なくなります。

Q 【空白】

空白はどのように使えばよいか。タブは使ってもよいか。

 C言語においては、空白を書いていい所なら、いくつ空白を書くのもプログラマー の自由です。また、書かなくてよい場合に書かないのも自由です。例えば、カン マの前後に空白を置くかどうかは書く人の判断に任されています。これはプログ ラムの処理には何等影響を及ぼしません。

 空白は、その両側にあるモノを分かれて見せる働きがあります。適切に空白を 使うと、プログラムをぱっと見た時に、随分見易くなるものです。


Q 【インデントの原則】

インデントをどのような規則で付けたらよいのかわからない。

 基本的には、ある特別な条件においてのみ実行される箇所は、その外よりも一 段深く書く、という規則です。具体的には、K&R 2nd.を真似すればよいでしょう。

Q 【インデントの幅】

インデントはいくつの空白にするのが一番よいか。タブを使ってもいいか。

 これは難しい質問です。インデント幅が小さいと、段差が少なくなるため、区 別が付きに区々なります。逆に、インデント幅が大きいと、距離が離れるので、 視線の動きが大きくなり、見るのに疲れるでしょう。

 K&R 2nd.では、4文字分のインデントが使われています。これが一つの目安 になるでしょう。

 タブは使ってもいいのですが、空白と混在させると話がややこしくなります。 使うのなら、タブ一つをインデントの階層に対応させるとよいでしょう。


Q 【括弧{}の位置】

{}はどのような位置に書けばよいのか。

 これは最も意見が分かれるテーマで、永年にわたって結論は出ていません。(1) はK&R風の書き方です。(2)を好む人も多いようです。いずれにしても、その ブロックがどこで終わるのか、ぱっと見た時にわかるというのがポイントです。 括弧の位置は、インデントと密接な関係がありますが、C言語として意味のあるの は、あくまで括弧があるということです。コンパイラは括弧を頼りにコードを生 成し、インデントは全く無視します。人間はコードを見る時にインデントを優先 しがちです。

  1.     while ((c = getchar()) != EOF) {
            putchar(c);
        }
    

  2.     while ((c = getchar()) != EOF)
        {
            putchar(c);
        }
    

  3.     while ((c = getchar()) != EOF)
          {
            putchar(c);
          }
    

  4.     while ((c = getchar()) != EOF)
            {
            putchar(c);
            }
    

Q 【caseの位置】

case はどのような位置に書くべきだろうか。次のような書き方がよくあるが、どのよ うな規則なのか分からない。
    switch (c) {
    case 'a':
        /* ... */
        break;

    default:
        break;
    }

 インデントの大原則を、まず覚えてください。 「ブロックの中に書かれた内容はその外よりも一段深くインデントする。」 これが全ての基本になります。

 switch 文の書式は、

    switch (式) 文
 です。これは iffor と同じように考えれば、ブロックにする場合、基本的には次のよう位置に文を書くことになります。
    switch (式) {
        文
        文
        ...
    }
 さて、問題は case です。ラベルを書く位置については、次の原則を覚えておきましょう。

  「ラベルは本来のインデントの位置よりも一段浅くインデントする」

 このような例外を設けるのは、ラベルは本来の処理の流れとは違った所から飛 んでくる目印ですから、他の文の外にはみ出すように書くことによって、より目 立つようにした方が分かりやすくなるからです。

 この原則は gotoの飛び先のラベルの場合だけでなく、 casedefault ラベルにも適用します。その結果、質問の例にあるような位置に case を書くことになります。

 この他の書き方として、ラベルを本来の位置より半分だけ浅くインデントする 場合もあります。

    switch (c) {
      case 'a':
        /* ... */
        break;

      default:
        break;
    }
 また、 switch の中に限って二段深くインデントして、ラベルはそれより一段浅くする人もいま す。この場合、 switch がネストすると、すぐにインデントが深くなりすぎて見にくくなるという欠点が あります。
    switch (c) {
        case 'a':
            /* ... */
            break;

        default:
            break;
    }

その他


Q 【goto】

goto は使ってはいけないと言われた。なぜか。

 goto を使うと処理の流れが分かりにくくなることが多いからです。

しかし、 goto はC言語の仕様に定められている正しいキーワードです。使ってもC言語として問 題が生じることはありません。また、 goto を使わない方がむしろ分かりにくくなる場合もあります。典型的な例としては、 多重のループの中から一番外への脱出があります。

    for (i = 0; i < 10; i++) {
        for (j = 0; j < 10; j++) {
            if (a[i][j] == 0)
                goto found;
        }
    }
    printf("not found¥n");
    return;

found:
    /* 見つかった時の処理 */
 この例では for のループが二重になっているため、 break だけでは抜け出ることができません。

 このような処理も、 goto を避けることは不可能ではありません。大きく分けて、二つのやりかたがありま す。まず、 goto を書きたい箇所に return が書けるように、関数を工夫して使うという方法です。

void foo(void)
{
    for (i = 0; i < 10; i++) {
        for (j = 0; j < 10; j++) {
            if (a[i][j] == 0) {
                /* 見つかった時の処理 */
                return;
            }
        }
    }
    printf("not found¥n");
    return;
}
 このように書くためには、新たに関数を用意しなければならないかもしれませ ん。その結果、処理のオーバーヘッドが増えるかもしれません。しかし、多くの 場合、それは些細なことにすぎないでしょう。

 もう一つの典型的な方法は、フラグを使うというものです。

    for (not_found = 1, i = 0; not_found && i < 10; i++) {
        for (j = 0; not_found && j < 10; j++) {
            if (a[i][j] == 0) {
                not_found = 0;
            }
        }
    }
 これも、ループの判断の度にフラグの検査をしなければならず、ループの回数 によってはオーバーヘッドが問題になるかもしれません。また、プログラムその ものが繁雑になってしまいます。このようなフラグを導入するよりは、gotoを書 いた方がましだ、と思う人は多いでしょう。

Q 【マクロの使い方】

 次のような定義をしておけば、Pascalに慣れている私にはプログラムが理解し やすいと思うが、これはよいアイデアだろうか。
    #define begin {
    #define end }

 あなたは確かにPascalの方が向いていると思います。従ってCでプログラムを書 くのがそもそも間違いの元です。Pascalでプログラムを書きましょう。

 その他大勢のCプログラマにとって、このような定義は邪魔物以外の何物でもあ りません。記号を使うことによって、他のキーワードや名前を目立たせるという、 C言語の持っている特徴を放棄することにもなります。 {} の意味が分からないとい うなら別ですが、そうでなければ {} を使うべきです。

 もしかすると、あなたの使っている端末は特殊で、 {} を入力することが大変困難なのかもしれません。それならやむを得ないという考 え方もありますが、むしろ端末をリプレースした方がよいという考え方もありま す。


Q 【,】

カンマを使って次のような書き方をしたコードを見た。
    a = 1, b = 2, c = 3;
 なぜ次のように書かないのか。
    a = 1; b = 2; c = 3;

 あくまで想像ですが、このコードを書いた人が、  と考えたのだと思われます。

 一つの文を一行に書くという原則は、全く改行しないで複数の文を一行に書く よりも、ずっと見やすいコードを作る点で有意義です。昔、BASICという言語では、 実行速度とメモリの節約のために、複数の文をできるだけ一行で書くという技法 が好まれたことがあります。C言語をコンパイラで使う場合は、そのように詰めて 書いてもメモリの節約になるわけではありません。

 カンマ演算子は、左から右へ一つずつ式を実行していきます。殆どの場合、セ ミコロンを使った場合と同じ振る舞いになるはずです。次のような場合は注意が 必要です。

    if (reset)
        a = 1, b = 2, c = 3;
 このカンマをセミコロンに置き換えると、 if で条件判断された結果は a = 1; という文のみに掛かり、それ以外の文は reset の値にかかわらず実行されてしまいます。

Q 【if (NULL == fp)】

あるコードは、比較のとき、定数を左に書いていた。なぜそのように書くのか。 そうした方がよいのか。

 これは、初心者の(あるいは熟練者でも油断した)プログラマーは代入の = と比較の == を間違う、という伝説が根拠になっています。変数には代入することができます が、定数には他の値は代入することはできません。比較の左辺に定数を書くよう にすれば、間違って=と書いてしまった時に、コンパイラがエラーにしてくれるの で、変なバグが入ったプログラムを実行して致命的な結果になることを避けられ ます。

 しかし、このような癖を身に付けても、比較の両辺が変数である場合には何の 役にもたちません。また、多くの人が、プログラムを左から右に読んでいきます。 ある変数の値が何であるか調べる、という発想があるのなら、左に変数を書いて、 「もし fpNULL なら」と考える方が自然です。「もし NULLfp なら」と考える人は滅多にいないでしょう。日常の言語(日本語や英語)では、 主語が先に現れますから、普段、そのような発想をしているはずです。変な順序 で物事を考えると、かえってミスを招きかねません。個人的には、このような理 由により、

    if (fp == NULL)
 と書く方が望ましい、と考えます。

Q 【変数名】

変数名に意味のある名前を付けたい。 CheckMode と書くべきか、それとも check_mode と書くべきか。

 これは完璧に好みの問題です。どちらでも構いません。個人的には、 check_mode の方が、checkとmodeの間に隙間があるため見やすいと思います。しかし、文字数 が増えるのは嫌だ、という考え方もあるでしょう。

Q 【return (0);】

return (0); のようなコードを見た。なぜ括弧が必要なのか。

 実は必要ありません。

 return の後に括弧が必要な処理系が大昔にあったという伝説があります。実際、K&R の初版では、 return の戻す値には必ず括弧が付いていました。しかし、いまや return の後には括弧はありません。

 括弧がある方が見やすいと主張する人もいますが、たいした根拠はありません。 括弧がない場合、うっかり retrun (0); と書いてしまった時に、全てのコンパイル が終了してリンクし終わる寸前に「 retrun という関数が定義されていない」というエラーが発生するまで気付かないかもし れません。たまたま retrun という名前の関数があったら、もっと面白いことになります。


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