6章 配列とポインター



6.1:
あるソースファイルでchar a[6]と定義して、別のファイルでextern char *aと宣言した、なぜこれはうまくいかないのか。


A:
extern char *aという宣言が、実際の定義と食い違うからである。 「タイプTへのポインター」は「タイプTの配列」とは異なる。extern char a[]を使え。

References:
ANSI Sec. 3.5.4.2; ISO Sec. 6.5.4.2; CT&P Sec. 3.3 pp. 33-4, Sec. 4.5 pp. 64-5.


6.2:
でもchar a[]はchar *aと同じと聞いたことがあるが。


A:
全然別のものである(君が聞いた話というのは、関数の仮引数の話だ。 質問2.4参照)。配列はポインターと違う。配列の宣言「char a[6]」 は6文字分の領域を確保して、それを「a」という名前で識別すること を要求する。すなわち「a」という名前の場所があって、そこには6文 字を収めることができる。一方、ポインターの宣言「char *p」はポ インターを収める場所を要求する。ポインターはpという名前で識別 され、ほとんどどんなものでも指すことができる。どんなchar、ある いは連続したcharの列を指すこともできるし、どこも指さなくても構 わない(質問5.11.30参照)。

例によって、百聞は一見に如かずである。文

char a[] = "hello"; char *p = "world";

は以下のように表現できるデータ構造を初期化する。

                   +---+---+---+---+---+---+
                a: | h | e | l | l | o |\0 |
                   +---+---+---+---+---+---+
                   +-----+     +---+---+---+---+---+---+
                p: |  *======> | w | o | r | l | d |\0 |
                   +-----+     +---+---+---+---+---+---+

x[3]を参照したときに産み出されるコードが、xがポインターか配列 かで違うのだと理解することは大事なことである。上記の宣言を与え られたとして、コンパイラはa[3]という式を見たところで、「a」の ところから始めて、そこから3つ進んで、そこにある文字を取り出す、 というコードをはきだす。p[3]という式を見ると、「p」に進み、そ こに存在するポインターの値を取り出し、ポインターの値に3を加え、 最後にポインターが指す場所から文字を取り出す、というコードをは きだす。言い換えればa[j]はaという名前のオブジェクト(の先頭)か ら3つ目の場所で、p[3]はpが指すオブジェクトから3つ目の場所であ る。上の例ではa[3]、p[3]はたまたま同じ文字'l'を指すがコンパイ ラはそこにたどり着くのに別の道をたどるのである。

References:
K&R2 Sec. 5.5 p. 104; CT&P Sec. 4.5 pp. 64-5.


6.3:
Cで"ポインターと配列は同等"というのは何を意味しているのか。


A:
Cにおけるポインターに関する混乱の多くは、上の文の誤解から来て る可能性がある。配列とポインターが"同等"といっても、この二つが まったく同じとか交換可能であるということは意味していない。

"同等"というのは、この問題を解く手掛かりになる以下の定義を指し ている。

式中に現われる型「Tの配列」という左辺値(3つの 例外を除いて)、配列の最初の要素を指すポインター に意味が格下げになる。結果としてできるポインター の型は「Tへのポインター」となる。

(3つの例外とは、1)配列がsizeofの引数となるとき、2)アドレス演算 子&の引数となるとき、3)char型の配列を文字列リテラルで初期値す るとき)。

この定義により、演算子[]を配列に使っても、ポインターに使っても たいして違いはない。 a[i]と書いたとき、上の規則により配列の参 照「a」はポインターへと成り下がる。これでポインター変数に対し てp[i]と書くのと同じことになってしまった(ただし質問6.2で説明し たように、メモリのアクセスのしかたは異なるかもしれない)。配列 のアドレスをポインターに代入することになったら、

p = a;

p[3]とa[3]は同じ要素を指す。

質問6.8も参照のこと。

References:
K&R1 Sec. 5.3 pp. 93-6; K&R2 Sec. 5.3 p. 99; ANSI Sec. 3.2.2.1, Sec. 3.3.2.1, Sec. 3.3.6; ISO Sec. 6.2.2.1, Sec. 6.3.2.1, Sec. 6.3.6; H&S Sec. 5.4.1 p. 124.


6.4:
それではなぜ関数の仮引数では配列とポインターの宣言が交換できる のか。


