C言語編 第32章 ポインタA(基礎)

先頭へ戻る

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

この章の概要

この章の概要です。

アドレス計算

&演算子を使えばアドレスを取得でき、printf関数(⇒リファレンス)の "%p"フォーマットを使って、 その値を確認できます。 これは前章でも試しました。

今回は、配列の場合を確認してみましょう。

#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 ずつ増えていることです。 この 4 という値の正体は、sizeof(int) です。 そのため、int型のサイズが 4以外の環境であれば結果は変わりますが、とにかく、int型のサイズ分ずつずれていきます。

これは、配列の特徴の1つです。 配列は、各要素がメモリ上で連続的に隙間なく並ぶことが保証されています

この性質を利用して、配列の要素数を調べる方法があります。

#include <stdio.h>

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

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

実行結果:

5

このように、同じ配列内にある要素のアドレス同士を減算すると、その間にある要素数が取得できます。 このとき取得できる値は、ptrdiff_t(→リファレンス)という型です。 この型は、環境によってサイズは異なる可能性がありますが、符号付きの整数であることは確かです。

printf関数で ptrdiff_t型の値を出力する際、"%d" や "%ld" を用いることができますが、限界値を越えないように注意して下さい。 C99 では、"%td" を使えば、確実に ptrdiff_t型の値を扱えるようになっていますが、VisualC++ 2013 では対応していません。

なお、&array[5] のように、配列の末尾を1つ超えたところのアドレスを調べていますが、これは合法です。 許されるのは、末尾の1つ先までで、それより先は不正ですし、先頭より手前を参照するのも、やはり不正です


また、アドレスの加算も同様に有効です。

#include <stdio.h>

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

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

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

実行結果:

0
10
20
30
40

ポインタ変数に対するインクリメント操作では、アドレスが +1 されるのではなく、sizeof(指し示す先の型) 分だけ加算されます。 デクリメント操作も同様ですし、2以上の加算や減算でも同様です。
このため、配列要素を指すポインタをインクリメントすると、「次の要素へ」という感覚で扱えることになります。 ただし、これもやはり、同じ配列内に限った話だという点に注意して下さい。


ところで、ポインタ変数に対するインクリメントやデクリメントに関して、優先順位の問題があります。 次のように書いた場合に、

*p++;

これは、ポインタ変数がインクリメントされてから間接参照するのか、間接参照した先にある変数をインクリメントするのか、という問題です。

この場合、「ポインタ変数がインクリメントされてから間接参照される」が正解ですが、非常に分かりづらいです。
コンパイラが優秀でなかった時代には、このような書き方をすると、効率が良くなることもあったようですが、いまや時代錯誤です。 現代的には、こういう小手先の手段は避けて、読みやすさを重視すべきです。

こういう書き方を好む人は、すでに慣れきっていて、読みにくいと感じていないことにも問題があるのですが…。

そこで、先ほどのような書き方はやめて、

*(p++);

のように ( ) を使って、優先順位を明確化すると良いでしょう。

配列とポインタの関係性

配列とポインタには深い関わりがあります。 しかし、とんでもなく多い間違いなので、先に書いておきますが、「配列とポインタは別物です」。 初めてC言語を学ぶ人からすれば、「そりゃそうだろう」と思うでしょうが、経験者の中には両者が同じものだと本気で思っている人たちがいるようです。

まず、配列を使ったサンプルプログラムを挙げます。

#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\n", array[i] );
	}
	
	return 0;
}

実行結果:

0
10
20
30
40

このプログラムは、次のように書き換えることが可能です。

#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;
	int* p = &array[0];

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

実行結果:

0
10
20
30
40

ポインタ変数p を追加し、配列の先頭要素のアドレスで初期化しています。 printf関数を呼び出す際には、array[i] の代わりに、p[i] と記述しましたが、先ほどの配列版プログラムとまったく同じ実行結果になります。

配列とポインタが同じものであるという錯覚の1つがここにあります。 実のところ、

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

この2つは本当に同じ意味です。 しかし、配列=ポインタではありません。

同じ意味になる理由は、「C言語のルールでは、配列は配列のまま処理されることはなく、常に暗黙的にポインタに置き換わるから」です。
もう少し明確に書けば、「式の中で array のような配列名が現れたとき、暗黙的に &array[0] に置き換わる」となります。 ですから、

int array[5];
int* p;

p = array;    /* p = &array[0]; と同じ */

となります。 第25章で配列を扱ったとき、配列を引数や戻り値にはできないと書きましたが、その理由はまさにこれです。 配列は配列のままでは扱えず、関数に渡す際にも、関数から戻される際にも、ポインタに置き換えられてしまうのです。 関数と絡めた例は、次章で説明します


