C言語編 第38章 理解の定着・小休止C

この章の概要

この章の概要です。

理解の定着・小休止C

さて、この章ではこれまでに見てきた内容の理解を再確認しましょう。 また、1章丸ごとを割くほどでも無い細かい部分について、少し触れていきます。

今回は、以下の範囲が対象です。 ポインタがテーマとなります。

ポインタの構文

ポインタ変数は、メモリアドレスを保持し、その位置を指し示すような変数です。

ポインタ変数の宣言は、次のように行います。

int* p;

この場合、int型の変数のアドレスを保持し、その変数を指し示すようなポインタ変数が宣言されます。 アドレスを得るためには、

int num = 100;
int* p = #  /* 変数num のアドレスを保持 */

このように、&演算子を使用します。

ポインタ変数を経由して、指し示す先にある値を参照するには、*演算子を使用します。 この操作は、間接参照(逆参照)と呼ばれ、*演算子を間接参照演算子(逆参照演算子)と呼びます。

printf( "%d\n", *p );  /* ポインタ変数p を間接参照した値を出力する */

アドレス計算

printf関数の "%p"フォーマットを使えば、アドレスを出力させることができます。

#include <stdio.h>

int main(void)
{
	int num = 100;
	int* ptr = &num;

	printf( "%p\n", ptr );
	printf( "%p\n", &num );
	
	return 0;
}

実行結果:

0021FE40
0021FE40

配列の場合、各要素が連続的に並ぶことが保証されています

#include <stdio.h>

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

int main(void)
{
	int array[] = { 0, 10, 20, 30, 40 };
	int i;

	for( i = 0; i < SIZE_OF_ARRAY(array); ++i ){
		printf( "%d: %p\n", i, &array[i] );
	}
	
	return 0;
}

実行結果:

0: 002DF7F4
1: 002DF7F8
2: 002DF7FC
3: 002DF800
4: 002DF804

実行結果をみると、4 ずつ増加していますが、これは実行した環境での int型が 4Byte であるからです。 このように、隙間なく連続的に増加していくことから、これを利用して配列の要素数を知ることが可能です。

#include <stdio.h>

int main(void)
{
	int array[5] = { 0, 10, 20, 30, 40 };

	printf( "%d\n", &array[5] - &array[0] );
	
	return 0;
}

実行結果:

5

配列の末尾の更に1つ先のところのアドレスから、先頭の要素のアドレスを減算することで、要素数を計算しています。 なぜこれでうまくいくかと言うと、ポインタに対する加算および減算には、特別なルールがあるからです。

ポインタ変数に対するインクリメント操作では、アドレスが +1 されるのではなく、sizeof(指し示す先の型) 分だけ加算されます。 同様に、デクリメント操作では、sizeof(指し示す先の型) 分だけ減算されます

ヌルポインタ

どこも指し示していないポインタを、ヌルポインタと呼びます。 何も指し示していないので、間接参照を行うことは不正な行為です

ヌルポインタは、NULL(⇒リファレンス)というマクロで表現することができます。 NULLマクロは、stdlib.h など幾つかの標準ヘッダで定義されています。 また、NULLマクロの正体は、何かしらの型で表現された 0 です。

#include <stdlib.h>

int* ptr = NULL;  /* ヌルポインタ */
int* ptr2 = 0;    /* ヌルポインタ */

上の例において、ポインタ変数ptr、ptr2 はいずれもヌルポインタになります。 C言語のルールでは、アドレスを必要としている箇所で登場した 0 はすべてヌルポインタを意味するものとして扱うことになっています。

なお、ヌルポインタは、ヌルポインタ以外のポインタと比較したとき、一致しないことが保証されています。 malloc関数(⇒リファレンス)などで動的なメモリ割り当てを行う際、失敗を表す戻り値として NULL が使われるのはこのためです。 このように、ポインタを返すべき場面での失敗の意味で、ヌルポインタを使うことはよくあります。

汎用ポインタ