A:
そのほうが便利だからだと考えられているからである。

配列はすぐにポインターに成り下がるので、配列が実際に関数に渡る ことはない。ポインター引数を配列のように見せて宣言することを許 すのは、配列が渡されたように見せるためである。プログラマーは仮 引数が昔から配列のように扱われるとか、配列が(厳密に言えば配列 のアドレスだが)昔から渡されるということを強調したいのかもしれ ない。コンパイラに都合がいいように、配列のように"見える"仮引数 宣言は、以下に例を挙げるように

f(a) char a[]; { ... }

すべてコンパイラによってポインターが渡されたかのように扱われる。 なぜなら配列が渡されたときに、関数が実際に受け取るのはポインター であるからである。

f(a) char *a; { ... }

この変換は、関数の仮引数の仮引数宣言でのみ有効である。その他の 場所では起こらない。この変換が気に食わないなら使わなければよい。 関数の呼び出しや関数内での使いかたに宣言を似せることによる小さ な利点よりも、それが引き起こす混乱のほうが大きいと考える人は多 い。

質問6.21も参照のこと。

References:
K&R1 Sec. 5.3 p. 95, Sec. A10.1 p. 205; K&R2 Sec. 5.3 p. 100, Sec. A8.6.3 p. 218, Sec. A10.1 p. 226; ANSI Sec. 3.5.4.3, Sec. 3.7.1, Sec. 3.9.6; ISO Sec. 6.5.4.3, Sec. 6.7.1, Sec. 6.9.6; H&S Sec. 9.3 p. 271; CT&P Sec. 3.3 pp. 33-4.


6.7:
どうして配列は代入できないのに左辺値(lvalue)なのか。


A:
ANSI C規格は「変更可能な左辺値(modifiable lvalue)」を定義して いる。その中に配列は入っていない。

References:
ANSI Sec. 3.2.2.1 p. 37.


6.8:
実際のところ、配列とポインターの違いは。


A:
配列は自動的に領域を割り当てる。ただし別の場所に移したり大きさ を変えることはできない。ポインターには、確保した割り付けられた 領域(たぶんmalloc()を使って割り付けた領域)を指すアドレスを明示 的に代入しなければならない。そのかわり好きなように値を変えるこ とができる(すなわち別のものを指すことができる)。またメモリブロッ クを指す以外にも色々と使い道がある。

俗にいう配列とポインターが同等(質問6.3参照)により、配列とポイ ンターが同じに見える場合が多い。特にmalloc()により確保した領域 を指すポインターが、しばしば本物の配列のように(実際[]を使って 参照することができる)扱われる。質問6.146.16を参照のこと(ただ しsizeofには要注意)。

質問1.3220.14を参照。


6.9:
配列とは定数ポインターにすぎないと説明してくれた人がいた。


A:
それはちょっとものごとを単純に考えすぎである。配列の名前は、名 前には代入できないという点で"定数"であるが、質問6.2の議論と図 を見れば配列がポインターでないことははっきりするだろう。質問 6.36.8を参照。


6.11:
5["abcdef"]という式を含んだジョークのコードを見たことがある。 どうしてこの式がC言語で文法上正しいことになるのか。


A:
配列の添字演算子[]の二つのオペランドは交換可能で、Y[X]と書いて もX[Y]と書いても同じ意味になるというのをご存知ない? この奇妙 な事実は、配列の添字付けのポインターの定義からきている。すなわ ち、どちらかがポインターを表す式で、残りが整数である限り、どん なaとeをもってきても、a[e]は*((a)+(e))と同じものであるという定 義である。このとんでもない交換可能性は、よくC言語について扱う 文章の中で、誇らしく思うかのように記述されているが国際難解Cプ ログラムコンテスト以外では役に立たない(質問20.36参照)。

References:
Rationale Sec. 3.3.2.1; H&S Sec. 5.4.1 p. 124, Sec. 7.4.1 pp. 186-7.


6.12:
配列を参照することはポインターに成り下がることを考えれば、arr という配列があったとしてarrと&arrの違いは。


A:
データ型。

