[C言語入門]ポインタを理解する その1:メモリとポインタ変数入門

今回の説明はポインタ変数です。
ポインタ変数は、特に難しいというわけではないのですが、C言語での表記の仕方の問題で混乱する人も多いので、今回はゆっくりと順を追って説明していきます。

ポインタの理解には、前提としてコンピュータの構成要素であるメモリ(主記憶装置とも言います)の理解が必要となるので、まずこちらから説明していきます。

変数はメモリ上でどのように保持されているのか?


まずは,変数とメモリについての説明です。今まで、”i=10;”といったコードを”変数に値をセットする”と説明していましたが、変数の値はコンピュータ上でどのように管理されているのでしょうか? コンピュータの構成要素の1つであるメモリの仕組みを確認しつつ、この謎を確認していきます。


実行中のプログラムが使用している変数(データ)は、メモリによって保持されています。
メモリの論理的な構造を図で表すと、以下のような感じになります。


メモリには、複数の値の格納しておくことが可能で、それぞれの格納場所には番地(アドレス)が割り振られています。

アドレス(表の左側)は連番になっていて、通常は0番地から順番に振られています。値の欄(表の右側)は、実際には何らかの数字が入っており、値の読み書きを行う事がが可能なのですが、何の値が入っているかはまだ重要では無いため、上記の絵では”???”と表記しています。(余談ですが、何が入っているか分からないことを情報の世界では”不定値が入っている”といいます。)

順番に振られているので、同一番地の格納場所が複数存在するという事はありえません。この事を、”アドレスはユニークである”と言います。
ここで”ユニーク”というのはコンピュータ用語で、”重複しない”、”一意である”という意味です。



実際のメモリがどの程度の大きさ(どの程度の情報量を記憶できるか)をイメージしてもらうために、例を挙げて説明します。
メモリに振られたアドレスの最後の番地はいくつぐらいになるかというと、これは使っているパソコンに搭載されているメモリ容量によります。ざっくりな値ですが、4GBのメモリを乗せたパソコンの場合は”42億番地”ぐらいまで有ります。(2の32乗≒42億です)


このメモリというものは、かなり大きなモノだということが分かります。(メモリ構造の図でいうと、縦方向の長さが膨大になります)また、このサイズは”大きいけど有限だ”という事も同時に理解しておいてください。

また、入れておく値の大きさ、通常は1つの番地に対して1byte(=8bit)です。
なので、1つのアドレスには、0~255の256通りの値が格納できます。(これは、”上記図の???には0~255の何れかの値がセットされている。”と言い換える事も出来ます)
こっちは、”結構少なめだな…”という印象を持ってもらえればOKです。
また、ここまでC言語を学んできた人にとっては、このサイズが”char型の変数が表せる範囲とちょうど同じ”だという事も押さえておいてください。


C言語の変数とメモリの関係


次に、C言語の変数と、メモリの関係について説明します。
C言語での変数は、コンピュータではメモリに保存されています。

変数を定義すると、コンパイラ(あるいはOS)が、どの変数を何番のアドレスでで管理するかを、(プログラマから見えないところで)勝手に割り当てをしてくれます。


例えば、以下のプログラムがあった場合に…

char c;
c = 20;




コンパイラがaを1000番地に割り当てたとすると、メモリの中身は以下の様になります。
※他の番地は何が管理されているか不明(不定値)なので、ここでは???と表記していますが、実際は何らかの値が格納されています。




charは1byteなので上記の様な形になりますが、intだと様子が少し変わってきます。
intは(32bit環境だと通常は) サイズが4byteになります。
なので、以下のプログラムは…

int i1;
int i2;
int i3;
 
i1 = 10;
i2 = 500;
i3 = 4000000;



仮に、変数i1が1000番地、i2が1004番地、i3が1008番地に割り当てられたとすると、このようなメモリ構造になります。



ここで、上記の構造がイメージできない場合は、以下の事を思い出してください。

メモリ1番地あたりに格納できるデータサイズは8bit
  (=8bitであらわせる値は256通りしかない)



この為、intの値はメモリ1番地分(1byte)だけでは保存しきれず、4マス分の領域(4byte)が必要となるという仕組みです。



ここまでの例で説明したint型やchar型の変数ですが、これは、”数字や文字”を保持するデータ型です。
一方ポインタというのは、”あるデータが保持されている場所”を保持するデータになります。
※余談ですが、ポインタは英語で書くとpointerとなり、”指し示すもの”という意味になります。

C言語でのポインタ型をまだ説明していませんが、ポインタ型がどのようなものかをイメージしてもらうために、ポインタ変数がメモリ上で何を記憶しているかを説明します。

int      i;     /* "int  "型の変数 "i"                                    */
int *    ip;    /* "int *"型の変数 "ip" (=intへのポインタ変数 ipとも呼ぶ) */
 
i  = 10;
ip = &i;



