配列とポインタ: 密接な関係

配列名の実体はポインタである。C言語の衝撃的な事実。

配列の崩壊 (Decay)
配列名が、式の中で「先頭要素へのポインタ」に変換される現象。
糖衣構文
読み書きしやすくするための記法。nums[i] は *(nums+i) のこと。
クラスの代表者 (Class Representative)

配列`arr`を関数に渡すとき、クラス全員(配列全体)が移動するわけではありません。「代表者(先頭の要素のアドレス)」だけが派遣されます。 これを受け取った関数(先生)は、代表者の後ろに何人続いているか(サイズ)を知りません。だからこそ、「代表者」と一緒に「人数(サイズ)」も教えてあげる必要があるのです。

配列名 = アドレス

C言語における配列名(例: arr)は、ほとんどの場面で 「配列の先頭要素のアドレス」として振る舞います。つまり arr&arr[0] は同じです。

このため、配列を関数に渡すと、実際には配列全体がコピーされるのではなく、 「先頭の住所(代表者)」だけが渡されます。これを「配列の崩壊 (Decay)」と呼びます。巨大な配列でも一瞬で関数に渡せるのはこのためです。

配列の崩壊 (Array Decay)

関数内では配列のサイズ情報が失われます。ポインタとして扱われるからです。

Decay to Pointer
void func(int *arr) { // int arr[] と書いても同じ
// ここでは arr は単なるポインタ
// 配列のサイズはわからない! (sizeof(arr) は 8 になる)
}
int main(void) {
int nums[5] = {1, 2, 3, 4, 5};
// 配列名 nums は &nums[0] と等価
func(nums);
// 証明
if (nums == &nums[0]) {
printf("Same address!\n");
}
return 0;
}

[] の正体

普段使っている <code>[]</code> は、実はポインタ演算のショートカット(糖衣構文)に過ぎません。

Syntactic Sugar
int nums[5] = {10, 20, 30, 40, 50};
// 実は同じ意味
printf("%d\n", nums[2]); // 30
printf("%d\n", *(nums + 2)); // 30
// なので、こんな気持ち悪いことも書ける(書くな)
printf("%d\n", 2[nums]); // 30
// 理由: *(2 + nums) と展開されるから

実践テクニック

セットで渡す

配列を関数に渡すとサイズがわからなくなるため、C言語では一般的に「ポインタ」と「サイズ(要素数)」をセットで渡します。

Standard Pattern
void process(int *arr, int size) {
for(int i=0; i<size; i++) { ... }
}

演習課題

課題1: 最大値検索
整数配列とそのサイズを受け取り、最大値を返す関数 int max(int *arr, int size) を実装してください。

合格ライン

配列名がポインタになることを説明できる
arr[i] と *(arr+i) が同じだと知っている
配列を関数に渡すときはサイズも必要だと理解している