コンピューター:C言語講座:アドレスとポインタについて


アドレスとポインタについて

 C言語の初心者が必ず躓くのがアドレスとポインタでしょう。アドレスとポインタは実は難しいわけではなく、どちらかというとC言語の文法に対して頭が適応するのに時間がかかるということではないかと思っています。

 C言語は比較的スマートな表記が特徴で、ソースをざっと見ても'$'などのような暗号のような文字が少なく、小文字ですっきりしています。そんな中、アドレスの'&'やポインタの'*'や、構造体ポインタのメンバアクセスの"->"はちょっと近寄りがたい印象があるのかもしれません。さらにポインタのポインタで"**"のような記述が出てきたり、関数のポインタで"int (*func)();"というような記述が出てくるとますます拒否反応が出るのでしょう。さらにアドレスとポインタがいっしょに登場するので両方とも分からなくて逃げ出し
たくなるのではないかと思います。

 まずはアドレスをしっかり頭で理解することが大切です。CPUは基本的に自分のアクセスできるアドレス空間に対して読み書きを行うことと計算などの演算を行うことが主な仕事です。一般的に例えば32ビットCPUであ
れば32ビットで表現できる範囲のアドレスを扱うことが出来ます。C言語の型で言えばunsigned intとなり、32ビットコンパイラではunsigned longと同じ4バイトの符号無し整数ということになります。CPUがアクセスするアドレス空間には、BIOSやビデオメモリなども割り当てられていますが、CPUからしてみればあくまでも符号無し32ビット整数で番地が割り当てられた場所にアクセスし、値を得たり、書き込んだりします。

 C言語で変数を定義すると、実行時にアドレス空間のどこかにその変数を格納できるエリアが確保されます。例えば「char c;」と言う感じに定義すると1バイトの値を格納できるエリアをアドレス空間のどこかに確保してくれます。「c='a';」という感じに代入すると、そのエリアに'a'という値が格納されます。単に「c」と記述すると、そのエリアに格納されている'a'という値が得られます。普通に値を扱うのであればここまでの記法だけで十分です。

一般的にUNIX系OSではプログラム実行時、下記のようなメモリマップに展開されます。

0xFFFFFFFF+------------------------+
          〜                       〜
          +------------------------+--
          |                        |
          |                        |  スタック・セグメント
          |                        |
          +------------------------+-- ←SP
          |        ↓拡張          |
          〜                       〜
          |                        |
          +------------------------+
          |     共有ライブラリ     |
          +------------------------+
          |     共有ライブラリ     |
          +------------------------+
          |                        |
          〜                       〜
          |        ↑拡張          |
          +------------------------+--
          |       ヒープ領域       |
          +------------------------+ 
          |         BSS            | データ・セグメント
          +------------------------+ 
          |        データ          |
          +------------------------+-- 
          |                        | ←PC
          |                        |  テキスト・セグメント
          |                        |
          +------------------------+--
          |                        |
          〜                       〜
          |                        |
0x00000000+------------------------+

テキスト・セグメント:マシン語命令を格納する為のエリア。
 PC:プログラムカウンタ:次にどのアドレスの命令を読み込むかを記憶しているレジスタ。メモリから命令を読み込むたびに1ずつ増える。

データ・セグメント:データを格納する領域。
 データ:静的変数や大域変数で初期値がある変数が格納される。
 BSS:静的変数や大域変数で初期値を指定しない変数(ゼロになる)、が格納される。実行ファイルにデータとして含まれない部分。
 ヒープ:malloc()などで動的に確保する領域。

共有ライブラリ:ヒープとスタックの間にあるが割り当てられる番地はシステム依存。

スタック・セグメント:関数実行などの作業領域。自動変数はここに置かれる。
 SP:スタックポインタ:レジスタの値を一時的に保存したり,サブルーチンコールなどの戻りアドレスを保存するために使われるメモリ領域のアドレスを示すカウンタ。

 プログラムで確認してみましょう。

[test1.c]
#include        <stdio.h>
#include        <stdlib.h>

int     gloval_i;
static int      static_gloval_i;

int main()
{
int     local_i;
static int      static_local_i;
int     *p=(int *)malloc(sizeof(int));

        printf("&gloval_i=%x\n",(unsigned int)&gloval_i);
        printf("&static_gloval_i=%x\n",(unsigned int)&static_gloval_i);
        printf("&local_i=%x\n",(unsigned int)&local_i);
        printf("&static_local_i=%x\n",(unsigned int)&static_local_i);
        printf("p=%x\n",(unsigned int)p);
	printf("main=%x\n",(unsigned int)main);

        return(0);
}

