C言語で2次元データをいろいろな方法で扱ってみる(二次元配列・ポインタのポインタなど)

二次元データの扱い方を解説するページのアイキャッチ

プログラミングをしていると二次元データを扱いたくなる時ってどうしても出てくると思います。例えば画像データ(特にモノクロ画像)を扱ったり、統計を取ったりする時に二次元データが扱えると便利です。

C言語では二次元データを扱う方法はたくさんあります。このページでは、「二次元配列」「ポインタの配列」「ポインタのポインタ」および「一次元データを二次元配列として扱う」方法について解説していきます。配列とポインタの違いもイメージしやすいと思いますのでよろしければ是非読んでみてください。

ポインタや malloc の知識がある前提の解説になっていますので、ポインタについての理解に自信がない方はまず先に下のポインタ解説のページを読んでいただけると、理解しやすくなると思います。

ポインタ解説ページのアイキャッチ徹底図解!C言語ポインタを初心者向けに分かりやすく解説

二次元配列

二次元のデータとして真っ先に思い浮かぶのはこの二次元配列だと思います。

二次元配列とは

二次元配列とは配列自体を配列の要素としてもつ配列です。

2次元配列の説明図

イメージとしては、縦方向と横方向にデータ格納領域が広がる配列として考えると良いと思います。

2次元配列のイメージ

二次元配列による二次元データの作り方

二次元配列の場合、変数宣言を行うだけで二次元データを作成することが可能です。

配列名(変数名)の後ろに配列のサイズを二方向分、つまりサイズを二つ指定することで二次元配列の変数宣言を行うことが可能です。

int array_2d[3][2];

二次元配列の場合、この変数宣言を行うだけで、指定したサイズ分のデータを格納するためのメモリが確保されます。そして、このメモリへのデータの格納やこのメモリからのデータの取得を行うことで、様々なアプリやプログラムを作成することが可能です。

二次元配列の各要素へのアクセス

二次元配列で配列の要素にアクセス(値の取得や値の代入)するためにはインデックス(添字)を二つ指定します。

array_2d[2][1] = 100;

例えば一つ目のインデックスを縦方向に対するインデックス、二つ目のインデックスを横方向に対するインデックスとすれば、一つ目のインデックスを “2”、二つ目のインデックスを “1” の場合、下の図のように array_2d[2][1] の要素にアクセスすることが可能です。

2次元配列の要素へアクセスする様子

プログラム例

二次元配列を用いたプログラムの一例は下記のようになります。

#include <stdio.h>
  
#define M 5 /* 横方向のサイズ */
#define N 4 /* 縦方向のサイズ */

int main(void){
  int m, n;
  int array_2d[N][M];

  for(n = 0; n < N; n++){
    for(m = 0; m < M; m++){
      array_2d[n][m] = n * M + m;
    }
  }

  for(n = 0; n < N; n++){
    for(m = 0; m < M; m++){
       printf("%d, ", array_2d[n][m]);
    }
    printf("\n");
  }

  return 0;
}

ポインタの配列

続いてポインタの配列を用いた2次元データの扱い方について解説します。

ポインタの配列とは

ポインタの配列とはポインタを要素としてもつ配列のことです。

ポインタの配列

ポインタの配列による二次元データの作り方

二次元配列との大きな違いは下記の通りです。

  • 二次元配列:変数宣言で二次元データがメモリ上に作成される
  • ポインタの配列:変数宣言でポインタ格納用の配列のみメモリ上に作成される

つまり、ポインタの配列で変数宣言でメモリ上に確保されるメモリは、アドレスを格納するためのメモリだけで、値やデータなどは格納するためのものではありません。

したがって、ポインタの配列の場合は変数宣言だけでなく、プログラム中で値やデータを格納するためのメモリを動的確保に確保し、そのメモリを配列に格納されたポインタで指してやる必要があります。

もう少し具体的に見ていきましょう。ポインタの配列の変数宣言は下記のようにポインタの変数名の後に配列のサイズを指定して行います。サイズとしては二次元データの縦方向のサイズを指定しましょう。

int *p_array[3];

