音ゲー、fps、DTM、プログラミング雑記置き場

ブログタイトル通りに雑記を垂れ流す。

【C】配列とポインタの関係について

多次元配列の扱いについて

  • Cで多次元配列を定義する際下記に注意
  • 仮に2*3の多次元配列を定義したい場合、下記のコードのように定義と初期化が必要
  • イメージ的には要素数3の配列に要素数2の配列が含まれている状態
    • 多次元配列はn*mの行列をイメージしがちだがCの配列はメモリを連番で確保するため配列の中に配列がネストされているというイメージのが正しい
    • 下記のコードだと、要素数2の配列にそれぞれ要素数3の配列を格納している
/*Cの多次元配列の定義と初期化*/
int array[2][3] = {{1,2,3},{4,5,6}};

配列とポインタの関係

  • 下記コードを実行してみる
#include <stdio.h>

int main(void){

  int array[2][3] = {{1,2,3},{4,5,6}};

  for(int i = 0; i < 2; i++){
    for(int j = 0; j < 3; j++){
      printf("i:%d, j:%d, array:%d \n",i,j,array[i][j]);
    }
  }

}

実行結果

i:0, j:0, array:1 
i:0, j:1, array:2 
i:0, j:2, array:3 
i:1, j:0, array:4 
i:1, j:1, array:5 
i:1, j:2, array:6 
  • iがネスト元の配列でjがネストされた配列のインデックスを示している
    • i,jの組み合わせで特定の配列の要素を参照している
  • 上記でも説明した通り、Cでは配列の中に配列をネストさせてあくまで連番であるようなイメージで扱う
    • この時、int型(4バイト)に要素数3のint型の配列(12バイト)を格納するという謎の状態が発生する
    • これだとメモリが足りない為ネスト元の配列にはネストされる配列へのポインタが格納される
      • int型のポインタなら4バイトなのでこれで辻褄が合う

array[2][3] = {{1,2,3},{4,5,6}}のメモリ確保のイメージ図

  • [n]はint型の1つのメモリ(4バイト)のイメージ
  • 我流の記法の為、こんなコードは実在しない事に注意
1.最初にarray[2]で2つの要素の配列を確保
>[][]

2.array[2]の後に[3]があるので配列の中に配列が入る多次元配列だと分かる
>[[1],[2],[3]][[4],[5],[6]] 

3.これだと一つの配列(array[2])に入り切らない為ポインタで退避させる
>[pointerA][pointerB][1][2][3][4][5][6]

4.3の状態だとデータ参照ができない(ポインタがデータのアドレスを指していない)ため、
 array[3]よりメモリがint型3つ分のデータが必要と判断し12バイト分を2つ確保する
>[pointer_from_1_to_3][pointer_from_4_to_6][1][2][3][4][5][6]
  • 以下の流れからCでは配列への参照はポインタを利用しているためarray[i]と宣言すると
    iの要素分メモリを確保し先頭要素へのポインタをarrayに格納する
  • そのため上記のpointer_from_a_to_c,pointer_from_d_to_f経由でも配列の参照は可能となる
    • 下記コードが多次元配列をポインタ経由で展開する代替案となる
  • 2次元配列のポインタによる参照は、2重にポインタを用いる
    • *(array + k)*(array + n)が上記のpointer_from_1_to_3,
      pointer_from_4_to_6に当たる。これを合わせたのが*(*(array + k) + n)となる
    • *array自身は配列の先頭要素のアドレスを示すポインタで、
      配列は連番でメモリを確保するCの特性より+kでずらすことで他の要素も参照できる
      ただ2次元配列のため2つのポインタで配列の先頭アドレスを保持しているため2重ポインタという形になった
for (int k = 0; k < 2; k++)
{
  for (int n = 0; n < 3; n++)
  {
    printf("k:%d, n:%d, array:%d \n", k, n, *(*(array + k) + n));
  }
}
  • 応用例として2次元配列を1次元配列のように扱うこともできる
  • array_pointer[i * 3 + j]より、添え字内の総数が全要素数になるような式にすると
    全ての要素を展開できる
#include <stdio.h>

int main(void){

  int array[2][3] = {{1,2,3},{4,5,6}};
  /*対象の配列の先頭ポインタをインデックスとして扱いたいためキャストして宣言する*/
  int *array_pointer = (int *)array;

  for (int i = 0; i < 2; i++)
  {
    for (int j = 0; j < 3; j++)
    {
      printf("i:%d, j:%d, array_pointer:%d \n", i, j, array_pointer[i * 3 + j]);
    }
  }

}

配列の先頭アドレスについて

  • 配列として宣言した変数に対しarrayのように単体で記述すると、 その配列の先頭アドレスへのポインタとなる
  • 下記コードのように添え字が足りない配列にポインタ型以外の変数は代入できないので注意
    • 多次元配列の場合はarray[1]とすると2番目の配列の先頭ポインタを指す
int array_a[2] = { 0 };
int array_b[2][2] = { 0 };

// array_a = 1;    //error
array_a[0] = 1;    //OK

// array_b = 1;    //error
// array_b[1] = 1; //error
array_b[0][1] = 1; //OK

まとめ

  • 配列‘array‘のように添え字なしで記載するとその配列へのポインタを返す
  • 配列は連番でメモリを確保する性質を利用し、多次元配列でも添え字内の
    ポインタを先頭からいくつずらすという計算をすることで1次元配列っぽく扱える