へぼいいいわけ このページをアンテナに追加 RSSフィード Twitter

2009年10月12日

C言語の配列とポインタの違いを理解したい方へ

C言語配列ポインタは違います。どう違うのかWEBで調べてみると、みんな説明がバラバラで面白いです。というわけで、C言語配列ポインタの違いについて説明してみて、このWEB上の混沌としている状況をさらに悪化させようと思います。


配列

ソース
#include <stdio.h>

int main()
{
	char data[4] = "abc";

	/* アドレスを調べる */
	printf("01:%p\n", data);
	printf("02:%p\n", &data);
	printf("03:%p\n", &(data[0]));

	/* abcを表示させる */
	printf("04:%s\n", data);
	
	/* 値を書き換える */
	data[0] = 'd';
	data[1] = 'e';
	data[2] = 'f';
	printf("05:%s\n", data);
	return 0;
}
実行結果
01:0x22cd74
02:0x22cd74
03:0x22cd74
04:abc
05:def

01〜03まで、アドレスを表示させています。全て同じアドレスを指し示しています。


ここで気になるのが02の結果です。

「&」を使用して「data」のアドレスを取得していますが、同じアドレスを指し示しています。「data」は配列であってポインタでは無いため、「data」自身はアドレスを保存する領域を持っていません。でも、「data」は配列の先頭を表しているため、ややこしいことにポインタのように使えます。でも、ポインタのように使えるだけで、アドレスを保存する領域が無いため、インクリメントしたり、アドレスを代入したりすることは出来ません。

f:id:heiwaboke:20091011225627p:image

「&」でアドレスを取得しても「data」は配列自身なので「data」と「&data」は同じアドレスを指します。


ポインタ

ソース
#include <stdio.h>

int main()
{
	char *data = "abc";

	/* アドレスを調べる */
	printf("01:%p\n", data);
	printf("02:%p\n", &data);
	printf("03:%p\n", &(data[0]));

	/* abcを表示させる */
	printf("04:%s\n", data);

	/* 値を書き換える */
	data[0] = 'd';
	data[1] = 'e';
	data[2] = 'f';
	printf("05:%s\n", data);
	return 0;
}
実行結果
01:0x402020
02:0x22cd74
03:0x402020
04:abc
05:def

01〜03まで、配列と同じくアドレスを表示させています。配列の時とは違い、02が違うアドレスを指しています。

「data」はポインタのため、アドレスを保持しています。そのアドレスとは「"abc"」の先頭、つまり「'a'」のアドレスです。

「data」の領域は「"abc"」とは別の場所にあり、そのため「&」でアドレスを取得すると「data」自身の領域のアドレスを指します。

f:id:heiwaboke:20091011225628p:image


まとめ

配列はアドレスを保管するための領域を持っていないため、「&」で配列自身のアドレスを取得しても、配列自身と何も変わらない。それに対し、ポインタはアドレスを保管する変数であり、保管するための領域を持っている。ポインタ配列のように使うと、配列のときとは違いポインタ配列へのアドレスを保管する形になる。そのため、「&」を使用してアドレスを取得すると、ポインタの領域(配列へのアドレスを保管している)のアドレスが取得され、「ぽいんたのぽいんた」になる。

C言語の配列とポインタの違いを理解したい方へ その2

C言語の配列とポインタの違いを理解したい方への続き

  • 配列ポインタは違う。
  • アドレスだけではなく、型も考慮するべき。
  • 譬え話ではなく、仕様書を読もう。
http://d.hatena.ne.jp/kilrey/20091011#p1

ご指摘ありがとうございます。

型の考慮なんて頭の片隅にもありませんでした…。

提示して下さったサンプルの一部の動きが理解できていなかったため、GDB(6.8.0.20080328-cvs)を使って確認してみました。環境はWindowsXP Home 32bit上のcygwin1.7です。


ソース

#include <stdio.h>

int main()
{
	char a1[2] = "1";
	char a2[8] = "8888888";

	char *p1 = "1";
	char *p2 = "8888888";

	return 0;
}

GDB(整形済み)

(gdb) p a1
$1 = "1"
(gdb) p &a1
$2 = (char (*)[2]) 0x22cd36
(gdb) p &a1 + 1
$3 = (char (*)[2]) 0x22cd38
(gdb) p &a1[0]
$4 = 0x22cd36 "1"
(gdb) p &a1[0] + 1
$5 = 0x22cd37 ""

(gdb) p a2
$6 = "8888888"
(gdb) p &a2
$7 = (char (*)[8]) 0x22cd28
(gdb) p &a2 + 1
$8 = (char (*)[8]) 0x22cd30
(gdb) p &a2[0]
$9 = 0x22cd28 "8888888"
(gdb) p &a2[0] + 1
$10 = 0x22cd29 "888888"

(gdb) p p1
$11 = 0x402020 "1"
(gdb) p &p1
$12 = (char **) 0x22cd24
(gdb) p &p1 + 1
$13 = (char **) 0x22cd28
(gdb) p &p1[0]
$14 = 0x402020 "1"
(gdb) p &p1[0] + 1
$15 = 0x402021 ""

(gdb) p p2
$16 = 0x402022 "8888888"
(gdb) p &p2
$17 = (char **) 0x22cd20
(gdb) p &p2 + 1
$18 = (char **) 0x22cd24
(gdb) p &p2[0]
$19 = 0x402022 "8888888"
(gdb) p &p2[0] + 1
$20 = 0x402023 "888888"

配列ポインタに対してポインタ演算を行うと、配列の大きさ分のアドレスが変化するんですね。その辺が良く分かっていませんでした。上の例では「&a2」が(char (*)[8])になり、「&p2」が(char **)であるため、ポインタ演算(+1)を行うと、前者はアドレスが「8(要素数×型の大きさ)」加算され、後者はアドレスが「4(この環境のポインタ型のサイズ)」加算される動きをするようです。

(gdb) p &a2
$7 = (char (*)[8]) 0x22cd28
(gdb) p &a2 + 1
$8 = (char (*)[8]) 0x22cd30   ←「8」加算されている
(gdb) p &p2
$17 = (char **) 0x22cd20
(gdb) p &p2 + 1
$18 = (char **) 0x22cd24   ←「4」加算されている

配列の宣言は指定した要素数×型の大きさ分のサイズを持つ新しい型を定義するのとあまり変わらない感じですね。


仕様書を見ようとしたら、日本語のC99の仕様書は、有料じゃないと無い事が分かりショックを受けました。14000円は高すぎます。

仕方がないので英語の仕様書を探してみたのですが、

http://www.open-std.org/jtc1/sc22/wg14/www/docs/C99RationaleV5.10.pdf

の6.3.2がそれっぽい?(英語が分かっていないため、そもそもこれが仕様書かどうかも怪しい。)

英語が日本語のように読めるようになれば、どれだけ世界が広がることやら。