C言語編 第36章 ポインタE(データ構造)

先頭へ戻る

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

多重間接参照

ポインタ変数が保持している情報は、メモリ上のアドレスに過ぎません。 従って、メモリ上に存在するものであれば、何でも指し示すことが可能です。

それでは、ポインタ変数が、他のポインタ変数を指し示すことは可能でしょうか? ポインタ変数自身、変数であることに変わりは無いですし、当然、ポインタ変数自身もメモリ上に存在するのですから、これも可能です。 このように、ポインタ変数が別のポインタ変数を指し示すような使い方は、 俗に「ポインタのポインタ」と呼ばれています。

ポインタのポインタを宣言するには、次のように書きます。

int** pp;

このように、ポインタ変数であることを表す * を 2つに増やせば、ポインタのポインタを表すことになります。 ご察しのように、* の個数を更に増やせば、「ポインタのポインタのポインタ」のようなこともできます

ポインタのポインタに、アドレスを代入するには、

int num  = 100;
int* p   = #
int** pp = &p;

このような感じで書きます。

変数num はポインタではないので、初期値として渡している 100 はアドレスではなく、単なる 100 という数値データです。
変数p はポインタなので、アドレスを渡します。ここでは、変数num のアドレスを渡しています。
変数pp はポインタのポインタなので、ポインタ変数のアドレスを渡します。ここでは、ポインタ変数p のアドレスを渡しています。

今書いたことを理解しておくのは重要です。 「ポインタのポインタ」になった途端に、イメージが掴めなくなる人は多いと思いますが、 「この変数に格納できるものは、単なる値なのか、アドレスなのか、ポインタのアドレスなのか」を常に考えるようにすれば、ある程度は頭を整理できるはずです。

次に、間接参照です。

printf( "%p\n", pp );   /* pp が保持しているポインタ(p) のアドレス */
printf( "%p\n", *pp );  /* pp が保持しているポインタ(p) が保持している変数(num) のアドレス */
printf( "%d\n", **pp ); /* pp が保持しているポインタ(p) が保持している変数(num) の値 */

間接参照の際にも、*演算子を 2つ以上重ね合わせることができます。 2つ重ねれば、2つ先まで間接参照するという意味合いになるので、ポインタのポインタpp から、ポインタp を経由して、更にその先にある変数num が参照できる訳です。 このように、2つ以上先まで間接参照することを、多重間接参照と呼びます。

3つ先なら ***ppp、4つ先なら ****pppp といった具合に使うことはできますが、普通、2つ先が限度です。 もし 3段階以上の間接参照を行うことになったなら、そのプログラムは少々病的な状態になっていると考えられます。 ほとんどの場合、プログラムの構造を整理すれば、2段階の間接参照までで事足りるはずです。

また、間接参照の段階(レベル)を意識するように考えると理解しやすいでしょう。 「int**」として宣言されたポインタのポインタがあるとすれば、これは 2 というレベルを持っていると考えます。 そして、アドレス演算子& が登場するたびにレベルは +1 され、間接参照演算子* が登場するたびに -1 されます。
このように考えたとき、レベルが 0 のときにはどこも指し示していない(=ポインタではなく、アドレスを保持していない)ということになります。

int num  = 100;  /* どこも指し示していないので レベル0 */
int* p   = # /* レベル0 である num に &演算子を付けて レベル1 */
int** pp = &p;   /* レベル1 である p に &演算子を付けて レベル2 */

printf( "%p\n", &pp );  /* レベル2 である pp に対する &演算子によって、レベル3 */
printf( "%p\n", *pp );  /* レベル2 である pp に対する *演算子によって、レベル1 */

この考え方を思い出せば、自分が今、どこを指し示しているつもりでいるのかイメージしやすくなるはずです。 どうしてもコンパイルエラーが取れないだとか、思った通りの動作にならないときには、落ち着いて、今のレベルを確認すると良いでしょう。


最後に、プログラムの全体像を載せておきます。

#include <stdio.h>

