第60回 プログラミングについて『配列とポインタ/どっちにする?』

よく迷うことに、配列形式で表現するか、ポインタ形式で表現するかの選択です。まず、単純な配列に値を入れる場合を考えてみましょう。
#define MAX_DATA 10
int data[MAX_DATA];
int i;
for(i=0;i<MAX_DATA;i++)data[i]=i; ー(1)
これは、配列の形式で表現したものです。これをポインタの形式で表現すると、(1)は、
for(i=0;i<MAX_DATA;i++)*(data+i)=i; ー(2)
となります。この2つは表現の方法こそ違っているけれども、意味としては全く同じものです。ポインタで表現している方が理解し辛いものになっています。ポインタで表現する場合には、もう1つの方法があります。
#define MAX_DATA 10
int data[MAX_DATA],*d;
int i;
for(i=0,d=data;i<MAX_DATA;i++)*d++ =i; -(3)
このように、データの方のポインタ変数を1つ用意して代入した後にインクリメントする方法です。この方法は基本的には(1)と(2)とは違いますが、この例だけを見る限りは動作上のメリットはあまりなさそうです。
(1)の動作はループごとに、
1. data のアドレスに i*sizeof(int) を加算。
2. 1のアドレスの示す場所に i を代入。
(2)の動作はループごとに、
1. data のアドレスに i*sizeof(int) を加算。
2. 1のアドレスの示す場所に i を代入。
と同じ動作をし、
(3)の動作は、
1. d に sizeof(int) を加算(加算後 d の値とする)。
2. 1のアドレスの示す場所に i を代入。
という動作を行ないます。乗算を行なわない分、動作的には速いかもしれませんが際立った差は出ないと思います。
次に多次元配列の場合を考えてみましょう。
#define MAX_X 10
#define MAX_Y 10
int data[MAX_X][MAX_Y],*d;
int x,y;
for(x=0;x<MAX_X;x++){
for(y=0;y<MAX_Y;y++)data[x][y]=x+y; ー(1)
}
これは配列形式です。これをポインタ形式で表現すると、代入部分は、
d=(int *)data;
for(x=0;x<MAX_X;x++){
for(y=0;y<MAX_Y;y++)*d++ =x+y; ー(2)
}
と表現することができます(このポインタ形式での表現は配列の先頭から順に代入していくので記述が可能です)。
(1)での内側のループでの動作は、
1. data のアドレスに x*sizeof(int)*MAX_Y+y*sizeof(int) を加算。
2. 1のアドレスの示す場所に x+y を代入。
(2)での内側のループでの動作は、
1. d に sizeof(int) を加算(加算後 d の値とする)。
2. 1のアドレスの示す場所に x+y を代入。
となります。この場合では、乗算と加算の回数が少ない分、ポインタ表現の方が速度的には有利になります。ただし、(1)は直感的に理解しやすいけれども、(2)はそうはいきません。
構造体の配列の場合はどうでしょうか。
#define MAX_DATA 10
struct data_strct{
int x,y,z;
};
struct data_strct data[MAX_DATA],*d;
int i;
for(i=0;i<MAX_DATA;i++){ ー(1)
data[i].x=i ;
data[i].y=i+1;
data[i].z=i+2;
}
この配列形式での表現をポインタ形式の表現にすると、
for(i=0,d=data;i<MAX_DATA;i++,d++){ ー(2)
d->x=i ;
d->y=i+1;
d->z=i+2;
}
となります。(*d).x としても同じ意味になります。この場合の動作を見てみると、まず(2)のポインタ形式では、
1. d に x のオフセット値を加算。
2. 1のアドレスの示す場所に i を代入。
3. d に y のオフセット値を加算。
4. 3のアドレスの示す場所に i+1 を代入。
5. d に z のオフセット値を加算。
6. 5のアドレスの示す場所に i+2 を代入。
となります。これに対し配列形式では、
1. data のアドレスに i*sizeof(struct data_strct) を加算。
2. 1に x のオフセット値を加算。
3. 2のアドレスの示す場所に i を代入。
4. data のアドレスに i*sizeof(struct data_strct) を加算。
5. 4に x のオフセット値を加算。
6. 5のアドレスの示す場所に i+1 を代入。
7. data のアドレスに i*sizeof(struct data_strct) を加算。
8. 7に x のオフセット値を加算。
9. 8のアドレスの示す場所に i+2 を代入。
となるのが基本です。コンパイラの最適化によって、次のようになります。
1. data のアドレスに i*sizeof(struct data_strct) を加算。
2. 1の値を t とする。
3. t に x のオフセット値を加算。
4. 2のアドレスの示す場所に i を代入。
5. t に x のオフセット値を加算。
6. 5のアドレスの示す場所に i+1 を代入。
7. t に x のオフセット値を加算。
8. 8のアドレスの示す場所に i+2 を代入。
ということになります。この場合でもポインタ形式の方が速度的には有利なようです。
このようにして考えてみるとポインタ形式の方が利があるようですが、複雑な計算のときには、理解し辛いプログラムになってしまいます。ですので、構造体に代入するときに、メンバーが多いときには次のような折衷方式にする手もあります。
#define MAX_DATA 10
struct data_strct{
int a,b,c,d,e,f,g;
};
struct data_strct data[MAX_DATA],*d;
int i;
for(i=0;i<MAX_DATA;i++){
d=data+i;
d->a=i;
d->b=i;
d->c=i;
d->d=i;
d->e=i;
d->f=i;
d->g=i;
}
例が単純なのであまりメリットはないようですが、配列のポインタ d を順次加算するときよりもプログラムは読みやすくなり、単純になることもあります。
ポインタはC言語の強力な武器ですが、使い方を誤るととんでもなく読み辛いプログラムになってしまうことがあります。私の場合でもケースバイケースで使い分けています。どうも配列の形式で表現しても、ポインタの形式で表現しても実際の実行速度には大きな差が出ることは殆どありませんし、加算が何回、乗算が何回と数えるのもあまり意味がないかも知れません。 速度を高めるにはアルゴリズムが最大に影響するので、どちらの形式を使うかはプログラムの可読性を高めることを最優先した方が賢い選択のような気がします。
ではまた次回。