プログラミング系 Q&A 2


Index
  1. C で、ポインタに数値の 0 を代入しても、コンパイルエラーにならないが?
  2. fclose が正常に終了しないことはあるのか?
  3. ソケットを使って送信したサイズと受信したサイズが異なる時がある。
  4. malloc() したメモリが free() で OS に戻らないそうだが本当か?
  5. char* ss[80]; と char (*ss)[80]; とは違いがあるのか?
  6. sizeof 演算子の返す値の単位は何か?
  7. ANSI C では「1 バイト = 8 ビット」には決まっていないって本当か?
  8. ANSI C では、1 バイトが 8 ビットでない場合、charって何バイトで何ビット?
  9. exit() すると segmentation fault になる。
  10. char a[4] = "ABCD"; という初期化は、'\0' も含めて 5 文字分必要では?
  11. printf() に float の引数を渡すと double に変換されるというのは仕様か?
  12. C で、外部変数定義 int data; を二重に行ってもエラーにならない。
  13. C で、複数のソースファイルに仮定義が複数ある場合、リンク時にどうなるのか?
  14. バスエラーとセグメントエラーの違いは?
  15. 何故 printf("%d",a) ではカンマなのに for(i=1;i<10;i++) ではセミコロン?
  16. 浮動小数点処理の丸め込みについて
  17. connect 関数が失敗するまでの時間を短くする方法はないか?
  18. cal.c (カレンダプログラム) で、1752年前後で閏年の計算方法が違う。

1. C で、ポインタに数値の 0 を代入しても、コンパイルエラーにならないが?

そもそもポインタに 0 を代入する行為は、そのポインタを「ヌルポインタ」にする書式である。コンパイルエラーにならないのは、多くの C の処理系の仕様であり、定数 0(ゼロ) に関しては、あらゆるポインター型の変数にエラーなく代入することが許されている。

詳しくは FAQ の NULL に関する項 http://www.catnet.ne.jp/kouno/c_faq/c5.html#0 参照。


2. fclose が正常に終了しないことはあるのか?

実装にもよるが、fputs などで書込み操作をした後、バッファにデータが溜まっているまま fclose をすると、バッファ内容を掃き出してからファイルを閉じる。その際、ライトエラーが生じると、正常終了とはならない。

例えば、Microsoft Visual C++ 5.0 で次のようなプログラムを Win32 コンソールアプリケーションとしてビルドし、空きが 512 バイトのフロッピーディスクをカレントドライブとして実行すると, fclose() でエラーになる。

#include <stdio.h>

#define SIZE  (512 + 512)

main()
{
    int     i, ret;
    FILE    *fp;
    char    buffer[SIZE];

    if ((fp = fopen("./output.fil", "wb")) == NULL) {
        perror("fopen() returns NULL.\n");
        return 1;
    }
    for (i = 0; i < sizeof(buffer); ++i) {
        fputc(buffer[i], fp);
        if (ferror(fp)) {
            perror("ferror() returns TRUE.\n");
            fclose(fp);
            return 2;
        }
    }
    if ((ret = fclose(fp)) < 0) {
        fprintf(stderr, "fclose() returns %d.\n", ret);
        perror("");
        return 3;
    }
    return 0;
}

実行結果
fclose() returns -1.
No space left on device


3. ソケットを使って送信したサイズと受信したサイズが異なる時がある。

TCP socket ではデータは単にバイトストリームとして扱われるので、例えば送り側が 2048 Bytes を 1 回の send() で送ったからと言って、受信側が 1 回の recv() で受け取れる保証はない。

転送の都合で send したデータが適宜切り分けられ、複数のパケットで送られる場合がある。そして recv() は「読め」と言われたバイト数が溜るまで待たず、少しでもデータが送られてくれば return する。相手が何バイト送ったかなどをチェックするようにはなっていないということである。

一般的には、これから送るデータのサイズを通知して、受け手の方でこのサイズを受け取るまで繰り返して recv() するという方法がとられるだろう。また、fdopen(sock) しておいて、fread() を使うという手もある。


4. malloc()したメモリが free() で OS に戻らないそうだが本当か?

free() しても OS にメモリは戻さないような実装は有り得る。ただし、free() したメモリを次に malloc() で再利用できない、という意味ではない。

malloc()/free() の内部処理は、もちろん実装によるが、おおむね以下のようになっている。

  1. アプリケーションが最初に malloc() を呼ぶ。
  2. malloc() は OS からメモリを取得し、それをアプリケーションに渡す。
  3. アプリケーションが free() を呼ぶ。
  4. free()は、「このメモリは未使用」という印をつけるが、そのメモリを OS に返さず、以降の malloc() のためにとっておく。
  5. アプリケーションがまた malloc() を呼ぶ。
  6. malloc() は、以前 free() した空きメモリを再利用しようと努力する。要求されたメモリ量が多かったり、断片化のために再利用できない場合は、OS から追加のメモリを取得する。

