
C言語の「ポインタ」と配列についてお話していきたいと思います。 ポインタのお話をする前提となる知識として、C言語からどのようにメモリを使っているかの基礎知識を前ページでまとめていますのでよければそちらもご覧ください。
ポインタと配列の関係を確認するための例として下のプログラムを確認ください。
#include <stdio.h>
int main(void) {
int ar1[3];
int ar2[4];
printf("-------- ar1 変数のポインタ --------\n");
printf("ar1 の中身 = %p\n",ar1);
for (int i = 0; i < 3 ; i++) {
printf("ar1[%d] のアドレス = %p\n", i, &ar1[i]);
}
printf("-------- ar2 変数のポインタ --------\n");
printf("ar2 の中身 = %p\n", ar2);
for (int i = 0; i < 4; i++) {
printf("ar2[%d] のアドレス = %p\n", i, &ar2[i]);
}
}
コードを実行すると、下のような結果になりました。
-------- ar1 変数のポインタ --------
ar1 の中身 = 012FF960
ar1[0] のアドレス = 012FF960
ar1[1] のアドレス = 012FF964
ar1[2] のアドレス = 012FF968
-------- ar2 変数のポインタ --------
ar2 の中身 = 012FF944
ar2[0] のアドレス = 012FF944
ar2[1] のアドレス = 012FF948
ar2[2] のアドレス = 012FF94C
ar2[3] のアドレス = 012FF950
int型の配列である ar1 と ar2 を宣言し、その配列のそれぞれの添え字が、メモリ上どのような位置に存在するかを確認しています。
ローカル変数として配列を用意するときの、メモリの配置を確認してみましょう。 サンプルソースコードの main関数の最初に、int型配列 ar1 を宣言しています。
int ar1[3];
配列は宣言しただけで、中身に代入していませんが、この時点で代入することのメモリの領域が準備されます。 ただし、初期化はされていませんので、例えば ar[1] などと、配列の中身を参照すると、どんな値が入っているかは保障されません。 (以前他のプログラムで作って破棄されたゴミのメモリデータの可能性もあります)
次に、下のプログラムの中で配列のそれぞれの要素がどのメモリアドレスに割り当てされているのかを表示します。
printf("-------- ar1 変数のポインタ --------\n");
printf("ar1 の中身 = %p\n",ar1);
for (int i = 0; i < 3 ; i++) {
printf("ar1[%d] のアドレス = %p\n", i, &ar1[i]);
}
この部分の結果は下のようになっていました。
-------- ar1 変数のポインタ --------
ar1 の中身 = 012FF960
ar1[0] のアドレス = 012FF960
ar1[1] のアドレス = 012FF964
ar1[2] のアドレス = 012FF968
図にしてみると下のような配置になっていますね。
変数もしくは配列 | アドレス |
---|---|
ar1、 ar1[0] | 012FF960 |
ar1[1] | 012FF964 |
ar1[2] | 012FF968 |
処理系によって変わりますが、int 型の変数は一つで 4 バイトのサイズであるとき、4バイトづつで連続して配列の中身の場所が確保されていることがわかります。
また、配列そのものを表す変数 ar1 の中身には ar1 の添え字 0 番目 の格納先メモリアドレスと同じメモリアドレスが格納されていることがわかります。つまり配列の変数 ar1 は、配列の先頭のアドレスが入っていることになります。
続きて ar2 の中身を表示した結果についても確認してみましょう。
printf("-------- ar2 変数のポインタ --------\n");
printf("ar2 の中身 = %p\n", ar2);
for (int i = 0; i < 4; i++) {
printf("ar2[%d] のアドレス = %p\n", i, &ar2[i]);
}
この部分の結果は下のようになっていました。
-------- ar2 変数のポインタ --------
ar2 の中身 = 012FF944
ar2[0] のアドレス = 012FF944
ar2[1] のアドレス = 012FF948
ar2[2] のアドレス = 012FF94C
ar2[3] のアドレス = 012FF950
ar2 の結果も含めて図にしてみると下のような配置になっていますね。
変数もしくは配列 | アドレス |
---|---|
ar2、 ar2 | 012FF944 |
ar2[1] | 012FF944 |
ar2[2] | 012FF94C |
ar2[3] | 012FF950 |
メモリアドレスの配置は、通常のローカル変数と同じように、スタック上に ar1、ar2 の配列の順で積まれている(メモリアドレスが大きいほうから小さいほうに向かって、順番に確保されている)ことがわかります。
ポインタにはメモリアドレスを格納していますが、ポインタに対して演算をすることで、ポインタが指し示しているメモリアドレスを変更することができます。
#include <stdio.h>
int main(void) {
int hp[3] = {100, 120, 145};
int mp[3] = {4, 7, 9};
int *pointer = hp;
printf("-------- hp 変数のポインタ --------\n");
printf("hp の中身 = %p\n",hp);
for (int i = 0; i < 2 ; i++) {
printf("hp[%d]=%d, hp[%d] のアドレス = %p\n", i, hp[i], i, &hp[i]);
}
pointer++;
printf("pointerのアドレスは%p, hp = %d\n", pointer, *pointer);
}
コードを実行すると、下のような結果になりました。
-------- hp 変数のポインタ --------
hp の中身 = 000000000065FE04
hp[0]=100, hp[0] のアドレス = 000000000065FE04
hp[1]=120, hp[1] のアドレス = 000000000065FE08
pointerのアドレスは000000000065FE08, hp = 120
配列 hp のメモリアドレスと配置は下の図のようになっています。 また、int型を指し示すポインタであるポインタ変数 pointer には、5行目で配列 hp の先頭のアドレスが入っています。 先ほどお話しした通り、配列の変数名 hp は配列の一番先頭のアドレスを指しています。
int* pointer = &hp[0]
と書いているのと同じこととなります。
これに対して、11行目の下の計算
pointer++;
printf(“pointerのアドレスは%p, hp = %d\n”, pointer, *pointer);
で、pointer に加算をしています。 「++」はインクリメントという処理で、普通 1 加算されますが、ここでは 4 加算されています。 これは、ポインタ変数 pointer が int型のデータを指し示すと宣言していたからです。 このため、 int型のデータ分(4バイト分)だけ1つぶんずらした(++した)位置のメモリアドレスが参照される仕組みとなっています。
配列は宣言するときにスタック領域に指定したサイズが確保されます。 下のプログラムを見てみましょう。
#include <stdio.h>
int main(void) {
int hp[2] = {100, 120};
int mp[2] = {4, 9};
printf("-------- mp 変数のポインタ --------\n");
printf("hp[0]=%d, hp[0] のアドレス = %p\n", hp[0], &hp[0]);
printf("hp[1]=%d, hp[1] のアドレス = %p\n", hp[1], &hp[1]);
printf("-------- mp 変数のポインタ --------\n");
printf("mp[0]=%d, mp[0] のアドレス = %p\n", mp[0], &mp[0]);
printf("mp[1]=%d, mp[1] のアドレス = %p\n", mp[1], &mp[1]);
printf("-------- mp 配列の壁を越えたアクセス --------\n");
printf("mp[6]=%d, mp[6] のアドレス = %p\n", mp[6], &mp[6]);
}
コードを実行すると、下のような結果になりました。
-------- mp 変数のポインタ --------
hp[0]=100, hp[0] のアドレス = 000000000065FE18
hp[1]=120, hp[1] のアドレス = 000000000065FE1C
-------- mp 変数のポインタ --------
mp[0]=4, mp[0] のアドレス = 000000000065FE10
mp[1]=9, mp[1] のアドレス = 000000000065FE14
-------- mp 配列の壁を越えたアクセス --------
mp[3]=120, mp[3] のアドレス = 000000000065FE1C
配列 mp は 2 個の要素を持つと宣言しており、つまり mp[0]、mp[1] に値が入っています。しかし、ソースコードの中で、その要素数を超えて mp[3] の要素を見にいっています。 このような時、Java 等の他の言語では配列の最大を超えたとしてエラーとなりますが、C言語では添え字の最大数をチェックするような処理をしておらず、エラーにはなりません。
C言語は高速に動作します。 高速に動作するかわり、このように配列の数を超えないように自分でチェックすることや、自分で確保した範囲しかアクセスしないようにコードを書くなどの考慮が必要となります。
また、今回は mp 配列の 3 番目にアクセスしていますが、どこにアクセスしているのでしょうか。 これは実際には int型ポインターで、配列の頭から数えて int型データサイズ(1つの int で 4バイト分のサイズ)の 3 個目にアクセスしているのと同様の結果となります。
メモリアドレスと中身の状態をみてみると、 mp[3] は、hp の配列の要素のメモリ参照位置までまでメモリの参照位置表示しているのと同じ結果となります。
変数 hp 配列 | 変数 mp 配列 | アドレス | アドレスに入っている値 |
---|---|---|---|
mp[0] | 65FE10 | 4 | |
mp[1] | 65FE14 | 9 | |
hp[0] | mp[2] | 65FE18 | 100 |
hp[1] | mp[3] | 65FE1C | 120 |
このようにまったく違う変数の中身を見てしまうこととなり、値を代入することで、別の変数の中身を壊してしまうこととなるので、注意が必要です。
配列へのアクセスは、メモリアドレスへのアクセスという点でみると、ポインタの操作と非常に近いことをしてることがわかります。 また、スタック領域への配列の領域の確保のされ方を知っておくことで、配列の要素の限界をこえてアクセスしてしまった時にどのようなことが起こるかが一部確認していただければと思います。
今回はここまでです!おつかれさまでした。
更新日 | 更新内容 |
---|---|
更新なし |
コメント、ありがとうございます。
ごめんなさい。エラーでうまく送信できませんでした。ご迷惑をおかけします。しばらくおいてから再度送信を試していただくか、以下から DM などでご連絡頂ければと思います。
Twitter:@NodachiSoft_jpお名前:以下の内容でコメントを送信します。よろしければ、「送信」を押してください。修正する場合は「戻る」を押してください
お名前: