[ << ] [ >> ]           [Top] [Contents] [Index] [ ? ]

5. Cの一番良い使い方

この節では、GNUソフトウェアを書くときの一番良いC言語の使い 方について助言を与える。

5.1 あなたのソースコードの書式  
5.2 あなたの仕事のコメント  
5.3 Cの構成のきれいな利用  
5.4 変数と関数の名前付け  
5.5 システム間の移植性  
5.6 CPU間の移植性  
5.7 システム関数の呼び出し  "標準"ライブラリ関数の移植性
5.8 国際化  
5.9 Mmap  mmapの安全な使い方


5.1 あなたのソースコードの書式

C関数の本体を開始する開き大括弧をゼロ列目に置き、他の開き大括弧や開き丸 括弧や開き角括弧をゼロ列目に置かないようにするのは重要だ。いくつかのツー ルは、C関数の始まりを探すのに、ゼロ列目の開き大括弧を捜す。これらのツー ルはそういう風にフォーマットされていないコードでは上手く動かないだろう。

関数定義で、関数の名前がゼロ列目で始まっていることも重要だ。人々はこれの おかげで関数定義を探すのが楽になり、あるツールがそれらを認識するのも楽に なるかもしれない。こうして、適切な書式は次のようになる。

 
static char *
concat (s1, s2)        /* Name starts in column zero here */
     char *s1, *s2;
{                     /* Open brace in column zero here */
  ...
}

あるいは、もしANSI Cを使いたいなら、次のように定義をフォーマットす る。

 
static char *
concat (char *s1, char *s2)
{
  ...
}

ANSI Cでは、もし引数が上手く一行に収まらないなら、次のようにそれを 分ける。

 
int
lots_of_args (int an_integer, long a_long, short a_short,
              double a_double, float a_float)
...

関数の本体では、次のようにフォーマットされたコードを好んでいる。

 
if (x < foo (y, z))
  haha = bar[4] + 5;
else
  {
    while (z)
      {
        haha += foo (z, z);
        z--;
      }
    return ++x + bar ();
  }

我々は開き丸括弧の前とコンマの後にスペースがあるとプログラムを読むのがよ り簡単であることを見出している。とりわけコンマの後は。

式を複数行に分けるとき、演算子の後ではなく、それの前で分ける。こうするの が正しいやり方だ。

 
if (foo_this_is_long && bar > win (x, y, z)
    && remaining_condition)

字下げが同じところで、異なる優先度の二つの演算子を持たないようにしなさい。 例えば、こう書いてはいけない。

 
mode = (inmode[j] == VOIDmode
        || GET_MODE_SIZE (outmode[j]) > GET_MODE_SIZE (inmode[j])
       ? outmode[j] : inmode[j]);

代わりに、字下げが入れ子を表すよう、余分な丸括弧を使う。

 
mode = ((inmode[j] == VOIDmode
         || (GET_MODE_SIZE (outmode[j]) > GET_MODE_SIZE (inmode[j])))
        ? outmode[j] : inmode[j]);

Emacsがそのコードを適切に字下げするよう、余分な丸括弧を入れなさい。例え ば、次の字下げは手でやるといい感じだが、Emacsは台なしにしてしまう。

 
v = rup->ru_utime.tv_sec*1000 + rup->ru_utime.tv_usec/1000
    + rup->ru_stime.tv_sec*1000 + rup->ru_stime.tv_usec/1000;

でも開き括弧一組を加えると問題は解決する。

 
v = (rup->ru_utime.tv_sec*1000 + rup->ru_utime.tv_usec/1000
     + rup->ru_stime.tv_sec*1000 + rup->ru_stime.tv_usec/1000);

do-while文は次のようにフォーマットする。

 
do
  {
    a = foo (a);
  }
while (a > 0);

フォームフィード文字 (control-L) を使って、プログラムを(関数の中ではなく) 論理的な位置でページに分割してほしい。ページがどれぐらいの長さかなんて問 題じゃない。印刷されるページに合わせなくていいのだから。フォームフィード は行の中にそれ自身だけを置くべきだ。