int main(void)
{
	int num  = 100;
	int num2 = 200;
	int* p   = &num;
	int** pp = &p;


	printf( "%p\n", pp );   /* pp が保持しているポインタ(p) のアドレス */
	printf( "%p\n", *pp );  /* pp が保持しているポインタ(p) が保持している変数(num) のアドレス */
	printf( "%d\n", **pp ); /* pp が保持しているポインタ(p) が保持している変数(num) の値 */

	num = 999;
	printf( "%d\n", **pp ); /* pp が保持しているポインタ(p) が保持している変数(num) の値 */

	p = &num2;
	printf( "%d\n", **pp ); /* pp が保持しているポインタ(p) が保持している変数(num2) の値 */

	return 0;
}

実行結果:

0032FC28
0032FC40
100
999
200

文字列へのポインタ

文字列も配列なので、文字列へのポインタは、自然とポインタのポインタになります。 これが、ポインタのポインタの必要性が最もよく現れる場面でしょう。

#include <stdio.h>

#define SIZE_OF_ARRAY(array)	(sizeof(array)/sizeof(array[0]))

void printMessage(char** messages, size_t max);

int main(void)
{
	char* messages[] = {
		"OK!",
		"Error!",
		"Hello, World!",
		"Thank you!",
		"Good Bye!"
	};


	printMessage( messages, SIZE_OF_ARRAY(messages) );

	return 0;
}

/*
	メッセージを一覧表示 
	引数:
		messages:	表示するメッセージへのポインタを保持するポインタ配列。
		max:		messages に含まれる要素数。
*/
void printMessage(char** messages, size_t max)
{
	size_t i;
	
	for( i = 0; i < max; ++i ){
		puts( messages[i] );
	}
}

実行結果:

OK!
Error!
Hello, World!
Thank you!
Good Bye!

printMessage関数の第1引数は、ポインタのポインタになっています。 この場合、配列へのポインタですから、printMessage関数の宣言は、

void printMessage(char* messages[], size_t max);

このように記述することも可能です。 第33章で説明したように、 関数の仮引数に限ってのみ、[] を使っても * を使っても同じ意味になります
[] を使う方法は、配列へのポインタであることを明確に表現できるという利点があります。

ところで本来ならば、このサンプルプログラムにおいて、変数messages や、printMessage関数の第1引数には、 const修飾子を付けることをお勧めします。 const修飾子については、次章で説明します。

多次元配列

第25章で配列について解説して以来、たびたび配列は登場していました。 実際、配列をまったく使わないプログラムの方が少ないと言えます。

配列のイメージは、メモリ上に連続的に要素が並んだ状態です。 例えば、array[6] という配列であれば、次のようなイメージになります。

配列のイメージ

これに対し、今回説明するのは多次元配列です。 例えば、次のようなイメージになります。

二次元配列のイメージ

厳密にいえば、このような形での多次元配列は、他のプログラム言語から言えば多次元配列とは呼べません。 正確に言うのなら、配列の配列であると考えるべきですが、多次元配列と呼ぶことが多いのも事実なので、今後も多次元配列と表現します

これまでに扱ってきた配列は、要素が一方向に向かって並ぶ一次元配列だと言えます。 これに対し、要素が2つの方向(行と列と考えても良い)に並ぶこの形は、二次元配列と呼びます。 先ほどの図の配列は、次のように宣言できます。

int array[5][6];

二次元配列の場合、行と列の両方を指定しないと位置が特定できないため、添字が 2つ必要になります。

同様に、三次元配列であれば、次のようなイメージになります。

三次元配列のイメージ

この三次元配列は、次のように宣言できます。

int array[3][4][4];

次元数はこの先もどんどん増やすことができます。 しかし、ほとんどのプログラムは二次元配列まで使えば十分であり、稀に三次元配列を使う機会がある程度です。 よほど特殊な分野でもない限り、それ以上の次元数が必要になることはありません。

イメージとしては、これらの図の通りですが、メモリ上にはいつも連続的なアドレスに配置されます。 例えば、先ほどの二次元配列であれば、

このような順番に配置されます。 もし先頭の array[0][0] のアドレスが 1000 であり、int型が 4Byte だとすると、 array[0][1] は 1004 の位置にあり、array[1][0] は 1024 の位置にあるでしょう。

要素を順番にアクセスする必要がある場合には、できるだけ、アドレスも連続するような順番で行う方が、処理効率が高まる可能性があります。 ですから、二次元配列の全要素を順番にアクセスするときには、

for( i = 0; i < 6; ++i ){
	for( j = 0; j < 5; ++j ){
		array[j][i] = i * j;
	}
}