このプログラムで、変数iが2000番地、変数ipが2004番地に割り当たったとすると、メモリ構造は以下の形になります。iは値10を記憶していますが、ipは”変数iがどこの番地に割り当たったか”の場所を記憶しています。



これまで、アドレスの表記は10進数で行っていましたが、コンピュータでは16進数を使う事が多いです。
この習慣にそって、本記事ではこれ以降16進数で表記します。
10進数表記と16進数の違いを示すために、例を1つ挙げておきます。
下記の2つは同じ意味である事に注意して見比べてみてください。




変数が割り当てられたアドレスを確認する


ここまで説明したところで、C言語のプログラムに入っていきます。
まずは、今まで作ってきたようなプログラムで、変数が具体的に何番地のアドレスに割り当たったかを確認する方法を説明します。

というわけで、最初のサンプルです。

#include <stdio.h>
 
int main()
{
    int  i1;
    int  i2;
    char c1;
    int  i3;
 
    i1 = 10;
    i2 = 20;
    c1 = 'a';
    i3 = 30;
 
    printf( "i1=%d, &i1=%p\n", i1, &i1 );
    printf( "i2=%d, &i2=%p\n", i2, &i2 );
    printf( "c1=%c, &c1=%p\n", c1, &c1 );
    printf( "i3=%d, &i3=%p\n", i3, &i3 );
 
    return 0;
}



手元の環境で確認したところ、実行結果は以下のようになりました。
(カンマの後に表示される値は環境によって変わる可能性があります)

$ gcc ptr1.c -o ptr1
 
$ ./ptr1.exe
i1=10, &i1=0x22ac8c
i2=20, &i2=0x22ac88
c1=a, &c1=0x22ac87
i3=30, &i3=0x22ac80




プログラムの内容を順に説明します。
まず、変数に値をセットする箇所は、今まで説明したとおりです。
特に難しい事はしていません。

    i1 = 10;
    i2 = 20;
    c1 = 'a';
    i3 = 30;




次のprintfですが、今までは以下のような書き方でした。

    printf( "i1=%d\n", i1 );


上記の記法は、変数i1の値を画面に表示しています。
i1はint型なので%dの書式指定を行っていますし、c1はchar型なので%cで表示しています。


これに対して、今回は以下の表記になっています。

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


ここで、&i1と変数の前に”&”記号がついていますが、これはアドレス演算子というものです。
変数に対してアドレス演算子をつけることで、変数が割り当てられた場所(アドレス)を求める事ができます。
アドレス演算子を使えば、どのような型の変数でもその場所を求める事が可能です。

アドレス演算子で求めたアドレス値をprintfで表示させるためには、書式指定で%pを使用します。

ここまで理解したところで、再度プログラムの出力結果を見てみます。

$ ./ptr1
i1=10, &i1=0x22ac8c
i2=20, &i2=0x22ac88
c1=a, &c1=0x22ac87
i3=30, &i3=0x22ac80



出力結果を元に、このプログラムが実行されていた時のメモリ構造を再現してみます。


先ほど書いたようにint型は4マス分(4byte分)、char型は1マス分(1byte分)の領域を使用します。
また、int型変数ののアドレスを%pで表示させた場合、先頭アドレス(一番若いアドレス)が表示されるため、&i1=0x22ac8cと出た場合は、0x22ac8c, 8d, 8e, 8fの4番地を占有します。
charは1byteなので、このプログラムでは0x22ac85,86,87番地は使用されていません。

ちなみに、上記のプログラムはwindowsにインストールしたcygwin環境での結果です。
今回は0x22ac80~90番地あたりに変数が割り当てられましたが、この番地はコンパイラやOSや実行タイミングが変わると、異なる番地になる可能性もあります。


コンパイラやOS環境によって番地が変わることがある事を確認するために、全く同じプログラムを別のOSであるLinux上でも実行してみました。
以下が実行結果です。

$ ./ptr1
i1=10, &i1=0xbff65160
i2=20, &i2=0xbff65164
c1=a, &c1=0xbff6516f
i3=30, &i3=0xbff65168



同様にメモリ構造を再現してみると、以下のような形です。


これをみると番地はもちろん、メモリ内での変数の並び順すら変わる(場合がある)という事が分かるかと思います。



ポインタ変数を使用する


次は、ポインタ変数を使用したプログラムのサンプルです。

#include <stdio.h>
 
int main()
{
    int  i;
    int *ip;
 
    /* int の変数に値をセット*/
    i = 10;
    printf( "i=%d, &i=%p\n", i, &i );
 
    /* intの変数がある場所を覚える */
    ip = &i;
    printf( "ip=%p, &ip=%p *ip=%d\n", ip, &ip, *ip );
 
    /* ipが覚えた場所の値を書き換える */
    *ip = 5;
 
    /* iを操作していないのにも関わらず */
    /* iの内容が5に変わってしまった    */
    printf( "i=%d, &i=%p\n", i, &i );
 
    return 0;
}