5.2 あなたの仕事のコメント

どんなプログラムでもそれが何なのか簡単に表すコメントで始まるべきだ。例: `fmt - filter for simple filling of text'.

英語は全ての国のほとんど全てのプログラマが読むことのできる唯一の言語なの で、GNUプログラムでは英語でコメントを書いてほしい。もしあなたが英語を上 手く書けないなら、出来るだけ上手く英語でコメントを書き、他の人々にそれら を書き直すのを手伝ってくれるよう頼んでください。もし英語でコメントを書く ことができないなら、一緒に仕事してくれる誰かを探して、あなたのコメントを 英語に翻訳してもらってください。

それぞれの関数に、その関数が何をやり、どういう引数を受け取り、引数のあり 得る値が何を意味し、そして何に使われるのかを表すコメントを書いてください。 もしCの型が習慣的なやり型で使われるなら、Cの引数宣言の意味をくどくどと複 製する必要はない。もしその利用が(実際には文字列の最初ではなく、二文字目 のアドレスであるchar *型の引数のような)標準的でないものだったら、 あるいは、(改行を含む文字列は動作保証されない、というような)期待される方 法では働かない値があり得るなら、そう書くのを忘れないようにしなさい。

また、もしあるなら、返り値の意味を説明しなさい。

Emacsのセンテンス・コマンド(sentence command)が働くように、コメントの行 の最後の後に二つのスペースを置いてください。また、完全な文を書き、最初の 単語を大文字で書いてください。もし小文字の識別子が文の最初に来たら、それ を大文字で書いてはいけない! 綴りを変えると違う識別子になる。もし小文字で 文を始めるのが好きじゃないなら、文を違うように書きなさい(例えば、"The identifier lower-case is ...")。

関数の上のコメントは、引数の値について言うときにその引数の名前を使えば、 ずっとはっきりする。変数名それ自体は小文字であるべきだが、変数そのもので はなく、その値について言っているときには大文字で書きなさい。従って、"an inode"よりも、"the inode number NODE_NUM"である。

普通コメントに関数の名前を再び言うことに意味はない。なぜなら、読者は自分で それを見ることができるからだ。関数自身がスクリーンの一番下からはみ出てし まうぐらいコメントが長いときは例外かもしれない。

静的な変数それぞれにも、次のようにコメントがあるべきだ。

 
/* Nonzero means truncate lines in the display;
   zero means continue them.  */
int truncate_lines;

すべての`#endif'に、入れ子になっていない(たった数行の)短い条件分岐 の場合を除いて、コメントを付けるべきだ。そのコメントには、 その意味を含めて、終了する条件分岐の状態を記すべきだ。 `#else'はその条件と続くコードの意味を記述するコメントを持つ べきだ。例えば、

 
#ifdef foo
  ...
#else /* not foo */
  ...
#endif /* not foo */
#ifdef foo
  ...
#endif /* foo */

