変数とアドレス
変数はデータを格納するための論理的な単位ですが、物理的にはデータを格納する媒体は半導体メモリです。当然、変数の内容のデータはメモリのどこかに格納されているわけですが、メモリにはデータの位置を識別するためのアドレス (4 バイトの整数値) が割り当てられています。C 言語を用いたプログラミングでは、メモリのアドレスを用いることで、変数の中身のデータだけを扱うよりも様々な処理が実現できます。ある変数のメモリ上のアドレスを取得するには、& (アンパサンド) 演算子を使います。例えば、abc という変数のアドレスを取得するには &abc と書きます。変数のアドレスは今後色々なところで使うことになるのでよく理解しておいてください。
ポインタとは?
変数には、そのデータが格納されているメモリ上の位置を識別するアドレスという数値が関連していることを説明しました。C 言語のプログラムではアドレスをよく使いますので、「アドレスを保存しておくための変数」が必要になります。メモリのアドレスを格納するための変数を「ポインタ」といいます。Windows 95 以上では、通常メモリのアドレスは 32 ビット、つまり 4 バイトで表現されることになっていますので、ポインタ型の変数のサイズは常に 4 バイトです。しかし、ポインタが指し示しているアドレスのメモリ上に格納されているデータの種類を識別したいので、ポインタ型にも通常の int, float などの型を付ける要があります。ポインタ型の変数を宣言する方法は普通の変数を宣言する場合とほとんど同じで、ポインタであることを示すために変数名の前に * (アスタリスク) を付けます。ポインタ型の変数に、普通の変数のアドレスを格納したい場合は、= の左辺にはポインタの変数名を書き、このときは * をつけません。= の右辺は先に説明したように、変数名に & を付けてアドレスを取得します。それでは、ポインタ型変数にアドレスを格納する例を見てみましょう。
ポインタにアドレスを格納する
void main(){ int val = 5; // 普通の変数 int *ptr; // ポインタ型変数 ptr = &val; // ptr に val のアドレスを格納 // val のアドレスと ptr の内容を表示 SysDialog("val のアドレス: "+&val+", ptr の内容: "+ptr); }
解説
説明した通り、ポインタの宣言には * を付け、ポインタにアドレスを代入するときは * を付けません。val のアドレスと ptr の内容を同時に表示することで、ポインタ型の変数 ptr に val のアドレスが正しく代入されていることを確認します。メモリのアドレスは 4 バイトの整数なのですが、表示する場合には 10 進数よりも 16 進数がよく使われます。HTML を書いたことがある方は、色の指定で 16 進数を見たことがあると思いますが、それと同じです。16 進数って何?という方は、とりあえず 0〜9 と A〜F の合計 16 種類の文字を使って数値を表したものだと思ってください。
アドレスを使った値の参照
上の例ではポインタにアドレスを格納する方法を示しましたが、次は逆に、ポインタに格納されたアドレスを用いて、メモリに格納されているデータにアクセスする方法を示します。方法は簡単で、アドレスが格納されたポインタ型変数の名前に * を付けるだけです。次に例を示します。
ポインタの指すアドレスの内容を参照する
void main(){ int val = 5, *ptr = &val; // 変数とポインタの宣言 // アドレスを使って、メモリの内容を参照します SysDialog("val の内容: "+val+"\nptr の指すアドレスの内容: "+*ptr); *ptr = 10; // ポインタの指すアドレスの内容を書き換える SysDialog("val の内容: "+val+"\nptr の指すアドレスの内容: "+*ptr); val = 30; // 元の変数の内容を書き換える SysDialog("val の内容: "+val+"\nptr の指すアドレスの内容: "+*ptr); }
解説
今度の例では、変数の宣言とポインタの宣言を同時に行っています。int, float などのデータ型さえ同じであれば、ポインタ変数、非ポインタ変数を同時に宣言することができます。SysDialog 関数の呼び出し中に、ptr の指すアドレスの内容を参照するために *ptr という表現を使っていますね。この方法を使えば、指定アドレスのメモリの内容を読み取るだけでなく、代入によって書き換えることもできます。2 回目のメッセージでは、ポインタ側からメモリの内容を書き換えたら元の変数の内容も同時に変化していることがわかります。val と *ptr では同じアドレスのデータを参照しているわけですから、当然連動しているわけです。逆に、元の変数 val の内容を書き換えても、val という変数が存在するメモリ上のアドレスは変わらないので、ポインタから見たデータも同じ内容に変化します。
関数に変数のアドレスを渡す
戻り値によって関数から 1 個の値を受け取ることができることはわかりました。しかし、2 個以上の値を受け取りたい場合はどうすればよいのでしょうか。このような時こそ、ポインタを使えばよいのです。値を受け取りたい格納先の変数のアドレスを関数に渡し、そのアドレスに関数で処理したデータを入れてもらえばいいわけです。早速サンプルスクリプトを見てみましょう。
スクリプト
/* * エントリーポイント */ void main(){ int a = 32, b, c; split(a, &b, &c); // 関数に値とアドレスを渡す SysDialog(""+a+" の十の位は "+b+"、一の位は "+c+" です。"); } /* * 2 桁の数値の 10 の位の数字と 1 の位の数字を調べる */ void split(int val, int *b1, int *b0){ *b1 = val/10; // 10 の位 *b0 = val%10; // 1 の位 }
解説
上のプログラムでは、渡された 2 桁の値 val を 10 の位の数字と 1 の位の数字に分解して、b1、b0 によって与えられるメモリ上のアドレスに格納します。10 の位の数字は元の値を 10 で割ることで、1 の位の数字は元の値を 10 で割った余りで求められます。なお、この例では関数呼び出しを関数定義より先に行っています。Queek C ではこのような使い方も許されますが、一般の C 言語では、関数定義より先に呼び出しを行いたい場合は、呼び出しよりも前にプロトタイプ宣言を行わなければならないという決まりがありますので注意してください。
NULL ポインタとアクセスバイオレーション
これまでの例では、ポインタにきちんと有効なアドレスを代入してからその内容にアクセスしていました。しかし、もし間違えてポインタにアドレスを代入する前 (アドレスとしてどんな値が入っているかわからない) にその内容を参照してしまった場合、そのアドレスのメモリが呼び出し元のプログラムに割り当てられていなければ不正なアクセス (Access Violation) となり、OS (Windows) によって強制終了されてしまいます。特に、ポインタの内容がアドレス 0 で初期化されていた場合、これを NULL (ヌル;ドイツ語でゼロを表す) ポインタといいます。Windows では、有効なアドレスは必ずある一定より大きな値を持つことになっているので、NULL ポインタは必ず無効なアドレスを指しています。また、NULL = 0 であるため、if 文などで無効判定が可能なことから、ポインタを NULL で初期化することはよく行います。NULL というキーワードは、"Const.qc" というスクリプトファイル内で値 0 の定数として宣言されているので、ポインタの初期化に使うことができます。定数については後述します。