ポインタ変数を使ってみる
ポインタ変数の宣言
前節では、3種類のポインタに関する説明を行いましたので、
ここでは、実際にポインタ変数を宣言して、感覚をつかんでみたいと思います。
と言うわけで、早速ポインタ変数を宣言する例を示したいのですが、
実は、これがまたやっかいなシロモノだったりするのです。
とりあえず、intへのポインタ型の変数を宣言する例を2つ示します。
これが、多くの入門書で紹介されている、ポインタ変数の宣言の書き方です。
この2つは、pという名前のintへのポインタ型の変数を宣言する書き方です。
1つ目の書き方は*pという名前のようですが、*はポインタ型を意味する記号で、
実際には、int型変数のアドレスを記憶するpという変数を宣言しています。
それならば、型名に*の付く2つ目の宣言の方が読みやすくも思えるのですが、
2つ以上の変数を宣言すると、2つ目以降は見かけの型名と違ってしまいます。
次の例では、2つ目のp2は、普通のint型変数になってしまいます。
どちらの書き方でもとてもわかりにくいという、大変困った問題なのですが、
とりあえず、ここでは1つ目の書き方で統一することにしましょう。
つまり、変数名の前に*をつければ、ポインタ変数を宣言できるのです。
この時、*がついていても、変数名はあくまでも
pになります。
アドレスを代入する
前節で説明した通り、ポインタ変数とは、アドレスを代入する変数です。
ポインタ変数の宣言の次は、早速アドレスを代入してみたいと思います。
ところで、アドレスを代入するのは良いとして、代入するアドレスはどうするのでしょうか。
理屈の上では、そのコンピュータが搭載しているメモリの範囲内の番号であれば、
たとえば、メモリ4GBのコンピュータであれば、0 ~ 42億 の範囲内の数値であればなんでもOKです。
電卓のような極めてシンプルなコンピュータや、ファミコンのような旧式ゲーム機の場合、そうやって使うこともできます。
ですが、皆さんがC言語の学習に使用しているのは、おそらくは現代的なパソコンです。
パソコンには、Windows、macOS、Linux、などのオペレーティングシステム(OS)が搭載されており、
これが仮想メモリと呼ばれる仕組みによって、勝手気ままにメモリを使えないように管理しています。
【仮想メモリ】
OSがメモリを管理して、多数のアプリに適切にメモリを振り分ける仕組みのこと。
多数のアプリが同時に動く環境で、個々のアプリが勝手気ままにメモリを使うと、
別々のアプリが使うメモリがかぶってしまい、正常に動作しなくなるので、
OSが管理して、個々のアプリが使うメモリがかぶらないようにしている。
したがって、テキトウなアドレス番号では、OSによって管理されているアドレス番号にならないため、
テキトウなアドレス番号を代入したポインタ変数を使うと、OSにより異常動作だと判定されて、強制終了してしまいます。
ポインタ変数には、OSによって管理されたアドレス番号を代入する必要があります。
実は、正常に管理されたアドレス番号を代入する簡単で確実な方法があります。
方法は簡単なことで、もう1つ別の変数を宣言し、そのアドレスを代入する方法です。
宣言された変数は、OSによって管理されたメモリ領域に作られているため、問題なく使用できるのです。
次のプログラムは、ポインタ変数pに変数のアドレスを代入する例です。
int main(void)
{
int *p;
int i;
p = &i;
return 0;
}
まず、変数名の前に*をつけるとポインタ変数として宣言できるのだから、
変数名の前に*の付いていないiは普通の変数であることを理解してください。
この例では、&演算子で変数iのアドレスを求めてポインタ変数pに代入しています。
つまり、この段階で、ポインタ変数pにはiのアドレスが入っています。
と言うことは、iのアドレスとポインタ変数pの中身は、当然同じになるはずです。
次のプログラムは、printf関数でアドレスを表示して確かめる例です。
#include <stdio.h>
int main(void)
{
int *p;
int i;
p = &i;
printf("p = %p\n", p);
printf("&i = %p\n", &i);
return 0;
}
このプログラムの実行結果は、次の通りになるかもしれません。
p = 0012FF80
&i = 0012FF80
見事に同じ値になっています。
このことは、ポインタ変数も変数であることからすれば、ある意味当然です。
だって、pに&iを代入して、直後にその値を表示しているのですから。
ただし、ここでは型に注意してください。
ポインタ変数pの型は、intへのポインタ型という型です。
変数iの型はint型ですが、&演算子を使って得られるアドレスはポインタ型です。
したがって、&iをpに代入出来、また両方共に%p指定子で表示できるのです。
ポインタ変数も宣言した直後はデタラメな値が代入されています。
その値が使用可能なアドレスなのかはまったくわからないので、
勘違いでそのアドレスを使ってしまうと確実にバグになります。
これを防ぐにはアドレスを代入したかを区別する必要があります。
そこで、C言語には、ヌルポインタが用意されています。
NULL という記号をポインタ変数に代入しておけば、
アドレスが代入されていない、つまり、まだ使える状態になっていないことを示せます。
int *p = NULL;
このようにすれば、if文で p == NULL であるか比較すれば、
p にアドレスが代入されているか区別できます。
int *p = 0;
としてもヌルポインタが代入されます。
これはC言語の文法として決まっていることであり、
NULL が 0 であるということではありません。あくまでも NULL は NULL です。
NULL は 正しいアドレスが代入されていないことを示すための識別用の値であり、
計算に使うための数値である 0 とは明確に区別されるものです。
もっとも、ほとんどのコンパイラでは NULL は 0 になってると思いますが・・・
モードの切り替え
前節で説明した通り、ポインタ変数は、2モードを持っています。
それは、通常変数モードと、ポインタ変数モードです。
とくに何も指定せずにポインタ変数を使っている場合はポインタ変数モードになります。
通常変数モードに切り替えるには、変数の前に*記号をつけます。
*記号がつけられたポインタ変数は、通常変数とまったく同じ機能になります。
次のプログラムは、ポインタ変数を通常変数モードに切り替えて使う例です。
#include <stdio.h>
int main(void)
{
int *p;
int i;
p = &i;
*p = 10; /* 通常変数モードに切り替えたポインタ変数に代入 */
printf("*p = %d\n", *p);
printf("i = %d\n", i);
return 0;
}
このプログラムの実行結果は、次の通りになります。
このプログラムでは、ポインタ変数pに*をつけて、通常変数モードに切り替えています。
*pは、通常変数モードに切り替わったポインタ変数pです。
*pである限りは、通常の変数とまったく同じように扱うことができます。
通常変数モードに切り替わったポインタ変数は通常の変数と同じように機能しますが、
その時使われるメモリは、ポインタ変数モードの時に代入されたアドレスです。つまり、
ポインタ変数モードの時に読み書きしたいメモリのアドレスを代入して、
その後、通常変数モードに切り替えてそのメモリを操作する。
と言うのが、ポインタ変数のもっとも基本的な使い方となります。
直接、何番のメモリを書き換えろ、と指定するのではなくて、
書き換えたいメモリのアドレスを代入し、モードを切り替えて書き換える、という、
いわば
2段構になっているため、直感的にはわかりにくいかもしれません。
先ほどのプログラムでは、5行目でポインタ変数pに変数iのアドレスを代入し、
6行目で、pを通常変数モードに切り替えて、pが記憶したアドレスに10を代入しています。
この時、pが記憶したアドレスとは、つまりは変数iのアドレスなので、
結果として、変数iの値は10に書き換えられていることになります。
もう少し具体的に説明すれば、この時、変数iと通常変数モードの*pは、
まったく
同じメモリ領域を使っているということです。
*pに10を代入すると、iも自動的に10に切り替わると言うのではなく、
この2つはそもそも同じメモリ位置を示しているのです。
*の記号は、実に3通りの意味を持っており、混乱の原因になります。
ここで、3つの区別をはっきりさせておきます。
1つ目は、乗算演算子です。いわゆる掛け算のことです。
式の中で使用する記号で、kai = 5 * 8 のようにして使用します。
2つ目は、間接参照演算子です。ポインタ変数を通常変数モードにします。
式の中で使用する記号で、*p のようにして使用します。
ポインタ変数モードの時のポインタ変数では掛け算が出来ないため、
乗算演算子と同じ記号を使っていても区別が付きます。
3つ目は、ポインタ変数を宣言する時に使用する記号です。
宣言の時にのみ使用され、int *p のようにして使用します。
ここがややこしいのですが、通常変数モードに切り替える間接参照演算子*と、
宣言の時に使用する*の記号は、何の関係もないまったく別の記号です。
たまたま同じ文字を使っているだけのことに過ぎません。
この3つにはすべて別の文字を使う方がわかりやすいはずだと思います。
同じ文字を割り当てているのはC言語の欠陥の1つです。
でも、いまさら直しようがありませんから、
皆さんはこの3つが別の意味の記号であることをしっかり認識してください。
すなわちショートカット
前項までで、
ポインタ変数の機能はすべて説明しました。
実際、ポインタは、前項までで説明した通りの機能しか持っていません。
ポインタ変数モードの時にメモリのアドレスを代入して、
通常変数モードに切り替えてからそのメモリを操作する、これがポインタの全機能です。
ここまでを理解した上で、当然でてくる疑問があります。
結局の所、ポインタとは
何の役に立つ機能なのでしょうか。
前項のように、ポインタ変数モードで変数のアドレスを代入して、
通常変数モードに切り替えて操作する、なんて面倒なことに何の意味があるのでしょう。
これはもう、疑問に思った通りで、そのような使い方では何の役にも立ちません。
普通に変数を操作した方が、よほど楽で間違いも少なくなります。
ポインタの本当の使い方は、
ショートカットとして使用することです。
Windowsのデスクトップに並んでいる、あのショートカットと同じです。
ショートカットは、どこか別の場所にあるファイルを指し示すファイルです。
ショートカットを開けば、その指し示しているファイルが開かれます。
にも関わらず、ショートカットは指し示すファイル自体ではないので、
ショートカットはどこにでも自由に作ることができますし、
複数個作ったり削除したりしても、指し示すファイルには何の影響もありません。
これこそが、まさにポインタの役割そのものです。
ポインタ変数に、実際に存在する変数のアドレスを記憶しておけば、
そのポインタ変数が使える場所であれば、元の変数が使えない場所であっても、
ポインタ変数を通常変数モードに切り替えれば、元の変数と同じく使うことができます。
まさに、ショートカットのような働きをさせることができるわけです。
一般には、ポインタはC言語とC++のみの機能だと言われています。
確かに、指定したメモリのアドレスを操作するという意味ではその通りです。
しかし、ポインタの本当の使い方はショートカットとして使うことであり、
その観点ならば、**実用的なほとんどの言語にポインタがあります**。
Javaの参照はまさしくそんな機能で、しかも頻繁に使われますし、
VisualBasicのSETステートメントなども同様と言って良いでしょう。
そもそも、ポインタがないのでは、連結リストや木構造などの、
複雑なデータ構造を実現できませんし、オブジェクト指向も困難です。
その意味では、仕組みが不明なJavaやVisualBasicのポインタより、
仕組みがはっきりしているC言語のポインタの方が理解しやすいです。
他の言語の参照と、C言語のポインタの最大の違いは、自動なのか手動なのか、です。
他の言語の参照は、ほとんど自動でショートカットとして機能するようになっていますが、
C言語のポインタは、完全に手動であり、プログラマーが完全に理解して使わなければなりません。
そのかわり、上級者がC言語のポインタを使いこなすと、ポインタだけで、
ほぼあらゆる制御構造、あらゆるデータ構造、を実現可能な強力すぎる機能となります。
実際、C言語のほとんどの機能が、ポインタで成り立っています。