ここで疑問なのは、[]演算子の役割です。 配列がポインタに置き換えられるし、ポインタ変数p に対してであっても、 p[i] のような記述が許されるというのはどういうことでしょうか。
実は、次の2つの文の意味が同じになります。

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

アドレス計算の話のところで説明したように、ポインタ変数に対する加算は、指し示す先の要素のサイズ分だけ加算されます。 ですから (p+i) において、p が配列の先頭要素を指していたのなら、i要素分だけ先の要素を指すことになります。 その位置に対して *演算子が適用されるので、結局のところ array[i] のことを意味します。

つまり、[]演算子は一切使わなくても、*(p+i) のような記述で書くことも可能なのです。 しかし明らかに面倒そうですから、[]演算子が用意されています。 これは要するに、構文糖です。

a[3] と *(a+3) が同じということは、*(3+a) とも同じな訳です。 そのため、実は a[3] は 3[a] とも書けます。そんなことをする理由は一切ありませんが。

文字列

ここで文字列について再考しておきます。 文字列は、

"abcde"

というように、"" で囲んで表現され、これを文字列リテラルと呼びました(第25章)。 上記のように、単に "abcde" とだけ書かれていると、これが一体何型なのかという疑問があります。 実は、 "abcde" の正体は char型の配列です

単なる char型の配列であるため、実は書き換えることができてしまうという落とし穴があります (だから、定数と呼ぶのは本来正しくなく、代わりにリテラルと呼んでいます)。 しかし、文字列リテラルを書き換える行為を行うと、どんな結果になるかは未定義です

C++ の場合は const char型の配列です。 そのため、書き換える行為は明確に禁じられています。

よく、文字列リテラルがポインタであると勘違いしている人がいるようです。 文字列リテラルが配列であることの根拠は、次のコードにおいて、

printf( "%u\n", sizeof("abcde") );

この出力結果が 6 になることから分かります(末尾に '\0' があるので 5 ではありません)。 もし文字列リテラルがポインタであり、ポインタ変数のサイズが 32bit の環境であれば、出力結果は 4 になるはずです。

文字列リテラルがポインタであるという勘違いが生まれる原因の1つは、次の2つがともに有効であることでしょう。

char str[] = "abcde";
char* str = "abcde";

一見して、この2つの文の初期値は同じものに見えますが、実は違います。 配列の方に関しては、次の2つが同じ意味です。

char str[] = "abcde";
char str[] = { 'a', 'b', 'c', 'd', 'e', '\0' };

つまり、配列の初期化の際に現れる "abcde" は、「配列の各要素に与える初期値」という意味合いでしかありません。 それ以上、何も意味はないと考えて良いです。

一方、ポインタ変数str を初期化する際に現れた "abcde" は、文字列リテラルそのものです。 前述したように、文字列リテラル自体は char型の配列ですが、「配列とポインタの関係性」のところで触れたように、 配列が配列のまま処理されることはなく、常にポインタに変換されるのでした。
そのため、文字列リテラル"abcde" (繰り返しますが、これは char型配列です)を、 ポインタ変数str に代入しようとすると、自然にポインタに変換されるのです。
より正確に言えば、メモリ上のどこかに存在する "abcde" という配列の先頭アドレスが、ポインタ変数str に与えられます。

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

配列とポインタの概念図

この図が示すように、配列版とポインタ版は明確に異なります。 決して、「同じこと」だなんて思わないで下さい。

配列とポインタの実用上の違い

char str[] = "abcde";
char* str = "abcde";

この2つの意味は異なる訳ですが、実用上の違いはあるのでしょうか?

まず、配列版の方は要素を書き換えられますが、ポインタ版は出来ません(正しくは、してはいけませんが正解)。

#include <stdio.h>

int main(void)
{
#if 1
	char str[] = "abcde";
#else
	char* str = "abcde";
#endif

	str[2] = 'x';
	puts( str );
	
	return 0;
}

実行結果:

abxde

配列版ならば、このプログラムは問題なく実行できますが、ポインタ版だと、正常動作する保証はありません。 コンパイルが通ってしまうのが困りものですが。

一方で、ポインタ版の方は、指し示す先を切り替えることが可能です。

#include <stdio.h>

int main(void)
{
#if 0
	char str[] = "abcde";
#else
	char* str = "abcde";
#endif

	str = "xyz";
	puts( str );
	
	return 0;
}

実行結果:

xyz

こちらは配列版だとコンパイルエラーになります。 配列版だと、

