0. はじめに
今回はC言語で一番最初につまずくであろうポインタについて整理して、C言語での配列の扱い方についてまとめます。2次元配列は画像処理などでもよく使用されるので、C言語で画像処理をしたい方はぜひ参考にしてみてください。
1. アドレスとポインタ変数
配列に入る前に、アドレスとポインタ変数についておさらいしておきます。
1.1. アドレスとポインタ変数
C言語では、ある変数を宣言する際にはその変数の型や大きさに応じたメモリが割り当てられます。アドレスとは、そのメモリの番地であり、いわゆる住所のようなものです。変数の前に & (アンパサンド)をつければその変数のアドレスを取り出すことができます。
ポインタ変数とは、そのアドレスを格納するための変数のことです。以下でも詳しく説明しますが、ポインタ変数を使用することで、その先の値を取り出すこともできます。
1.2. ポインタ変数の基本的な使い方
それでは具体的にこれらを扱ってみましょう。
#include <stdio.h>
intmain(){inta;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);//ポインタ変数のアドレスにはまた別のアドレスが入っているreturn0;}
出力結果は以下のようになります。
a=3pa=0x7ffeefbff498&a=0x7ffeefbff498*pa=3&pa=0x7ffeefbff490Programendedwithexitcode:0
ここで押さえておくべきことは、以下の3点です。
- ポインタ変数の中にはアドレスが入る。
- ポインタ変数には、指す先の値を * (アスタリスク)で取り出すことができる機能がある。
※ 当たり前ではありますが、ポインタ変数として宣言されていない変数にはこの機能はありません。 - ポインタ変数のアドレスはまた別のアドレスが入っている。
1.3. ポインタ変数を介して値を渡す
ポインタ変数を用いることで、このようにポインタ変数を介した値渡しができます。
#include <stdio.h>
intmain(){inta;int*pa;//ポインタ変数intb;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=3a=4b=3Programendedwithexitcode:0
アドレスとポインタ変数に関するおさらいはこんな感じです。
2. ポインタを使った1次元配列の作成
2.1. メモリの静的確保と動的確保
この記事の本題であるポインタを用いた1次元配列について説明していきたいのですが、その前にメモリの静的確保と動的確保について説明します。
静的確保とは、配列を作成する際に、必要なメモリサイズがコンパイル時点で決まっている際のメモリの確保です。一方、動的確保とは、必要なメモリサイズが実行時にしか決まらない際のメモリの確保です。画像処理などのように、処理するデータによって必要な配列の大きさが異なる際には動的なメモリ確保が必要になります。
下は、実際に配列用メモリの静的確保をしてみた例です。
#include <stdio.h>
intmain(){/*静的確保①*/intA[]={1,2,3,4,5};/*静的確保②*/constintn=5;intB[n];for(inti=0;i<n;i++){B[i]=i;}return0;}
2.2. 1次元配列の動的確保
各要素にfloat型が入る1次元配列を動的に作成した例は以下のようになります。
#include <stdio.h>
#include <stdlib.h> //malloc関数,free関数を使用する際に必要
intmain(){intn=5;float*Array;//行列の先頭をポインタとして定義Array=(float*)malloc(n*sizeof(float));//ポインタArrayの先のメモリを動的に確保printf("Array = %p\n",Array);//A配列の0番目の要素のアドレスが入るfor(inti=0;i<n;i++){printf("Array[%d] = %f &Array[%d] = %p\n",i,Array[i],i,&Array[i]);}/*配列の要素に値を代入する*/printf("\n**奇数の配列を作成**\n");for(inti=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関数で確保したメモリを解放return0;}
出力結果は以下のようになります。
Array=0x10042d310Array[0]=0.000000&Array[0]=0x10042d310Array[1]=0.000000&Array[1]=0x10042d314Array[2]=0.000000&Array[2]=0x10042d318Array[3]=0.000000&Array[3]=0x10042d31cArray[4]=0.000000&Array[4]=0x10042d320**奇数の配列を作成**Array[0]=1.000000Array[1]=3.000000Array[2]=5.000000Array[3]=7.000000Array[4]=9.000000*Array=1.000000Programendedwithexitcode: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関数を使用する際に必要
intmain(){float**Array;//ダブルポインタ型として宣言intn=5,m=4;Array=(float**)malloc(n*sizeof(float*));//Arrayには各行の先頭を指すポインタを格納するfor(inti=0;i<n;i++){Array[i]=(float*)malloc(m*sizeof(float));//各行にm個分のメモリを確保してその先頭を指すポインタをArray[i]に格納する}/*メモリの確保に失敗した場合は終了する*/for(inti=0;i<n;i++){if(Array[i]==NULL){printf("メモリの確保に失敗しました\n");exit(1);}}/*各要素に値を代入する*/for(inti=0;i<n;i++){for(intj=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(inti=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(inti=0;i<n;i++){free(Array[i]);}free(Array);return0;}
出力結果は以下のようになります。
Array=0x100738750&Array[0]=0x100738750Array[0]=0x100731b20Array[1]=0x100731130Array[2]=0x100730260Array[3]=0x1007302f0Array[4]=0x10072f4a0*Array=0x100731b20&Array[0][0]=0x100731b20Programendedwithexitcode: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関数を使用する際に必要
intmain(){float**Array;//ダブルポインタ型として宣言float*BaseArray;intn=5,m=4;Array=(float**)malloc(n*sizeof(float*));BaseArray=(float*)malloc(n*m*sizeof(float));//各要素が入る配列を連続したメモリで確保for(inti=0;i<n;i++){Array[i]=BaseArray+i*m;//Arrayの各要素にBaseArrayのアドレスをm個ずつずらしながら格納する}/*メモリの確保に失敗した場合は終了する*/if(BaseArray==NULL){printf("メモリの確保に失敗しました\n");exit(1);}/*各要素に値を代入する*/for(inti=0;i<n;i++){for(intj=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(inti=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);return0;}
出力結果は以下のようになります。
Array=0x1005baf30&Array[0]=0x1005baf30Array[0]=0x1005baf60Array[1]=0x1005baf70Array[2]=0x1005baf80Array[3]=0x1005baf90Array[4]=0x1005bafa0*Array=0x1005baf60&Array[0][0]=0x1005baf60Programendedwithexitcode:0
それではポイントを整理していきます。
1. 連続したメモリをまとめて確保する
この方法ではBaseArrayとして連続したメモリをまとめて確保しています。そのことによるメリットとデメリットは以下の通りです。
○メリット
- 解放時もまとめて解放できる。
- メモリが連続しているため高速化が図れる。
○デメリット
- 連続したメモリが確保できない場合は使えない。
2. ポインタ演算を用いてBaseArrayのアドレスをArrayに格納する
ポインタに足し算でBaseArrayの中の各行の先頭の要素のアドレスを直接指定して、そのポインタをArrayに格納します。こうすることで、BaseArrayとArrayを接続させることができ、2次元配列として使用することができます。
4. まとめ
今回はポインタを使った2次元配列の作成方法についてまとめてみました。C言語始めたてのほとんどの方がポインタにつまづき、「これってどんなことに使えるの?」などと悩むと思います。今回は、その一例として配列を扱いましたが、ポインタについての理解を深めることでもっといろんなことができそうですね。
コメント記事ネタも随時募集してます。