しかし、対照的に、`#ifndef'では次のようなコメントを書く。

 
#ifndef foo
  ...
#else /* foo */
  ...
#endif /* foo */
#ifndef foo
  ...
#endif /* not foo */


5.3 Cの構成のきれいな利用

関数への全ての引数を明示的に宣言してください。それらが単にintだか らという理由で省いていけない。

外部関数とソースファイルの後ろに現れる関数の宣言は、ファイルの先頭の近く 一箇所か、ヘッダファイルの中に書くべきだ。関数の中にextern宣言を 置いてはいけない。

以前、一つの関数内で繰り返し繰り返し異なる値のために(temのような 名前で)同じ局所変数を使うのが普通のやり方だった。こうする代わりに、別の 目的毎に別の局所変数を宣言し、意味のある名前を付ける方がより良い。これで プログラムがより理解しやすくなるだけでなく、良いコンパイラの最適化を促進 するのである。また、局所変数の宣言をそれぞれ、それを全て使用する一番小さ い領域に入れることができる。こうすると、プログラムがさらにきれいになるの だ。

大域識別子を隠す局所変数や引数を使ってはならない。

複数行に及ぶ一つの宣言で複数の変数を宣言してはいけない。代わりに、それぞ れ行で新しく宣言を始めなさい。例えば、こうする代わりに、

 
int    foo,
       bar;

こう書くか、

 
int foo, bar;

あるいは、こうする。

 
int foo;
int bar;

(もしそれらが大域変数なら、いずれにせよその前にコメントを付けるべきだ。)

他のif文に入れ子になるif-else文があるとき、必ずその if-elseの周りに大括弧を付ける。従って、次のように決して書 いてはならない。

 
if (foo)
  if (bar)
    win ();
  else
    lose ();

常に次のようにする。

 
if (foo)
  {
    if (bar)
      win ();
    else
      lose ();
  }

もしelse文の中に入れ子になるif文があれば、次のように、 then部分をその前のthen部分のように字下げして一行に else ifを書くか、

 
if (foo)
  ...
else if (bar)
  ...

あるいは、次のように大括弧の中に入れ子のifを書く。

 
if (foo)
  ...
else
  {
    if (bar)
      ...
  }

同じ宣言で、構造体のタグや変数、typedefを一緒に宣言してはならない。代わ りに、構造体のタグを別に宣言して、それから変数やtypedefを宣言する。

if条件文内で代入しないようにしなさい。例えば、こう書いてはいけ ない。

 
if ((foo = (char *) malloc (sizeof *foo)) == 0)
  fatal ("virtual memory exhausted");

代わりに、こう書く。

 
foo = (char *) malloc (sizeof *foo);
if (foo == 0)
  fatal ("virtual memory exhausted");

lintをおとなしくするのに、プログラムを見苦しくしてはならない。 voidへのキャストを入れないでください。キャストなしのゼロは、可変 引数の関数を呼ぶときを除くと、ヌル・ポインタ定数として全く結構である。


5.4 変数と関数の名前付け

プログラムの大域的な変数や関数の名前はコメントのように働く。だから、簡潔 な名前を選ばないように。---代わりに、その変数や関数の意味について役に立 つ情報を与える名前を探しなさい。GNUプログラムでは、名前は他のコメント と同様英語であるべきである。

局所変数の名前はもっと短くていい。なぜなら、それらは一つの文脈の中でだけ 使われ、そこでは(たぶん)コメントがそれらの目的を説明している。

ある名前の単語を分けるのに、Emacsの単語コマンドがその中で使えるように、 アンダースコアを使ってください。小文字にしておきなさい。大文字をマクロや enum定数や一定の取り決めに従う接頭辞のために取っておきなさい。

例えば、ignore_space_change_flagのような名前を使うべきだ。 iCantReadThisのような名前を使ってはいけない。

コマンドラインのオプションが指定されたかどうかを示す変数は、オプションの 文字ではなく、オプションの意味にちなんだ名前を付けるべきだ。コメントがオ プションの正確な意味とその文字の両方を記述すべきだ。例えば、

 
/* Ignore changes in horizontal whitespace (-b).  */
int ignore_space_change_flag;

一定の整数値に名前を定義したいとき、`#define'よりもenumを使 いなさい。GDBは列挙定数について知っている。

古いSystem Vシステムで不必要な問題を引き起こさないよう、14文字以下のファ イル名を使いなさい。これを試験するのにdoschkというプログラムを使 うことができる。doschkはまた、MS-DOSファイルシステムにファイルが 置かれたとしたら、名前が衝突する可能性を試験する。---注意してもしなくて も構わないものだ。


5.5 システム間の移植性

Unixの世界では、"移植性"は異なるUnixバージョンに移植することを言ってい る。GNUプログラムにとって、この種の移植性は望ましいが、最も重要ではない。

GNUソフトウェアの主要な目的は、GNUカーネルの上で走り、GNU Cコンパイラで コンパイルされ、様々なCPU上で動くことだ。異なるCPU上のGNUシス テム間の多様性の量と種類は、今日のLinuxに基づくGNUシステムやBSDシステム 間の多様性と比較できる程度であろう。だから、絶対に必要な移植性の種類 はかなり限られている。