通常、ポインタ変数は、どんな型を指し示すのかが決まっています。 例えば、char*型のポインタが指し示すのは、char型の変数です。

一方、汎用ポインタ(総称ポインタ)と呼ばれるポインタは、どんな型でも指し示し、そのアドレスを保持できる特別なポインタです。 汎用ポインタは、void*型で表現します。

int num = 100;
double f = 15.5;

void* ptr = &num;
void* ptr2 = &f;

汎用ポインタを間接参照することはできません。 これは、間接参照した先がどんな型であるか不明であるためです。

その代わりに、汎用ポインタは他の型のポインタ変数にそのまま代入できます。 一旦、型が明確なポインタへ代入した後であれば、そこから間接参照することはもちろん可能です。

int num = 100;
int* int_ptr;

void* ptr = &num;

int_ptr = ptr;                 /* OK。汎用ポインタから他の型のポインタへは暗黙的に型変換できる */
int_ptr = (int*)ptr;           /* OK。明示的にキャストしても構わない (C++ では常にこうしなければならない) */

printf( "%d\n", *ptr );        /* エラー。汎用ポインタは間接参照できない */
printf( "%d\n", *(int*)ptr );  /* OK。一旦、int*型にキャストした後、間接参照する */

構造体変数へのポインタ

構造体変数へのポインタも作れます。

#include <stdio.h>

typedef struct {
	int    x;
	int    y;
} Point;

int main(void)
{
	Point point;
	Point* p = &point;

	point.x = 10;
	point.y = 20;

	printf( "%d %d\n", (*p).x, (*p).y );
	printf( "%d %d\n", p->x, p->y );
	
	return 0;
}

実行結果:

10 20
10 20

構造体変数を指し示すポインタから、間接参照によって、メンバにアクセスする際、これまで通りに *演算子を使っても構いませんが、 これはやや面倒な記述になってしまいます。
そこで、->演算子(アロー演算子、矢印演算子)という構文糖が用意されています。 次の2つは同じ意味です。

x = (*p).x;
x = p->x;

配列とポインタ

配列とポインタは、一見同じように見えることがありますが、両者はまったく別物です。 例えば、次の2つは同じ結果を生みます。

printf( "%d\n", array[i] );
printf( "%d\n", p[i] );     /* ただし p は配列array の先頭要素を指すポインタ */

これが同じ結果になるのは、「式の中で array のような配列名が現れたとき、暗黙的に &array[0] に置き換わる」からです。 そのため、配列の名前だけが現れると、あたかもポインタを使ったかのような結果を生むことがあり、これが両者を混同する一因になっています。

更に、次の2つも同じ結果になります。

printf( "%d\n", p[i] );     /* ただし p は配列の先頭要素を指すポインタ */
printf( "%d\n", *(p+i) );

ポインタ変数に対する加算は、そのポインタ変数が指し示す先の要素のサイズ分だけアドレスを進めるので、*(p+i) の結果は、 配列の i番目の要素へのアクセスと同義です。
とはいえ、いちいち *(p+i) と書くのは、ちょっと暗号めいて分かりづらいですし、書きづらくもあります。 []演算子は、記述を容易にするための構文糖の一種です。

文字列

文字列リテラルの "abcde" は、char型の配列です。 ただの char型配列なので、書き換えできてしまいそうですが、書き換えようとする行為がうまくいく保証はありません。

文字列変数の表現には2通りの考え方があります。 次の宣言はいずれも正しいですが、意味が異なるものも含まれています。

char str1[] = "abcde";
char str2[] = { 'a', 'b', 'c', 'd', 'e', '\0' };
char* str3 = "abcde";

str1 と str2 は配列、str3 はポインタです。 str1 と str3 はいずれも初期値として "abcde" を指定していますが、文字列リテラルの "abcde" なのは str3 の方だけです。 str1 に与えている "abcde" は、「配列の各要素に与える初期値」という意味合いしかありません。 要するに、str2 のように 1文字ずつ指定する行為を一発で行っているだけのことです。