例えば、MS-DOS の MS-C 等は、上記の方法で行っている。free() で OS にメモリを戻さない理由としては、

  1. free() された領域は断片化されていて、必ずしも連続した領域を OS に返却できるわけではない。断片化された領域を連続した領域に戻す一般的な方法も存在しない。
  2. free() された領域はふたたび malloc() される可能性が高い。逆に、大きな領域を malloc()/free() するようなアプリケーションでは、free() したあとですぐにプロセス自体が終了する可能性が高い。
  3. MS-DOS ではアプリケーションが子プロセスを生むことがあまりないので、未使用のメモリーを OS に返却しても、その労力があまり報われない。
  4. 上記を総合して、あるプロセスにはそのプロセスが使用する最大のメモリーを割り当てておき、それ以上細かい管理をしない、という方針が考えられる。これはそれほど悪いものではない。

といったことが考えられる。


5. char* ss[80]; と char (*ss)[80]; とは違いがあるのか?

全然、違う。

char* ss[80]; では、ss[i] (i=0〜79)というのは、それぞれが char* 型である。つまりポインタ型なので、文字へのポインタ (一番多いのは文字列の先頭文字へのポインタ) を格納できる。文字列そのものはどこか別の場所に確保ずみで、その先頭番地が格納される。また ss というのは配列の名前であり、ss = ○; のように値を入れることはできない。

一方、char (*ss)[80] というのは、ss はポインタであり、(*ss)、つまりポインタをだとった先にあるものは char[80]、つまり 80 文字ぶんの文字が入る配列がある。上と違って、ss は配列ではなくポインタである。ということは、ss は配列の先頭番地を指すこともできるが、ただしその配列は char[80] つまり「80文字ぶんの文字を格納できる領域」が並んだ配列だということになる。

例えば char* ss[80] の場合、

  char* ss[80] = {"abcdef", "ghijk", "lmn", ..〜80個〜.. "xyz"};

のときには

  ss
┌───┬───┬───┬∫──┬───┐
│ ss[0]│ ss[1]│ ss[2]│      │ss[79]│
│      │      │      │      │      │
└─┼─┴─┼─┴─┼─┴∫──┴─┼─┘
    │      │      │              │
    │      │      ↓              ↓
    │      ↓      "lmnop"        "xyz"
    ↓      "ghijk"
    "abcdef"

となり、char (*ss)[80]の場合、

  char data[80]="abcdefghijklmnopqrstuvwxyz... 〜79文字〜...XYZ";
  ss = &data;

というときには

  ss
 ┌───┐
 │  │
 │  │
 └─┼─┘
     │
 data↓
┌────┬────┬────┬∫───┬─────┬─────┐
│(*ss)[0]│(*ss)[1]│(*ss)[2]│        │ (*ss)[78]│ (*ss)[79]│
│  'a'   │  'b'   │  'c'   │        │   'Z'    │   '\0'   │
└────┴────┴────┴∫───┴─────┴─────┘

となる。


6. sizeof 演算子の返す値の単位は何か?

sizeof の返し値の単位は 1 byte。

ISO 9899;1990 の 6.3.3.4 The sizeof operator の Semantics の項に、

    The sizeof operator yields the size (in bytes) of its operand.

とある。また、「プログラミング言語C第2版」の A7.4.8 では 「バイト数を求める」となっている。


7. ANSI Cでは「1 バイト = 8 ビット」には決まっていないって本当か?

ISO 9899:1990 の 3.4 で byte が定義されている。

3.4 byte: The unit of data strage large enough to hold any member of the basic character set of the execution character set.  It shall be possible to express the address of each individual byte of an object uniquely.  A byte is composed of a contiguous sequence of bits, the number of which is implementation-defined. (以下略)

つまり、ANSI C における 1 バイトは何 bit になるかは決まっていない。


8. ANSI Cでは、1 バイトが 8 ビットでない場合、char って何バイトで何ビット?

char は 1 バイトである。

char 型データも含め、 ANSI C 言語でのすべてのデータは、(C 言語でいうところの) object (定義は 3.14) である。そして、「object は byte の列である」の規定 (3.14) と、「sizeof(char) == 1 byte」の規定 (6.3.3.4) により、char 型のサイズは 1 バイトとせざるを得なくなる。
(章番号は、ISO C 規格書のもの)

