先日、C言語を学習中の友達に「本を読んでたら、なんかポインタってのが出てきて、何をやってるのか分かるんだけど分からない。だから、もうちょっと分かるように教えてくれない?」みたいな感じのことを言われまして、質問に答えてました。
その時の解説が我ながらなかなか分かりやすかったのではないかと思いまして、一応こちらでも公開してみます。(ただ、長くなりすぎてしまったので、いつもの如く数回に分かれています)
一応確認です。そもそもポインタというのは、C言語やC++言語というプログラミング言語の中で出てくる概念(僕が知らないだけで、他の言語でもポインタを使うかも)で、「メモリのアドレスを指すための変数である」として説明されるものです。
この記事はそのような説明を読んだことがある(「理解できた」や「使えるようになった」ではなく、とりあえず見かけたことがある程度で大丈夫です)人向けの記事になります。
ポインタを使って何をしたいのか?
そもそも、技術というものは目的を実現するに当たっての何かしらの障害を克服するために作られるものです。(ここで、「技術」という単語の意味が定義されていませんので、人によって想像する物の範囲が違っているとは思いますが、さしあたっては問題ないと思うので、「技術」という単語には深入りしないでおきます。)
ポインタも技術の一種なわけですが、技術である以上は何かしらの障害を克服するために必要とされた(現状でも必要とされている)ものなわけです。
では、その”障害”とは何なのか、”ポインタ”なる技術はその障害をどのように克服するのかから説明していこうというのが、今回の記事の目的になります。
そもそも、変数とは何か?
では、ここで突然ですが、int型のaという変数を宣言して、そこに10を代入するようなプログラムを考えます。ソースコードで言えば、次のような感じ。
#include <stdio.h>
int main()
{
int a;
a = 10;
return 0;
}
このプログラムのreturn文の前にprintf()関数なんかを書き加えれば、aの中身(10)を画面上に出力することができます。
では、このとき、コンピュータ内部では何が起こっていたのでしょうか?というのが、この節で考えたいことです。
コンピュータの中にはメモリが用意されていて、その中に数値を書き込んで一時的なデータの保存を実現しているということはご存知の方も多いかと思います。
上のプログラムは、普通の説明文の中では「変数aに10という数値を代入するプログラム」というように表現されるでしょうが、メモリの話を併せて考えると、上のプログラムは「メモリへのデータ書き込みを行っていた」とも捉えられるわけです。
というより、コンピュータ的には、むしろ後者の表現の方が正しいわけです。つまり、上に書いたプログラムは、”a”という変数名を使ってメモリ操作を行っていたわけです。
ここで、変数aには数値の書き込みしかできないかと言えば、そうではないことを皆さんもご存じのはずです。例えば、a=10;という文の後に、
printf( "%d", a );
という文を書き加えれば、10という数値を画面上に出力できます。つまり、”a”という変数から数値を読み取ったわけです。このような数値の読み取りも一種のメモリ操作として考えられます。
まとめると、
命題1 : 変数への数値の代入や、変数からの数値の読み取りとは、メモリ操作である
ということになります。コンピュータ内部では何が起こっていたのか?という先の問いに答えるとすれば、「メモリの中で”a”という名前を付けた領域に対して、数値を代入したり、あるいは数値を読み取ったりしていた」ということになります。
ちなみに、命題1という名前を付けたのは、後でこの命題を繰り返し使うので便宜上そうしました。分かりやすくするために番号を振ろうとしただけです。命題とする意味も無ければ、番号が1である意味もありません。というより、他のところで意識することはないので、これを命題とする必要性もありません。
メモリ上の表現はどのようになっているか?
次に、ポインタが解決したい「障害」について説明していきます。
例えば、上のプログラムを次のように改良したとします。
#include <stdio.h>
int main()
{
int a, b;
a = 1123;
b = 2358;
return 0;
}
これは、変数aと変数bという2つの変数を作っておいて、それぞれに1123と2358を格納するというだけのプログラムです。メモリという単語を使って表現すれば、「メモリ上で”a”という名前が付けられた領域と、”b”という名前が付けられた領域に、それぞれ1123と2358という数値を格納するプログラム」といった感じになります。
では、このプログラムの中で、例えばaの値とbの値を交換したいと思ったとします(どんなときにそう思うのか、具体例は後述)。
その場合、次のように改良できるでしょう。
#include <stdio.h>
int main()
{
int a, b, temp;
a = 1123;
b = 2358;
printf( "a = %d, b = %d\n", a, b );
temp = a;
a = b;
b = temp;
printf( "a = %d, b = %d\n", a, b );
return 0;
}
何をしているかと言えば、いきなりaの値を書き換えてしまうと、元々持っていたaの数値が上書きされてしまって無くなってしまいます。
aの値が無くなると、bにaの元の値を上書きするという操作ができなくなってしまいますから、aの値を書き換える前に、tempという一時的に使う変数を用意しておいて、そこにaの値をコピーしておくわけです。
その後、aの値をbで上書きして、aの元の値であるtempをbに上書きします。
こうして、aとbの値を交換することができたわけです。ですが、このプログラムはあまり良いプログラムとは言えません。なぜかと言えば、バグが起こる可能性が高いプログラムになってしまっているからです。
(ここからは2段落、非初心者向け)
というのも、tempという変数は本来main()関数の中では、本質的には必要ないはずの変数です。これくらい規模が小さいプログラムであれば、それほど問題にはなりませんが、もっと規模の大きいプログラムになれば、うっかりミスで値を変な値に書き換えてしまうミスなんかが出てきてしまいます。
そのような値の不正書き替えをなくすためにも、使う変数を最小限に抑えておきたいものです。そして、同時にmain()関数の中も短くしたいものです。
(ここまで、非初心者向け)
なので、新しくswap(int, int)という関数を作って、その中で値を交換することを考えます。つまり、次のようにプログラムを変更します。ところが、このプログラムは意図した動作にはなりません。
#include <stdio.h>
void swap( int, int );
int main()
{
int a, b;
a = 1123;
b = 2358;
printf( "a = %d, b = %d\n", a, b );
swap( a, b );
printf( "a = %d, b = %d\n", a, b );
return 0;
}
void swap( int i, int j )
{
int temp;
temp = i;
i = j;
j = temp;
return;
}
実行していただければわかりますが、aは元の1123のままと出力されて、bは2358と出力されます。
つまり「2つの変数の数値を入れ替えるという操作を関数化する」という目的を達成したいんだけど、「2つの変数の数値を入れ替える関数を作れない」という障害があるという状況になっています。
この障害に何かしらの方法で対処する必要があるわけですが、一般的に、障害が発生したら、その根本原因を明らかにして理解した上で対策を講じるというのが最善の対処法です。なので、ここでも同じような思考手順で対処法を考えていきます。
そのためにも、まずは、なぜこのような障害があるのかを理解する必要があります。ただ、その話を理解するためには、C言語に用意されている「スコープ」という概念を理解する必要があります。
まとめ
今回は長くなりすぎたのでここまで。
とりあえず、この記事では次の2点さえ理解していただければOKです。
- 変数操作とは、任意に指定した名前を介して行うメモリ操作である
- この記事の最後に書いたようなプログラムでは、2変数を入れ替えることができない
次回はスコープという概念を解説して、swap関数がなぜ上手く動作しなかったのかを解説します。運が良ければ、そのまま正しいswap関数まで行けるかも?
ただ、途中で後述しますと言っていたswap関数が必要なプログラムについては、その後になると思うので、結構先の話になってしまいそうです。もし興味のある方がいても、すみませんがもう少しお待ちくださいませ~。