str = "xyz";

この文は、

&str[0] = &"xyz"[0];

これと同じです。 右辺がものすごく不思議な感じに見えますが、これ自体は有効な構文です。
この文がコンパイルできない理由は、左辺側の表現が何かを受け取るような形ではないからです。 普通、配列の先頭要素へ何かを代入するときには、

str[0] = 100;

のように書きます。このとき、&演算子は付いていませんが、先ほどの形では &演算子が付いてしまっています。 ちなみに、このような左辺側には存在できず、右辺側にあるべき形を、右辺値と呼びます。


このように、配列が先頭要素のアドレスに置き換えられるというルールのため、 文字列の全要素が一致しているか調べる際に、==演算子が使えず、strcmp関数(⇒リファレンス)が必要になります。

#include <stdio.h>
#include <string.h>

int main(void)
{
#if 1
	char str[] = "abcde";
#else
	char* str = "abcde";
#endif

	if( str == "abcde" ){
		puts( "==演算子による比較" );
	}
	if( strcmp( str, "abcde" ) == 0 ){
		puts( "strcmp関数による比較" );
	}
	
	return 0;
}

実行結果:

strcmp関数による比較

配列版で ==演算子を使うと、==演算子の左辺も右辺も、ポインタに変換されて、

if( &str[0] == &"abcde"[0] )

こういう比較をしていることになります。 変数str 自身のアドレスと、文字列リテラル"abcde" のアドレスは異なるため、この if文は絶対に真になりません。
そのため、strcmp関数を使う必要があります。 strcmp関数は、2つの文字列の先頭から1文字ずつ順番に比較処理を行っているだけです。

一方、ポインタ版では、==演算子でも strcmp関数でも動作します。 ==演算子の場合、

if( str == &"abcde"[0] )

こういう比較をしていることになりますが、ここで変数str は最初からポインタであり、 しかも、"abcde" のアドレスで初期化されていますから、この if文は真になります。
strcmp関数を使って、先頭から1文字ずつ順番に比較処理を行ったとしても、やはり同じ文字で構成されているので、真になります。

文字列の長さ

ある文字列が、どれだけの文字数を含んでいるのか知りたい場面はよくあります。 配列として宣言された文字列変数であれば、sizeof演算子を利用することが考えられます。

#include <stdio.h>

int main(void)
{
#if 1
	char str[] = "abcde";
#else
	char* str = "abcde";
#endif

	printf( "%u\n", sizeof(str)-1 );
	
	return 0;
}

実行結果:

5

配列の要素数は、末尾の '\0' の分まで確保されているので、sizeof演算子の返す値から -1 する必要があります。 もちろん、'\0' も含めた文字数が欲しいのなら別ですが。

sizeof演算子を使う方法だと、ポインタ版の方では文字数を取得できません。 ポインタはポインタに過ぎないのであって、どこを指し示していようと、同じサイズを返します。 32bit環境であれば、恐らく、4 を返すことでしょう。


文字数を知るための汎用的な手段は、strlen関数(⇒リファレンス)を使うことです

#include <stdio.h>
#include <string.h>

int main(void)
{
#if 1
	char str[] = "abcde";
#else
	char* str = "abcde";
#endif

	printf( "%u\n", strlen(str) );
	
	return 0;
}

実行結果:

5

strlen関数は、引数で渡された文字列の文字数を返します。 関数には、配列であろうと、ポインタであろうと、結局はポインタとして渡されます。

strlen関数は、末尾にある '\0' までの文字数をカウントして返します。 そのため、

"abc\0de";

のように、間に '\0' が挟み込まれているような文字列を渡すと、全体の文字数を返してくれないことには注意が必要です


練習問題

問題@ 配列に文字列を代入するとき、strcpy関数(⇒リファレンス)を使わないといけない理由を説明して下さい。

問題A 次の4つの文の出力結果をそれぞれ答えて下さい。

char str1[] = "abcd";
char* str2 = "abcd";

printf( "%u\n", sizeof(str1) );
printf( "%u\n", strlen(str1) );
printf( "%u\n", sizeof(str2) );
printf( "%u\n", strlen(str2) );

問題B strlen関数(⇒リファレンス)と同じことをする処理を書いて下さい(関数化する必要はありません)。


解答ページはこちら

参考リンク

更新履歴

'2017/4/29 ptrdiff_t型についての記述を追加。

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

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

'2013/8/11 「*p++;」を分割するコード例が不適切だったので削除。

'2012/3/31 「文字列の長さ」のサンプルプログラム中で、include するヘッダの名前が間違っていたのを修正。

'2009/11/18 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