初級C言語Q&A(16)

初出: C MAGAZINE 1996年9月号
Updated: 1996-09-25

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


 今回は、今までの分類に漏れている質問からいくつかピックアップしてみました。

種々の質問


Q 【文字列のための領域】

 次のコードはなぜ思った通りに動かないのか。
    len = strlen(s);
    buf = malloc(len);
    strcpy(buf, s);

 文字列とは、文字の並びに加えて、終了を示す'\0'が必要です。 strlenが戻す値は、'\0'を含まないので、 malloc(len)ではなく malloc(len + 1)としなければなりません。

Q 【文字列のための領域】

 文字列を格納するための領域をmallocで得ようとした。 次のコードどちらがよいのか。
    /* 1 */
    len = strlen(s);
    buf = malloc(len + 1);

    /* 2 */
    len = strlen(s) + 1;
    buf = malloc(len);

 どちらでも結果は同じはずなので、動作という点からはどっちでも構わないこ とになります。しかし、後のことを考えると、lenという名前の変 数には文字列の長さを入れておくという一貫した発想でプログラムを作った方が、 修正時のトラブルを防ぐなどの意味があります。

Q 【mallocのキャスト】

 次のコードのキャストは不要と言われた。なぜか。
  int *array;

  array = (int *) malloc(sizeof(int) * 16);

関数mallocの型は(void *)となっています。 この型は、他の型のポインタに代入する時に、キャストしなくても自動的に適切 な変換がほどこされることが保証されています。ですから、キャストは必要あり ません。  “voidへのポインタは、任意の不完全型若しくはオブジェクト型へのポインタ に、又はポインタから、型変換してもよい。任意の不完全型若しくはオブジェク ト型へのポインタは、voidへのポインタに型変換してよく、再び戻してもよい。 その結果は元のポインタと比較して等しくなければならない。”
(JIS X3010 6.1.2.5 型)  “不完全型”とは、例えば大きさの分からない配列型や、内容が確定していな い構造体型のことです。

Q 【'a'の型】

'a'の型は何か?

'a'のような定数は文字定数と呼ばれています。 C言語では、文字定数の型はintであることになっています。 charだと誤解しそうですが、sizeof('a')を 調べればcharではないことが分かります。
(JIS X 3010 6.1.3.4 文字定数、意味規則)

Q 【ディレクトリの作成時刻変更】

 ディレクトリの作成時刻を変更するにはどうすればよいか。

 例えば、MS-DOSの場合、ディレクトリの作成時刻は変更することができません。 そのようなシステムコールが用意されていないからです。処理系によっては、デ ィレクトリの作成時刻変更のシステムコールがあるかもしれませんが。  なお、ファイルの作成時刻は、setftimeを使って変更することが できます。  ところで、実際にディレクトリの時刻を変更できるツールが出回っています。 これらのツールはシステムコールで時刻を変更するのではなく、ディスクの内容 を直接読み書きするシステムコールを使って、直接セクタの内容を処理している ようです。  なお、絶対にやってはいけませんが、システムの時刻を変更して、ディレクト リを作成し、その後システムの時刻を元に戻す、という方法があります。  くどいですが、絶対にしないように。

Q 【古いC】