コンパイル・実行
# !gcc
gcc -Wall -g test1.c -o test1
# ./test1
&gloval_i=80495b0
&static_gloval_i=80495ac
&local_i=bffffa54
&static_local_i=80495a8
p=80495c0
main=804835c

 C言語では関数の決まりとして、引数は値渡しとなります。先ほどの「char c;」と定義したものを関数の引数として「func(c);」という感じに記述すると、func()には変数cに格納されている'a'という値が渡されます。その'a'という値はfunc()を実行するためにスタックエリアに儲けられた作業エリア内に格納され、その格納場所はfunc()がリターンするとスタックエリアごと消滅します。もともとの変数cの値を書き換えることは出来ません。これを実現するには、関数に格納場所を知らせる必要があります。格納場所はアドレスです。

 C言語では「&c」というように記述すると変数のアドレスが得られます。func(&c)と記述すると関数には変数cのアドレスが渡ります。受ける側では「func(char *c)」というように、*をつけてcは値ではなく、格納場所だ、ということを記します。これがポインタです。場所を格納するための変数がポインタなのです。この関数内で単に「c」と記述すると格納場所のアドレスが得られます。「*c」というように'*'をつけて記述すると格納されている値が得られます。「*c='b';」というように代入すれば、cが指している格納場所を書き換えることが出来ます。cが指している場所は呼び出し元の「char c;」の格納場所ですから、呼び出し元で「c」の値を見ても'b'と書き換えられています。

 関数を例にアドレスと、アドレスを格納するためのポインタの説明をしてみましたが、少しは頭の整理が出来ましたでしょうか。次のソースを実際に実行して理解してみてください。

[test2.c]
#include        <stdio.h>

int func1(char c)
{
        printf("func1:c=%c\n",c);       /* 渡ってきた'a'という値が表示され

る */
        printf("func1:&c=%x\n",(unsigned int)&c);       /* スタックエリア内のローカルなアド

レスが表示される:mainの&cとは全く違う場所のはず */

        c='b';
        printf("func1:c=%c\n",c);       /* 'b'と表示される */

        return(0);
}

int func2(char *c)
{
        printf("func2:c=%x\n",(unsigned int)c); /* 渡ってきたアドレスが表示される:

mainの&cと同じ場所 */
        printf("func2:*c=%c\n",*c);     /* その場所に格納されている値が表示

される */

        *c='b';
        printf("func2:*c=%c\n",*c);     /* 'b'と表示される */

        return(0);
}

int main()
{
char    c;

        c='a';
        printf("c=%c\n",c);     /* cの値'a'が表示される */
        printf("&c=%x\n",(unsigned int)&c);     /* cのアドレスが表示される */

        func1(c);
        printf("c=%c\n",c);     /* 'a'のまま */

        func2(&c);
        printf("c=%c\n",c);     /* 'b'と表示される */

        return(0);
}

コンパイル・実行
# gcc -Wall -g test2.c -o test2
# ./test2
c=a
&c=bffffa57
func1:c=a
func1:&c=bffffa37
func1:c=b
c=a
func2:c=bffffa57
func2:*c=a
func2:*c=b
c=b

 「char c;」と変数を定義すると、cは1バイトの値を格納できる変数として実行時にアドレス空間のどこかに確保されます。C言語ではsizeof()を使うとバイト数を得ることが出来ます。cは1バイトを格納するための変数なのでsizeof(c)は1となります。

 「&c」はどうでしょうか?cのアドレス、要するに1バイト格納するためのエリアの番地ですので、32ビットCPUであれば、4バイトのアドレス空間が扱えますので、4バイトのサイズが必要です。従ってsizeof(&c)は4となります。

 では、「char *p」はどうでしょう?アドレスを格納するための変数ですので、32ビットCPUであれば4バイトのアドレス空間を表す必要がありますので、これもsizeof(p)は当然4となります。ではsizeof(*p)はどうでしょう?pが指している場所の値ですので、char型の場所と定義していますので1バイトのエリア、すなわちsizeof(*p)は1となります。

 同様に、「double d;」と変数を定義すると、sizeof(d)は倍精度実数の値を格納できるように8バイト確保され、sizeof(d)は8となります。sizeof(&d)はその格納場所すなわちアドレスですので、sizeof(&d)はsizeof(&c)と変わらず4となります。変数の型が変わっても格納場所はあくまでもアドレス空間の番地ですので、変わりません。

 ソースで確認してみましょう。

[test3.c]
#include        <stdio.h>

int main()
{
char    c;
char    *p;
double  d;

        printf("sizeof(c)=%d\n",sizeof(c));
        printf("sizeof(&c)=%d\n",sizeof(&c));
        printf("sizeof(p)=%d\n",sizeof(p));
        printf("sizeof(*p)=%d\n",sizeof(*p));

        printf("sizeof(d)=%d\n",sizeof(d));
        printf("sizeof(&d)=%d\n",sizeof(&d));

        return(0);
}

