Nodachisoft Nodachi Sword Icon
  
@あまじ✎ 2021年1月18日に更新

第2章36 ポインタと配列

イチからゲーム作りで覚えるC言語
NEXT : Page Title Not found :

この記事でやること

C言語の「ポインタ」と配列についてお話していきたいと思います。 ポインタのお話をする前提となる知識として、C言語からどのようにメモリを使っているかの基礎知識を前ページでまとめていますのでよければそちらもご覧ください。

ポインタと配列の関係

ポインタと配列の関係を確認するための例として下のプログラムを確認ください。

 
PointerAndArray1.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]);
  }
}

コードを実行すると、下のような結果になりました。

PointerAndArray1.c実行結果
-------- 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 を宣言しています。

 
PointerAndArray1.c
int ar1[3];

配列は宣言しただけで、中身に代入していませんが、この時点で代入することのメモリの領域が準備されます。 ただし、初期化はされていませんので、例えば ar[1] などと、配列の中身を参照すると、どんな値が入っているかは保障されません。 (以前他のプログラムで作って破棄されたゴミのメモリデータの可能性もあります)

次に、下のプログラムの中で配列のそれぞれの要素がどのメモリアドレスに割り当てされているのかを表示します。

 
PointerAndArray1.c
printf("-------- ar1 変数のポインタ --------\n");
printf("ar1 の中身 = %p\n",ar1);
for (int i = 0; i < 3 ; i++) {
  printf("ar1[%d] のアドレス = %p\n", i, &ar1[i]);
}

この部分の結果は下のようになっていました。

PointerAndArray1.c実行結果
-------- 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 の中身を表示した結果についても確認してみましょう。

 
PointerAndArray1.c
printf("-------- ar2 変数のポインタ --------\n");
printf("ar2 の中身 = %p\n", ar2);
for (int i = 0; i < 4; i++) {
  printf("ar2[%d] のアドレス = %p\n", i, &ar2[i]);
}

この部分の結果は下のようになっていました。

PointerAndArray1.c実行結果
-------- 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 の配列の順で積まれている(メモリアドレスが大きいほうから小さいほうに向かって、順番に確保されている)ことがわかります。

配列とポインタの演算

ポインタにはメモリアドレスを格納していますが、ポインタに対して演算をすることで、ポインタが指し示しているメモリアドレスを変更することができます。

 
PointerAndArray2.c
#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);
}

コードを実行すると、下のような結果になりました。

PointerAndArray2.c実行結果
-------- 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行目の下の計算

 
PointerAndArray2.c
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]);
}

コードを実行すると、下のような結果になりました。

PointerAndArray3.cの実行結果
-------- 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

このようにまったく違う変数の中身を見てしまうこととなり、値を代入することで、別の変数の中身を壊してしまうこととなるので、注意が必要です。

あとがき

配列へのアクセスは、メモリアドレスへのアクセスという点でみると、ポインタの操作と非常に近いことをしてることがわかります。 また、スタック領域への配列の領域の確保のされ方を知っておくことで、配列の要素の限界をこえてアクセスしてしまった時にどのようなことが起こるかが一部確認していただければと思います。

今回はここまでです!おつかれさまでした。

非常に参考になったサイトさまや、参考文献など

ページの更新履歴

更新日 更新内容
更新なし
イチからゲーム作りで覚えるC言語
NEXT : Page Title Not found :
 
 
送信しました!

コメント、ありがとうございます。

なんかエラーでした

ごめんなさい。エラーでうまく送信できませんでした。ご迷惑をおかけします。しばらくおいてから再度送信を試していただくか、以下から DM などでご連絡頂ければと思います。

Twitter:@NodachiSoft_jp
お名前:
 
連絡先:
 
メッセージ:
 
戻る
内容の確認!

以下の内容でコメントを送信します。よろしければ、「送信」を押してください。修正する場合は「戻る」を押してください

お名前:
 
連絡先:
 
メッセージ:
 
Roboto からの操作ではないという確認のため確認キーを入れてください。
確認キー=95
戻る
 / 
送信確認へ
コメント欄
コメント送信確認へ

関連ありそうな記事(5件)です!

ゲーム等で使えるつなぎ目のないループするテクスチャ画像の作り方

#ツール#ゲームプログラミング✎ 2021-01-24
ゲームなどで使えるループ画像、パターンテクスチャのツール、手動での作り方をまとめ
広告領域
追従 広告領域
目次
第2章36 ポインタと配列
第2章36 ポインタと配列
この記事でやること
この記事でやること
ポインタと配列の関係
ポインタと配列の関係
配列を利用するときのスタック領域
配列を利用するときのスタック領域
配列とポインタの演算
配列とポインタの演算
配列の添え字が宣言を超えたとき
配列の添え字が宣言を超えたとき
あとがき
あとがき
非常に参考になったサイトさまや、参考文献など
非常に参考になったサイトさまや、参考文献など
ページの更新履歴
ページの更新履歴
Nodachisoft © 2020