WisdomSoft - for your serial experiences.

6.3 配列とポインタ

配列を指すポインタを計算し、任意の要素に間接参照する方法を解説します。

6.3.1 配列に間接参照する

これまで、配列の先頭要素のアドレスを取得するために &iArray[0] というような書き方をしてきました。しかし、実は配列変数というのは特別で、変数名だけを指定すると配列の先頭へのアドレスを表すことになっています。例えば、これまでは配列変数 iArray の先頭要素のメモリアドレスを取得するには、次のように記述しました

int *iPo = &iArray[0];

これは間違いではありませんが、単に配列名から先頭へのアドレスを取得できるため、以下のように書くことができます。

int *iPo = iArray;

詳しく触れませんでしたが、以前 printf() 関数に %s で文字列を表示する場合、配列名だけを渡していた理由がここにあります。printf() 関数の書式指定文字 %s に対応した引数は、文字配列へのポインタでなければならなかったのです。通常はアドレス演算子を使ってアドレスを取得しますが、文字配列の場合はつけませんでした。なぜならば、配列変数の場合は、添字省略の変数名は配列の先頭アドレスを表すからだったのです。

ただし、配列変数の変数名はポインタではないので勘違いしないように注意してください。添字を省略した配列変数の名前は、先頭要素のアドレスを表す定数のようなものであり、これに何らかの値を代入することはできません。

コード1
#include <stdio.h>

int main() {
	char chStr[] = "Kitty on your lap";
	printf("&chStr[0] = %p : chStr = %p\n" , &chStr[0] , chStr);

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

コード1は &chStr[0] という記述と chStr が同一のアドレスを表していることを証明しています。printf'() 関数は &chStr[0] と chStr を引数で受け取り、これを 16 進数で画面に表示します。プログラムは、まったく同じ数値を画面に表示することでしょう。

この性質を利用すれば、添字の代わりに間接演算子 * を使って配列を制御することができるようになります。配列がメモリ上でどのように配置されているのかという本質に関わるため、この手法による要素へのアクセスを理解することは極めて重要です。次の文は、配列変数 iArray のうち、iArray[2] に間接演算子を使って参照しています。

*(iArray + 2)

このとき注意しなければならないのですが、間接演算子 * は加算演算子 + よりも優先順位が高いため、括弧をはずしてしまうと先に * iArray が評価されてしまいます。そのため、括弧で iArray + 2 を先に評価するように記述しなければなりません。このように、演算と間接参照が同一の式に存在する場合は、* 演算子の優先順位に十分注意する必要があります。特にインクリメント演算子と間接参照演算子を同時に使う場合、*iPo++ と記述すると iPo ポインタのアドレスをインクリメントすることを表し、(*iPo)++ と記述すれば iPo が指す内容をインクリメントするということを表すことになります。こうした微妙な違いで戸惑うことが多いため、気をつける必要があります。

コード2
#include <stdio.h>

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

	printf("間接参照=%d,%d,%d\n", *iArray , *(iArray + 1) , *(iArray + 2));
	printf("添字指定=%d,%d,%d\n" , iArray[0] , iArray[1] , iArray[2]);
	return 0;
}
実行結果
コード2 実行結果

コード2を実行すれば *(iArray + 1) と iArray[1] や *(iArray + 2) と iArray[2] が同じ値を参照していることを確認することができます。

実質的に iArray[index] という添字演算は *(iArray + index) と同じ操作です。そのため、iArray[index] と *(iArray + index) は同じであると考えることができます。同様に、&iArray[index] という文は iArray + index と同一です。これは、ポインタと配列の関係において最も重要な関係なので、十分理解してください。

この性質を逆に考えると、添字を指定して配列変数の要素にアクセスするということは、間接参照であるということです。ということは、配列へのアドレスを格納するポインタ iPo からも iPo[index] というように添字指定で間接参照を行うことができるということです。

6.3.2 多次元配列とポインタ

記憶領域とアドレスの関係は 1 次元の配列のようなものです。多次元配列とはプログラミング言語が概念上の階層化を行うためのものであり、物理的に(機械語レベルで)多次元化されるものではありません。2 つの要素を持つ 2 次元配列 Array[2][2] は、4つの要素を持つ配列 Array[4] と要素の総数が同一であるため、割り当てるメモリサイズは同じなのです。

コード3
#include <stdio.h>