のように書くよりも、

for( i = 0; i < 5; ++i ){
	for( j = 0; j < 6; ++j ){
		array[i][j] = i * j;
	}
}

のように書いた方が良いです。 つまり、array[i][j][k] のように添字を指定する場合なら、後ろにある添字を先にインクリメントしていき、 手前側にある添字のインクリメントを後に回す方が良いということになります。


ではここで、二次元配列を使ったプログラムの例を見ておきます。

#include <stdio.h>

int main(void)
{
	int array[5][6];
	int i, j;
	

	/* 全要素へ値を格納 */
	for( i = 0 ; i < 5; ++i ){
		for( j = 0 ; j < 6; ++j ){
			array[i][j] = i * 10 + j;
		}
	}
	
	/* 要素を出力 */
	for( i = 0 ; i < 5; ++i ){
		for( j = 0; j < 6; ++j ){
			printf( "%02d ", array[i][j] );
		}
		printf( "\n" );
	}

	return 0;
}

実行結果:

00 01 02 03 04 05
10 11 12 13 14 15
20 21 22 23 24 25
30 31 32 33 34 35
40 41 42 43 44 45

printf関数(⇒リファレンス)のところで、"%02d" というフォーマット指定を行っていますが、 これは、通常の "%d"フォーマットを拡張するような指定方法です。 "2" が 2桁表記で出力させることを表しており、"0" が 2桁に満たない場合には頭に 0 を補うことを表しています。

このプログラムの出力処理部分を、関数化することを考えてみます。 これまで、関数に配列を直接渡すことはできないので、先頭要素のアドレスを渡すことで実現してきました。 これは、int型配列は int*型で受け取れるということです。

二次元配列の正体は、配列の配列ですから、int型の二次元配列は int型配列へのポインタで受け取れます。 int**型で受け取ろうと考えてしまうかも知れませんが、暗黙的に変換できるのは「配列からポインタ」だけであって、 「配列の配列 から ポインタのポインタ」に一気に変換されることはありません

したがって、次のように関数化できます。

#include <stdio.h>

#define ARRAY_COL_NUM    6		/* 配列の列の数 */
#define ARRAY_ROW_NUM    5		/* 配列の行の数 */

void printArray(int (*array)[ARRAY_COL_NUM], int row, int col);

int main(void)
{
	int array[ARRAY_ROW_NUM][ARRAY_COL_NUM];
	int i, j;
	

	/* 全要素へ値を格納 */
	for( i = 0 ; i < ARRAY_ROW_NUM; ++i ){
		for( j = 0 ; j < ARRAY_COL_NUM; ++j ){
			array[i][j] = i * 10 + j;
		}
	}
	
	/* 要素を出力 */
	printArray( array, ARRAY_ROW_NUM, ARRAY_COL_NUM );

	return 0;
}

/*
	二次元配列の要素を出力。
	引数:
		array:		二次元配列の先頭アドレス。
		row:		列の数。
		col:		行の数。
*/
void printArray(int (*array)[ARRAY_COL_NUM], int row, int col)
{
	int i, j;

	for( i = 0 ; i < row; ++i ){
		for( j = 0; j < col; ++j ){
			printf( "%02d ", array[i][j] );
		}
		printf( "\n" );
	}
}

実行結果:

00 01 02 03 04 05
10 11 12 13 14 15
20 21 22 23 24 25
30 31 32 33 34 35
40 41 42 43 44 45

int (*array)[ARRAY_COL_NUM] という少々複雑な表現になっていますが、( ) は必要です。 ( ) が無いと意味が変わってしまいます。

int (*array)[6];  /* int型で要素数6 の配列 へのポインタ */
int* array[6];    /* int型へのポインタ の配列で要素数は 6 */

今まで意識しなかったかも知れませんが、* を付ける位置は関係無いのです。 例えば、

int* p;
int *p;

この 2つには一切違いはありません。 そのため、先ほどの複雑な例でも * を「int」にくっつけようと、「array」にくっつけようと関係は無く、( ) を使って明確に表現しなければなりません。

ついでに触れておくと、

int* p1, p2;

こう書くと、p1 はポインタ変数ですが、p2 は単なる int型変数になってしまいます。 両方ともポインタ変数にしたいのなら、