変数宣言直後のポインタの配列のメモリ上の様子を図示すると下のようになります。

変数宣言直後の配列ポインタの様子

ポインタを格納する配列が存在するだけですね。ただの一次元データです。そして、配列の中に格納されているポインタには不定値が格納されているのでどこを指しているか分からない状態になります。

続いて malloc 関数を用いてメモリの確保を行い、そのメモリをポインタで指します。

変数宣言時にポインタ自体は配列サイズ分存在していますので、下記のようにループしながら各々ポインタがメモリを指すようにします。

for(i = 0; i < 3; i++){
    p_array[i] = (int*)malloc(sizeof(int) * 2);
}

malloc 関数の引数には確保するメモリのサイズをバイト単位で指定します。例えば上記のプログラムでは int 型のサイズ × 2 を指定していますので、下記のように配列の変数宣言を行なった時と同様のメモリが確保されることになります。

int array[2];

そして、配列に格納されたそれぞれのポインタがループ内で確保したメモリを指している状態になります。

動的確保したメモリを配列ポインタが指す様子

データが飛び飛びなのであまり実感できないかもしれませんが、この状態でこれらのデータを二次元データとして扱うことが可能です。

ちなみにですが、malloc したメモリは解放しないとメモリリークになってしまいます。特に長時間稼働しているようなアプリやプログラムだと、メモリリークがたくさん起こると途中で動作できなくなってしまいます。

配列のポインタを用いて malloc した場合は、下のようにループを行い、その中で全てのポインタの指す先を free してあげましょう。

for(i = 0; i < 3; i++){
 free(p_array[i]);
}

ポインタの配列の各要素へのアクセス

ポインタの配列の各要素へのアクセスは二次元配列と全く同じ方法で行うことが可能です。

つまりインデックスを二つ指定すれば良いです。

例えば下記であれば、

p_array[1][0] = 100;

まず配列 p_array[1] にアクセスされ、さらに p_array[1] に格納されたポインタの指すアドレス の先頭アクセスし、100を格納することができます。

ポインタの配列から要素にアクセスする様子

また二つ目のインデックスを +1 した p_array[1][1] の要素にアクセスする時の様子を図示すると下のようになります。

ポインタの配列から要素にアクセスする様子2

また一つ目のインデックスを +1 した p_array[2][0] の要素にアクセス時の様子を図示すると下のようになります。

ポインタの配列から要素にアクセスする様子3

つまり、メモリ的にはそれぞれが離れていますがプログラム上では二次元配列と同じようにして扱うことが可能です。イメージ的には下の図のように考えると良いと思います。

ポインターの配列を二次元配列同様に扱うイメージ

プログラム例

ポインタの配列を用いたプログラムの一例は下記のようになります。

#include <stdio.h>
#include <stdlib.h> /* malloc用 */

#define M 5 /* 横方向のサイズ */
#define N 4 /* 縦方向のサイズ */

int main(void){
  int m, n;
  int *p_array[N];

  for(n = 0; n < N; n++){
    p_array[n] = (int*)malloc(sizeof(int) * M);
  }

  for(n = 0; n < N; n++){
    for(m = 0; m < M; m++){
      p_array[n][m] = n * M + m;
    }
  }

  for(n = 0; n < N; n++){
    for(m = 0; m < M; m++){
       printf("%d, ", p_array[n][m]);
    }
    printf("\n");
  }

  for(n = 0; n < N; n++){
    free(p_array[n]);
  }

  return 0;
}

スポンサーリンク

ポインタのポインタ

続いてポインタのポインタを用いた2次元データの扱い方に解説します。

ポインタのポインタとは

ポインタのポインタとはポインタを指すポインタのことです。ダブルポインタとも呼ばれます。

ダブルポインタで2次元データを扱う例

ポインタのポインタによる二次元データの作り方

二次元配列およびポインタの配列とポインタのポインタとの大きな違いは下記の通りです。

  • 二次元配列:変数宣言で二次元データがメモリ上に作成される
  • ポインタの配列:変数宣言でポインタ格納用の配列のみメモリ上に作成される
  • ポインタのポインタ:変数宣言でポインタのポインタ変数のみメモリ上に作成される

