WisdomSoft - for your serial experiences.

6.2 ポインタの演算

ポインタに保存されるアドレスは実質的に整数なので、ポインタを演算することで参照先を変更できます。これを応用することで、配列のような構造的なデータへのポインタから任意の要素を指せます。

6.2.1 アドレスを計算する

ポインタはメモリアドレスを保存する特殊な変数です。ポインタも他の変数と同様にデータ型が存在し、変数のように扱うことができます。重要なのは、ポインタの実体は単純な整数値であり、これを算術演算子で計算をすることができるということです。

とはいえ、でたらめな数字をメモリアドレスとして参照しても、その情報に意味はありません。変数などから取得した正しいアドレス以外を間接参照することはプログラムのクラッシュにつながります。

ポインタの演算は、連続したメモリ領域を確保しているアドレスに対して有効なのです。連続したメモリ領域とは、例えば配列です。配列のような連続した領域を持つ合成体は、メモリアドレスも連続しているため、その範囲を予想することができます。そのため、添字を使う代わりにポインタを演算することで、特定の要素を参照することができるようになるのです。

コード1
#include <stdio.h>

int main() {
	char chStr[] = "Kitty on your lap";
	int iCount;

	for(iCount = 0 ; chStr[iCount] ; ++iCount)
		printf("&chStr[%d] = %p\n" , iCount , &chStr[iCount]);
	return 0;
}
実行結果
コード1 実行結果

コード1は chStr 配列の各要素のメモリアドレスを順に表示します。この結果で興味深いのは配列とアドレスの関係です。面白いことに、アドレスが配列の先頭から順に単純増加しています。

これは、配列が連続したメモリ領域を確保していることを表しています。この性質をうまく利用すれば、ポインタを算術演算子で計算して間接参照を行うことができるのです。上記の例で考えれば、chStr[0] のアドレスに対して 5 を加算すれば chStr[5] のアドレスと同じになります。

コード2
#include <stdio.h>

int main() {
	int iArray[] = { 10 , 100 , 1000 };
	int *iPo = &iArray[0] + 1;

	printf("*iPo = %d\n" , *iPo);
	return 0;
}
実行結果
コード2 実行結果

コード2は、配列が連続したアドレスを持つことを利用し、iPo ポインタに iArray 配列変数の先頭のアドレスを代入する時、加算演算子 + を用いてメモリアドレスに 1 を加算しています。その結果、本来は iArray[0] を示すはずだったアドレスが、この次の iArray[1] に変更されているのです。printf() 関数で iPo ポインタから間接参照した値を出力していますが、結果からポインタが iArray[1] を示していることが確認できるでしょう。

ポインタの演算では、全てのアドレスが 1 バイト単位で割り当てられているということに注意する必要があります。32 ビットコンピュータでは int 型は 4 バイトのメモリ領域を確保します。この場合、int 型変数の次のアドレスは、この変数のアドレスに 4 を加算するということになります。コード1の実行結果が単純に 1 ずつ増加されていたのは chStr 配列変数が 1 バイトの char 型だったからです。

コード3
#include <stdio.h>

int main() {
	int iArray[] = { 2 , 4 };

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

	return 0;
}
実行結果
コード1 実行結果

コード3コード1の結果とは異なり、アドレスが 4 単位で増えていることがわかります。この結果は int 型が 4 バイトのコンピュータで実行したものです。iArray 配列変数は int 型なので、各要素が 4 バイト単位で扱われているのです。

幸いなことに、開発者はこの事実をほとんど気にする必要はありません。例えば iArray[0] へのポインタ iPointer が存在するとして、このポインタを使って iArray[1] のアドレスを算出したい場合、iPointer += 4 とする必要はありません。直観的に iPointer += 1 でよいのです。

6.1 ポインタ」でも説明しましたが、ポインタの型はコンパイラが参照するアドレスを何バイト単位で扱うべきか判断するために利用されます。コンパイラはアドレスの計算が行われた場合、ポインタの型に応じて正しいと思われる演算結果を算出します。32 ビットコンピュータで int 型変数へのポインタに 1 を加算した場合、実際のメモリアドレスには 4 が加算される仕組みになっているのです。

つまり、ポインタに対して算術演算が行われた場合、暗黙的に整数にポインタ型のサイズを乗算した値が評価されるのです。コンパイラがこの計算を行ってくれるため、開発者はポインタ型に応じた面倒なメモリアドレスの計算を行う必要がありません。

コード4
#include <stdio.h>

int main() {
	int iArray[] = { 2 , 4 };
	int *iPo = &iArray[0];

	printf("*iPo = %d : iPo = %p\n" , *iPo , iPo);
	iPo += 1;
	printf("*iPo = %d : iPo = %p\n" , *iPo , iPo);

	return 0;
}
実行結果
コード4 実行結果

アドレスは実行した時に状況に応じて異なりますが、それは重要ではありません。結果を見ると、プログラムを実行した時に iArray 配列変数の先頭要素に割り当てられたメモリアドレスは 0033FDA4 というアドレスでした。int 型は 32 ビットコンピュータで実行されていると想定して 0033FDA4 、0033FDA5、0033FDA6、0033FDA7 の 4 バイトを iArray[0] が占有していると考えられます。

プログラムでは、 iArray 配列変数の先頭要素へのポインタ iPo が定義されています。最初に iPo のポインタが指す値と格納するメモリアドレスを表示しますが、これは当然 iArray[0] の値 2 とアドレス 0033FDA4 が表示されます。次に iPo += 1 によって、iPo に整数 1 を加算していますが、ここで iPo の値が 0033FDA5 ではなく 0033FDA8 となることがポイントです。

これは、2回目の printf() 関数の結果から確認することができます。iPo のアドレスに 1 を加算すると、int 型のサイズ分だけメモリアドレスが進み、正しく 2 番目の要素 iArray[1] を指しています。