C言語編 第31章 ポインタ@(概要)

先頭へ戻る

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

この章の概要

この章の概要です。

ポインタとは

ポインタとは、メモリ上のある場所を表現する型のことです。 メモリ上の場所のことを、メモリアドレスあるいは、単にアドレスと言います。

変数にせよ、関数にせよ、実体のあるものは必ずメモリ上に存在しています。 従って、こういった変数や関数は、アドレスを使うことによって、(メモリ上の)どこに存在するか表現できます。
ポインタが保持する値は、このアドレスです。 つまり、ポインタを使えば、ある変数や関数を指し示す(ポイントする)ことができます。

なお、アドレスは、常に正の整数で表現できます。 ただ、それが何Byte あれば表現できるかは、実行環境によって異なります。 通常、long型のサイズがあれば表現できますが、int型のサイズでは保証がありません。

C99 では、intptr_t型という型が追加されており、この型は確実にアドレスを表現できるサイズを持ちます。

ポインタを使った最小のプログラム

それでは、ポインタを使ったプログラムを実際に見てみましょう。

#include <stdio.h>

int main(void)
{
	int num = 15;
	int* p = &num;

	printf( "%d\n", *p );  /* 15 */
	
	return 0;
}

実行結果:

15

ポインタ変数を使うには、まず宣言しなければいけません。 何かを指し示すという性質をもつポインタは、宣言する際に「何を指し示したいのか」を表すように型を決めます
例えば、int型の変数num を指し示したいのならば、

int* p;

という宣言になります。 型の直後に付いている「*」は、これが単なる変数ではなく、ポインタ変数だと示すマークになります。 ちなみに、

int *p;

このように書いても構いません。 違いは * を型の名前の方ではなく、変数名の方に付けている点ですが、両者の意味に違いはありません。

なお、今回のサンプルプログラムの場合、宣言と同時に初期値を与えています。

int* p = &num;

ポインタにとっての初期値とは、「どこを指し示したいのか」を表す値で、それはつまりアドレスだということです。 前述したように、アドレスとは突き詰めれば単なる整数ですが、「何がどこにあるかは分からないのが普通」なので、直接的に、

int* p = 1000;

のように指定することは、通常はできません。 何がどのアドレスにあるかは、普通は分りません。

そこで、「&」という演算子に、アドレスを取得する役目を任せます。 &演算子は、変数のアドレスを取得する演算子であり、アドレス演算子と呼ばれています。 (アンドマークでアドレスを取得できるので、「アンド」レスと覚えておくのも良いでしょう)。

これまでにも sscanf関数を使うときに、実引数に &演算子を付けていましたが、正体はこれです。 つまり「このアドレスに結果を格納せよ」という指示を与えていた訳です。 引数や戻り値でポインタを使う例は、第33章で改めて取り上げます。

先ほどのサンプルプログラムでは、このあと、printf関数(⇒リファレンス)を呼び出しています。 実引数は「*p」となっていますが、今度の「*」は間接参照演算子逆参照演算子などと呼ばれる演算子で、 ポインタ変数を宣言するときの「*」とは別物です。
間接参照演算子は、ポインタとセットで使用し、「そのポインタが指し示す先にあるものを参照する」という効力を持ちます。 ということで、

printf( "%d\n", *p );

とすれば、ポインタ変数p が保持しているアドレスにあるもの(=変数num)を参照し、その値を printf関数の "%d"フォーマットで出力できます。 このようにポインタが指し示す先にあるものを参照することを、間接参照(逆参照)といいます。


ポインタは少し難しい概念ですが、最初の難関は構文の方だという話もあります。 ここまでに登場した構文についてまとめるとこうなります。

ポインタの使用例

次のプログラムを見てください。

#include <stdio.h>

int main(void)
{
	int num = 15;
	int* p = &num;

	*p = 30;
	printf( "%d\n", num );  /* 30 */
	
	return 0;
}

実行結果:

30

変数num の初期値は 15 であり、その後、num に何かほかの値を代入しているようには見えません。 しかし、printf関数で num の値を出力してみると、30 と出力されました。