これを実行すると、以下のようになります。

$ gcc -o ptr2 ptr2.c
 
$ ./ptr2
i=10, &i=0x22ac8c
ip=0x22ac8c, &ip=0x22ac88 *ip=10
i=5, &i=0x22ac8c




実行結果を見ると分かるのですが、変数iを変えて無いにも関わらず、値が5に変わってしまいました。
どうして値が変わったのか、プログラムを順に追っていきます。

    int  i;
    int *ip;



ここで新登場の表記が”int *ip”です。
ここでは、ポインタ変数を定義しています。
このポインタ変数の定義ですが、”int *”型の変数”ip”と読んでください。
※言葉で説明する場合は、”いんとあすたがた”の変数(アスタリスクを省略してアスタと言ってます)や、”int型へのポインタ変数”と発音する事が多いです。


ポインタの勉強ではここで勘違いしてしまい後の説明が分からなくなってくる人が非常に多いので、もう一度説明します。
大事な事なので、以下の文章を3回音読して下さい!!(周りに人がいない事を確認した上で)

**重要**
 
ポインタ変数の定義"int *ip;"は、"int *" 型の変数 "ip" という意味です。
 
C言語の入門書によっては "int" 型のポインタ変数 "*ip" と書かれている場合も有りますが、これは間違いです。
 
なぜなら、アスタリスク記号(*)までが変数の型指定なので"int *"型が正解なのです。
 
プログラム中に"int *ip;"と書かれていると、途中にスペースがあるので、つい変数名が"*ip"であると
勘違いしがちですが、変数名は"ip"です。
 
**重要**




ポインタ変数を使用すると、以下のコードで”ある変数の値が記憶されている場所”を記憶できます。

    ip = &i;



これで、ポインタ変数ipにはiのアドレスが代入されました。


ポインタ変数のipの前に、”*”演算子を使用すると、”ポインタ変数が指している場所”に入っている値にアクセス出来ます。この演算子”*”を、間接演算子と呼びます。

説明の順番が前後しますが、サンプルプログラム中の以下のコードでは、間接演算子を使用して”変数ipが指している,変数i”の値を変更しています。

    *ip = 5;




printfで、ipと、ipに対して先ほど説明したアドレス演算子”&”、間接演算子”*”を適用した値を出力しています。ポインタ変数も単なる変数ですから&ipでアドレスが分かります。

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



再度iの値を表示していますが、ip経由でiの値を変更しているので、ここではi=5が出力されます。

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




プログラムの実行結果を元にプログラムが終了した時のメモリ構造を描くと、以下のようになります。

プログラムの実行結果(再掲)

i=10, &i=0x22ac8c
ip=0x22ac8c, &ip=0x22ac88 *ip=10
i=5, &i=0x22ac8c


メモリに格納された値



ポインタ型の指定では、例で”int *”型となっている事からも分かるように、ポインタ変数が指す対象のデータ型を指定する必要があります。例えば、int型ではなくchar型を指すポインタ変数を作りたい場合は以下のように変数定義を行います。

char *cp;




“*”記号の意味、勘違いしてませんか?

今回の説明では、ポインタ演算子である”&”と、間接演算子”*”を紹介しました。

ここで、”初心者が間違いやすいポイントその2″ がありますので書いておきます。
例えば以下のコードがあった場合…

    int  i = 10;
    int *ip;
 
    ip = &i;
    printf( "%d\n", *ip );
    return 0;
}



変数定義の”int *ip;”で書かれている”*”記号と、printfの引数で使用している”*ip”の”*”演算子(間接演算子)は全く関係が有りません。

ここで、変数名が”ip”ではなく、”*ip”だという勘違いをしていると、printfにある”*ip”が変数名だと勘違いする人が多いです。
(しつこいようですが”int *ip;”の変数宣言は、データ型が”int *”で、変数名は”ip”です)

ですので、printf分で書かれている*ipは、以下の意味であるという事を忘れないようにしてください。

printf( "%d\n", *ip ); の *ipは...
  → 変数"ip"に対して、間接演算子の"*"を使用することで、ポインタ変数が指す先の値を取得している。




説明した事を整理すると、以下の内容になります。

ポインタ変数の型名の一部と、間接演算子で、同じ記号"*"を使用していますが、両者には関係が有りません。
さらに言うと、掛け算の演算子も"*"ですが、これも間接演算子とは関係有りません。



このようにC言語では、同じ記号を状況によって違う意味で使っている場合があります。
紛らわしいので、勘違いしないよう注意して下さい。



以上でメモリの概念と、ポインタ変数の基本的な考え方の説明は終了です。
次回はもう少し色々なサンプルを通して、再度ポインタ変数の使い方を説明します。

関連記事

コメントを残す

メールアドレスが公開されることはありません。