もくじ
まえおき
巷では「プログラマーになりたい人に初学者にとって、ポインタという考え方がわけわかめ」という話がよくあります。
そこでいろいろな人が「ポインタは住所だ」とか「変数がハコで」とか手を変え品を変え分かりやすいように説明してくれています。
それでもなお「ポインタがわかりづらい」という人が後を絶ちません。
もういっそのこと、例え話をやめてド直球で攻めたらいいんじゃないでしょうか。
Hello, Worldより簡単に
サンプルコード
以下のコードを考えます。
void main() { int a; int b; int c; a = 1; b = 2; c = a + b; }
「#include <stdio.h>」なんていう謎のオマジナイはこの際ナシです。あんなもの無くたってC言語は成り立ちます。
まぁ見ての通り、どこにも何も出力されませんが。
このプログラムは、「c = a + b」という計算を行います。このとき、変数aが1で変数bが2です。ここまでは良いですね?
さて、この「変数c」というのは、いったいどこにあるでしょうか。
変数の置き場所
答えはメモリです。
コンピューターの世界なんて、突き詰めればほとんどのものがCPUかメモリかストレージかネットワークのどれかです(暴論)。
「変数c」というものが、メモリのどこかにあります。
「メモリってなんなのかわかんないよー」という人は、パソコン屋さんに行ってメモリの実物を見てくるといいでしょう。DDR4だのDIMMだの難しい言葉と一緒に売られています。
とにかくあの、パソコンショップで売られている緑の板に黒い四角いのが付いたアレ、あのメモリの中に「変数c」というのが入るわけです。
ふだん、「変数cがどこにあるか」というのは、プログラマーが気にするべき部分ではないです。
pythonだのなんだの最近の流行の言語では特にそうです。そういう低レベルなお話を気にせずにプログラミングできるようになっています。
でも、ポインタの話をしたいなら別です。
「変数cがどこに入っているか気になる!」というのがポインタの本質であり、ポインタの存在意義です。
変数が入っているところを見てみる
せっかくです。変数cが実際におかれている場所を見てみましょう。
以下の手順は別にどんな操作をやっているかわからなくても良いです。
さっきのコードを適当に保存して、gccに-gオプションをつけてコンパイルしてしまいます。
$ gcc -g test.c
できたバイナリをgdbで開いて、main関数にブレークポイントを張ってから実行しましょう。
$ gdb a.out
(gdb) b main
Breakpoint 1 at 0x4004f1: file test.c, line 8.
(gdb) run
Starting program: /home/givemegohan/tmp/a.out
Breakpoint 1, main () at test.c:8
8 a = 1;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7_4.2.x86_64
main関数のアタマで止まりました。
さて、変数cの場所を確認します。
(gdb) p &c
$1 = (int *) 0x7fffffffe494
変数cは、メモリ上の「0x7fffffffe494」という場所にあるようです。
メモリ上の場所のことを「アドレス」と言います。
さあ、混乱してきましたね。
とりあえず初学者の皆さんは以上のことを深く考えなくても良いです。
とにかく変数がメモリ上にある、ということを覚えてください。
C言語とメモリ
変数がメモリ上にあることがわかりました。
さて、C言語は非常に低次元のプログラミング言語です。
プログラムの管理下にあるメモリは好きにいじることができます。
いいですか、メモリは好きにいじれます。
変数cの「まわり」を見てみる
変数cは、変数cだけがぽつんと置いてあるわけではありません。
メモリというのは前にも後ろにも連続しています。
まわりを見てみましょう。
(gdb) x/16wx 0x7fffffffe480
0x7fffffffe480: 0x00400510 0x00000000 0x00400400 0x00000000
0x7fffffffe490: 0xffffe580 0x00000003 0x00000002 0x00000001
0x7fffffffe4a0: 0x00000000 0x00000000 0xf7a39c05 0x00007fff
0x7fffffffe4b0: 0x00000000 0x00000000 0xffffe588 0x00007fff
上から2行目、左から2列目に「0x00000003」と書いてありますね。ここが変数cです。
前後にもいろいろ書いてありますね。ひとつ右の「0x00000002」が変数bで、もうひとつ右が変数aです。
それ以外にもいろいろありますが、まあよくわからないので深く考えないでおきましょう。
このように、プログラミング言語で書いた変数は、プログラムを実行するときにはメモリのどこかに割り当てられるわけです。
変数ってなんだ
変数a,b,cがメモリ上では隣り合ったアドレスに割当たっていることがわかりました。
ではここで質問です。変数というのは、コンピュータにとって何でしょう。
教科書なんかには「変数っていうのはハコみたいなものでー」とか書いてありますね?
でもメモリはハコみたいなものではないです。緑の板に黒いナンカが付いたやつです。教科書に書いてあるのは例え話です。
C言語にとっての答えは、「変数とは、メモリのアドレスに付けられた名前のこと」です。
いいですか、「変数c」があるのではなくて、「0x7fffffffe494」という場所(アドレス)に「変数c」という名前を付けているんです。
アドレスはどう決まるの?
じゃあ「0x7fffffffe494」が「変数c」だと誰が決めたんでしょうか。
プログラマーではないですね?だって最初に書いたc言語のコードには、「0x7fffffffe494」なんて書いてません。
正解はコンパイラです。コンパイラが、「変数cという場所が必要なようだから、そのぶんメモリを用意しておこう。0x7fffffffe494を変数cと名付けよう」と決めているんです。なので、コンパイラの種類によっては他のメモリアドレスが割当たることもあります。
コンパイラがこういったことを考えてくれるおかげで、われわれプログラマーは「変数c」というものの正体を深く考えずにプログラミングできるわけです。
ポインタを使ってみよう
あたらしいコードを考えます。
void main() { int a; int b; int c; int*p; a = 1; p = 0x7fffffffe48c; *p = 2; c = a + b; }
ポインタ変数pが出てきました。
そしてこのコードは最後に「c = a + b」を計算していますが、bはどんな値も代入していませんね。さて、cには何が入るでしょう。
答えは3です。
なぜ答えが3なのか
c = a + bを計算するとき、aは1です。そこは皆さん疑問がないでしょう。
そしてbは2です。なぜでしょうか。
答えは、ポインタ変数pを使って、変数bという名前のついたメモリアドレスに”2″を代入したからです。
いいでしょうか。変数とは結局のところ、メモリアドレスに名前がついているだけなんです。
メモリアドレスを別の方法で特定することができれば、C言語は好きなアドレスを書き換えることができます。
今回の例では、「変数b」はメモリ上で「0x7fffffffe48c」というアドレスに割り当てられています。
なので、「0x7fffffffe48c」のアドレスのメモリを書き換えれば、変数bの値を書き換えたのと同じことになります。
…まぁ、ふつうはプログラミングの段階で変数に割り当てられるメモリのアドレスを把握するのはムリなので、以下のように書くのが自然です。
void main() { int a; int b; int c; int*p; a = 1; p = &b; *p = 2; c = a + b; }
ポインタとは
ポインタは、「メモリ上の他のアドレスを指す変数」です。
アドレスと言ってもしょせんは数値です。数値なので、アドレスそのものをメモリに代入することができます。
何をいっているかわかりにくいでしょうか?でも、さっき書いたコードがそのままの意味です。
p = 0x7fffffffe48c;
メモリには数値が代入できます。メモリのアドレスも数値です。なので、メモリには他の変数のアドレスを代入できます。
この仕組みは便利なので、C言語はアドレスを指すための変数(ポインタ)を特別扱いします。
- “int*p”のように宣言するときに*をつければ、それはポインタ変数になります。
- “p = &b”のように&をつければ、ほかの変数のアドレスを参照することができます。
- “*p = 2″のように*をつければ、ポインタ変数の指すアドレスの中身を参照/更新することができます。
1と3で同じ”*”という記号を使っているくせにイミが違うのは少しヒドいんですが、まぁ、そこは慣れるしかないところです。
なぜポインタという仕組みがあるのか
それは皆さんで考えてほしいところです。
なぜなら、ポインタというのは用意された仕組みであって、それをどう使うかはプログラマーの自由であるからです。
ここでは主な用途を軽くだけ説明しておきます。
1つの関数で複数の値を返す
C言語は、関数の返り値が一つしかありません。
そのため、2つ以上の値を返却したい場合に、ポインタを利用して値を返すことになります。
例えばこんな感じに。
void warizan(int warareru, int waru, int* syou, int* amari) { *syou = warareru / waru; *amari = warareru % waru; } void main () { int a; int b; int c; int d; a = 17; b = 5; warizan(a, b, &c, &d); // c = 3, d = 2になる }
サイズの大きな変数を効率よく扱う
C言語は、関数の引数として値を渡す際に、「引数用のアドレス」に変数をコピーします。
int型やchar型のような小さい変数であれば問題ないですが、大きなサイズの構造体を引数に取るような関数の場合、メモリコピーにCPUを使ってしまって性能が落ちてしまいます。
どれだけ大きな構造体でも、先頭のアドレスだけ渡してしまえば効率よくやり取りできます。
変数として名前のついていないメモリを確保して、そこを操作する
C言語は基本的に、変数はすべて事前にサイズを決めて宣言しておきます。
しかし、ファイルを読み込む際など、必要なサイズがあらかじめ分からないことがあります。
そんなときmallocコールなどで動的にメモリを確保することになりますが、動的に確保したメモリには名前がついていません。
そこで、事前にポインタ変数だけは用意しておいて、あとで動的に確保したメモリをポインタ変数を経由して参照する、ということをします。
おわりに
プログラミング言語はどんどん進歩し、ポインタを必要としない言語が多くなってきています。
純粋なC言語を必要とする場面は極めて少なく、ポインタを理解しなければならない場面はほとんどなくなってきました。
しかしポインタという考え方が消えたわけではなく、どの言語にもその根底としてポインタの概念が存在し、「プログラマーにポインタを意識させないようにする」努力をしています。
ポインタを理解することで、ポインタの無い言語においてもより微細なプログラミングが可能となるでしょう。
そのためにはポインタをあやふやな理解で留めず、コンピュータのアーキテクチャから真に理解することが必要だと思います。
本記事が読者の理解の一助となれば幸いです。