言うまでもなく、これはポインタの仕業です。 変数num のアドレスを、ポインタ変数p に格納してあります。 この状態で、「*p = 30;」という一文を実行することにより、間接的に変数num の値を書き換えています。

反対に、変数num の方を直接書き換えてから、ポインタ変数p を間接参照しても結果は同じになります。

#include <stdio.h>

int main(void)
{
	int num = 15;
	int* p = &num;

	num = 30;
	printf( "%d\n", *p );  /* 30 */
	
	return 0;
}

実行結果:

30

アドレスを確認する

アドレスが実際にはどんな値なのかは、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

実行結果は、環境によって大きく異なる可能性がありますが、2つの値は同じはずです。 要するに、変数num のアドレスと、ポインタ変数ptr が保持しているアドレスは一致するということです。

この方法で変数のアドレスは確認できる訳ですが、この実行結果を確認した後でプログラムを、

#include <stdio.h>

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

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

に書き換えるようなことを考えてはなりません。 変数に割り当てられるアドレスは変化する可能性があるからです。 素直に &演算子を使うのが正解なのです。

なお、後で登場するヌルポインタを、printf関数に渡した場合の動作は未定義です。

汎用ポインタ

int*型のポインタばかり登場していますが、もちろん、char*型や double*型といったポインタも作れます。 また、unsigned short int*型といった、やや複雑な表現も可能です。

ポインタにとっての型とは、「その型のアドレスを保持できる」という意味を持ちます。 int*型のポインタに、double型変数のアドレスを保持させることは、危険性を伴います。

そう言いつつも、他の型のアドレスを保持させるようなポインタの使い方をする場面もまれにあります。 ただし、他の環境で問題なく動作するという保証はなくなってしまいます。

一方、汎用ポインタ(総称ポインタ)と呼ばれる特殊なポインタがあり、これはどんな型のアドレスでも保持できます。 汎用ポインタは、void*型で表現されます。

void* ptr;

汎用ポインタの利用法については、第34章で説明します。

ヌルポインタ

ポインタは、何かを指し示すためにアドレスを保持している訳ですが、実はどこも指し示していない状態を作ることも可能です。 どこも指し示していない状態のポインタを、ヌルポインタと呼びます。

ヌルポインタを作るためには、NULL(⇒リファレンス)というマクロを使います。 NULLマクロの定義は、stdio.h や stddef.h を初めとして、幾つかのヘッダに重複して定義されています

int* ptr = NULL;

NULLマクロの正体は、コンパイラ依存ではありますが、何かしらの型で表現された 0 です。 一般的には、ただの整数としての 0 か、void*型にキャストされた 0 です。

C++ の場合は事情が異なり、void*型の 0 になっていることはありません。 C++ では、void*型から、他のポインタ型へは暗黙的にキャストできないので、void*型の 0 だと不便で仕方ありませんが…。

なぜ 0 なのかというと、C言語のルールでは、 アドレスを必要としている箇所で登場した 0 はすべてヌルポインタを意味するものとして扱うことになっているからです。 ですから、

int* ptr = 0;

このように書くことも合法です。 一見、型が不一致( ptr は int*型で、0 は int型)なように見えますが、 この場面では、「ポインタ変数の初期値を与えようとしている」=「アドレスを必要としている箇所」なので、0 はヌルポインタの意味だと解釈されるのです。
本サイトでは、単なる 0 ではなく NULL を使うようにしていきます。

C++ だと逆に、NULL よりも 0 を使うべきだと言われますが、C言語編なのでC言語的な事情を優先することにします。

なお、ヌルポインタは、ヌルポインタでないポインタと比較したとき、一致しないことは保証されています


ヌルポインタは、何も指し示していないという状態なので、*演算子を使って間接参照してはなりません。 そのため、ヌルポインタになっている可能性があるときには、if文でチェックする必要があります。

#include <stdio.h>

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

#if 0
	ptr = &num;
#endif

	if( ptr != NULL ){
		printf( "%d\n", *ptr );
	}
	
	return 0;
}