コンパイル・実行
# gcc -Wall -g test3.c -o test3
# ./test3
sizeof(c)=1
sizeof(&c)=4
sizeof(p)=4
sizeof(*p)=1
sizeof(d)=8
sizeof(&d)=4

 ポインタは型によらず32ビットCPUであれば4バイトということですが、いったい何のために型を明記するのでしょうか?それはポインタの演算の単位を決めるためです。「char *p」で、*pはpの指しているアドレスの値を得られます。*(p+1)はその場所の次の場所の値です。次の場所に移動するのに何バイト移動する必要があるかが型によって決まるのです。charの場合は1バイト移動すれば次の場所になりますが、doubleの場合は8バイト移動しないと次の場所にならないのです。ちなみに、まとめて格納場所を得る場合、配列が良く使われます。「char cc[3];」という感じに記述すると1バイトを格納できるエリアを連続して8個確保してくれます。ccはそのエリアの先頭アドレスを指しますので、「char *p=cc;」というように、ポインタにアドレスを代入することが出来ます。こうすると、pは&cc[0]、p+1は&cc[1]と同じ場所を指し、*pはcc[0]、*(p+1)はcc[1]と同じ場所の値を得ることになります。

[test4.c]
#include        <stdio.h>

int main()
{
char    cc[3];
char    *p;

        cc[0]='0';cc[1]='1';cc[2]='2';
        p=cc;
        printf("p=%x,&cc[0]=%x\n",(unsigned int)p,(unsigned int)&cc[0]);
        printf("p+1=%x,&cc[1]=%x\n",(unsigned int)(p+1),(unsigned int)&cc[1]);
        printf("*p=%c,cc[0]=%c\n",*p,cc[0]);
        printf("*(p+1)=%c,cc[1]=%c\n",*(p+1),cc[1]);

        return(0);
}

コンパイル・実行
# gcc -Wall -g test4.c -o test4
# ./test4
p=bffffa40,&cc[0]=bffffa40
p+1=bffffa41,&cc[1]=bffffa41
*p=0,cc[0]=0
*(p+1)=1,cc[1]=1

 さて、アドレスは32ビットCPUであればあくまでも符号無し4バイト整数ですので、やろうと思えば「unsigned long」の変数に格納することも全く問題ありません。これもソースを見たほうが分かりやすいです。

[test5.c]
#include        <stdio.h>

int main()
{
char    c;
unsigned long   l;
char    *p;

	printf("&c=%x\n",(unsigned int)&c);
        c='a';
        l=(unsigned long)&c;
	printf("l=%x\n",(unsigned int)l);
        p=(char *)l;
        printf("*p=%c\n",*p);

        return(0);
}

コンパイル・実行
# gcc -Wall -g test5.c -o test5
# ./test5
&c=bffffa57
l=bffffa57
*p=a

 だいぶ理解できたのではないかと思います。もう少し続けましょう。

 「char *p」は1バイトを格納できる場所(アドレス)を格納するための変数です。その場所を得たい場合は「&p」で得られます。ポインタのアドレスです。それを格納したい場合は、「char **pp;」というように定義します。ppはchar型のポインタを格納する場所になります。ポインタのポインタ、あるいはダブルポインタなどと呼ぶようですが、順番に解釈する癖をつけましょう。**ppで値が、*ppで値の格納されている場所が、ppで値の格納されている場所の場所が得られます。複雑ですが、実はポインタはあくまでも場所を指すだけなので単なる符号無し4バイト整数です。*の多さに惑わされずに、単なる場所、と理解しましょう。ポインタのポインタは関数に格納場所の場所を渡したい場合や、2次元配列風のデータを保持したい場合に使われます。

 さらに、C言語では関数も変数同様に扱うことが出来ます。関数名のみでアドレスを得られ、それを関数のポインタに格納することが出来ます。これはソースを見ていただくのが分かりやすいでしょう。

[test6.c]
#include        <stdio.h>

int func(int v)
{
        return(v+1);
}

int main()
{
int (*p)();
int     v=0;

        printf("func=%x\n",(unsigned int)func);
        p=func;
        v=(*p)(v);
        printf("v=%d\n",v);

        return(0);
}

コンパイル・実行
# gcc -Wall -g test6.c -o test6
# ./test6
func=8048328
v=1

 最後に、構造体のポインタのメンバアクセスの「->」表記ですが、これは単純に慣れるしかありません。「struct _test_ s;」が構造体の変数定義ですが、この場合メンバには、「s.data1」という感じに'.'でアクセスします。「struct _test_ *p」と定義すると構造体のポインタとなりますが、普通の変数同様に、「(*p).data1」と記述できます。が記述が面倒なので、「p->data1」と書くことも可能としただけのことです。

 アドレスとポインタ、難しく考えずに、単なる場所という理解から再度整理してみましょう。分からなくなったらprintf()などで表示してみればすぐに気が付くと思います。


よろしければブログもご覧ください
ゴルフ練習場紹介サイト:ゴルフ練習場行脚録更新中!
ipv400067250 from 1998/3/4