何ビットかについては、ISO 9899;1990 に以下の様に書かれている。

5.2.4.2.1 Sizes of integral types <limits.h>

(前略) Their implementation-defined values shall be equal or greater in magnitude (absolute value) to those shown, with the same sign.

-- number of bits for smallest object that is not a bit-field (byte)
  CHAR_BIT       8
(中略)
-- minimum value for an object of type int
  INT_MIN        -32767
-- maximum value for an object of type int
  INT_MAX        +32767
-- maximum value for an object of type unsigned int
  UINT_MAX       65535

上記から、char 型オブジェクトの大きさは 8 bit 以上、int 型オブジェクトの大きさは 16 bit 以上となると考えてよい。

従って、「char 型のサイズは 1バイトだが、1 バイトが何 bit になるかが決まっていない (8 ビット以上)」というのが正しい。


9. exit() すると segmentation fault になる。

システムライブラリで使われる領域を壊している可能性が高い。

例えば、FreeBSD 2.1.0R の exit() は、(void (*)()) __DTOR_LIST__[2] という配列を参照する。 もしこの領域をプログラムの方で無効なアドレスになるような値 (0 など) を代入して壊していれば、プログラムを終了するときに segmentation fault を起こしてしまう。

あるいは、exit() を行なうことで fopen() 状態にある stdout を fclose() し、この時 FILE 構造体にバッファリングされている値を吐き出して close() するが、FILE 構造体が抱えるバッファ領域を指し示すポインタが、とんでもない所を指していたりすると segmentation fault が起きる。

ポインタ操作をしている箇所や、配列の範囲外アクセス等を疑ってチェックしてみるとよい。


10. char a[4] = "ABCD"; という初期化は、'\0' も含めて 5 文字分必要では?

ANSI C なら必要ない。

要素数が指定されている char 型配列の初期値として "ABCD" が使われた場合は、

  { 'A', 'B', 'C', 'D' }

の省略記法とみなされる。従って「すでに 5 バイトある」ということにはならない。また、初期値の個数が配列の要素数より多い場合はエラーなので、他の変数を破壊するという心配は無用である。


11. printf() に float の引数を渡すと double に変換されるというのは仕様か?

ANSI/ISO の仕様で、整数昇格と浮動小数点数昇格のルールである。

このルールと、C 言語ではプロトタイプ宣言が (必ずしも) 必須ではないというルールのため、C 標準ライブラリ関数には、size_t 型など例外は除いて char, short, float 型を引数、あるいは返り値にとるものが存在しない。getchar() 関数が int 型を返す理由も実はここにあると言える。(EOF を返すためというのは教訓話だと思われる。初心者向けの FAQ に対する答えとしては、EOF を返すためで良いだろう)。


12. C で、外部変数定義 int data; を二重に行ってもエラーにならない。

C では、int data; は「宣言」であり「仮定義」と呼ばれていて、同じファイルにいくつあっても構わない。よって、エラーにならないのは「正しい」といえる。

どこにも定義がない場合、0 に等しい初期化子を持った宣言、すなわち int data = 0; という定義があるものとして動作する。これらについては、規格書の「6.7.2 外部オブジェクト定義」、または K&R2 の「A10.2 External Declarations」を参照すると書いてある。

ちなみに、C++ の場合は、int data; は「定義」なので、2 つ以上あってはいけない。


13. C で、複数のソースファイルに仮定義がある場合、リンク時にどうなるのか?

規約は、

仮定義 (tentative definition) は、1 つのコンパイル単位 (a translation unit) に複数あっても良い。それはコンパイル単位の終わりで 1 つにまとめられて、 0 の初期化子 (initializer) を持ったものと同様に扱われる

と述べているだけで、複数のコンパイル単位 (つまり別のソースファイル) に仮定義が複数ある時に、リンク時に 1 つにまとめられることまでは規格化していない。つまり、“未定義動作”である。規格では、未定義動作について何も定めておらず、処理系がエラーにするのも、何も言わずに実行するのも、あるいは特別な意味を持たせるのも認めている。


14. バスエラーとセグメントエラーの違いは?

「バス」というのは計算機の各構成装置がデータをやりとりするときの通信路で、そこにうまく乗れないようなデータがあるとバスエラーになる。

たとえば最近の CPU (中央処理装置) では CPU がメモリーと値をやりとりするときに、その値が占める大きさのブロック単位の区切り位置でしかやりとりできないものが少なくない。つまり、CPU がメモリーから 4 バイトの大きさのデータを読み込もうとすると、4 で割り切れるバイトアドレス (メモリー上の番地) からしか読み込めない。そのような制限があるとき、4 で割り切れないアドレスを指定してメモリーを読みにいくとバスエラーが起きる。