実行結果:




コメントアウトされている箇所を有効にすれば、100 が出力されることになります。
安全性を追求するのなら(基本的にはすべきです)、ポインタ変数がヌルポインタになっていないかどうかは逐一チェックするように コーディングした方が良いと言えます。 もし、「ここでヌルポインタになっていることなんて、絶対にありえない」と思うのならば、アサートを使うと良いです。

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

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

#if 0
	ptr = &num;
#endif

	assert( ptr != NULL );
	printf( "%d\n", *ptr );
	
	return 0;
}

実行結果:

Assertion failed: ptr != NULL, file c:\main.c, line 13


最後に、文字列の末尾に付く '\0' がヌル文字と呼ばれることを思い出してください。 「ヌル」という共通の文字が付いていますが、ヌル文字とヌルポインタはまったく別物です
よく両者を混同したプログラムを書く人がいますが、'\0' には文字列の終端としての意味合いしかないですし、 ヌルポインタ(NULL) には、何も指し示していないポインタとしての意味合いしかありません。 両者は交換可能なものではないので注意して下さい。

配列要素へのポインタ

配列の要素を指すようなポインタも作れます。 これは特に特殊なものではなく、

int array[10];
int* p = &array[5];

のように書きます。 int型配列といっても、要素1つ1つは単なる int型に過ぎないので、ポインタ変数の型は 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 );
	
	return 0;
}

実行結果:

10 20

構造体を指し示すようなポインタを作ること自体は、特に変わったことはなく、これまで通り、型名に * を付けるだけです。

ポインタを経由して、構造体のメンバにアクセスする際に、少し面倒な記述が登場しています。 一旦、*演算子を使って間接参照を行い、改めて .演算子を使ってメンバをアクセスしています。
これはこれで正しいのですが、ポインタ経由で構造体のメンバにアクセスする機会は結構多いので、いちいちこういう記述をするのは面倒です。 そこで、->演算子というものが用意されており、そちらを使うのが一般的です。 次の2つの文は同じ意味です。

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

->演算子は、その形状からアロー演算子(矢印演算子)と呼ばれることもあります。 ->演算子は、 (*p).x のような記述に対する構文糖であると言えます。


練習問題

問題@ 次のプログラムの実行結果を答えて下さい。

#include <stdio.h>

int main(void)
{
	int num, *p;
	
	num = 50;
	p = &num;
	
	printf( "%d\n", *p );
	
	return 0;
}

問題A 次のプログラムの実行結果を答えて下さい。

#include <stdio.h>

int main(void)
{
	int num1 = 0, num2 = 15;
	int* p;
	
	p = &num1;
	*p = *p + num2;
	
	p = &num2;
	*p = *p + num1;
	
	printf( "num1:%d  num2:%d\n", num1, num2 );
	
	return 0;
}

問題B 次のプログラムの実行結果を答えて下さい。

#include <stdio.h>

int main(void)
{
	int array[5] = { 1, 2, 3, 4, 5 };
	int* p;
	int i;
	
	for( i = 0; i < 5; ++i ){
		p = &array[i];
		printf( "%d ", *p );
	}
	printf( "\n" );
	
	return 0;
}

問題C 次のプログラムの実行結果を答えて下さい。

#include <stdio.h>

typedef struct {
	float    x;
	float    y;
} PointF;

typedef struct {
	PointF	lt;
	PointF	rb;
} RectF;

int main(void)
{
	RectF   rect;
	RectF*  pRect = &rect;
	PointF* pPoint = &rect.lt;

	pPoint->x = 15.0f;
	pPoint->y = 20.0f;
	pRect->rb.x = 10.0f;
	pRect->rb.y = 30.0f;

	printf( "%f %f %f %f\n", rect.lt.x, rect.lt.y, rect.rb.x, rect.rb.y );
	
	return 0;
}


解答ページはこちら

参考リンク

更新履歴

'2015/8/25 リンク先を修正。

'2013/8/11 ポインタが必ず変数であるかのように受け取れるので、冒頭の解説を修正。

'2009/11/4 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