わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

配列か否かを見極めるにはsizeof

1990年代のCプログラミング - わさっきの脚注とコメントから派生する話題を一つ.
Cでは,実行時に変数が何型なのかを教えてくれるような機能が提供されていません.
しかしながら,sizeof演算子を使えば,変数が配列か否かを識別することができます.
そこで,配列に要素数を記述しない「不完全型の配列」について,どんな値になるかを検証するコードを書いてみました.
まずは,関数の仮引数に,不完全型の配列を記述した場合.よく使いますものね.

/* incomplete1.c */
#include <stdio.h>

void x3(double x[])
{
  int i;

  for (i = 0; i < 3; i++) {
    printf("x[%d] = %f\n", i, x[i]);
  }

  printf("sizeof(x) = %d\n", sizeof(x));
}

int main(void)
{
  double x[] = {1.0, 3.0, 5.0};

  x3(x);

  return 0;
}

2箇所,「double x[]」という変数宣言があります.このうち,x3関数の仮引数のほうが,不完全型です.一方,mainの中のは,初期化の値で要素数が決まるので,こちらは不完全型ではなく通常の配列変数です.
さて実行結果ですが

x[0] = 1.000000
x[1] = 3.000000
x[2] = 5.000000
sizeof(x) = 4

となりました.mainのxを「double x[3]」にしても「double x[300]」にしても,結果は変わりません.mainのxを「double x[]」に戻して,x3の仮引数のほうを「double x[3]」にしても「double x[300]」にしても,やはり結果は変わりません.
以上において,gccコンパイルして「-Wall -ansi -pedantic」というオプションをつけましたが,警告は一つも出ませんでした.
あらゆるポインタ型に対するsizeofの値も4だったので,最終的にin conclusion,関数x3の仮引数のxは,ポインタ型と推定できます.

次に,異なる(ただし実用度の低い)不完全型の配列の例を.

/* incomplete2.c */
#include <stdio.h>

double x[] = {1.0, 3.0, 5.0};

int main(void)
{
  extern double x[];
  int i;

  for (i = 0; i < 3; i++) {
    printf("x[%d] = %f\n", i, x[i]);
  }

  printf("sizeof(x) = %d\n", sizeof(x));

  return 0;
}

これをコンパイルすると,残念ながらエラーです*1

incomplete2.c: In function `main':
incomplete2.c:15: error: invalid application of `sizeof' to incomplete type `({anonymous})'

プログラムを見直しましょう.「extern double x;」は不完全型で,実体としてはグローバルに宣言している配列変数xと同一視されます.しかしコンパイル時には,「不完全型のdouble配列」であり,その要素数は依然として不明です.
心理的に「double [3]」型になってほしいのですが……ソースが分割されていて,配列変数xの実体は別のソースファイルで定義されており,要素数はリンク時にならないと分からない,というようなケースも考えられます.
ということで,main関数の中の外部変数xの要素数は,(狭義の)コンパイル時には分からず,そんな不完全型の配列変数に対してsizeofできない,というのはまあ妥当なルールだと思います.
ここで,「extern double x
;」を削除すると,問題なくコンパイルでき,結果は以下のようになりました.

x[0] = 1.000000
x[1] = 3.000000
x[2] = 5.000000
sizeof(x) = 24

さらにグローバル変数のxに要素数を明記すれば,出てくるsizeof xの値は,その数に比例するものとなります.ここから,main(に限りませんが)の中で,グローバルに宣言された配列変数を参照するときは,それは実際に配列であると言えます.
ここで「extern double x
;」を復活させまして,もし要素数を明記したらどうなるか,やってみました.
「extern double x[3];」と書いたら,「sizeof(x) = 24」です.
「extern double x[5];」と書いたら,「sizeof(x) = 40」です.
「extern double x[2];」と書いたら,「sizeof(x) = 16」です.
sizeofがそうなるのは理解できるけど…これ,コンパイルが通っていいの!? 結合時にエラーがでないの? ここでも-Wall -ansi -pedanticのオプションをつけてコンパイルしてみましたが,いずれもエラー・警告なしでした*2

*1:Cygwin環境で,コンパイラは「gcc (GCC) 3.4.4 (cygming special) (gdc 0.12, using dmd 0.125)」を使って検証しました.Debian環境のコンパイラ,具体的には「gcc (GCC) 4.1.2 20061115 (prerelease) (Debian 4.1.1-21)」で試すと劇的に結果が異なります.手短に報告すると,「extern double x[ ];」なら問題なし,「extern double x[3];」もOK,「extern double x[2];」や「「extern double x[5];」はコンパイルエラー.最後に,mainの中は「extern double x[ ];」に戻し,「double x[ ] = {1.0, 3.0, 5.0};」を別ファイルにして,別々にコンパイルしたところ,これもエラーになりました.

*2:この挙動については,Debian環境のコンパイル結果のほうが,直感に合います.