しかしたくさんのユーザがGNUソフトウェアをGNUでないUnixやUnix-likeシス テムで走らせている。だから、さまざまなUnix-likeシステムをサポートするこ とが望ましい。最も重要ではないけれど。

ほとんどのUnix-likeシステムへの移植性を得る一番簡単な方法はAutoconfを使 うことだ。あなたのプログラムがAutoconfが提供できる以上にホスト・プラット ホームに関する情報を知る必要があることはあまりない。単にそういう情報を必 要とするプログラムの大部分はすでに書かれているから。

準内部的データベース(例えば、ディレクトリ)のフォーマットを使わないように しなさい。もっと高水準の方法(readdir)があるときは。

MSDOS、Windows、Macintosh、VMS、MVSのような、Unixに似てないシステムにつ いて言うと、それらをサポートするのは普通しない方がいいぐらい大変な仕事だ。

計画されているGNUカーネルはまだ出来てないが、GNU Cライブラリのマニュアル を見ることで、それが提供するであろう機能がどれなのか分かる。GNUカーネル はMachに基づいているから、Machの機能も利用できるだろう。しかしながら、 Machの機能を使用すると、おそらくあなたのプログラムを今日デバッグする困難 に見舞われるだろう(4)


5.6 CPU間の移植性

GNUシステムでさえ、CPUタイプ間の違いのせいで異なってしまうだろう。--- 例えば、バイト順序や境界の必要性の違いなどだ。こういう違うを扱うことは絶 対に不可欠だ。しかし、intが32ビットより小さい可能性を扱うために努 力してはいけない。GNUでは16ビットのマシンはサポートしない。

intオブジェクトのアドレスがまたその一番下のバイトのアドレスである とみなしてはいけない。これはビッグ・エンディアンのマシンでは誤りだ。だか ら、次の間違いをしてはいけない。

 
int c;
...
while ((c = getchar()) != EOF)
  write(file_descriptor, &c, 1);

関数を呼ぶとき、さまざまな型のポインタ間やポインタと整数間での違いを心配 する必要はない。ほとんどのマシンでは、いずれにせよ違いはない。違いのある わずかなマシンについて言うと、それらの全てがANSI Cをサポートしてい るので、それらのシステム上でそのコードが動くように、(ANSI Cでだけ使 われるように条件付けされた)プロトタイプを使うことができる。

ある場合には、整数とポインタの引数を無差別に同じ関数へ渡し、いかなるシス テムでもプロトタイプを使わないでも構わない。例えば、多くのGNUプログラム はprintfやその類いに引数をどんどん渡すエラー報告関数を持っている。

 
error (s, a1, a2, a3)
     char *s;
     int a1, a2, a3;
{
  fprintf (stderr, "error: ");
  fprintf (stderr, s, a1, a2, a3);
}

実際、これは全てのマシンで動作し、他の"正しい"やり方よりずっと単純だ。 そのような関数に対してプロトタイプを使うことをしないように。

しかしながら、本当に必要としているのでないなら、ポインタを整数にキャスト しないようにしなさい。これらの仮定は実に移植性を減らしており、ほとんどの プログラムでは簡単に避けられる。ポインタから整数にキャストすることが不可 欠な ---アドレスだけでなく型情報をあるワードに収めるLispインタープリタ、の ような--- 場合には、そうして構わないが、異なるワードサイズを扱う明示的な 準備をしなくてはならないだろう。


5.7 システム関数の呼び出し

Cの実装は十分に違う。ANSI Cは非互換性を減らすが、無くなりはしない。 その一方では、多くのユーザがGNUソフトウェアをANSI以前のコンパイラで コンパイルしたがる。この章では、移植性を不必要に失くさないよう、標準Cラ イブラリ関数をどれぐらいたくさん、あるいは、少なく使うか、推奨する方法を 見せる。

ここでは、HAVE_STRCHRHAVE_STRRCHRが対応する関数が存在す るシステムでは定義されるマクロだとみなしている。それらを適切に定義する一 つのやり方はAutoconfを使うことだ。


