プログラミングをしていると2次元データを扱いたくなる時ってどうしても出てくると思います。
例えば画像データ(特にモノクロ画像)を扱ったり、統計を取ったりする時に2次元データが扱えると便利です。
C言語では2次元データを扱う方法はたくさんあります。このページでは、「2次元配列」「ポインタの配列」「ポインタのポインタ」および「1次元データを2次元配列として扱う」方法について解説していきます。
配列とポインタの違いもイメージしやすいと思いますのでよろしければ是非読んでみてください。
ポインタや malloc
の知識がある前提の解説になっていますので、ポインタについての理解に自信がない方はまず先に下のポインタ解説のページを読んでいただけると、理解しやすくなると思います。
Contents
2次元配列
2次元のデータを扱う際の方法として真っ先に思い浮かぶのはこの2次元配列だと思います。
2次元配列とは
2次元配列とは「配列自体を」配列の要素としてもつ配列です。
イメージとしては、縦方向と横方向にデータ格納領域が広がる配列として考えると良いと思います。
スポンサーリンク
2次元配列による2次元データの作り方
2次元配列の場合、変数宣言を行うだけで2次元データを作成することが可能です。
配列名(変数名)の後ろに配列のサイズを2方向分、つまりサイズを2つ指定することで2次元配列の変数宣言を行うことが可能です。
int array_2d[3][2];
2次元配列の場合、この変数宣言を行うだけで、指定したサイズ分のデータを格納するためのメモリが確保されます。
そして、このメモリへのデータの格納やこのメモリからのデータの取得を行うことで、様々なアプリやプログラムを作成することが可能です。
2次元配列の各要素へのアクセス
2次元配列で配列の要素にアクセス(値の取得や値の代入)するためにはインデックス(添字)を2つ指定します。
array_2d[2][1] = 100;
例えば1つ目のインデックスを縦方向に対するインデックス、2つ目のインデックスを横方向に対するインデックスとすれば、1つ目のインデックスとして 2
を、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次元データの扱い方について解説します。
ポインタの配列とは
ポインタの配列とは「ポインタ」を要素としてもつ配列のことです。
ポインタの配列による2次元データの作り方
2次元配列との大きな違いは下記の通りです。
- 2次元配列:変数宣言で2次元データがメモリ上に作成される
- ポインタの配列:変数宣言でポインタ格納用の配列のみメモリ上に作成される
つまり、ポインタの配列で変数宣言でメモリ上に確保されるメモリは、アドレスを格納するためのメモリだけで、値やデータなどは格納するためのものではありません。
したがって、ポインタの配列の場合は変数宣言だけでなく、プログラム中で値やデータを格納するためのメモリを動的確保に確保し、そのメモリを配列に格納されたポインタで指してやる必要があります。
もう少し具体的に見ていきましょう。ポインタの配列の変数宣言は下記のようにポインタの変数名の後に配列のサイズを指定して行います。サイズとしては2次元データの縦方向のサイズを指定しましょう。
int *p_array[3];
変数宣言直後のポインタの配列のメモリ上の様子を図示すると下のようになります。
ポインタを格納する配列が存在するだけですね。ただの1次元データです。
そして、配列の中に格納されているポインタには不定値が格納されているのでどこを指しているか分からない状態になります。
続いて malloc
関数を用いてメモリの確保を行い、そのメモリをポインタで指します。
変数宣言時にポインタ自体は配列サイズ分存在していますので、下記のようにループしながら各々ポインタがメモリを指すようにします。
for(i = 0; i < 3; i++){
p_array[i] = (int*)malloc(sizeof(int) * 2);
}
malloc
関数の引数には確保するメモリのサイズをバイト単位で指定します。例えば上記のプログラムでは 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]
の要素にアクセスする時の様子を図示すると下のようになります。
また一つ目のインデックスを +1
した p_array[2][0]
の要素にアクセス時の様子を図示すると下のようになります。
つまり、メモリ的にはそれぞれが離れていますがプログラム上では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次元データがメモリ上に作成される
- ポインタの配列:変数宣言でポインタ格納用の配列のみメモリ上に作成される
- ポインタのポインタ:変数宣言でポインタのポインタ変数のみメモリ上に作成される
ですので、ポインタの配列同様に、ポインタのポインタにおいても変数宣言だけでなく、malloc
関数を用いてデータ格納用のメモリを確保する必要があります。
またポインタのポインタ変数の場合、指すことができるのはポインタのみです。従って、malloc
変数を2段階的に使用することで2次元データの作成を行うことになります。
まず1段階目で、一旦ポインタを格納するためのメモリを malloc
で確保してその先頭アドレスをポインタのポインタ変数に指させます。
さらに2段階目でデータ格納用のメモリを 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次元データの縦方向のサイズ」分のメモリが確保され、ポインタのポインタがそのメモリの先頭を指すことになります。
この確保されたメモリは、int*
型のサイズ3の配列同様に扱うことが可能です。
先ほどと同様に malloc
関数を使用してメモリの確保を行います。次は実際にデータを格納するためのメモリ領域の確保です。ここはポインタの配列の時と同様ですね。
for(i = 0; i < 3; i++){
dptr[i] = (int*)malloc(sizeof(int) * 2);
}
ポイントは事前に malloc
関数で確保したメモリ領域(つまり dptr
が指す先のメモリ領域)に 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
を格納する処理が行われます。
こちらもポインタの配列同様に各メモリ領域は飛び飛びですが、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次元データのように扱います。
例えば下記の変数宣言を行なった場合、
int array[6];
サイズ 6
の1次元配列分のメモリがメモリ上に確保されることになります。
例えば縦方向にサイズ 3
、横方向にサイズ 2
の2次元データとして考える場合、下の図のように青部分は一行目のデータ、緑部分は二行目のデータ、オレンジ部分は三行目のデータとして自分で制御することで、元々は1次元のデータを2次元データのように扱うことができます。
スポンサーリンク
1次元配列による1次元データの作り方
これはもはや説明不要かもしれませんが一応。
下記のように変数宣言することでサイズ 6
の1次元データをメモリ上に作成することができます。
int array[6];
1次元データを2次元データとして扱った時の各要素へのアクセス方法
この方法ではこのアクセス方法がポイントになります。
扱おうとしている2次元データの縦方向のサイズを N
、横方向のサイズを M
とした場合、(m, n)
座標へのアクセスは下記により行うことができます。
array[n * M + m];
例えば N = 3
、M = 2
とすれば、(1, 2)
座標へのアクセスは
array[2 * 2 + 1];
で行えます。
イメージ的には下の図のようになります。
この、「縦方向のサイズを N
、横方向のサイズを M
とした場合、(m, n)
座標へのアクセス」が、
array[n * M + m];
であることを覚えておけば、1次元データを簡単に2次元データとして扱うことができますので、是非覚えておいてください!
スポンサーリンク
プログラム例
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;
}
スポンサーリンク
まとめ
このページでは、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/