0. はじめに
今回はC言語で一番最初につまずくであろうポインタについて整理して、C言語での配列の扱い方についてまとめます。2次元配列は画像処理などでもよく使用されるので、C言語で画像処理をしたい方はぜひ参考にしてみてください。
1. アドレスとポインタ変数
配列に入る前に、アドレスとポインタ変数についておさらいしておきます。
1.1. アドレスとポインタ変数
C言語では、ある変数を宣言する際にはその変数の型や大きさに応じたメモリが割り当てられます。アドレスとは、そのメモリの番地であり、いわゆる住所のようなものです。変数の前に & (アンパサンド)をつければその変数のアドレスを取り出すことができます。
ポインタ変数とは、そのアドレスを格納するための変数のことです。以下でも詳しく説明しますが、ポインタ変数を使用することで、その先の値を取り出すこともできます。
1.2. ポインタ変数の基本的な使い方
それでは具体的にこれらを扱ってみましょう。
#include <stdio.h>
int main(){
int a;
int* pa; //ポインタ変数
a = 3;
pa = &a; //ポインタ変数にはアドレスを入れる
printf("a = %d \n", a);
printf("pa = %p \n", pa);
printf("&a = %p \n", &a); //ポインタ変数にはaのアドレスが入っている
printf("*pa = %d \n", *pa); //ポインタ変数の指す先の値が入っている
printf("&pa = %p \n", &pa); //ポインタ変数のアドレスにはまた別のアドレスが入っている
return 0;
}
出力結果は以下のようになります。
a = 3
pa = 0x7ffeefbff498
&a = 0x7ffeefbff498
*pa = 3
&pa = 0x7ffeefbff490
Program ended with exit code: 0
ここで押さえておくべきことは、以下の3点です。
- ポインタ変数の中にはアドレスが入る。
- ポインタ変数には、指す先の値を * (アスタリスク)で取り出すことができる機能がある。
※ 当たり前ではありますが、ポインタ変数として宣言されていない変数にはこの機能はありません。 - ポインタ変数のアドレスはまた別のアドレスが入っている。
1.3. ポインタ変数を介して値を渡す
ポインタ変数を用いることで、このようにポインタ変数を介した値渡しができます。
#include <stdio.h>
int main(){
int a;
int* pa; //ポインタ変数
int b;
a = 3;
pa = &a; //ポインタ変数にはアドレスを入れる
b = *pa;
printf("b = %d\n",b);
/*paの指す先を書き換える*/
*pa = 4;
printf("a = %d\n",a); //paの指す先はaなのでaの値が書き換えられる
printf("b = %d\n",b); //bにはポインタの先を渡しただけなのでbの値は変わらない
}
出力結果は以下のようになります。
b = 3
a = 4
b = 3
Program ended with exit code: 0
アドレスとポインタ変数に関するおさらいはこんな感じです。
2. ポインタを使った1次元配列の作成
2.1. メモリの静的確保と動的確保
この記事の本題であるポインタを用いた1次元配列について説明していきたいのですが、その前にメモリの静的確保と動的確保について説明します。
静的確保とは、配列を作成する際に、必要なメモリサイズがコンパイル時点で決まっている際のメモリの確保です。一方、動的確保とは、必要なメモリサイズが実行時にしか決まらない際のメモリの確保です。画像処理などのように、処理するデータによって必要な配列の大きさが異なる際には動的なメモリ確保が必要になります。
下は、実際に配列用メモリの静的確保をしてみた例です。
#include <stdio.h>
int main(){
/*静的確保①*/
int A[] = {1,2,3,4,5};
/*静的確保②*/
const int n = 5;
int B[n];
for(int i=0; i<n; i++){
B[i] = i;
}
return 0;
}
2.2. 1次元配列の動的確保
各要素にfloat型が入る1次元配列を動的に作成した例は以下のようになります。
#include <stdio.h>
#include <stdlib.h> //malloc関数,free関数を使用する際に必要
int main(){
int n = 5;
float* Array; //行列の先頭をポインタとして定義
Array = (float*)malloc(n * sizeof(float)); //ポインタArrayの先のメモリを動的に確保
printf("Array = %p\n", Array); //A配列の0番目の要素のアドレスが入る
for(int i=0; i<n; i++){
printf("Array[%d] = %f &Array[%d] = %p\n", i, Array[i], i, &Array[i]);
}
/*配列の要素に値を代入する*/
printf("\n**奇数の配列を作成**\n");
for(int i=0; i<n; i++){
Array[i] = 2*i+1;
printf("Array[%d] = %f\n", i, Array[i]);
}
printf("*Array = %f\n", *Array); //ポインタArrayの先の要素はArray[0]と同じ
free(Array); //malloc関数で確保したメモリを解放
return 0;
}
出力結果は以下のようになります。
Array = 0x10042d310
Array[0] = 0.000000 &Array[0] = 0x10042d310
Array[1] = 0.000000 &Array[1] = 0x10042d314
Array[2] = 0.000000 &Array[2] = 0x10042d318
Array[3] = 0.000000 &Array[3] = 0x10042d31c
Array[4] = 0.000000 &Array[4] = 0x10042d320
**奇数の配列を作成**
Array[0] = 1.000000
Array[1] = 3.000000
Array[2] = 5.000000
Array[3] = 7.000000
Array[4] = 9.000000
*Array = 1.000000
Program ended with exit code: 0
これについて、解説していきましょう。
1. malloc関数でメモリを確保して、free関数で確保したメモリを解放する
malloc関数について、こちらにもある通り、引数に確保するメモリのサイズを指定して使用します。今回の場合は、float型のメモリ(4byte)を要素の個数分確保することになります。
さらに、malloc関数の戻り値は汎用ポインタというものです。汎用ポインタとは、型の指定されていないポインタのことで、「指示された分のメモリは確保したので、とりあえずその先頭のポインタを返しますよ。」と言われているようなものです。ですので、型がわかる場合はこちらでキャストしなおしてください。
free関数は、malloc関数で確保したメモリを解放する際に使用します。これをしないとメモリが確保されたままになります。ですので、メモリ効率を高めるために、確保したメモリは不要になった時点で解放する習慣をつけましょう。
2. 配列をあらわす変数はポインタ変数として宣言する
malloc関数の戻り値は汎用ポインタですので、確保したメモリの先頭のポインタを受け取るためのポインタ変数が必要です。結果的に、それが配列をあらわす変数(上の例だとArray)になります。なぜなら、変数Arrayは配列の先頭の要素(Array[0])を指すポインタだからです。実際に、上の例からも*ArrayにはArray[0]である1が入っていることが分かります。
3. 作成した配列の各要素は隣あって並んでいる
今回の配列の各要素は4byteのfloat型であるので、上の例からもわかるように各要素のアドレスは4byteおきに並んでいることが分かります。(ちなみにアドレス16進数です。)
3. ポインタを使った2次元配列の作成
この章を書く上で参考にさせて頂いた記事はこちらです。この記事は視覚的にもとても分かりやすかったです。
3.1. 【方法1】 各行のデータを格納する配列と各行へのポインタを格納する配列に分けて確保する
早速コードをみてみましょう。
#include <stdio.h>
#include <stdlib.h> //malloc関数,free関数を使用する際に必要
int main(){
float** Array; //ダブルポインタ型として宣言
int n=5, m=4;
Array = (float**)malloc(n * sizeof(float*)); //Arrayには各行の先頭を指すポインタを格納する
for(int i=0; i<n; i++){
Array[i] = (float*)malloc(m * sizeof(float));
//各行にm個分のメモリを確保してその先頭を指すポインタをArray[i]に格納する
}
/*メモリの確保に失敗した場合は終了する*/
for(int i=0; i<n; i++){
if(Array[i]==NULL){
printf("メモリの確保に失敗しました\n");
exit(1);
}
}
/*各要素に値を代入する*/
for(int i=0; i<n; i++){
for(int j=0; j<m; j++){
Array[i][j] = 10*i+j;
}
}
/*変数の中身をみてみる*/
printf("Array = %p\n", Array); //Arrayの先頭の要素のアドレスが入る
printf("&Array[0] = %p\n\n", &Array[0]);
for(int i=0; i<n; i++){
printf("Array[%d] = %p\n", i, Array[i]);
}
printf("\n*Array = %p\n", *Array);
//Arrayの先頭の要素のアドレスは2次元配列の先頭を指す
printf("&Array[0][0] = %p\n", &Array[0][0]);
/*確保したメモリを解放する*/
for(int i=0; i<n; i++){
free(Array[i]);
}
free(Array);
return 0;
}
出力結果は以下のようになります。
Array = 0x100738750
&Array[0] = 0x100738750
Array[0] = 0x100731b20
Array[1] = 0x100731130
Array[2] = 0x100730260
Array[3] = 0x1007302f0
Array[4] = 0x10072f4a0
*Array = 0x100731b20
&Array[0][0] = 0x100731b20
Program ended with exit code: 0
それではポイントを整理していきます。
1. ダブルポインタを用いて各行の先頭のポインタを格納する配列を用意する
n行m列の配列Arrayを作成するには、m個分の要素を入れる配列がn個数分必要になります。その各行の先頭のポインタが配列Arrayに格納されます。ですので、配列Arrayはポインタを入れる配列で、配列のメモリを確保するためにはそのポインタが必要になるので、変数Arrayはダブルポインタ型になります。
2. 指定した大きさのメモリが確保できているかを確認する
大きな配列を扱うためのメモリを確保する際に、メモリが確保できないことがあります。malloc関数は指定した大きさのメモリが確保できない場合はNULLポインタを返します。ですので、各行の先頭のポインタがNULLポインタになっていないかを確認することで、メモリが確保できているかを確認することができます。
3. メモリ解放時は2種類のメモリを解放する
この方法では、各行の先頭のポインタが入る大きさnの配列を1つ分のメモリと、各要素が入る大きさmの配列をn個分のメモリを確保しました。ですので、メモリの解放時も1+n個分の配列のメモリを解放する必要があります。
3.2. 【方法2】 各行のデータを格納する配列を連続した領域で確保する
こちらの方法も、先にコードを見てみましょう。
#include <stdio.h>
#include <stdlib.h> //malloc関数,free関数を使用する際に必要
int main(){
float** Array; //ダブルポインタ型として宣言
float* BaseArray;
int n=5, m=4;
Array = (float**)malloc(n * sizeof(float*));
BaseArray = (float*)malloc(n * m * sizeof(float));
//各要素が入る配列を連続したメモリで確保
for(int i=0; i<n; i++){
Array[i] = BaseArray + i * m;
//Arrayの各要素にBaseArrayのアドレスをm個ずつずらしながら格納する
}
/*メモリの確保に失敗した場合は終了する*/
if(BaseArray == NULL){
printf("メモリの確保に失敗しました\n");
exit(1);
}
/*各要素に値を代入する*/
for(int i=0; i<n; i++){
for(int j=0; j<m; j++){
Array[i][j] = 10*i+j;
}
}
/*変数の中身をみてみる*/
printf("Array = %p\n", Array); //Arrayの先頭の要素のアドレスが入る
printf("&Array[0] = %p\n\n", &Array[0]);
for(int i=0; i<n; i++){
printf("Array[%d] = %p\n", i, Array[i]);
}
printf("\n*Array = %p\n", *Array);
//Arrayの先頭の要素のアドレスは2次元配列の先頭を指す
printf("&Array[0][0] = %p\n", &Array[0][0]);
/*確保したメモリを解放する*/
free(Array);
free(BaseArray);
return 0;
}
出力結果は以下のようになります。
Array = 0x1005baf30
&Array[0] = 0x1005baf30
Array[0] = 0x1005baf60
Array[1] = 0x1005baf70
Array[2] = 0x1005baf80
Array[3] = 0x1005baf90
Array[4] = 0x1005bafa0
*Array = 0x1005baf60
&Array[0][0] = 0x1005baf60
Program ended with exit code: 0
それではポイントを整理していきます。
1. 連続したメモリをまとめて確保する
この方法ではBaseArrayとして連続したメモリをまとめて確保しています。そのことによるメリットとデメリットは以下の通りです。
○メリット
- 解放時もまとめて解放できる。
- メモリが連続しているため高速化が図れる。
○デメリット
- 連続したメモリが確保できない場合は使えない。
2. ポインタ演算を用いてBaseArrayのアドレスをArrayに格納する
ポインタに足し算でBaseArrayの中の各行の先頭の要素のアドレスを直接指定して、そのポインタをArrayに格納します。こうすることで、BaseArrayとArrayを接続させることができ、2次元配列として使用することができます。
4. まとめ
今回はポインタを使った2次元配列の作成方法についてまとめてみました。C言語始めたてのほとんどの方がポインタにつまづき、「これってどんなことに使えるの?」などと悩むと思います。今回は、その一例として配列を扱いましたが、ポインタについての理解を深めることでもっといろんなことができそうですね。
コメント記事ネタも随時募集してます。