ですので、ポインタの配列同様に、ポインタのポインタにおいても変数宣言だけでなく、malloc 関数を用いてデータ格納用のメモリを確保する必要があります。

またポインタのポインタ変数の場合、指すことができるのはポインタのみです。従って、malloc 変数を2段階的に使用することで二次元データの作成を行うことになります。

まず一段階目で、一旦ポインタを格納するためのメモリを malloc で確保してその先頭アドレスをポインタのポインタ変数に指させます。さらに二段階目でデータ格納用のメモリを malloc で確保し、その先頭アドレスを一段階目に確保したメモリに格納します(つまり確保したメモリの先頭アドレスを指させます)。

ポインタのポインタでmallocを活用する箇所

ポイントは malloc 関数で指定するメモリサイズですが、一段階目で指定したサイズが二次元データにおける縦方向のサイズ、二段階目で指定したサイズが二次元データにおける横方向のサイズと捉えることができます。例えば上の図であれば 3 x 2 のサイズの二次元データとして扱うことができるというわけです。

次はプログラムを見ながら具体的に解説していきます。ポインタのポインタの変数宣言は下記のように、変数名の前に「**」を記述します。

int **dptr;

変数宣言直後のメモリ上の様子を図示すると下のようになります。

変数宣言直後のダブルポインタ

単にポインタのポインタ変数が一つのみ存在するだけです。

続いて、malloc 関数を用いてメモリの確保を行い、そのメモリをポインタで指します。

dptr = (int**)malloc(sizeof(int*) * 3);

ポイントは malloc 関数に渡す引数です。この引数には「ポインタのサイズ x 二次元データの縦方向のサイズ」を指定します。ポインタのサイズは sizeof(ポインタの型名) で取得できますので、この sizeof 関数を積極的に使うと良いと思います。

これによりメモリ上に「ポインタのサイズ x 二次元データの縦方向のサイズ」分のメモリが確保され、ポインタのポインタがそのメモリの先頭を指すことになります。

一段階目のmalloc直後の状態

この確保されたメモリは、int* 型のサイズ3の配列同様に扱うことが可能です。

ポインタのポインタからポインタにアクセスするイメージ

先ほどと同様に malloc 関数を使用してメモリの確保を行います。次は実際にデータを格納するためのメモリ領域の確保です。ここはポインタの配列の時と同様ですね。

for(i = 0; i < 3; i++){
    dptr[i] = (int*)malloc(sizeof(int) * 2);
}

ポイントは事前に malloc 関数で確保したメモリ領域(つまり dptr が指す先のメモリ領域)にmalloc 関数の戻り値であるアドレスを格納することです。これにより下記のようにメモリが確保され、各々のポインタがそれぞれ異なるメモリを指すことになります。

二段階目のmalloc直後の状態

ポインタの配列同様に、こちらも malloc したメモリは free で解放してあげましょう。二段階的に malloc しているので、free も二段階的に行う必要があります。

まずポインタの指す先の解放を行い、

for(i = 0; i < 3; i++){
    free(dptr[i]);
}

続いてポインタのポインタの指す先の解放を行えばオーケーです。

free(dptr);

ポインタのポインタの各要素へのアクセス

ポインタのポインタの各要素へのアクセスは前述した二次元配列およびポインタの配列と全く同じ方法で行うことが可能であり、インデックスを二つ指定すれば良いだけです。

例えば下記であれば、

dptr[1][0] = 100;

まず dptr の指すメモリ領域の2つ目(つまり dptr[1])のメモリにアクセスされ、続いて dptr[1] の指すアドレスの先頭にアクセスすることで、dptr[1][0] にアクセスし、100を格納する処理が行われます。

dptr[1][0]にアクセスする様子

こちらもポインタの配列同様に各メモリ領域は飛び飛びですが、2次元配列同様に連続した領域のイメージを持って扱うことが可能です。

ポインタのポインタを利用した時の各領域へアクセスするインデックス

プログラム例

ポインタのポインタを用いたプログラムの一例は下記のようになります。

#include <stdio.h>
#include <stdlib.h> /* malloc用 */

