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

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

このページにはプロモーションが含まれています

プログラミングをしていると2次元データを扱いたくなる時ってどうしても出てくると思います。

例えば画像データ(特にモノクロ画像)を扱ったり、統計を取ったりする時に2次元データが扱えると便利です。

C言語では2次元データを扱う方法はたくさんあります。このページでは、「2次元配列」「ポインタの配列」「ポインタのポインタ」および「1次元データを2次元配列として扱う」方法について解説していきます。

配列とポインタの違いもイメージしやすいと思いますのでよろしければ是非読んでみてください。

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

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

2次元配列

2次元のデータを扱う際の方法として真っ先に思い浮かぶのはこの2次元配列だと思います。

2次元配列とは

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

2次元配列の説明図

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

2次元配列のイメージ

スポンサーリンク

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

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

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

2次元配列の変数宣言
int array_2d[3][2];

2次元配列の場合、この変数宣言を行うだけで、指定したサイズ分のデータを格納するためのメモリが確保されます。

そして、このメモリへのデータの格納やこのメモリからのデータの取得を行うことで、様々なアプリやプログラムを作成することが可能です。

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

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

2次元配列のデータへのアクセス
array_2d[2][1] = 100;

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

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

スポンサーリンク

プログラム例

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

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次元データの扱い方について解説します。

ポインタの配列とは

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

ポインタの配列

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

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

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

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

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

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

ポインタの配列の変数宣言
int *p_array[3];

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

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

ポインタを格納する配列が存在するだけですね。ただの1次元データです。

そして、配列の中に格納されているポインタには不定値が格納されているのでどこを指しているか分からない状態になります。

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

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

mallocでのメモリ確保
for(i = 0; i < 3; i++){
    p_array[i] = (int*)malloc(sizeof(int) * 2);
}

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

宣言によるint型のサイズ×2のメモリ確保
int array[2];

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

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

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

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

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

メモリの解放
for(i = 0; i < 3; i++){
 free(p_array[i]);
}

スポンサーリンク

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

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

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

例えば下記であれば、

ポインタの配列のデータへのアクセス
p_array[1][0] = 100;

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

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

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

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

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

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

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

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

スポンサーリンク

プログラム例

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

ポインタの配列の利用例
#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次元データを扱う例

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

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

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

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

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

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

さらに2段階目でデータ格納用のメモリを malloc で確保し、その先頭アドレスを一段階目に確保したメモリに格納します(つまり確保したメモリの先頭アドレスを指させます)。

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

ポイントは malloc 関数で指定するメモリサイズです。

1段階目で指定したサイズが2次元データにおける縦方向のサイズ、2段階目で指定したサイズが2次元データにおける横方向のサイズと捉えることができます。

例えば上の図であれば 3 x 2 のサイズの2次元データとして扱うことができるというわけです。

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

ポインタノポインタの変数宣言
int **dptr;

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

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

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

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

縦方向のメモリの確保
dptr = (int**)malloc(sizeof(int*) * 3);

ポイントは malloc 関数に渡す引数です。

この引数には「ポインタのサイズ x 2次元データの縦方向のサイズ」を指定します。ポインタのサイズは sizeof(ポインタの型名) で取得できます。

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

一段階目のmalloc直後の状態

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

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

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

横方向のメモリの確保
for(i = 0; i < 3; i++){
    dptr[i] = (int*)malloc(sizeof(int) * 2);
}

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

二段階目のmalloc直後の状態

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

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

横方向のメモリの解放
for(i = 0; i < 3; i++){
    free(dptr[i]);
}

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

縦方向のメモリの解放
free(dptr);

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

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

例えば下記であれば、

ポインタのポインタのデータへのアクセス
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;
}

1次元データ

ここまでは2次元データを作成してそれをそのまま2次元データとして扱う方法について解説してきました。

しかし、実は1次元データでも、2次元データとして捉えて扱うことで2次元データを扱うことが可能です。

1次元データを2次元データとして扱う考え方

メモリ上に確保するのは1次元データです。

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

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

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

1次元配列の変数宣言
int array[6];

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

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

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

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

スポンサーリンク

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

これはもはや説明不要かもしれませんが一応。

下記のように変数宣言することでサイズ 6 の1次元データをメモリ上に作成することができます。

1次元データの作成
int array[6];

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

この方法ではこのアクセス方法がポイントになります。

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

(m,n)座標へのアクセス
array[n * M + m];

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

(1,2)座標へのアクセス
array[2 * 2 + 1];

で行えます。

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

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

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

(m,n)座標へのアクセス
array[n * M + m];

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

スポンサーリンク

プログラム例

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

1次元データで2次元データを扱う
#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;
}

スポンサーリンク

まとめ

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

一番とっつきやすいのは2次元配列ですかね。

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

個人的には1次元データを2次元データとして扱う方法が1番使いやすいですね。

画像処理のページなんかも公開していますが、全てこちらの方法で画像を扱うプログラムを紹介しています。

慣れてくるとこの方法が一番使いやすいと思います。ポインタの配列が一番使わないかなぁ…。

オススメの参考書(PR)

C言語一通り勉強したけど「ポインタがよく分からない」「ポインタの理解があやふや」「もっとC言語の理解を深めたい」という方には、下記の「C言語ポインタ完全制覇」がオススメです!

この本の主な内容は下記の通りで、通常の参考書では50ページくらいで解説するポインタを、この本では約 "360ページ" 使って幅広く・深く解説しています。

  • C言語でのメモリの使い方
  • 配列とポインタの関係性
  • ポインタのよくある使い方
  • ポインタの効果的な使い方

一通りC言語を学んだだけだと "理解があやふやになってしまいがち" "疑問に思いがち" な内容に対する明確な解説が多いため、特にポインタやC言語の理解があやふやという方にはオススメの本です。

また、C言語においてポインタはまさに "肝" となる機能ですので、ポインタについてより深く学ぶことでC言語全体の理解を深めることにもつながります。

ポインタ・C言語についてより深く理解するための本としては現状1番のオススメの本です。

ただし、他の入門書等で "一通りC言語を学んでいる" 方向けの解説になっているので、"C言語を始めるにあたっての最初の入門書" として利用すると難易度が高いので注意してください。

入門用のオススメ参考書は下記ページで紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示