(結論) 配列はポインタではない。が、大半の場合で配列名はポインタにdecay(減衰)されるので、あたかも同じように(同じとは言っていない)使っても実生活に差し支えない。
抑えるポイントは3つ。
- 配列の宣言は、配列として定義する。また、ポインタの宣言はポインタとして定義する*1。
//file1.cpp /*「定義」とは、オブジェクトの型を指定し、必要な領域を確保する。*/ //pをint*型として定義する。arrを要素の型がintで、要素数、100の配列として定義する。 int* p; int arr[100]; //file2.cpp #include <stdio.h> /*「宣言」とは、別ファイルで定義されたオブジェクトを参照することを伝えるために記述する*/ //externオブジェクトの宣言は、オブジェクトの名前と型、そしてどこかほかでオブジェクトのメモリ確保が住んでいるということを、コンパイラに対して伝える。 //pをint*型として宣言する。arrを大きさの指定されない(不完全型)のintの配列として宣言する。 extern int* p; extern int arr[]; int main(void) { //pを参照するコード int* q = p; //arrを参照するコード int elem = arr[3]; return 0; }
- 関数定義での引数定義において、(効率の観点から*2、)配列名とポインタは等価である。
#include <stdio.h> //func1, func2, func3は関数としては、完全に同等である。 void func1(int* a) { printf("%zu\n", sizeof(a)); } void func2(int a[]) { printf("%zu\n", sizeof(a)); } //aは配列名ではなく、ポインタとして、解釈される。つまり要素数4という情報は消滅する。 //(配列名からポインタへdecay(減衰)すると表現しても構わないのかな?) void func3(int a[4]) { printf("%zu\n", sizeof(a)); }
C++では、式中の配列名はポインタにdecay(減衰)できる。(左辺値参照宣言子(lvalue reference declarator)が導入されたので、配列の参照を実現できる*5
#include <stdio.h> void func(int* a) { printf("%d\n", *a); } int main(void) { int a[5] = {100,1,2,3,4}; func(a); // aは配列名でint[5]型だが、(式中にあるので、)int*にdecay(減衰)される。 //aは配列名だが、int*にdecay(減衰)され、[]演算子はポインタのオフセットと解釈される。 printf("%d\n", a[3]); return 0; }
C++では、式中の配列名は必ずしもポインタにdecayされるわけではない:
#include <iostream> using namespace std; using arrInt5 = int(&)[5]; //typedef int (&arrInt5)[5];と同等。 int[5]の参照。 void func1(int* a) { cout << sizeof(a) << endl; //pointer(8byte) = 8となる。 } void func2(arrInt5 m) { cout << sizeof(m) << endl; // int(4byte) * 5 = 20となる。 } int main(void) { int a[5] = {100,1,2,3,4}; // aは配列名でint[5]型だが、(式中にあるので、)int*にdecay(減衰)される。 func1(a); //aは配列名でint[5]型で、参照渡しされる。(配列名aはdecayされていないことに注意。) func2(a); return 0; }
多分、こういう理解であってるはず(・ω・)ノ
decayについては、もう少し、詳しく書きたいなぁ。
配列パラメータとポインタパラメータの減衰(decay)
char c[8][10] //=> char (*c)[10] # 配列の配列 char* c[15] //=> char** c # ポインタの配列 char (*c)[64] //=> char (*c)[64]; # 配列へのポインタ