#define M 5 /* 横方向のサイズ */
#define N 4 /* 縦方向のサイズ */

int main(void){
  int m, n;
  int **dptr;

  dptr = (int**)malloc(sizeof(int*) * N);

  for(n = 0; n < N; n++){
    dptr[n] = (int*)malloc(sizeof(int) * M);
  }

  for(n = 0; n < N; n++){
    for(m = 0; m < M; m++){
      dptr[n][m] = n * M + m;
    }
  }

  for(n = 0; n < N; n++){
    for(m = 0; m < M; m++){
       printf("%d, ", dptr[n][m]);
    }
    printf("\n");
  }

  for(n = 0; n < N; n++){
    free(dptr[n]);
  }
  free(dptr);

  return 0;
}

一次元データ

ここまでは二次元データを作成してそれをそのまま二次元データとして扱う方法について解説してきました。しかし、実は一次元データでも、二次元データとして捉えて扱うことで二次元データを扱うことが可能です。

一次元データを二次元データとして扱う考え方

メモリ上に確保するのは一次元データです。この一次元データの確保は配列でもポインタと malloc を用いてでもどちらでも問題ありません(このページでは配列を用いて解説します)。

この一次元データの「ここからここまでを一行目」、「ここからここまで二行目」…のように 、自分で制御しながらデータを扱うことで二次元データのように扱います。

例えば下記の変数宣言を行なった場合、

int array[6];

サイズ6の一次元配列分のメモリがメモリ上に確保されることになります。

1次元データを確保した状態

例えば縦方向にサイズ3、横方向にサイズ2の二次元データとして考える場合、下の図のように青部分は一行目のデータ、緑部分は二行目のデータ、オレンジ部分は3行目のデータとして自分で制御することで元々は一次元のデータを二次元データのように扱うことができます。

1次元データを2次元データとして扱う様子

一次元配列による一次元データの作り方

これはもはや説明不要かもしれませんが一応。下記のように変数宣言することでサイズ6の一次元データをメモリ上に作成することができます。

int array[6];

一次元データを二次元データとして扱った時の各要素へのアクセス方法

この方法ではこのアクセス方法がポイントになります。扱おうとしている二次元データの縦方向のサイズをN、横方向のサイズをMとした場合、(n, m)座標へのアクセスは下記により行うことができます。

array[n * M + m];

例えば N = 3、M = 2 とすれば、(2, 1) 座標へのアクセスは

array[2 * 2 + 1];

で行えます。

イメージ的には下の図のようになります。

1次元データを2次元データにマッピングする様子

この、「縦方向のサイズをN、横方向のサイズをMとした場合、(n, m)座標へのアクセス」が、

array[n * M + m];

であることを覚えておけば、1次元データを簡単に二次元データとして扱うことができますので、是非覚えておいてください!

プログラム例

一次元配列を用いたプログラムの一例は下記のようになります。

#include <stdio.h>

#define M 5 /* 横方向のサイズ */
#define N 4 /* 縦方向のサイズ */

int main(void){
  int m, n;
  int array[N * M];

  for(n = 0; n < N; n++){
    for(m = 0; m < M; m++){
      array[n * M + m] = n * M + m;
    }
  }

  for(n = 0; n < N; n++){
    for(m = 0; m < M; m++){
       printf("%d, ", array[n * M + m]);
    }
    printf("\n");
  }

  return 0;
}>/code>

まとめ

このページでは、2次元データを2次元配列、ポインタの配列、ポインタのポインタで扱う方法および、一次元データを二次元データとして扱う方法について解説しました。

一番とっつきやすいのは二次元配列ですかね。ただ入力されるデータの縦サイズ・横サイズが変わるようなプログラムを作成する場合は、動的にサイズを指定できるポインタのポインタの方が省メモリなプログラムを作成することはできると思います。

個人的には一次元データを二次元データとして扱う方法が一番使いやすいですね。画像処理のページなんかも公開していますが、全てこちらの方法で画像を扱うプログラムを紹介しています。慣れてくるとこの方法が一番使いやすいと思います。ポインタの配列が一番使わないかなぁ…。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です