2012年6月13日水曜日


第四回:ポインタ(3)実用編2-配列とポインタ

 ポインタを多く使う場面の一つに配列があげられる。中には、配列を扱う際にそれと知らずにポインタを用いたりポインタの型変換を行ったりしているケースもあるだろう。
 今回はポインタと配列に関するちょっと踏み込んだ説明を行ってみようと思う。

  1. 配列の例

 まず一般的な配列の例を示す。

#include


int main(int argc, char * argv[])
{
int arrayScore[] = { 70 , 60 , 85 , 92 , 66 , 45 , 88 , -1 };
int i;
int sum = 0;
float avg;


for( i=0 ; arrayScore[i] >= 0 ; i++ )
{
sum += arrayScore[i];
}


avg = (float)sum / i;
printf( "Score Total : %d\n" , sum );
printf( "Score Average : %.1f\n" , avg );


return 0;
}

実行結果
Score Total : 506
Score Average : 72.3

 配列は一般的に、同一の型のデータを複数格納する連続した箱のようなものである。上記の例ではarrayScoreと言う配列を定義し、そこに数値を格納している。C言語において、上記のように初期データを指定して配列の定義を行うと、配列の要素数は指定した初期データの個数とおなじになる。この例では終端の-1を含め8個のデータがある。int型のデータサイズを4バイトとすると、上記の定義により32バイトの領域がスタック領域に確保されることになる。


  1. 配列とポインタ

 前項で紹介した配列のメモリ上の記述とポインタについて理解するために、以下のプログラムを実行してみる。
#include


int main( void )
{
int arrayScore[] = { 70 , 60 , 85 , 92 , 66 , 45 , 88 , -1 };

printf( "arrayScore = %p\n", arrayScore );
printf( "&arrayScore[0] = %p\n", &arrayScore[0] );

printf( "&arrayScore[1] = %p\n", &arrayScore[1] );
printf( "arrayScore+1 %p\n", arrayScore + 1 );

return 0;
}


実行結果
arrayScore = 0xbfbfec34
&arrayScore[0] = 0xbfbfec34
&arrayScore[1] = 0xbfbfec38
arrayScore+1 0xbfbfec38


 前項で説明したとおり、「arrayScore」と記述すると配列の最初の要素へのポインタとなる。それで、「arrayScore」と「&arrayScore[0]」とでは同じ場所を指していることがわかる。
 次の「&arrayAcore[1]」に関しては、私の実行しているシステムのint型が4バイトの長さであるため、arrayScore[0]の確保領域から4バイト足した場所を指していることがわかる。
 問題は最後の「arrayScore+1」の部分である。arrayScoreが示すアドレスに+1しているので、1バイト増加したアドレスを指しそうなものであるが、この結果を見るとarrayScore[1]と同じ場所を指している(つまり4バイト増加している)。
 ポインタの加算・減算を行った場合には、そのポインタの示す型分の加算・減算が行われる。
 今回の例で、arrayScoreint型の配列であり、「arrayScore」と書くと「int *」型のポインタを意味する。それで、「arrayScore+1」とした場合には「arrayScore」の指すアドレスに対してint型のサイズ分である4バイトが加算されるということになる。結果、「arrayScore+1」は「&arrayScore[1]」とおなじになる。
 次に以下のプログラムを実行してみる。
#include


int main( void )
{
int arrayScore[] = { 70 , 60 , 85 , 92 , 66 , 45 , 88 , -1 };

printf( "arrayScore = %p\n", arrayScore );
printf( "arrayScore+1 = %p\n", arrayScore+1 );
printf( "((char*)arrayScore) + 1 = %p\n", ((char*)arrayScore) + 1 );
printf( "((double*)arrayScore) + 1 = %p\n", ((double*)arrayScore) + 1 );

return 0;
}