5.8 国際化

GNUはあるプログラムのメッセージを様々な言語に翻訳するのを容易にするGNU gettextと呼ばれるライブラリを持っている。あらゆるプログラムでこのライブ ラリを使うべきだ。メッセージがプログラムに現れるとき、それらに英語を使い なさい。そして、それらを他の言語に翻訳するための方法をgettextで提供しな さい。

GNU gettextの使用は翻訳が必要かもしれない、それぞれの文字列の周りに gettextマクロの呼び出しを付けることを含む ---次のように。

 
printf (gettext ("Processing file `%s'..."));

こうすると、GNU gettextが文字列"Processing file `%s'..."を翻訳さ れたバージョンで置き換えられる。

一度プログラムがgettextを使うことになったら、翻訳が必要な新しい文字列を 加えるとき、gettextへの呼び出しを書く地点を作ってください。

あるパッケージでのGNU gettextの使用は、そのパッケージに対してテキス ト領域名を指定することを含む。テキスト領域名はこのパッケージの翻訳を他 のパッケージの翻訳と分離するのに使われる。通常、テキスト領域名はパッケー ジの名前と同じであるべきだ ---例えば、GNU file utilityのために `fileutils'が使われる。

gettextが上手く働くようにするために、単語や文の構造に仮定を設けるコード を書かないようにしなさい。文の正確なテキストがデータによって変わるのよう にしたいとき、条件付けられた単語や句を単一の文脈構成に押し込むよりも、そ れぞれ完全な文を含む二つ以上の文字列定数を使いなさい。

これがやるべきではないものの例だ。

 
printf ("%d file%s processed", nfiles,
        nfiles != 1 ? "s" : "");

この例の問題は複数形が`s'を加えることで行われると仮定していることだ。も し書式文字列にgettextを適用するなら、次のように、メッセージが異なる単語 を使うことができるが、

 
printf (gettext ("%d file%s processed"), nfiles,
        nfiles != 1 ? "s" : "");

複数形が`s'を使うようになお強制されている。これがより良い方法だ。

 
printf ((nfiles != 1 ? "%d files processed"
         : "%d file processed"),
        nfiles);

このやり方で、二つの文字列それぞれに独立してgettextを適用できる。

 
printf ((nfiles != 1 ? gettext ("%d files processed")
         : gettext ("%d file processed")),
        nfiles);

こうすると、"file"という単語の複数形を作る、いかなる方法でも実現でき、 "processed"に対して、単語が一致しないといけない言語を扱うこともできる。

同じような問題は次のコードで文脈の構造の水準で現れる。

 
printf ("#  Implicit rule search has%s been done.\n",
        f->tried_implicit ? "" : " not");

このコードにgettext呼び出しを与えても、すべての言語で正しい結果を 得られるわけではない。なぜなら、いくつかの言語で否定は文中に一つよりもた くさんの場所で単語を加える必要があるからだ。対照的に、gettext呼び 出しの追加は、もしそのコードが次のように始まるなら、簡単に行える。

 
printf (f->tried_implicit
        ? "#  Implicit rule search has been done.\n",
        : "#  Implicit rule search has not been done.\n");


5.9 Mmap

mmapがすべてのファイルに働くとも、すべてのファイルで失敗するとも、 みなしてはいけない。一部のファイルでは上手く行き、他では駄目かもしれない。

mmapを使う適切な方法は、使いたい特定のファイルで試してみることだ。 ---そして、もしmmapが働かなかったら、readwriteを 使う他の方法で作業することに頼りなさい。

この用心が必要である理由はGNUカーネル(HURD)はユーザが拡張可能なファイル システムを提供することで、そこではたくさんの異なる種類の"普通のファイル" があり得る。それらの多くはmmapをサポートするが、いくつかはしない。 プログラムをすべてのそういうファイルを扱えるようにすることは重要だ。


[ << ] [ >> ]           [Top] [Contents] [Index] [ ? ]

This document was generated by Akihiro Sagawa on January, 21 2003 using texi2html