配列による文字列と、ポインタによる文字列のイメージは次の図のようになります。

配列とポインタの概念図

配列版の文字列は各要素を書き換えることが可能ですが、ポインタ版ではそれができません。 もちろん、ポインタ版の方では、指し示す先を付け替えることができるので、配列版の文字列を指し示すように変更すれば、 ポインタ経由で要素を書き換えることは可能になります。

#include <stdio.h>

int main(void)
{
	char str1[] = "abcde";
	char* str2 = "abcde";


	str1[2] = 'x';

#if 0
	str2[2] = 'x';    /* これは正しくない */
#endif


	str2 = str1;      /* 書き換え可能な配列の方を指し示すように変更 */
	str2[2] = 'z';    /* これは正しい */


	puts( str1 );
	puts( str2 );

	return 0;
}

実行結果:

abzde
abzde

引数や戻り値でのポインタの使用

関数に、配列のコピーをそのまま受け渡しすることはできません。 これは、実引数や戻り値に配列を指定しても、暗黙的にポインタへ変換されてしまうためです。

ポインタとして受け渡しされることは、巨大なデータをやり取りする際には、むしろ好都合です。 例えば、数千バイトにも及ぶ巨大な構造体を、そのまま受け渡すよりも、 その構造体を指し示すポインタ( 32bit環境ならば恐らく 4Byte)を受け渡す方が、ずっと軽い処理で済みます。

引数にポインタを使うことによって、2つ以上の戻り値の代替になります。 結果を受け取る変数のアドレスを実引数とすることによって、関数内でそのアドレスに結果を代入してもらいます。
この方法の場合、関数の作成者の意図に反して、NULL が渡される可能性を考慮しなければなりません。

戻り値がポインタとなるケースは、ローカル変数のアドレスを返してしまうというミスを犯さないように注意が必要です。

#include <stdio.h>
#include <assert.h>

char* getString(int max);

int main(void)
{
	char* str;
	
	str = getString( 5 );
	puts( str );  /* 受け取った文字列を出力 */
	
	return 0;
}

/*
	標準入力から max以下の文字数の文字列を受け取る。
	引数:
		max:	最大文字数。1〜255
	戻り値:
		受け取った文字列。
*/
char* getString(int max)
{
	char str[256];

	assert( 1 <= max && max <= 255 );

	printf( "%d文字以内の文字列を入力して下さい。\n", max );
	fgets( str, max+1, stdin );		/* 改行文字のことを考えて +1 */
	
	return str;
}

この例のように、ローカル変数のアドレスを返すプログラムは、コンパイルできるものの、その動作は不定です。 ローカル変数は、関数を抜け出した時点で、メモリ上に存在している保証がなくなってしまうことが理由です。

動的なメモリ割り当て

プログラムの実行中、任意のタイミングでメモリ領域を確保し、任意のタイミングで解放することができます。 このようなメモリ割り当ての手法を、動的メモリ割り当てとか、ダイナミックアロケーションなどと呼びます。

動的メモリ割り当てを行うには、malloc関数(⇒リファレンス)、 calloc関数(⇒リファレンス)、 realloc関数(⇒リファレンス)のいずれかを使います。
動的に確保されたメモリ領域は、使い終わった後、free関数(⇒リファレンス)で後始末をする必要があります。

malloc関数が最も基本的な関数であり、指定したサイズ分の領域を確保します。
calloc関数は、配列のための領域確保を想定しており、要素1つ分のサイズと、要素数を指定して領域を確保します。 malloc関数の場合と異なり、確保した領域は自動的にゼロクリアされます。
realloc関数は、1度動的に割り当てた領域の大きさを、拡張あるいは縮小する関数です。

動的なメモリ割り当てを使用する場合、以下の点に注意する必要があります。

これだけ多くのことを考慮しなければならないため、動的なメモリ割り当ては難しい部類の機能と言えます。 本当に必要な場面に限って使用し、不用意に使いすぎないようにするべきです。