int* p1, *p2;

こう書かないといけません。 個人的には、ポインタ変数に限っては、まとめて宣言しない方がいいと思います。

int* p1;
int* p2;


最後に、二次元配列の宣言時に初期化を行う方法も取り上げておきます。 先ほどのサンプルプログラムを書き換えます。

#include <stdio.h>

int main(void)
{
	int array[5][6] = {
		{  0,  1,  2,  3,  4,  5 },
		{ 10, 11, 12, 13, 14, 15 },
		{ 20, 21, 22, 23, 24, 25 },
		{ 30, 31, 32, 33, 34, 35 },
		{ 40, 41, 42, 43, 44, 45 },
	};
	int i, j;
	

	/* 要素を出力 */
	for( i = 0 ; i < 5; ++i ){
		for( j = 0; j < 6; ++j ){
			printf( "%02d ", array[i][j] );
		}
		printf( "\n" );
	}

	return 0;
}

実行結果:

00 01 02 03 04 05
10 11 12 13 14 15
20 21 22 23 24 25
30 31 32 33 34 35
40 41 42 43 44 45

初期値全体を { } で囲み、その内側を更に 1行分の初期値ごとに { } で囲みます。 内側の { } の部分だけを見れば、一次元配列を初期化するのと同じ形です。
この構文を一般化すると、次のように表現できます。

データ型 配列名[要素数(省略可)][要素数] = {
    { [0][0] の初期値, [0][1] の初期値, ・・・ },
    { [1][0] の初期値, [1][1] の初期値, ・・・ },
    { [2][0] の初期値, [2][1] の初期値, ・・・ },
                     :
};

一次元配列の場合、要素数を省略すれば、初期値として与えた要素数を自動的に数えて判断してくれましたが、 それと同様に、多次元配列であっても、最初の次元の要素数だけは省略できます

また、要素数を指定したのに、その数よりも初期値の個数の方が少ない場合は、不足部分に 0 が埋められるのも、一次元配列と同様です。 初期値の個数の方が多い場合はコンパイルエラーになります。

動的な二次元配列

今度は、動的に二次元配列を作ることを考えます。

動的な二次元配列を作るには、2つの考え方があります。 まず 1つ目は、各次元を順番に形作っていく方法で、これは次のように書けます。

#include <stdio.h>
#include <stdlib.h>

#define ARRAY_COL_NUM    6		/* 配列の列の数 */
#define ARRAY_ROW_NUM    5		/* 配列の行の数 */

int main(void)
{
	int** array;
	int i, j;


	/* 二次元配列を動的に確保 */
	array = calloc( ARRAY_ROW_NUM, sizeof(int*) );
	for( i = 0; i < ARRAY_ROW_NUM; ++i ){
		array[i] = calloc( ARRAY_COL_NUM, sizeof(int) );
	}

	/* 二次元配列の要素を出力 */
	for( i = 0; i < ARRAY_ROW_NUM; ++i ){
		for( j = 0; j < ARRAY_COL_NUM; ++j ){
			printf( "%d ", array[i][j] );
		}
		printf( "\n" );
	}

	/* メモリ解放 */
	for( i = 0; i < ARRAY_ROW_NUM; ++i ){
		free( array[i] );
	}
	free( array );

	return 0;
}

実行結果:

0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0

この方法の場合、ポインタのポインタを使用することになります。 最初の calloc関数(⇒リファレンス) (もちろん malloc関数(⇒リファレンス)でも構いません)で、 必要な行数だけの領域を確保しています。 このときに確保される領域は、int型ではなくて、int*型のためのものであることに注意して下さい。
また、calloc関数や malloc関数の戻り値は void*型ですが、ここでの意味は「int*型へのポインタ」ですから、 多重間接参照のところで説明した考え方で言えば、レベル2のポインタです。 そのため受け取り側は、int**型でなければなりません。

そして、確保されたそれぞれの行から、今度は列の要素を作成します。 今度は、確保される1つ1つの要素は int型です。


もう1つの方法は、行も列もまとめて1つの一次元配列とみなしてしまう方法です。

#include <stdio.h>
#include <stdlib.h>

#define ARRAY_COL_NUM    6		/* 配列の列の数 */
#define ARRAY_ROW_NUM    5		/* 配列の行の数 */

