ポインタは、実際には変数なので、配列にすることもできます。ここでは、ポインタ配列を動的に確保する場合の注意点や、多次元配列と動的確保によって作り出した擬似的な多次元配列との違いなどを解説します。
変数は配列を考えることができますね。たとえば、次の例ではchar型変数10個の配列を宣言しています。
char charray[10];
ポインタといっても変数なので、他の型と同じように配列を宣言して使うことができます。
char* chr_ptr[10];
このポインタの配列とは何なのでしょうか。2次元配列のことをポインタの配列だという人がたまにいますが、ちょっと違います。ポインタの配列は2次元配列などではなくあくまでもポインタ変数の配列で、上のように宣言したものは1次元配列です。メモリの確保のされ方からいって、2次元配列とポインタ配列は全く違うものです。
ポインタ配列がどのようにメモリ上に確保されるかを確かめるため、次のようなプログラムを作ってみました。
#include <stdio.h>
#define N 5
int main(void)
{
char* parray[N];
int i;
printf("ポインタのサイズは %d バイト\n",sizeof(char*));
printf("先頭アドレスは %p\n",parray);
for(i=0;i<N;i++) printf("%p\n",&parray[i]);
return 0;
}
私の環境では、次のような結果になりました。処理系によりポインタのサイズは異なることがあります。この場合は32ビットの処理系なので4バイトになっています。また先頭アドレスの値はたまたまこうなっただけで、場合によって違うでしょう。
ポインタのサイズは 4 バイト 先頭アドレスは 0012FF78 0012FF78 0012FF7C 0012FF80 0012FF84 0012FF88
ポインタのサイズが4バイトなので、要素ひとつにつき4バイト必要で、それが配列として確保した5個隙間なく並んでいます。これがポインタ配列です。
メモリの領域をプログラム中で動的に確保するには、通常はmalloc()関数を使います。
#include <stdio.h>
#include <stdlib.h>
#define N 5
int main(void)
{
char* arr;
int i;
arr = (char*)malloc(N * sizeof(char));
for(i=0;i<N;i++) printf("%p\n",&arr[i]);
return 0;
}
メモリを確保するときには、malloc()関数を用いて、上のようにしてメモリを確保する例をよく目にすると思います。こういった例ではint型を使っていることが多いのですが、int型とポインタ型が使用するサイズが同じになっている処理系が多いので、混乱を避けるためにここではあえてchar型でメモリを確保しています。sizeof(char)の戻り値は1、つまりchar型は1バイトです。これはANSIで決まっていることなので、少なくともANSI以降の標準に準拠したCコンパイラなら確実です。
上のプログラムを実行すると、次のようになりました。
00B72CB4 00B72CB5 00B72CB6 00B72CB7 00B72CB8
確保した領域の先頭アドレスは00B72CB4で、そこから確保した数(ここでは5個)のchar変数が入る領域が連続して確保されます。もちろん先頭アドレスの値はたまたまこうなっただけです。この領域はarr[0]からarr[4]まで、配列のような指定方法で実体を指定し使うことができます。ここまではどこにでもある解説で、特に問題はないと思います。
ポインタ配列といっても、動的確保の考え方は同じです。ポインタ変数が入る大きさのメモリ領域を指定した数だけ確保します。ただし、実際の確保の仕方は次のようになります。ポインタの配列を動的に確保するには、ポインタのポインタ(ダブルポインタ?)が必要になります。ややこしいのですが、理由を考えてみてください。
#include <stdio.h>
#include <stdlib.h>
#define N 5
int main(void)
{
char** arr;
int i;
arr = (char**)malloc(N * sizeof(char*));
for(i=0;i<N;i++) printf("%p\n",&arr[i]);
return 0;
}
結果は次のようになりました。上の節のchar配列とアドレスを見比べてみてください。プログラムを実行した処理系のポインタのサイズが4バイトだったので、ひとつの要素につき4バイトの領域が確保されています。
00B72CB4 00B72CB8 00B72CBC 00B72CC0 00B72CC4
char型変数などの実体のための領域を確保するときには、char型に対応するポインタを用いました。malloc()関数で領域を確保し、確保された領域の先頭アドレスをポインタに代入しています。これは特に不思議な点はないですね。では、char型に対応するポインタのための領域を確保するにはどうしたらいいのでしょうか。
char型に対応するポインタを用いて、char型に対応するポインタのための領域の動的確保を行うとします。すると、重大な矛盾が生じます。
たとえば上の例でarrがchar**ではなくchar*であったとして、arr = (char*)malloc(N * sizeof(char*));のようにしたとしましょう。確かにこれでchar型に対応するポインタが5個収まるだけの大きさを持った領域を確保することはできます。しかし、ポインタarrはあくまでも「char型に対応する」ものであって、「char型に対応するポインタに対応する」ポインタではありません。
ここで、ポインタの性質を思い出してください。
ポインタ変数は、対応する型のサイズ単位で値が増減する変数である。
これを確かめるために、上のプログラムでchar**で宣言すべきポインタ変数arrをchar*に変えてみました。
#include <stdio.h>
#include <stdlib.h>
#define N 5
int main(void)
{
char* arr;
int i;
arr = (char*)malloc(N * sizeof(char*));
for(i=0;i<N;i++) printf("%p\n",&arr[i]);
return 0;
}
何度も述べるように、char型のサイズは1バイトです。つまり、char型に対応するポインタ変数の値は1(バイト)を変化の最小単位として増減します。一方、ポインタ変数のサイズは処理系に依存します。このプログラムを実行したBorland C++ 5.5.1では4バイトです。
00B72CB4 00B72CB5 00B72CB6 00B72CB7 00B72CB8
ポインタを1個格納するには、4バイトの領域が必要です。しかし、上の実行結果を見るとわかるように、要素1個につき1バイトしか確保されていません。arr[1]として「ポインタ配列の2番目の要素」を指定したつもりが、「ただの2バイト目」ということになってしまうのです。これは、char*で宣言されたポインタによって位置を指定される実体がcharであり、そのポインタはsizeof(char*)の単位で値が増減するということです。つまり、上の結果として00B72CB4という値が入っているポインタarrをarr++;と1回インクリメントすると、00B72CB5になるということです。もちろん、このようなポインタ配列のできそこないを使用したプログラムを実行するとエラーになります。
したがってchar*を実体とし、sizeof(char*)の単位で値が増減するするポインタは、char**で宣言されなければなりません。ということで、ポインタ配列を動的に正しく確保するためには、ポインタのポインタを使わなければならないのです。
多重ポインタを使ってmalloc()などの関数による領域の動的確保を繰り返すことで、擬似的な多次元配列を実現できます。この擬似的な多次元配列と通常の多次元配列の違いを説明します。
次のプログラムは、2次元配列として3バイトのchar配列を3個確保し、使用しているメモリアドレスを表示します。
#include <stdio.h>
#include <stdlib.h>
#define N 3
int main(void)
{
char arr[N][N];
int i,j;
printf("arr の先頭アドレスは %p\n",arr);
for(j=0;j<N;j++){
printf("arr[%d]:%p ",j,arr[j]);
for(i=0;i<N;i++){
if(i) printf("--------------- ");
printf("&arr[%d][%d]:%p\n",j,i,&arr[j][i]);
}
}
return 0;
}
実行結果は次のようになります。メモリアドレスの値そのものは場合により異なりますが、多次元配列はこのように連続したメモリ領域に確保されます。したがって、arr[1][0]は、arr[0][3]のように指定することもできます。
arr の先頭アドレスは 0012FF80 arr[0]:0012FF80 &arr[0][0]:0012FF80 --------------- &arr[0][1]:0012FF81 --------------- &arr[0][2]:0012FF82 arr[1]:0012FF83 &arr[1][0]:0012FF83 --------------- &arr[1][1]:0012FF84 --------------- &arr[1][2]:0012FF85 arr[2]:0012FF86 &arr[2][0]:0012FF86 --------------- &arr[2][1]:0012FF87 --------------- &arr[2][2]:0012FF88
同じように、3バイトのchar配列を3個、動的に確保するにはどうすれば良いでしょうか。次のプログラムを見てください。
#include <stdio.h>
#include <stdlib.h>
#define N 3
int main(void)
{
char** arr;
int i,j;
arr = (char**)malloc(N * sizeof(char*)); /* ポインタ配列を確保 */
/* 配列の要素それぞれにつき、メモリ領域を確保 */
for(i=0;i<N;i++) arr[i] = (char*)malloc(N * sizeof(char));
printf("char*,char**のサイズは %d,%d\n",sizeof(char*),sizeof(char**));
printf("arr の先頭アドレスは %p\n",arr);
for(j=0;j<N;j++) printf("&arr[%d]:%p,arr[%d]:%p\n",j,&arr[j],j,arr[j]);
for(j=0;j<N;j++){
printf("arr[%d]:%p ",j,arr[j]);
for(i=0;i<N;i++){
if(i) printf("---------------- ");
printf("&arr[%d][%d]:%p\n",j,i,&arr[j][i]);
}
}
return 0;
}
char*,char**のサイズは 4,4 arr の先頭アドレスは 00B72CB0 &arr[0]:00B72CB0,arr[0]:00B72CC0 &arr[1]:00B72CB4,arr[1]:00B72CD0 &arr[2]:00B72CB8,arr[2]:00B72CE0 arr[0]:00B72CC0 &arr[0][0]:00B72CC0 --------------- &arr[0][1]:00B72CC1 --------------- &arr[0][2]:00B72CC2 arr[1]:00B72CD0 &arr[1][0]:00B72CD0 --------------- &arr[1][1]:00B72CD1 --------------- &arr[1][2]:00B72CD2 arr[2]:00B72CE0 &arr[2][0]:00B72CE0 --------------- &arr[2][1]:00B72CE1 --------------- &arr[2][2]:00B72CE2
実行結果は上のようになります。
arr = (char**)malloc(N * sizeof(char*));
の部分で、まずchar型に対応したポインタが3個収まるだけの領域が確保されます。arrは先頭アドレスが0x00B72CB0で、arr[0]からarr[2]までの3個の要素を持つポインタ配列と捉えることができます。この時点では領域が確保されただけなので、各要素の値は不定です。
for(i=0;i<N;i++) arr[i] = (char*)malloc(N * sizeof(char));
その後、arr[0]からarr[2]までの各要素について、このように3バイトの領域を確保します。領域の確保が成功すると、ポインタ配列arrのそれぞれの要素の値は、確保された領域の先頭アドレスになります。
このように、多重ポインタを用いてまずポインタ配列を確保し、配列の各要素であるポインタを用いて領域を確保することで、多次元配列と同じように扱えるメモリ領域を確保することができます。しかし、通常の多次元配列のような連続した領域が確保されるわけではありません。
実行結果を見るとわかるように、擬似的な多次元配列の場合、確保に使った多重ポインタが値として持つアドレスが、配列(メモリ領域)の先頭アドレスになるわけではありません。このポインタは、ポインタ配列の先頭を示しているだけです。
この例の擬似的な2次元配列の場合、ポインタ配列の各要素が領域の先頭アドレスを保持しているポインタになっているので、arr[1][2]のような指定は、「ポインタ配列の2番目の要素が示すアドレスを先頭とする領域の」「3番目の要素」という意味になります。見た目には通常の2次元配列の指定方法と同じになりますが、実際のメモリ領域の確保のされ方は全く違うことに注意してください。