多重間接参照

あるポインタ変数が、別のポインタ変数を指し示すような使い方を俗に、ポインタのポインタと呼びます。 これは例えば、

int num  = 100;    /* 100 は単なる整数であり、アドレスではない */
int* p   = &num;   /* ポインタ変数へは、アドレスを格納する */
int** pp = &p;     /* ポインタのポインタへは、ポインタのアドレスを格納する */

このように記述します。 ポインタ変数宣言の際の「*」の個数が増えることで、ポインタのポインタを表しています。

上記のポインタのポインタpp から、一気に変数num の値を間接参照するような参照の仕方を、多重間接参照と呼びます。

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

多次元配列

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

配列のイメージ

これに対し、多次元配列は、次のようなイメージになります。

二次元配列のイメージ

この図の場合だと、要素が行と列という2つの方向に並ぶ二次元配列です。 最初の図の方は、一次元配列と呼ぶことができます。

次のサンプルは、二次元配列を使ったプログラムです。

#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

この二次元配列は、配列の配列ですから、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 */

const なポインタ

const修飾子は、変数の書き換えを不許可にする効果があります。 使い方には次の4通りがあります。

const int num = 100;          /* num は書き換えられない */
int* const ptr = &num;        /* ptr は書き換えられない */
const int* ptr = &num;        /* ptr が指し示す先の値を書き換えられない */
const int* const ptr = &num;  /* ptr自身と指し示す先の値を書き換えられない */

「const」と「*」の位置関係が重要です。 「*」より左側に「const」が登場するのなら、そのポインタ変数が指し示す先が書き換えられなくなり、 「*」より右側に「const」が登場するのなら、そのポインタ変数自身が書き換えられない(指し示す先を変更できない)ようになります

関数ポインタ

関数を指し示すようなポインタ変数を作ることもできます。 これを関数ポインタと呼びます。

例えば、次のような関数が宣言されていたとします。

int func(const char* str);

この関数を指し示す関数ポインタは、次のように宣言できます。

int (*func_ptr)(const char*) = func;

あるいは、typedef を使って、

typedef int (*func_ptr_t)(const char*);  /* 型名を定義 */
func_ptr_t func_ptr;                     /* 定義した型名を使って、変数宣言 */
func_ptr = func;                         /* 変数へ、関数のアドレスを代入 */

このように宣言された関数ポインタfunc_ptr を使って、指し示されている関数func を呼び出すには、次のように記述します。

int ret = func_ptr( "abcde" );
int ret = (*func_ptr)( "abcde" );

この2つの記述は同じ意味になるので、どちらを使っても構いません。


練習問題

まとめとして、多めに練習問題を用意しました。★の数は難易度を表します。

問題@ 次のプログラムで、標準出力に出力される内容を答えて下さい。[★]

#include <stdio.h>

int main(void)
{
	int num1 = 10;
	int num2 = 20;
	int* ptr = NULL;


	ptr = &num1;
	printf( "%d\n", *ptr );

	ptr = &num2;
	printf( "%d\n", *ptr );

	num1 = 15;
	num2 = 25;
	printf( "%d\n", *ptr );

	ptr = &num1;
	printf( "%d\n", *ptr );

	return 0;
}

問題A 次のように宣言された変数があります。

const char* str = "abcde";

このとき、「*str」のように間接参照した先にあるものは何ですか? [★]

問題B 次のプログラムを実行すると、"NG" と出力されます。 "OK" と出力されない理由を説明して下さい。[★]

#include <stdio.h>

int main(void)
{
	const char name1[] = "John";
	const char name2[] = "John";


	if( name1 == name2 ){
		puts( "OK" );
	}
	else{
		puts( "NG" );
	}

	return 0;
}

問題C 次のように定義された配列があります。

int table[] = { 0, 10, 20, 30, 40, 50, 60, 70 };