ANSI/ISO Cの元では&は「Tの配列へのポインター」を産み出す。これ は配列全体を指す。(ANCI Cが誕生する前は、arrの前に&を付けるこ とは、たいてい警告を招き、たいてい無視された。) すべてのCコン パイラで配列への(キャストのない)参照はポインターを産み出す。こ のポインターはTへのポインターで配列の最初の要素を指す((質問 6.3, 6.13, 6.18も参照のこと)

References:
ANSI Sec. 3.2.2.1, Sec. 3.3.3.2; ISO Sec. 6.2.2.1, Sec. 6.3.3.2; Rationale Sec. 3.3.3.2; H&S Sec. 7.5.6 p. 198.


6.13:
配列へのポインターをどうやって宣言するのか。


A:
たいていは、そんなポインターを宣言したいのではない。なにげなく 配列へのポインターというときは、たいてい配列の最初の要素へのポ インターのことをいっているのである。

配列へのポインターではなく、配列の要素へのポインターを使うこと を考えること。型Tの配列は型Tへのポインターに成り下がる(質問6.3 を参照)。これは都合がよい。なぜなら結果としてできるポインター を使って添字つきで参照したり、整数を加えることで配列の各要素に アクセスしたりできる。これに対して、本当の配列へのポインターは、 添字つきで参照したり整数を加えると、配列全体を飛び越してしまう。 これではせいぜい配列の配列を扱うときにしか役立たない(上の質問 6.18を参照)。

本当に配列そのものへのポインターが必要な場合は「int (*ap)[N];」 のような表現を使う。ここでNは配列のサイズを表す(質問1.21も参照)。 配列の大きさがわからない場合、Nを省略することができる。しかし 結果として得られる「大きさが未知の配列へのポインター」は役に立 たない。

上の質問6.12も参照のこと。

References:
ANSI Sec. 3.2.2.1; ISO Sec. 6.2.2.1.


6.14:
どうすれば配列の大きさをコンパイル時に設定することができるか。 固定の大きさの配列を使わなくて済ますにはどうすればいいか。


A:
配列とポインターが同等(質問6.3を参照のこと)であることによって mallocしたメモリーを指すポインターを使って配列をかなり効果的に 真似ることができる。

#include <stdlib.h> int *dynarray = (int *)malloc(10 * sizeof(int));

を実行した後では(かつmalloc()の呼び出しが成功したら)、dynarray で、普通の静的に確保した配列(int a[10])のように扱って dynarray[i] (0から9までのiに対して)を参照することができる。質 問6.16も参照のこと。


6.15:
引数として渡された配列の大きさに合ったローカルな配列をどうやっ て宣言することができるか。


A:
Cでは不可能だ。配列の大きさはコンパイル時に定数でなければなら ない。(gccはパラメータ付き配列を拡張機能として提供している。) malloc()を使って、関数から返る前にfree()を必ず呼ばなければなら ない。質問6.14, 6.16, 6.19, 7.22を参照のこと。 質問7.32を参照 する必要があるかもしれない。

References:
ANSI Sec. 3.4, Sec. 3.5.4.2; ISO Sec. 6.4, Sec. 6.5.4.2.


6.16:
多次元の配列を動的に割り付けるのはどうしたらよいか


A:
たいていポインターの配列を割り付けて、それぞれのポインターを動 的に割り付けた"列"に初期化するのが一番の解決策である。以下に2 次元配列の例を挙げる。

#include <stdlib.h>

int **array1 = (int **)malloc(nrows * sizeof(int *)); for(i = 0; i < nrows; i++) array1[i] = (int *)malloc(ncolumns * sizeof(int));

(もちろん"本物の"コードではmallocの戻り値のチェックが必要であ る。)

配列の中身をメモリ上連続にすることもできる。ただしこうすると、 後で一つ一つの列を再割り付けするのが面倒になる。以下のような、 明示的な、ちょっとしたポインター計算が必要となる。

int **array2 = (int **)malloc(nrows * sizeof(int *));
array2[0] = (int *)malloc(nrows * ncolumns * sizeof(int));
for(i = 1; i < nrows; i++)
array2[i] = array2[0] + i * ncolumns;

どちらを選択しても、動的に割り付けた配列の各要素は普通の配列の ように添え字でアクセスできる。 (0 <= i <= NROWS かつ 0 <= j <= NCOLUMNSで) arrayx[i][j] というふうに。

もし上のような二重間接アクセス方法がなんらかの理由で受け入れら れないなら、一つの動的配置の1次元配列で2次元配列を真似ることが できる。

int *array3 = (int *)malloc(nrows * ncolumns * sizeof(int));

しかしこうすると添字の計算を自分で行わなければならない、すなわ ちi,j番目の要素へのアクセスは array3[i * ncolumns + j]としなけ ればならない(マクロを使えば明示的な計算を隠すことができる。し かしマクロを使えばカッコやコンマを使う必要があるので多次元配列 へのアクセスのようには見えなくなる。同じくマクロは少なくとも一 つの次元の大きさを知る必要がある。質問6.19も参照のこと)。

最後に配列へのポインターを使う方法を紹介する。

int (*array4)[NCOLUMNS] = (int (*)[NCOLUMNS])malloc(nrows * sizeof(*array4));

けれど、この構文は読む人に恐怖感を与えるし、コンパイル時に一つ の次元を除くすべての次元が確定していなければならない。

どの方法をとるにしても、必要がなくなったら配列を解放することを 憶えておかなければならない(それは、何段かの手続きを踏まなけれ ばならないかもしれない)。また配列を動的に割り付けて他の関数に 渡す際に、渡す先の関数が、静的に割り付けた普通の配列を引数とし て受けとる場合は特に注意が必要である(質問6.20参照のこと。また 質問6.18も参照のこと)。

上のどの技法も3次元以上の配列に拡張することが可能である。


6.17:
ほらこのトリック。下のように書けば

int realarray[10]; int *array = &realarray[-1];

"array"は1始まりの配列のように使える。


A:
このテクニックは魅力的だが(NUMERICAL RECIPES IN Cの古い版でも 使われている)、厳密にいえばC言語の規格に従っていない。ポインター 演算は、一度に割り振られた領域と、仮想的な"終端"を越えた1つめ の要素にだけ定義されていて、それ以外では未定義である。このこと は、たとえポインターを参照に使っていないとしてもあてはまる。上 のコードはオフセットを引いた時に、とんでもないアドレスを作り出 して(たぶんアドレスが"ぐるっと回って"メモリセグメントの先頭を 越えて他のセグメントと重なるからだろう)うまくいかなくなる可能 性がある。

References:
K&R2 Sec. 5.3 p. 100, Sec. 5.4 pp. 102-3, Sec. A7.7 pp. 205-6; ANSI Sec. 3.3.6; ISO Sec. 6.3.6; Rationale Sec. 3.2.2.3.


6.18:
私が使っているコンパイラは、ポインターへのポインターを使うべき ところで2次元配列を使うと不満をいう。


A:
配列がポインターに成り下がるという規則は(質問6.3参照)、再帰的 には成り立たない。配列の配列(例えばC言語における2次元配列)は、 配列へのポインターに成り下がるのであって、ポインターへのポイン ターに成り下がるわけではない。配列へのポインターは、混乱を招く から、注意して扱わなければならない。質問6.13も参照のこと。(混 乱は行儀の悪いコンパイラ、が多次元配列を多段のポインターとして 受け付けることで増長されている。そういうコンパイラとしてはpcc の古い幾つかのバージョンと、そういうpccから派生したlintが含ま れる)。

もし2次元配列を以下の関数に渡すと

int array[NROWS][NCOLUMNS]; f(array);

関数の宣言は以下のどちらかでないといけない。

f(int a[][NCOLUMNS]) { ... }

あるいは

f(int (*ap)[NCOLUMNS]) /* apは配列へのポインター */ { ... }

最初の宣言では、コンパイラが仮引数を「配列の配列」から「配列へ のポインター」へ通常通り暗黙の書き換えを行う(質問6.36.4を参 照)。2つ目の定義では、ポインターの定義であるとはっきり書いてあ る。呼ばれる側の関数は配列分の領域を取るわけではないので、配列 全体の大きさを知る必要はない。列の数「NROWS」は省略することが できる。配列の"形"は大事であるから行の数「NCOLUMS」を(3次元以 上の配列の場合は最初の次元をのぞくすべての次元の大きさを含めて) 指定しなければならない。

もしポインターへのポインターを受け付けると関数が宣言していると きには、直接2次元配列を渡すことはやっても意味がないだろう。

質問6.126.15も参照のこと。

References:
K&R1 Sec. 5.10 p. 110; K&R2 Sec. 5.9 p. 113; H&S Sec. 5.4.3 p. 126.


6.19:
コンパイル時に"幅"が未定の2次元配列を引数とする関数はどうやっ て書けばよいか。
A:
これは結構難しい。一つは[0][0]要素へのポインターを、2つの次元 のそれぞれの大きさと一緒に渡して配列の添字付けを"手で"真似る方 法がある。

f2(aryp, nrows, ncolumns)
int *aryp;
int nrows, ncolumns;
{ ... ary[i][j] は実際にはaryp[i * ncolumns + j] ... }

この関数は質問6.18の配列を使って以下のように呼ぶこともできる。

f2(&array[0][0], NROWS, NCOLUMNS);

しかしながら このような方法によって"手で"多次元の配列の添字付 けをするプログラムはANSI C規格に厳密には従っていないことに注意 すること。x >= NCOLUMNSの場合に(&array[0][0])[x]にアクセスした ときの動作は定義されていない。

gccを使えば、関数の引数を使ってローカルの配列がある大きさを持 つと宣言することができる。しかしこれは標準ではない拡張機能であ る。

様々な大きさの多次元配列を引数に取る関数を使いたいなら、質問 6.16のようにすべての配列を動的に真似るやりかたがある。

質問6.18, 6.20, 6.15も参照のこと。


6.20 関数の引数として配列を渡すときに、静的に確保された配列も動的に 確保された配列も受け付けるようにするにはどうしたらよいか。


A:
唯一の完全な解というのは存在しない。以下の宣言があって、


int array[NROWS][NCOLUMNS];
int **array1; /* ragged */
int **array2; /* contiguous */
int *array3; /* "flattened" */
int (*array4)[NCOLUMNS];
質問6.16と同じように初期化するとする。また以下のように宣言した 関数が存在するとする。

f1(int a[][NCOLUMNS], int nrows, int ncolumns);
f2(int *aryp, int nrows, int ncolumns);
f3(int **pp, int nrows, int ncolumns);

関数f1()は通常の2次元の配列を引数とし、f2()は"平らにした"2次元 の配列を引数とし、f3()はポインターを指すポインターを引数として 配列を真似るとする(質問6.186.19も参照のこと)。以下の呼び出し はこちらの期待通りの動きをする。

f1(array, NROWS, NCOLUMNS);
f1(array4, nrows, NCOLUMNS);
f2(&array[0][0], NROWS, NCOLUMNS);
f2(*array, NROWS, NCOLUMNS);
f2(*array2, nrows, ncolumns);
f2(array3, nrows, ncolumns);
f2(*array4, nrows, NCOLUMNS);
f3(array1, nrows, ncolumns);
f3(array2, nrows, ncolumns);

以下の2つの例もたいていのマシンで多分うまくいく。ただし怪しい キャストが含まれているし、動的に割り当てたncolumnsが、静的に割 り当てたNCOLUMNSと一致するときしかうまく動かない。

f1((int (*)[NCOLUMNS])(*array2), nrows, ncolumns);
f1((int (*)[NCOLUMNS])array3, nrows, ncolumns);

ここでも&array[0][0]をf2に渡すのは(*arrayを渡すのも)厳密には規 格に準拠していないことに注意すること。質問6.19を参照。

もしなぜ上記の関数呼び出しがすべてうまくいくかということと、な ぜ上記のように記述されたかがわかっていて、洩れている組み合わせ がなぜうまくいかないかもわかっているのなら、C言語の配列とポイ ンターについての知識は(ほかの分野の知識も)結構いい線いっている と思っていいだろう。

さまざまな大きさの多次元の配列の、上に書いたようなことを気にす るかわりの扱いかたとしては、すべての配列を質問6.16にあるように 動的にする方法がある。静的な多次元の配列がなければ、つまりすべ ての配列が質問6.16のarray1やarray2のように確保されているなら、 すべての関数はf3()のように書くことができる。


6.21:
なぜサブルーチンの引数として渡された配列の大きさをsizeof()でき ちんと計算できないのか。
A:
コンパイラーは配列仮引数がポインターと宣言されたように振る舞い (質問6.4参照)、sizeof()はポインターの大きさを返す。

References:
H&S Sec. 7.5.2 p. 195.

目次へ戻る