dorivenの日記

気がついたら社会人。気になる技術的なことについて少しずつ書いていけたらと思っております。

ポインタを知ることはメモリ動作を知ることに等しい

どうも、色々やってたら気がついたらこんな時間ですね。

雪かきしたりで今日は大分体力を消耗してすごく眠いですが、頑張って記事を書きたいと思います。

といっても、記事の内容は前回に予告していたものとは違うのですが。

言ってしまえば逃げですね…すみません。

何故ポインタ?



ポインタとは全ての言語の裏側にあるデータのやり取りを扱っている低い層のデータ構造、と言えばいいのだろう。C言語を触ったのにも関わらずこのポインタについて理解している人が少なくない。このポインタを理解することで、コンピュータの裏側で行われている基本的なデータの流れを追うことが出来るし、C言語の裁量の大きさはまさしく禁断の果実にも等しい。

そのポインタが使えて何が嬉しいんですか?



先ほどの章でも触れたように、C言語はこのポインタという物もあってかプログラマに対する裁量が大きい。逆に言ってしまえば、このポインタを理解することでCにおいては大きな力を得ることが出来る。

  1. 動的なメモリ確保
  2. 配列を関数の引数に渡す
  3. 配列を関数の戻り値で受け取る

もちろん、他の言語からしてみれば当たり前のように出来るような機能ばかりだ。

しかし、その裏側では泥臭い処理が行われている事を理解することで、実装レベルでの細かいパフォーマンスに気配りをすることも可能になる。

これらを知らないで他の言語でも配列やインスタンスへの参照を取り扱うことももちろん出来るが、エンジニアとしてトラブルが発生した時の迅速な対処をするためにも、このような低層の知識は必要だろう。

ポインタとは?



解説する必要も無いと思うが、ポインタとは変数へのメモリ番地(アドレス)だ。
大体、どこの学校の授業でもこのような節で解説されるはずだ。
もっと分かりやすい言葉があればいいのだが、これ以上シンプルな説明は出来ない。

変数へのメモリアドレスという時点で、前提になる知識はOSでのメモリの扱いになる。
これさえ分かっていれば簡単に理解出来る。
が、そもそもこれを習う時にそのような知識をみにつけているはずもない。

プログラミングの初学者がポインタを恐れる原因はすべてここにある。

アプリケーションレベルでのポインタの理解



先程も触れたが、本来ポインタを完全に理解しようとする場合は、OSのメモリの扱いから学ばなければならない。
しかしここでは、その時に学んでるあるであろうはずの配列を用いてOSのメモリ周りの問題を扱うことで、ポインタの実体をつかむ事が出来る。

まずは以下のようなコードを見てもらおう。

int hoge, huga, hogehuga;
hoge = 3;
huga = 8;
hogehuga = hoge + huga;
printf("%d\n", hogehuga);

ただの足し算のコードだ。

それじゃ、このコードを全て配列に置き換えてみる。

int mem[10000];
mem[3235] = 5;
mem[7801] = 8;
mem[821] = mem[3235] + mem[7801];
printf("%d\n", mem[821]); // 13

このコードを見てどう思うだろか?
おそらく、ほとんどの人は見辛いと感じだことだろう。

このコードで出ているmemという配列で、OS上で扱っているメモリを表現している。
では、ここに変数がどのように解釈されているかを上記のコードを元に再現してみる。

int mem[10000];
int *hoge = &mem[3235], *huga = &mem[7801], *hogehuga = &mem[821];

*hoge = 5;
*hoge = 3;
*huga = 8;
*hogehuga = *hoge + *huga;
printf("%d\n", *hogehuga); // 13

printf("%d %d %d\n", &hoge, &huga, &hogehuga); // 3235, 7801, 821

最後の結果としてはこのように表示されるイメージだ。
※本来ならば16進数で表示させるのが一般的だが、ここでは10進数でメモリ番地を説明しているので、10進数表示にした

つまり、裏側では変数に対してメモリのアドレスが割り当てられたおり、そのアドレスに対して直接書き込みを行っている、ということだ。

変数というのはメモリ番地に対して分かりやすい名前を付けただけのものに過ぎないということが理解出来ただろうか?

参照渡し

これを理解することで、参照渡しは意図も簡単に理解できる。

なぜなら、先ほどのコードでは変数というものがその参照渡しと同様のプロセスを踏んでいるからだ。つまり、参照渡しもある変数の名前に違う名前を呼称している、ということに過ぎないのだ。

わざわざコードを貼り付けることもないと思うが、ここではC++で参照とポインタを用いてプロセスを再現してみる。

int huga = 3;
int& hugaRef = huga;

huga += 5;
// hugaの変更が、hugaRefに反映されている
printf("%d %d \n", huga, hugaRef); // 8 8

hugaRef *= 3;
// 先ほどと同様にhugaRefの変更が、hugaに反映されている
printf("%d %d \n", huga, hugaRef); // 24 24

これをポインタで表現すると…

int huga = 3;
int* hugaRef = &huga;

huga += 5;
// hugaの変更が、hugaRefに反映されている
printf("%d %d \n", huga, *hugaRef); // 8 8

*hugaRef *= 3;
// 先ほどと同様にhugaRefの変更が、hugaに反映されている
printf("%d %d \n", huga, *hugaRef); // 24 24

更にこれを配列の擬似メモリに置き換えてみると…

int mem[10000];
int* huga = &mem[9821];
*huga = 3;
int* hugaRef = huga; // ここで渡しているのは、メモリアドレスという点に注意。ポインタ変数なのでわざわざ&を付ける必要はない

*huga += 5;
printf("%d %d \n", *huga, *hugaRef); // 8 8

*hugaRef *= 3;
printf("%d %d \n", *huga, *hugaRef); // 24 24

もちろん、このmemというのは本来PC全体でグローバルに使用されているものだ。
※OSにもよるが、基本的には使用出来る領域がプログラム単位で制限されている状態で渡されるため、完全なメモリアドレスじゃない場合もある

そのため、このメモリの値を好きに弄ることが出来れば様々な事が可能だ。
身近な例で言えばプロアクションリプレイを使ったチートがそれだ。
あれは、ゲーム上での変数の番地を特定し、そのメモリ領域の値を書き換えることでパラメータを自由に変更しているのだ。
他にもプログラムのコードをバッファオーバーフローというテクニックで、任意のプログラムに差し替える、というセキュリティ問題にもメモリの問題が絡んでいる。

最後に



プログラムを行う上で、ここらへんのメモリの動作について理解をすることで、原理がわかりセキュリティ面やパフォーマンス面での配慮をすることが可能になると信じ、この回を終了する。