実行結果
arrayScore = 0xbfbfec34
arrayScore+1 = 0xbfbfec38
((char*)arrayScore) + 1 = 0xbfbfec35
((double*)arrayScore) + 1 = 0xbfbfec3c
 この例ではポインタに対する演算を行なっている。
 最初の表示はarrayScore配列の先頭要素へのアドレスとなる。
 2行目の表示はarrayScoreを通常通りint型のポインタとみなして+1した結果となる。これは先程の説明通りarrayScoreのアドレスに対して4バイト後ろのアドレスとなる。
 3行目の表示はarrayScoreを一端char型へのポインタにキャストし、それに+1している。
 ポインタが示すものはアドレスであるので、そのアドレスから始まるデータをint型とみなして処理を行うかその他の型とみなして処理を行うかをプログラム上で指定することができる。これをキャストという( キャストには他にも多くの効能があるため、後日改めて解説することになるかと思うが、このキャストはJavaVisualBasicC#などのポインタが前面に出てきていない言語でも頻繁に使われる)。
 今回の例では「arrayScoreは実はchar型へのポインタなんだよ」と教えた上で+1をしていることになる。このため、3行目の表示ではarrayScoreのアドレスの1バイト後ろのアドレスが表示されている。
 4行目の表示はarrayScoredouble型へのポインタにキャストして+1したものである。double型は私の使っているシステムでは8バイトの長さを持つため、arrayScoreのアドレスの8バイト後ろのアドレスが表示される。


 次に以下のプログラムを実行してみる
#include


int main( void )
{
int arrayScore[] = { 70 , 60 , 85 , 92 , 66 , 45 , 88 , -1 };
int i;

for( i=0 ; arrayScore[i] >= 0 ; i++ )
{
printf( "arrayScore[%d] = %d\t*(arrayScore+%d) = %d\n"
, i , arrayScore[i] , i , *(arrayScore+i) );
}

return 0;
}


実行結果
arrayScore[0] = 70 *(arrayScore+0) = 70
arrayScore[1] = 60 *(arrayScore+1) = 60
arrayScore[2] = 85 *(arrayScore+2) = 85
arrayScore[3] = 92 *(arrayScore+3) = 92
arrayScore[4] = 66 *(arrayScore+4) = 66
arrayScore[5] = 45 *(arrayScore+5) = 45
arrayScore[6] = 88 *(arrayScore+6) = 88


(arrayScore+1)が配列の次の要素へのポインタであるなら、*(arrayScore+1)は配列の次の要素へのポインタの中身であるからarrayScore[1]と同じになる。これは、どちらも同じ意味になるため、書き方は好みの問題となる。


 最後にポインタの第1回で出てきたエンディアンの話をしようと思う。
 私が使っているインテル系のシステムはリトルエンディアンを採用している。これは変数をメモリ上に格納する時に、下位バイトから格納するという方式のことである。例えば、int型の変数aの中に「123456789」という10進数の数字が格納されているとする。これを16進数に直すと「075BCD15」となる。メモリ上は1バイトごとの格納となるため、この16進数は「07」「5B」「CD」「15」の4バイトに分けられる。ビックエンディアンのコンピュータではこの数字を並び順通り「07」「5B」「CD」「15」と格納する。私が使っているリトルエンディアンのコンピュータでは「15」「CD」「5B」「07」という具合に下位バイトから順に格納する。
 このリトルエンディアン/ビッグエンディアンの判別を今回解説した配列とポインタの技術を使って確認することができる。以下のプログラムを実行していただきたい
#include


int main( void )
{
int a = 123456789;
size_t sizeInt = sizeof(int);
size_t i;
unsigned char * p = (char*)&a;

printf( "%d is ...\n", a );

for( i = 0; i < sizeInt ; i++ )
{
printf( "%02x ", *(p+i) );
}
printf( "\n" );

return 0;
}


実行結果
123456789 is ...
15 cd 5b 07


 この例では、int型の変数aを定義し、その中に123456789を格納している。
 int型の変数aのポインタをchar型のポインタにキャストしてpに格納している。この動作により、pchar型の4バイトの長さを持つ配列となる。
 sizeInt=sizeof(int)を行うことによって、int型のサイズ(4)がsizeIntという変数に格納されるので、sizeInt分だけのループを回して、pの中身を見ている。
 実行結果を見ていただくと、メモリ上に下位バイトから順に値が入っていることがわかる。


まとめ
 配列とポインタに関する考察を行った。
 配列の要素名だけを書くとその配列の先頭要素へのポインタを示すことを確認した。
 ポインタに加算・減算を行った場合、そのポインタの型に応じたアドレスの演算が行われることを確認した。
 ポインタに対してキャストを行い、別の型のポインタのように扱うことができることを確認した。


 次項では構造体に対するポインタの扱いと、構造体の配列に対するポインタについて示す。