int main(void)
{
	int* array;
	int i, j;


	/* 行と列をまとめた一次元配列を動的に確保 */
	array = calloc( ARRAY_ROW_NUM * ARRAY_COL_NUM, sizeof(int) );

	/* 一次元配列の要素を出力 */
	for( i = 0; i < ARRAY_ROW_NUM; ++i ){
		for( j = 0; j < ARRAY_COL_NUM; ++j ){
			printf( "%d ", array[i * ARRAY_COL_NUM + j] );
		}
		printf( "\n" );
	}

	/* メモリ解放 */
	free( array );

	return 0;
}

実行結果:

0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0

こちらの方法では、確保と解放はすっきりと分かりやすいと思いますが、これだとあくまでも一次元配列なので、 各要素へアクセスする際に工夫しないといけません。

例えば、1行目の 4列目の要素は、

array[1][4];

このように書きたいところですが、これを、

array[1 * ARRAY_COL_NUM + 4];

というように書かないといけません。 これは多次元配列の説明のところで書いたように、各要素のアドレスが次のような順番で並ぶことから導き出せます。

最初の方法は、malloc関数や free関数の呼び出し回数が多くなるため、実行効率もメモリの使用効率も、あまり良いものではありません。 その意味では、2つ目の方法の方が、malloc関数も free関数も 1回ずつで済むという利点があり優れています。

自己参照構造体

やや高度なデータ構造を扱おうとすると、ある構造体のメンバに、自分自身と同じ型の構造体を含めたいことがあります。

struct Student_tag {
	char*               name;
	int                 grade;
	int                 class;
	int                 score;
	struct Student_tag  next;
};

これはコンパイルできません。 このような、構造体のネストを行いたい場合には、ポインタを利用します。

struct Student_tag {
	char*               name;
	int                 grade;
	int                 class;
	int                 score;
	struct Student_tag* next;
};

これでコンパイルできます。 もちろんこれだと、メンバnext は構造体変数そのものではなく、構造体変数を指し示すポインタ変数になるので、 実際に使う際には、自分でアドレスを代入するなり、malloc関数等で確保するなりしなければなりません。

ちなみに、自分自身のアドレスを保持させることも当然可能です。 このような構造体は、自己参照構造体と呼ばれています。

#include <stdio.h>

struct Student_tag {
	char*               name;
	int                 grade;
	int                 class;
	int                 score;
	struct Student_tag* next;
};

int main(void)
{
	struct Student_tag student = { "Saitou Takashi", 2, 3, 80, NULL };
	student.next = &student;

	printf( "%s\n", student.next->name );

	return 0;
}

実行結果:

Saitou Takashi

構造体変数を宣言する時点で、いきなり自分自身のアドレスを使うことはできませんから、初期値としては NULL を与えています。

自己参照構造体は、リスト構造というデータ構造を形作る際に必須の手法です。 C言語そのものの学習から外れてしまうので、これ以上深入りしませんが、プログラムを続けていると必ず登場する必須の知識ではありますから、 調べてみると良いと思います(リスト構造については、アルゴリズムとデータ構造編【データ構造】第3章で解説しています)。


練習問題

問題@ 次のプログラムの誤りを修正して下さい。

#include <stdio.h>

int main(void)
{
	char str[81];
	char* pStr = str;
	char** ppStr = &pStr;
	
	puts( "80文字以内の文字列を入力して下さい。" );
	fgets( **ppStr, sizeof(str), stdin );
	printf( str );

	return 0;
}

問題A 二次元配列を使って、九九表を作成・出力するプログラムを作成して下さい。

問題B 適当な二次元配列を宣言し、各要素のアドレスの並び順を調べるプログラムを作成して下さい。

問題C 問題Bと同く、各要素のアドレスを調べるプログラムを、動的な二次元配列で試して下さい。


解答ページはこちら

参考リンク

更新履歴

'2017/5/13 配列の要素数を求めるマクロを、他のページと同じ形の SIZE_OF_ARRAYマクロに統一。

'2016/12/12 「row」「col」の表現が逆になっていたのを修正。

'2015/8/25 リンクを追加。

'2010/1/17 新規作成。



前の章へ

次の章へ

C言語編のトップページへ

Programming Place Plus のトップページへ