[メモ]C言語配列の本体


わたしはずっと勘違いしていました。

int array[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, };

このように配列 array を定義1したとき、その初期値を指定する代入記号 = の右にある { 0, 1, ..., 9, } もまた「配列」なのだと。

違いました。

配列の定義と初期化リスト

C 言語文法上、 int array[] = { ... } の右辺のブレースで囲まれた部分2は「初期化リスト」というのだそう3です。

ですので、配列はあくまで宣言部分に(のみ)存在するのです(つまり int array[...])。
言い方を変えると代入演算子の右辺に配列は存在しない4

これは、以下のコード断片がコンパイルエラーになることからも確認できます。

int a[3] = { 0, 1, 2, };
int b[3] = a;
error: array initializer must be an initializer list or wide string literal
  int b[3] = a;
      ^

また配列は左辺値になれない。(代入が許可されていない)

int a[3] = { 0, 1, 2, };
int b[3];
b = a;

このコードをコンパイルしようとすると、以下のようにエラーになります。

error: array type 'int [3]' is not assignable
  b = a;
  ~ ^

無理やりの代入操作

脱線ですが、配列のサイズがわかっている前提で(無理やり)配列の値をコピーすることはできます。

struct array_copy {
  int xs[3];
};

int a[3] = {0, 1, 2,};
int b[3];

*(struct array_copy*)b = *(struct array_copy*)a;

配列はアドレスでもありポインターとも見なせるので、配列を内包する構造体にキャストし、構造体の代入を利用して中身を移すというバッドハックです5

複合リテラル

なお、 C99 には「複合リテラル(compound literal)」があります(初期化リストに型を前置した見た目をもつ)。

複合リテラルによって「無名」の配列を「その場」で定義できます。

memcpy(b, (int[3]){ 2, 1, 0, }, sizeof(b));

配列の初期化と(配列)複合リテラル

先の配列の初期化では、(コンパイルエラーが出て)配列は配列で初期化できませんでした。

しかし gcc 4.2.1 (Apple LLVM version 9.1.0 (clang-902.0.39.2)) では、以下のコードを警告なしにコンパイルすることが可能です。

int a[] = (int[3]) { 4, 5, 6, };

複合リテラルによる配列の定義は合法のようです3

(配列)複合リテラル要素への代入

実用で何の意味があるのかわかりませんが、こんなことができます6

(int[]){ 0, 1, 2, 3, 4, }[2] = 8;

文字列リテラルに対してこれを実行すると(つまり "hello"[0] = 'H'; のようなこと)不思議なことがおきます(おそらく未定義動作を引き起こします3)。

複合リテラルのアドレス

複合リテラルは、その場にオブジェクトとしての実体を確保するようです3

つまり以下をコンパイルして実行すると Woo-foo をプリントします7

int *pa = (int[3]){ 0, 1, 2, };
int *pb = (int[3]){ 0, 1, 2, };
if (pa != pb) puts("Woo-foo!");

一方、おなじ翻訳単位に存在する文字列リテラルは同一のアドレスを持ちます。

const char *ps = "hello";
if (ps != "hello") puts("Woo-foo!");

これを翻訳・実行しても何も印字されません8


  1. defintion。 int array[10]; のように書くのは宣言(declaration)で、その値まで含めて書くのが定義(definition)。ただし Wikipedia によると definition と declaration は C++ において区別されていない(というより同じもの9な)のだとか。 

  2. オブジェクトと言いたいところですが、変数の初期化に使われる定義は、たぶんオブジェクトではないとおもいます。規格書が参照できれば……。 

  3. ほんとうはここで「C99 規格書によれば」と書きたいところですが、持っていないので伝聞の形にしています。すみません。 

  4. ちょっと待て、じゃあ定義における = はなんだよ? という声が聞こえてきそう。これまた規格書の文法を参照できなくて申し訳ないのだけれど、定義での代入記号は初期値の「定義」に過ぎず、代入ではないのだとおもいます。 

  5. ただし実用性は低い。おなじサイズの配列を使いまわすことがわかっているなら、あらかじめ構造体として「型」を与えておくに越したことはない。 

  6. ところで代入文は、その代入操作を実行した結果(評価結果)を「値」として持ちます。配列複合リテラルの要素への値の代入文は、その要素へ代入した値を評価結果として持つため、ますます実用上の意味がわからなくなります。つまり複合リテラルは「無名」のため、要素への代入の結果、要素を格納する配列(複合リテラル)そのものにアクセスする方法がなくなってしまう。 

  7. (const int[]){0} == (const int[]){0} も偽になりました。文字列リテラルは不変オブジェクト(型 const char[] と互換)で、同一翻訳単位にある同じ値を持つ文字列リテラルは同じアドレスを持つこと("hello" == "hello" は真に評価される)から、変更不能な配列であるとヒントを与えたら同じアドレスを参照するようになるかもしれないとおもったのですが。(処理系や最適化レベルによっては同一になる可能性があります) 

  8. C 言語において文字列リテラルの操作は、(その他の組み込み型と違って)string 関数を介することになっているので、アドレス比較によるオブジェクト同一検査がどの程度意味があるのかは、この際聞かないでください。 

  9. Many find it convenient to draw a distinction between the terms "declaration" and "definition", as in the commonly seen phrase "the distinction between a declaration and definition...", implying that a declaration merely designates a data object (or function). In fact, according to the C++ standard, a definition is a declaration. Still, the usage "declarations and definitions", although formally incorrect, is common. 10 

  10. (私訳)「宣言」と「定義」を区別すると便利だと多くの人がおもっています。これは「宣言と定義との違いは〜」というありふれた言い回しにも見ることができ、宣言は単なるデータオブジェクト(あるいは関数)の指定だと言われています。ところで C++ 規格書によれば、定義は宣言と同じものです。けれど、形式的には不正確と言っても、「宣言と定義」という言い回しは人口に膾炙しているのです。