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」は配列の先頭を表しているため、ややこしいことにポインタのように使えます。でも、ポインタのように使えるだけで、アドレスを保存する領域が無いため、インクリメントしたり、アドレスを代入したりすることは出来ません。
「&」でアドレスを取得しても「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」自身の領域のアドレスを指します。
まとめ
配列はアドレスを保管するための領域を持っていないため、「&」で配列自身のアドレスを取得しても、配列自身と何も変わらない。それに対し、ポインタはアドレスを保管する変数であり、保管するための領域を持っている。ポインタを配列のように使うと、配列のときとは違いポインタが配列へのアドレスを保管する形になる。そのため、「&」を使用してアドレスを取得すると、ポインタの領域(配列へのアドレスを保管している)のアドレスが取得され、「ぽいんたのぽいんた」になる。
C言語の配列とポインタの違いを理解したい方へ その2
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がそれっぽい?(英語が分かっていないため、そもそもこれが仕様書かどうかも怪しい。)
英語が日本語のように読めるようになれば、どれだけ世界が広がることやら。