「セグメント」というのは、あるプロセス (簡単に言ってしまえば実行中のプログラム) に割り当てられたメモリー領域 (アドレスの範囲) で、その領域の外のメモリーを読もうとするとセグメントエラーになる。そのような割り当ての管理は計算機の制御装置が行っている。

たとえば int * 型の外部変数 ip に、4 で割り切れない char へのアドレスがキャストして格納されている場合、次のような参照でバスエラーが出る。

  printf("%d", *ip);

一方、セグメントエラーは NULL への参照で発生することが多い。上の例なら ip に NULL が入っている状態で実行すればセグメントエラーになる。


15. 何故 printf("%d",a) ではカンマなのに for(i=1;i<10;i++) ではセミコロン?

何故 printf("%d", a) ではセミコロンではなく、カンマなのかというと、ほとんど理由は無い。(英語の影響により) 幾つかのものを並べる時にはカンマを使う方が自然なのだろう。for (i=1; i<10; i++) でセミコロンを使うのは、「カンマ演算子を使うため」だと言える。

因みに「引数区切りのカンマ」と「カンマ演算子」とは異なり、また「for のセパレータとしてのセミコロン」と「文末記号としてのセミコロン」も異なるものである。

引数区切りとしてのカンマ:
  printf("%d", a);

カンマ演算子としてのカンマ:
  for (i=0, j=0, flag=false; i<10 && !flag; ++i, ++j)

for のセパレータとしてのセミコロン:
  for (i=1; i<10; i++)

文末記号としてのセミコロン:
  printf("%d", a);

たまたま、C 言語を設計した人は、

  引数区切りの記号を     カンマ(,)     で
  カンマ演算子の記号を   カンマ(,)     で
  for のセパレータ記号を セミコロン(;) で
  文末記号を             セミコロン(;) で

表わすことにしただけである。実際、引数区切りの記号がパーセント記号 (%) で、for のセパレータ記号がコロン (:) であったとしても特に問題はない。


16. 浮動小数点処理の丸め込みについて
Q: 以下のコードが終了するのは何故か ?

float foo(float x) { return x; }

int main()
{
    float x;

    x = 2;
    while (foo(x + 1) - x == 1)
        x *= 2;
    return 0;
}

float の内部表現は、IEEE 規格や、DEC の VAX、IBM のメインフレームなどで異なっているが、ワークステーションや PC で一般的な IEEE 規格で説明すると、float は、符号 1ビット、指数 8ビット、仮数 23 ビットの合計 32ビット。仮数は、常に先頭が 1 になるように正規化され、その 1 を省略した残りを 23 ビットに納める。従って、精度は 24ビット。

                     <------- 23ビット ----->
2 の 23乗は 2進で、1.000000000000000000000000  指数 23。
それに 1 を足すと、1.000000000000000000000001  指数 23。
2 の 24乗は 2進で、1.000000000000000000000000  指数 24。
それに 1 を足すと、1.0000000000000000000000001 指数 24。

(2 の 24乗) は float で表現できるが、(2 の 24乗 + 1) を表現するには仮数部が 24 ビット必要である。そのため、(2 の 24乗 + 1) は丸められて (2 の 24乗) と同じになる。よって、(2 の 24乗 + 1) - (2 の 24乗) == 0 となり、1 にならず、while ループが終了する。


17. connect 関数が失敗するまでの時間を短くする方法はないか?

幾つかの方法が考えられる。

  1. connect の前に alarm を仕掛けて SIGALARM を拾う。

  2. socket で生成した descpiptor を NON BLOCKING にして、connect する。すると -1 を返してくるので、そのあとタイムアウト指定をして select で待つ。

18. cal.c (カレンダプログラム) で、1752年前後で閏年の計算方法が違う。

閏年の計算方法として馴染みのある方法は、

であるが、これはグレゴリウス暦 (Gregorian Calendar) である。

グレゴリウス暦が法王グレゴリウス 13 世によって制定されたのは 1582年で、それ以前のヨーロッパではシーザーが制定したユリウス暦 (Jullian Calendar: 閏年は 4 年に 1 回) が使われていた。

グレゴリオ暦を採用した年は、フランス、イタリア、ポルトガル、スペイン(1582年)、ドイツ(1583年)、スウェーデン(1753年)、プロシア(1774年)、ロシア(1918年) 等、国によってまちまちである。そして、1752年とは、イギリスがグレゴリオ暦を採用した年。

恐らく、cal コマンドはイギリス系の人が作ったのだろう。




Index