●変数とポインタ
アセンブリ言語の基礎知識で「スタックポインタ」など、ポインタという言葉が出てきました。
スタックポインタはスタックのアドレスを保存する専用のレジスタでした。
また、汎用レジスタは演算処理などを行う、値の保存に使用しましたし、またHLレジスタなどにアドレスを入れて「間接アドレッシング」なるメモリアクセスができると言うことを学びました。このときのHLレジスタもアドレスを扱う場合にはポインタと呼ばれたわけです。
C言語での変数も、汎用レジスタなどと同じように値の保存だけでなくアドレスの保存を行い、その値を使ってメモリアクセスなどを行うことができます。アドレスを扱える変数ですから、アセンブリと同じようにその変数はポインタと呼ばれるわけです。
ポインタ変数といっても他の変数とあまり変わりは無いように感じます。 普通の変数は値を扱うので「変数」。ポインタはメモリのアドレス扱うため「ポインタ変数」と呼ばれているだけで、変数には変わりないわけですから。
●ポインタ変数の宣言
まずはポインタ変数の宣言から見てみましょう。ポインタ変数の宣言は
int *
のような形で行われます。
int型変数の宣言が
int a;
であるのと同じようにint *の*(アスタリスク)がつくことでポインタ型だという宣言になります。
変数の宣言はどんな変数でも宣言されるとそのデータ型が保存できる大きさのメモリが割り当てられます。たとえはchar型だったら1バイト、int型なら2(4)バイトのデータ保存を行うエリアができるわけですね。もちろんポインタ変数は場所を示す数値(アドレス)が扱われるわけだから、そのアドレスが保存できることが可能なサイズが準備されるのは当然でしょう。
ポインタは別にそんなに特別な変数ではないと思っていいでしょう。char型だったら1バイトが確保されintなら2(4)バイトが確保されるわけで、PC系では普通、(例外をのぞいて)メモリのアドレスは16進4桁(2バイト)で扱われるから、ポインタ変数は2バイトの数が保存出来るエリアが出来ることになります。要するにintと同じ大きさのエリアが準備されるわけだから、別に特別なわけでなくintサイズのデータならポインタ変数に入れることもできるわけです。
ポインタ変数はサイズもintと同じだし、入るデータもintでもいける変数なわけで、なにが違うのかというと、唯一、ポインタ変数は「中に入ってる数値はアドレスなんだな」と解釈されるだけのことです。
ただし間違って欲しくないのは
char *po;
ってなってもこの変数poのサイズはPC系では通常2バイトです。
あくまで、アドレスを入れるわけだからchar *とかint *でも必ず2バイトになります。
なぜchar *とかint *と*の前にchar型、int型と指定されるのかは後ほど説明します。
●変数の型とメモリへの保存
Cでchar型とは1バイトを表すものです。int型は2(4)バイト、short型は2バイト、そしてlong型は4バイトです。
int a;
のようになぜ変数に型を指定するのかは、そのaという名の変数がどれだけの値を保存できるかを指定するためのものでしたよね。
くどいようですが
char c;
と宣言すればcという名の変数は-128〜0〜127(80h〜00h〜7Fh)までの1バイトの保存出来るメモリをとり、そのサイズの値が記憶出来るわけです。
そしてそこに値を入れるには
c=20;
として、値がそのメモリに保存されるわけです。
当然
char *po
と宣言すればポインタ型なのでアドレスが保存できるサイズ(2バイト)分のメモリが準備されるわけです。
さて、当然のことですが、値がメモリに保存されていると言うことは、その保存されている場所もメモリアドレスがあるわけです。
変数宣言すればメモリのどこかにその変数のサイズを保存する分のメモリが割り当てられますね。もし仮に
char a=1;
char b=2;
と宣言した時の例として次の表のように変数が割り当てられたとします。(アドレスは仮です)
変数名 | アドレス | データ |
a | 2000h | 01 |
b | 2001h | 02 |
そして次に、
int a=8;
int b=0x5020;(わかりやすいように16進数にします)
と宣言した時の例として(アドレスは仮です)
変数名 | アドレス | データ |
a (下位バイト) (上位バイト) | 2000h 2001h | 08 00 |
b (下位バイト) (上位バイト) | 2002h 2003h | 20 50 |
のように2バイトごとにエリアが出来ます。
ちなみにこれを見るとメモリの保存が下1バイトがはじめで、上1バイトが後になってます。CPUにより違いますが、インテルやザイログ系はこのようになっています。
これはアーキテクチャーの問題からなのですが、スタックは上位、下位の順番に積まれます。通常スタックはメモリの逆から積まれますから、アドレスの先頭から終わり方向で見るとこの順番になります。
スタックと同じ順番にすることで、効率的な面から下位が先で、上位が後になる仕様です。
まあ、保存の順番はC言語ではあまり意識する事はほとんどないんですけど、知っていて損ではないですね。(ちなみにモトローラ系は素直に順番通りに並びます)
そしてポインタ変数で宣言しても
char *po;
po=0x5000h;
変数名 | アドレス | データ |
po(下位バイト) (上位バイト) | 2000h 2001h | 00 50 |
というようにアドレスが保存できるエリアがメモリへ割り当てられる訳です。
結局、変数には変わりないのでいずれも「値」をメモリへ入れることになるわけです。
●ただの変数と変わりのないポインタ変数
さてポインタ変数はアドレスの「値」を入れるための変数です。
char *po;
po=0x0000; /* 0xは16進数の意味 */
これでpoという名前の変数には0x0000という値(アドレス)が代入されるわけです。
po=0;
でも10進数で書けば同じですね。
ちなみに基礎が重要なんで一応解説しますが「=」は演算子であり代入演算子といわれます。これで左辺の数値が右辺に入れられるわけですよね。ポインタ変数でも扱うのは値ですから一緒で
po=0x2000;
これでアドレス2000hという値がpoに入っただけのことです。
極端な話
char *po;
po=50;
printf("値は%dです",po);
とやれば結果に
値は50です
と表示されます。変数(値を入れるもの)なんで当然ですね。(この実験は使用するコンパイラによっては出来ないか若しくは警告が出るかも知れません)
ただし、変数として変わりない訳ですから、一見、50という数値を入れてるようにも見えます。しかし実際には50というアドレス値として代入されていることになります。ポインタ変数ではこの考え方が重要な訳です。
●ポインタ変数、唯一の能力
ポインタ変数が普通の変数と変わりない面を紹介しました。さて、ポインタ変数が「ポインタ」というからには、この変数、何らかの意味があって当然ですね。
char *po;
po=0x2000;
としたとします。ここで
po=po+1;
とするとpoの値は0x2001になります。
別に、普通の変数でも同じですね。
これはchar型のポインタ変数をつくったから同じなんです。
ここで再度、型の解説、Cで、charとは1バイトを表すものです。
intは2(4)バイト、shortは2バイト、そしてlongは4バイトです。
では
int *po;
po=0x2000;
po++;
としたらどうでしょう。
poには入ってる値はやはり0x2001でしょうか?
それじゃ意味ないしおもしろくも何ともないただの変数ですよね
答えから言うと、poの値は0x2002となります。
どうしてか。それは
int *po;
というようにintの型でポインタを宣言しているところに意味があります。
再度int型変数のメモリイメージを見ると
int a=8;
int b=0x5020;
と宣言した時の場合(アドレスは仮)
変数名 | アドレス | データ |
a (下位バイト) (上位バイト) | 2000h 2001h | 08 00 |
b (下位バイト) (上位バイト) | 2002h 2003h | 20 50 |
のようにint型の変数は2バイトの大きさでメモリが割り当てられています。
この上のようなイメージの場合は、変数a,bは連続して割り当てられています。
このように連続しているので、
int *po
po=0x2000;
po++;
とすると、poの値が0x2002で変数bと同じアドレスの値がpoに入るわけです。
ですからint *で宣言した場合そのポインタ変数int型に使えるように2バイト飛びにポインタ変数の値を変えてくれるわけです。当然long型なら4バイト飛びの値を入れてくれます。
ここでcharの型でポインタ変数を宣言した場合
char *po
po=0x2000;
po++;
とすると当然poの値は2001hとなりpoは変数aの上位バイトのアドレスを示すことになります。このようにポインタ変数はその値で、アドレスを指定することが出来るわけです。
一般的にアドレスを指定することを「アドレッシング」と呼びましたが、ポインタ変数は宣言の型により、char型で宣言されたポインタ変数はchar型のサイズでアドレッシングされ、int型で宣言されたポインタ変数はint型のサイズでアドレッシングされるわけです。
●ポインタ変数の利用法と*演算子によるデータの参照
さて、ここから実際にポインタ変数の実用に入ります。
po++やpo--でこのポインタ変数のアドレス値を増減させるのことをやりました。
とはいえ、ただ増減させることだけではあんまり実用的ではありません。
char a=1;
char b=2;
と宣言したとします。
変数名 | アドレス | データ |
a | 2000h | 01 |
b | 2001h | 02 |
というようなアドレスに割り当てられたとしましょう。ここで
char *po;
po=0x2000;
というようなポインタ変数を準備したとします。
さて、いまポインタ変数poには2000hのアドレス値が保存されていますよね。
せっかくこの変数はアドレスの値が入っているわけですから、メモリ上のアドレスの2000hにあるデータへアクセス出来れば便利ですね。
そこで、
*po
というように変数名の前に*をつけてそのpoのアドレス値にあるあたいへアクセスする事が出来ます。この場合ではpoは2000hなので*poは01というようになります。
ここで注意が必要なのはchar *poなどの「ポインタ変数の宣言」と混同しないことです。変数宣言で*記号はポインタ型という「型」を表す型指定の記号です。
ここで出てきた*は「*演算子」であり型指定子とは別物です。この*記号をメモリ間接参照演算子といいます。
あくまで演算子なので+−*/とかと同じ演算子なのです。演算子とはその記号により何らかの作用が起こるものです。この場合*がつくことでその変数の値のアドレスにある値が求められる訳です。
演算子はいろいろありますがたとえば-100とすれば100が負の数になる演算子ですよね。ですから-aとなればaが負の数になる。それと、同様に*演算子がつけばその変数の示すアドレスに格納されている値が得られるわけです。
このように、変数aのように変数名を使用してそのデータを参照するのが直接参照ですけど、それに対して*でアドレスを介してデータを参照する場合が間接参照となるわけです。
そういうわけで
char a=1;
char b=2;
char *po
po=0x2000;
b=*po; /* 2000hのデータをbへ代入 */
のような使い方が可能なわけです。
また、逆に
*po=b; /* bのデータを2000hへ代入 */
ということも記述も可能です。
●アドレス演算子
さてここまでは、アドレスを仮定してお話してきましたが、
char a;
char b;
のように変数を宣言したら、実際にはメモリのどこにエリアが割り当てられたのでしょうか。
そこで、変数や関数などがメモリのどこにあるかを求めことが出来る演算子があり、それがアドレス演算子です。
その演算子は
アドレス=&a;
のように&(アンパサンド)記号をつけることで演算出来ます。
そして
char a=1;
printf("変数aの割り当てられたアドレスは%pです",&a);
とすると&aにより変数aのアドレスが得られるのでその値がprintf関数により表示されます。ちなみに%pはわかりやすくアドレスを表示するための指定で4桁の16進数表示になります。
今度は本当のアドレスを使ってポインタ変数へ値をいれてみると
char a;
char *po
po=&a;
とすることでポインタ変数poへ変数aのアドレスを代入したことになります。そして
char a=1;
char b;
char *po
po=&a;
b=*po; /*アドレス&aのデータをbへ代入*/
これで、bへアドレス&aにあるデータが代入されたわけです。
まあ、結果的にやっていることは
b=a;
と何ら代わりはありません。
ただ、アドレスを介して間接的に行ったのがb=*po;なわけです。
ここまで見るとおそらく頭の中が整理出来なくこともあるかと思います。特に演算子とポインタ変数が混じるとそうなるでしょう。そこで演算子のみを使い、次がどのような動作になるか見てみます。
char a=1;
char b;
b=*(&a);
どうでしょうか。3行目の式を細かく解説します。
()で囲まれていますがまず()の中がはじめに考えられることは変わりないわけで&aでaのアドレス値を求めるというわけです。
ここで仮に変数aのアドレスが2000hだとしたら&aの部分は
b=*((char *)0x2000);
のように書き換えることができます。
これで&aは2000hを示すポインタに置き換わったわけです。
ちなみに、この例だとアドレスを直接、数値で指定していますが、0x2000だけだとただの数(定数)であり、それに対して間接参照を行うのでエラーになります。(間接参照はポインタに対して行うため、相手が定数の場合、明示的に型を示してあげないと何のポインタなのかが分かりません)
そこで(char *)をつけてchar型ポインタだということを明示的に変換する演算子です。(char *)という形でchar型ポインタに変換する演算子(キャスト演算子)といいます。
そして*によりその中身へ間接参照演算が行われます。
この演算子による演算で1という値がでてくるわけで
b=1;
という形の代入を行うような動作になったわけです。
結局
char a=1;
char b;
char *po
po=&a;
b=*po; /*アドレス&aのデータをbへ代入*/
では&aが2000hだとすれば4行目が
po=0x2000h;
と書き代わり5行目は
b=*(po);
よってpoは2000hのアドレス値が入ってるから
b=*((char *)0x2000h);
となり、最後には*((char *)0x2000)は1という値だから
b=1;
というわけです。
わかるように上では1〜3行目は宣言であり、変数が準備される、そして4,5行目では演算が行われるわけですからこの説明のように式通りに計算がおこなわれるわけです
式通りですから
x=100*50
よって
x=5000
のようなことを行っているだけなのです。
●まとめ
さて、ここまでを振り返り整理していきましょう。大きく分けて2分割できましたね。
1.宣言部(変数を準備)
ポインタ型変数宣言
基本構造: 型指定子 *変数名=[初期化子]
実例: int *name=0;
初期化子の部分は変数が宣言されると同時にその変数へ値を初期値として設定するものです。変数がメモリに割り当てられたと同時にそこへ値を入れるのがこの初期化子ですね。
2.演算部
間接参照演算子
基本構造: * op1 (op1はパラメータ)
実例: *name;
結果: 値
アドレス演算子
基本構造: & op1 (op1はパラメータ)
実例: &name;
結果: アドレス
●応用編
最後に応用編です。
#include<stdio.h>
main(){
int a=0x1234;
int *po;
int *p;
po=&a;
p=&po;
printf("%x\n",*p);
printf("%x\n",*((int *)*p));
}
さてこのプログラムではなにが表示されるでしょうか。
ちょっとまわりくどい問題ですが・・・・。