この配列table のアドレスを printf関数の "%p"フォーマットを用いて調べたところ、0x0013D684 でした。 このとき、配列table の末尾の要素70 のアドレスが幾つであるか答えて下さい。 ただし、sizeof(int) は 4 であるものとします。[★★]

問題D 次のような関数を作成しました。

char* strDuplicate(const char* s)
{
	char* dup = malloc( strlen( s ) + 1 );
	strcpy( dup, s );

	return dup;
}

この関数は何をしているか説明して下さい。 また、この関数を実際に使用したプログラムを作成して下さい。[★★]

問題E 「配列を指し示すポインタ」が欲しいと考えました。 これは可能ですか? どうすれば実現できるでしょうか? [★]

問題F 文字列の中から、特定の文字を探し出し、そのアドレスを返す strchr関数(⇒リファレンス)という標準関数が存在します。 この関数は、次のような仕様になっています。

char* strchr(const char* s, int c);

文字列s の先頭から順番に文字を調べ、c と一致するものが登場したら、そのアドレスを返します。 もし、文字列s の末尾までの間に一致するものが登場しなかったら NULL を返します。 c は int型ですが、関数内部では char型として比較されます。 また、文字列s の末尾にある '\0' を探し出すことも可能です。
この標準関数と同じことをする関数を自作して下さい。[★★★]

問題G 2つの文字列を連結する strcat関数(⇒リファレンス)という標準関数が存在します。 この関数は、次のような仕様になっています。

char* strcat(char* s1, const char* s2);

s1 の末尾に s2 を連結させます。戻り値は s1 と同じ値をそのまま返します。 この標準関数と同じことをする関数を自作して下さい。[★★★]

問題H 三角関数sin (サイン)、cos(コサイン)、tan (タンジェント) を求める標準関数が math.h に宣言されています。 それぞれの名前は sin(⇒リファレンス)、 cos(⇒リファレンス)、 tan(⇒リファレンス)で、double型の引数と、double型の戻り値を持ちます。 引数はラジアン単位で指定します。
これらの関数のいずれかを指し示して呼び出せるような関数ポインタを定義し、実際に使用するプログラムを書いて下さい。[★★]

問題I 標準入力から、int型のデータを次々と受け取り記憶していき、0 が入力されたら終了するとします。 全ての入力を受け取り終えた後、一番大きい数値から順番に上位の 10個を出力するプログラムを作成して下さい。
入力されたデータが 10個に満たない場合は、存在する個数分だけ出力して下さい。[★★★]

問題J 標準入力から、文字列のデータを次々と受け取り記憶していき、"exit" と入力されたら終了するとします。 全ての入力を受け取り終えた後、一番文字数の多い文字列から順番に上位の 10個を出力するプログラムを作成して下さい。
入力されたデータが 10個に満たない場合は、存在する個数分だけ出力して下さい。[★★★]

問題K C言語の関数は、戻り値を1つしか持てません。 複数の戻り値が必要な場合の解決手段を2つ以上挙げて下さい。[★]

問題L 標準関数の memcpy関数(⇒リファレンス)を自作して下さい。[★★]

問題M 次のプログラムは正しいですか? 正しければ実行結果を答えて下さい。間違っていれば、どう間違っているか指摘して下さい。[★★]

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

int main(void)
{
	int* value;
	int** ptr = &value;

	value = malloc( sizeof(int) );
	*value = 100;

	printf( "%d\n", *value );
	printf( "%d\n", **ptr );

	free( value );

	return 0;
}

問題N リバーシの盤面を表現する二次元配列を作り、ゲーム開始時点の状態を表現するように初期化して下さい。 初期化処理は1つの関数にまとめ、また状態を確認できるような出力関数を作成して下さい。 [★★]


解答ページはこちら

参考リンク

更新履歴

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

'2015/12/27 SIZE_OF_ARRAYマクロの定義を修正。

'2015/8/25 サンプルプログラムで、配列の要素数をマクロで計算するスタイルに修正。

'2010/4/25 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