int main() {
	char chArray[2][2] = { 2 , 4 , 8 , 16 };

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

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

コード3は多次元配列のアドレス構造を理解するためのものです。まず、添字を省略した配列名が常に配列の先頭のアドレスを示すのは同じです。2 次元配列において配列の先頭とは [0][0] 要素のことです。chArray の返す値が &chArray[0][0] が返す値と同じであることを確認することができます。

注目するべきは、各要素が返すメモリアドレスです。2 次元配列といっても、割り当てられているメモリは一列であることがこの結果から確認することができます。多次元配列でも、通常の 1 次元配列同様に連続したメモリ領域に割り当てられているのです。

この性質が分かれば、多次元配列でも目的の次元と要素にポインタからアクセスすることができるようになります。例えば 2 次元配列であれば次のような計算式を用いる方法があります。

*(iPointer + (一次元添字 * 二次元要素数) + 二次元添字)

繰り返し文のカウンタを利用して多次元配列を処理するというようなことが要求された場合は、このような方法で間接参照することができるでしょう。ところが、多次元配列の添字を省略した変数名から間接参照を試みると、意外な結果が得られます。次の文は、おそらく予想したものとは異なる結果となるでしょう。

int iArray[2][2] = { 1 , 2 , 3 , 4 };
printf("%d" , *iArray);

この場合 iArray は配列の先頭 iArray[0][0] のアドレスを示すため、 *iArray は 1 という結果を間接参照してくれることを期待しますが、まったく違う値を表示するでしょう。添字を省略した変数名 iArray を printf() 関数でためしに表示させてみると、確かに &iArray[0][0] に等しいにもかかわらず、間接参照で得られる値は iArray[0][0] が格納する値ではないのです。これは、極めて不思議な現象ですね。

以前、iArray[index] という文は *(iArray + index) と同じであると説明しましたが、これを逆に考えれば、多次元配列における *iArray は iArray[0] に等しいということです。これは次元を指定してますが要素を指定していません。2 次元配列変数において iArray[0] を指定した場合は、iArray[0][0] のアドレスを返します。そのため、上記した例文は期待した結果の 1 ではなく、&iArray[0][0] を表示することになるのです。

これを解決するひとつの方法としては iArray 2次元配列変数を (int *) 型にキャストしてしまうことです。そうすれば、次元を気にする必要なく、直接 int 型の変数への間接参照として各要素にアクセスすることができます。

コード4
#include <stdio.h>

int main() {
	int iCount1 , iCount2;
	int iArray[][9] = {
		{ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 } ,
		{ 2 , 4 , 6 , 8 , 10 , 12 , 14 , 16 , 18 } ,
		{ 0 } ,
	};
	int *iPo = (int *)iArray;

	for(iCount1 = 0 ; *(iPo + (iCount1 * 9)) ; iCount1++) {
		for(iCount2 = 0 ; iCount2 < 9 ; iCount2++) {
			printf("%d " , *(iPo + (iCount1 * 9) + iCount2));
		}
		printf("\n");
	}

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

コード4は for ステートメントを使って 2 次元配列を処理しますが、配列へのアクセスは添字ではなく、ポインタの演算による間接参照で実現しています。iArray を直接演算した場合は、間接参照を行っても要素のアドレスが返ってきてしまうため、一度 int 型ポインタ iPo にキャストしてからこれを操作しています。型変換は明示的に行う必要はありませんが、コンパイラから不正なポインタ操作であることを警告される可能性があるため、(int *)iArray というように明示的に変換しています。

このプログラムは、iArray の各要素を画面に表示し、第一番目の要素が 0 だった場合に処理を終了させます。

6.3.3 関数とポインタ

標準的な設計では、配列や構造体などの大きなデータを関数に受け渡しする方法としてポインタを利用します。ポインタを利用しないのは値を返さない入力目的の単純型を使う場合のみで、例えば char、int、double といった型の入力時です。

文字列やデータ配列を受け取ったり、呼び出し元に返す場合、関数はポインタを受け取るべきであると考えられています。ポインタではなく通常の配列として渡すことも可能なのですが、この場合はサイズが明確に決定されている必要があり不便です。ポインタ以外の方法で合成体を受け渡しする場合は、関数を呼び出すたびに配列などを丸ごとコピーする必要になり、これは実行速度やメモリに深刻な負担をかけることになります。この方法にメリットはないため、上級者は迷うことなくポインタで情報を交換するべきだと判断するのです。

コード5
#include <stdio.h>

void stringToLower(char *chpStr) { 
	int iCount;
	for(iCount = 0 ; *(chpStr + iCount) ; iCount++) {
		if (*(chpStr + iCount) > 0x40 && *(chpStr + iCount) < 0x5B)
		*(chpStr + iCount) += 0x20;
	}
}

int main() {
	char chStr[] = "~KITTY on YOUR lap~";
	stringToLower(chStr);

	printf("%s\n" , chStr);
	return 0;
}
実行結果
コード5 実行結果

このプログラムの関数 tolower() には、引数に文字列へのポインタを指定します。tolower() 関数は、与えられたポインタから文字に間接参照し、その文字がアルファベットの大文字かどうかを ASCII コードの性質を利用して調べています。ASCII コードのアルファベットの大文字に 16 進数の 0x20 を加算すると、そのまま小文字になるため、これを利用して、文字列内に大文字が存在した場合にこれを小文字に変換します。すなわち、この関数の役割は、与えられた文字配列の小文字に変換できる文字を全て小文字にすることです。ただし、日本語には対応していないので全角文字は与えないで下さい。

因みに、tolower の関数定義のパラメータにある

char *chpStr

という宣言は、次のように記述することもできます。

char chpStr[]

この 2 つは、関数のパラメータでのみ同一であると解釈されますが、前者の char *chpStr という記法の方が、引数がポインタであることを明確に示しているため推奨されます。