あるCのソースを見ると次のような感じで関数が始まっていた。どういう意味か。
int main(argc, argv)
int argc;
char *argv[];
{
    ....

 これはANSI Cが使われる前の形式で書かれたプログラムです。ANSI以前のCでは、 関数定義の時に、引数の型を仮引数の所に書くことができず、このように書いて いました。すなわち、ANSIの書き方なら、次のように書いたのと同じ意味です。
int main(int argc, char *argv[])
{
    ....

Q 【const】

char const *s; と、char *const s; は何が違うのか。

constが修飾する内容が違います。 constを理解するには、それに続く内容がconst (変更不可)であるという考え方が分かりやすいと思います。 この考え方に従えば、char const *s; と書いた場合、 constであるのは*sです。すなわち、s が指している内容は変更できません。 char *const s; とした場合は、constであるのは sです。すなわち、sそのものが変更できません。 表にすると次のようになります。
                             s               *s
    ---------------     ------------    ------------
    char const *s;      変更できる      変更できない
    char *const s;      変更できない    変更できる
 s*sの両方とも変更させたくない場合は、 char const *const s; と書きます。 (c.l.c. FAQ 11.9)

Q 【memmove】

memcpy関数とmemmove関数は何が違うのか。 どちらを使えばよいか。

 コピーする範囲に重複がある場合のふるまいが異なります。memcpy の実装として、次のようなものを考えます。これは正しい実装です。
void *memcpy(void *s1, const void *s2, size_t n)
{
    char *p1 = s1;
    const char *p2 = s2;

    while (n-- > 0) {
        *p1++ = *p2++;
    }

    return s1;
}
 これに対して、次のような呼び出しを考えます。
    char buf[] = "This is a sample data";

    memcpy(buf + 5, buf, 16);
 この結果としては、bufの中が、次のようになることを期待して いるのです。
    "This This is a sample"
 しかし、実際はこうなりません。次のようになります。
    "This This This This T"
 先頭からコピーすると、既に上書きしてしまった所からさらにコピーしようと するために、このような結果になるのです。期待通りにコピーするためには、後 ろからコピーしなければなりません。しかし、後ろからコピーするように memcpyを実装すると、今度は
    memcpy(buf, buf + 5, 16);
 がうまくありません。 memcpyについては、このような実装が許されています。 厳密にいえば、領域に重複がある場合にはmemcpyの結果は未定義 です。しかし、これでは困るという場合もあるので、memmoveとい う関数が用意されています。memmoveは、このようにコピーする領 域に重複がある場合にも期待通りの結果となることを要求されます。 すなわち、memmoveは、一旦どこか別のバッファに目的の文字列を コピーしてから、その結果は必要な所にコピーしたのと同じ結果になるようにふ るまいます。  ということは、memmoveだけあれば特に問題ないということにな るのですが、実装方法によっては、memcpyの仕様は高速に処理する 場合に有利なこともあるので、重複がないことが確実である場合には memcpyを使った方がよいかもしれません。

(参考: c.l.c FAQ list 11.25)


Q 【fgetpos】

fgetpos関数とftell関数は何が違うのか。 どちらを使えばよいか。

fgetposfpos_tという型の引数を使ってファイル 内の位置を表現しています。ftellの一つの弱点は、その値が longであるということです。longが32ビットの処 理系の場合は、表現できる範囲は2147483647(0x7fffffff)が最大です。これは約 2Gバイトに相当します。殆どの場合はこれで問題ないと言っても、最近は2Gバイ トより容量の大きいハードディスクも多くなってきました。一つのファイルのサ イズが2Gバイト以上だと、ftellの戻り値で表現することができま せん。fgetposの場合、fpos_tという型が適切に設定 されていて、かつコードがそれを意識した設計になっている限り、このような大 きなファイルにも対応したプログラムを書くことが可能です。

(参考: c.l.c FAQ list 12.25)


Q 【lint】

lintとは何か。

lintとは、C言語で書かれたプログラムに対して細かいチェックを行うためのツー ルです。具体的には、型の一致していない関数呼び出しや、使われていない変数、 初期化されていない変数を使っている箇所、全く実行されないコードなどを検出 することができます。また、ある所では関数の戻り値を使っていて、他の所で同 じ関数を呼び出しているのに関数の戻り値を使っていないとか、逆に一つの関数 内で、ある所では意味のある値をreturnしているのに、他では値を 戻さずにreturnしているような場合を検出することもできます。 lintは複数のソースファイルを指定することもできます。この場合は、定義さ れているのに使われていない関数を検出することができます。  最近のコンパイラは、細かい警告を出す機能が強化されているため、従来のコ ンパイラではlintを使わなければ発見できなかったような手違いは、コンパイル 時に発見することができるようになりました。 lintの出す警告は、しばしば「警告しすぎる」という状態になりがちです。こ れらを抑止するために特別なコメントが使われることがあります。例えば、コー ドの中に/*NOTREACHED*/というコメントを書いておけば、lintはそ の先のコードは実行されないという前提で検査を行います。これは、例えば exit関数を呼び出した後に一度も通過しない個所がある場合などに 使われます。

/* lintで警告が出る例。関数の戻り値が一部でのみ使われている場合 */
foo()
{
    if (f1() != 0) {
        ...
    }
}

bar()
{
    f1();
}

/* lintで警告が出る例。関数が戻り値を戻したり戻さなかったりする場合 */
int f1()
int a;
{
    if (a & 1)
        return 1
    return;
}

(参考: c.l.c FAQ list 18.7, 18.8)

Q 【undelete】

関数removeを実行して消してしまったファイルを復活するには どうすればよいか。

 Cの標準ライブラリには一旦消したファイルを復活する関数はありません。処理 系によっては、ディスクを直接読み書きするシステムコールが用意されているの で、それを使うことになります。中途半端な知識では大変危険な結果(といっても ハードディスク上のファイルが全滅する程度で済むはずですが)になるかもしれま せん。

Q 【ビジーウエイト】

 「ビジーウエイト」は使わないようにいわれた。ビジーウエイトとは何で、ど うして使ってはいけないのか。

 ある状態の変化を検出したい場合に、ループを作成し、状態が変化したかどう かを常に監視するという方法があります。このようなループをビジーループと呼 ぶことがあります。また、このようにして待つことをビジーウエイトと呼びます。
    for (;;) {
        extern int status;

        if (status & 1)
            break;
    }
 この例の場合は、外部変数statusの状態を監視していますが、 この他には、特 定のアドレスの様子(特にI/Oポート)や、キーボードやマウスの状態、データの受 信バッファの状態、などが検査の対象となります。キーボードが押されたことを 検出したい場合、それをどのようにして知るかは処理系に依存しますが、基本的 に次のようなループを使えば可能です。
    for (;;) {
        if (キーボードが押された)
            break;
    }
このようなループが嫌われる理由は、主に二つあります。
  1. 負荷が高くなる。
    マルチタスクのシステムでは、各タスクの処理が軽いと全体のパフォーマンス がよくなります。ビジーウェイトは状態の変化待ち状態で、全力でループを回る ことになるため、効率は良くありません。他に方法があれば、検討すべきです。

  2. デッドロックの危険がある。
    ビジーループは排他制御に使われることがあります。特定のフラグを参照して、 同期を取るのが目的ですが、安易に設計するとデッドロック状態を引き起こすこ とがあります。

 DOS上で単独のプログラムだけを実行するような場合には、ビジーウェイトは簡 単に書けるために使われることがあります。


Q 【プロトタイプ宣言】

 プログラムをコンパイルしたら、
   Warning test.c 22: Call to function 'printf' with no prototype
   in function main
 のような警告メッセージが出た。何が悪かったのか。

 警告の意味は直訳すれば「関数printfがプロトタイプなしで呼 び出されている」となります。  プロトタイプ宣言というのは、あらかじめ関数の引数および戻り値の型を宣言 しておくというもので、実際に関数が呼び出される所では、宣言された型と実際 に引数として使われている変数の型が一致しない場合には、必要な型への型変換 が自動的に行われます。  コンパイラは、与えられたコードをコンパイルする時に、コード中から呼ばれ ている関数に対するプロトタイプ宣言を探します。この宣言が見つからない場合 は、とりあえず引数の型がそのままで正しいとしてコンパイルすることになりま す。警告は、このような状況になったことを意味しています。関数呼び出しの時 に使われている変数や定数の型が、その関数の仕様通りならば特に問題はありま せん。しかし、引数の型が異なっていると、高い確率で、実行時に予期せぬふる まいを引き起こしたり、最悪の場合はプログラムが暴走、異常終了するかもしれ ません。 printfは標準関数なので、プロトタイプ宣言は対応する #includeを行うことによって自動的に行われます。この場合は #include <stdio.h>をプログラムの先頭に追加すれば解決します。  プロトタイプ宣言を行っていれば、その関数を呼び出す時に型が関数仕様と合 致しない場合には、自動的に型変換が行われます。この場合はキャストする必要 はありません。しかし、場合によっては、プログラマーが意図的に自動的な型変 換を期待したコードを書いたのではなく、間違った変数を引数に与えてしまった のかもしれません。このような場合には、コンパイル時に警告が出ることもあり ます。

Q 【標準関数のプロトタイプ宣言】

 コンパイル時の警告を出さないようにするため、次のようなプロトタイプ宣言 を先頭に付けたら確かに警告は消えた。
  int printf(const char *format, ...);
 いちいちリファレンスマニュアルを見てこのような宣言をするのは面倒だが、 よい方法はないのか。

 標準関数のプロトタイプ宣言は、その関数に対応しているヘッダーファイルを インクルードすれば、行ったことになります。つまり、対応ヘッダーファイルの 中には、あらかじめプロトタイプ宣言が書かれているので、その都度書く必要は ありません。この場合は、printf<stdio.h>で定義 されている関数ですから、#include <stdio.h>をコードの先頭に書 けば警告は出なくなります。

Q 【キャストは必要か】

 次のコードのlongsize_tへのキャストは必要か。
#include <stdio.h>
     ..

foo(FILE *fp)
{
    int a[100];

    ...

    fseek(fp, (long) 256, SEEK_SET);
    fread(buf, sizeof(int), (size_t) 100, fp);
    ...
}

 論理的にはキャストする必要はありません。<stdio.h>#includeすることにより、fseekfread のプロトタイプ宣言が行われています。従って、次のように書いても全く同じ結 果となるはずです。
#include <stdio.h>
     ..

foo(FILE *fp)
{
    int a[100];

    ...

    fseek(fp, 256, SEEK_SET);
    fread(buf, sizeof(int), 100, fp);
    ...
}
 ところで、キャストを省いてしまうと、例えばfseekの2番目の引 数はintの定数としての256ということになります。 最終的には、これは<stdio.h>内にある関数プロトタイプによって、 自動的にlong intに型変換されてからfseekが呼び出 されるため、問題はありません。ただ、この場合、コードを読んだだけでは、プ ログラマーが深く考えずにintとして256と書いたのか、 あるいはプロトタイプ宣言があるから自動的に型変換が発生することを期待した のか、判断することができません。もっとも、このような場合に何も考えずに 256と書けるというのが、プロトタイプ宣言の一つの利点なのかも しれません。  しかし、fseekの例のような場合は、キャストしないにしても、 longの引数であるべき所にintの定数が書いてある のは、個人的にはどうも落ち着かない感じがするので、せめて次のように longの定数としたい所です。
    fseek(fp, 256L, SEEK_SET);
 このような状況下で、プログラマーがあえて「ここはlongを書く べき所だが、いまintの変数に入っている値をlongと して与えてやりたい」という意志を表現したいのであれば、確かにプログラミン グ上は無駄かもしれませんが、自己主張の一貫としてあえて意図的にキャストを 付けても構わないのではないか、と思います。  なお、これに関しては「いや、無駄だから書かない方がよい」という人もいま す。文法上はどちらでも構いません。

Q 【if文を一行に書く】

 次のコードはどちらがよいか。
  /* 1 */
  if (a == 1) foo(a); else bar(a);

  /* 2 */
  if (a == 1)
      foo(a);
  else
      bar(a);

 Cのプログラムとしては、両者はコンパイルした結果が全く同じになるはずです から、差はありません。すなわち、これはスタイルの問題です。好みや慣れに左 右される要素が多く、簡単に決めることはできません。条件としては「ぱっと見 た時に分かりやすいか」「読み間違う可能性はないか」「修正した時に間違う可 能性はないか」などが重要です。この例の場合は、行数が多いと全体を把握しに くいという説と、一行に多くを書き過ぎると分かりにくくなるという説のトレー ドオフになると思われます。  個人的には/* 2 */の方がよいと思います。/* 1 */がよいという人もいるよう です。
※ c.l.c FAQ : comp.lang.c FAQ list

http://www.eskimo.com/~scs/C-faq.top.html
文中の項目番号は新しい版に対応しています。旧版とは異なります。

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