Hatena::ブログ(Diary)

偏見プログラマの語り!

2012-06-16

C 言語にポインタがある理由は省メモリ化・高速化・開発作業の省力化です

| 23:17

 前回の記事『プログラム初心者にC言語のポインタを不本意ながら教える羽目になったなら、こう教えると良いよ』ポインタの教え方を書きました。ソレに対して「そもそもどうしてポインタっていう仕組みがあるの?」という質問をもらったので、つらつらと書こうと思います。本稿は「ポインタがある理由の教え方」ではなく「ポインタがある理由」です。分かっている人には相当に退屈な文章ですのでそういう人は読まずにお帰りください。

 で、えーと、結論だけ先に言うと省メモリ化のため、次に速度アップのため、そして生産性アップのためです。


1. メモリは有限である。

 マシンに搭載されているメモリには限りがあります。メモリ空間は広大ですが、無限ではないのです。

f:id:kura-replace:20120616223014p:image

 好き放題にどんどんメモリを使ってデータを格納するわけにはいかないというわけです。しかしプログラムは計算のためにメモリ空間を占有します。仮に↓こんな感じに、わずかな有限メモリにどうしても計算に必要なデータ a が格納されている場合を考えます。

f:id:kura-replace:20120616223015p:image

 これを別の場所にコピーしたいと願っても、それは不可能です。なぜなら a 以外の場所に a と同じサイズの領域が余っていないからです。もっとメモリ空間が広ければコピー可能ですが、ジャブジャブ使うと、また上限に達してしまいます。これは避けようのない事実です。つまり、メモリを無駄遣いしたくないという要求があります。


2. 処理時間は有限である。

 一般に、プログラムの処理は遅いよりも速いほうが良いとされます。ゲームプログラムスピード感が損なわれたり、Web ブラウザで読み込みが遅いとイライラしますよね。プログラムは、計算ステップを踏めば踏むほど遅くなります。それは足し算であったり引き算であったりしますが、データのコピーであったりもするのです。大きなデータを何度も繰り返し、膨大な手間をかけてコピーすると、それだけでプログラムは遅くなります。これも避けようのない事実です。つまり、余計なコピーを避けて処理速度を上げたいという要求があります。


3. 開発工数は有限である。

 プログラマは無駄な作業を嫌います。例えば同じソース断片がプログラムのあちこちに散らばると、修正が必要になったときにその全てを修正しないといけないため、開発に手間がかかってしまいます。同じ処理が書かれている箇所が多ければ多いほど開発時間を奪われるのです。これも避けようのない事実です。つまり、似たようなソース断片はできるだけ一元管理して、無駄な記述を減らしたいという要求があります。


4. ポインタという仕組みは、以上 3 つの問題を解決するためにある

 これは C 言語のハナシではなく、ソフトウェア一般のハナシです。「データそのもの」だけでプログラムを作るのではなく、「データの場所を指すもの」を一緒に活用することでこうした問題にアプローチすることができます。C 言語では「データを指すもの」を表現するための仕組みをポインタと呼び、そのための文法が用意されている、というだけに過ぎません。

 例えばこんな場合を考えてみます。

    • 100 キロバイトのメモリを必要とする BigData 型の変数 a, b, c, d, e がある。
    • int の変数 user_input がある。
    • user_input が 0 だったら a の内容を画面に表示し、1 だったら b の内容を画面に表示し、… 4 だったら e の内容を画面に表示する。

 ソースにすると↓こんな感じ。

BigData a, b, c, d, e;
BigData that;
int user_input = (何らかの入力);
if( user_input == 0 ) {
  that = a;
} else if( user_input == 1 ) {
  that = b;
} else if( user_input == 2 ) {
  that = c;
} else if( user_input == 3 ) {
  that = d;
} else if( user_input == 4 ) {
  that = e;
}
display_one( that ); // BigData の内容を画面に表示する関数を呼ぶ

 ↑このプログラムを、ポインタを使ってより良いプログラムへと書き換えることを考えます。


5. ポインタを使って使用メモリを節約する

 まず、上のプログラムで使われている変数 that はメモリを無駄に消費しています。that は a や b と同じだけのデータが格納できるくらいに大きいからです。プログラムの構造を変えてやればメモリは節約できます。「if 分岐で a や b のコピーを得る」というプログラムを「if 分岐で a や b の位置を得る」ように変えるのです。ここで、ポインタ変数 p を使います↓。

BigData a, b, c, d, e;
BigData * p;
int user_input = (何らかの入力);
if( user_input == 0 ) {
  p = & a;
} else if( user_input == 1 ) {
  p = & b;
} else if( user_input == 2 ) {
  p = & c;
} else if( user_input == 3 ) {
  p = & d;
} else if( user_input == 4 ) {
  p = & e;
}
display_one( * p );

 that はメモリを 100 キロバイト消費していましたが、p は 4 バイト消費するだけで済みます。 p はメモリ上の位置を表現する整数値でしかないためです。


6. ポインタを使ってコピー回数を減らす

 5. で変数 that へのコピーを止めました。これでコピー回数も減ってますね。その代わり p へメモリ上の位置がコピーされるようになりましたが、p が小さいので that へコピーするより低コストで済みます。


7. ポインタを使って開発工数を減らす

 a, b, c, d, e のいずれかの値を表示するプログラムには柔軟性がありません。後から f, g, h,… が増えたときにそのぶんだけ else if を書き加えないといけないのは面倒ですよね。そこで、一つのアイデアを引っ張ってきます。「データを並べて、先頭にだけ名前をつける」というものです。前回の記事で使ったのと同じですね。

f:id:kura-replace:20120617174712p:image

BigData arr[5];
BigData * p;
int user_input = (何らかの入力);
p = & arr[user_input];
display_one( * p );

 ここまでの説明で、コピーが減ってソースコードの量も減りました*1。それを踏まえた上で、5. で書いたソースと比べてみてください。ポインタ変数 p は、a や b といった具体的な変数を指すこともできますし、arr[2] のように配列中の任意の要素を指すこともできることに気づくと思います。p が指す先に BigData がある、というただそれだけの仕組みなのでどちらの使い方もできるのです。

 さて、ここで注目すべきは「5 つの BigData がメモリ上に並んでいなくてはいけない」という制約が追加されてしまったことです。これはバカバカしい話です。たかがデータを表示するためのプログラムごときにデータの配置方法を決められてしまうなんて。それを改善するために、さらにまたポインタを使います。

 アイデアは、「5 つの BigData をメモリ上に並んでいなくてはいけない」を「5 つの BigData へのポインタがメモリ上に並んでいれば良い」に変えるというものです。BigData の配列 arr を使うのをやめて、BigData へのポインタ配列 parr を使うことにします。メモリ空間のイメージは↓こうです。

f:id:kura-replace:20120616223443p:image

↑こうすることで a や b が実際どこに配置されていても構わなくなります。これを C 言語のソースコードに書き下ろすと↓こんな感じになります。せっかくなので関数にしてしまいます。

void display( BigData * parr[] ) {
  BigData * p;
  int user_input = (何らかの入力);
  p = parr[user_input];
  display_one( * p );
}

 一番最初と比べると、いくらか洗練された事が分かると思います。


8. こんな話は、数あるポインタの使い方のほんの一部でしかない

 ポインタの使い方なんて無数にあります。教科書や本稿のような記事に書いてあるのはそのうちのほんの一部を具現化したサンプルでしかないのです。C 言語のプログラマ達は、ポインタを使ってメモリ使用量を抑えて実行速度を維持し、さらにデータ構造を抽象化して開発速度を上げる、ということを日常的にやっています。どういう使い方がどういう問題に対して有効であるか分からないうちは、他人のソースを読んで学ぶと良いでしょう。

 で、その前に本稿を読んで誤解してはいけないコトを書いておきますね。本稿ではコピー回数が少なければ少ないほど良い、的なニュアンスで説明をしました。しかしその考え方が万事通用するわけではありません。例えば、データのコピーをたくさんとっておいてそれらを戦略的に配置して圧倒的な速度アップを狙う、という場面はよくあります。また、ポインタを使って生産性を上げるという話を書きましたが、それも別の観点からみれば生産性を下げると言われていることにも留意すべきです。これは C 言語のポインタがあまりに強力すぎてバグ混入を容易にしてしまっているコトなどが主な理由です。


9. 「今どきポインタなんか使わざるを得ない C 言語を勉強するなんて終わってる」

 ... という指摘は、僕じゃなくて現場を仕切ってる人に言ってくださいね(何かそういう dis をいくつか受けたので一応)。

 あと、プログラミング初学者の人は「Javaポインタ無いじゃないか、Rubyポインタ無いじゃないか、なんで C 言語だけポインタあるんだよ!」っていう疑問を持っているようです。JavaRuby は C 言語のメモリ空間云々の話をそっくり覆い隠してくれているからポインタが無いのです。その代償として、C 言語ほど強力なチューニングはできません。しかしそれで良いのです。用途に応じて適切なプログラミング言語を使えば良いだけです。コンピュータを使う以上、ぎりぎりまでハードウェアの能力を引き出したいという要求があるので、その領域を C 言語が担っていて、だからこそポインタという仕組みが搭載されているのです。


10. 前回の記事でいただいたコメントへの返事

 前回の記事で "初心者" さんからこんなコメントをいただきました。

私はさぁこれからCを勉強しよう!としてすぐに挫折した屈強の初心者なので、こんなものを聞いてもさっぱり分かりませんでした。orz

もっと分かり易く説明お願いします。

どこが分からないかって?それが分からないくらい初心者です。。。

と言っても手の付けようがないでしょうから。

(1)ポインタがzの場所を教えるとありましたが、なぜ直前に出て来た変数aやbには必要ないのでしょうか?

(2)ポインタがメモリの有効利用が目的とありますが、ポインタを書かなければその分そちらの方がメモリが少なくて済むのではないでしょうか?

(3)ポインタを指定しない場合に起こる不都合というのを具体的に教えて頂けないでしょうか?(VBA程度は分かりますので、そこでの変数にはポインタがなくても不都合が生じません。なぜCには必要なのでしょうか?)

 回答、以下の通りです。

(1) もし "ポインタに数値を代入するとその場所に変数が生成される" と思っているのであればそれは間違いです。まずメモリが先にあって、それを指すデータをポインタと呼びます。質問された例でいうと、ポインタが「z の位置」を決定づけるのではありません。z があるから「z の位置」を得ることができるのです。z から「z の位置」を得る方法は "& z" です。もちろん z があるからといって「z の位置」を得ないといけないとか、z を指すポインタ変数を作らないといけないという決まりもありません。変数を指すポインタが無くても変数は存在できますし、ポインタが 300 個あっても構わないのです。直前に出てきた a や b は、「a の位置」や「b の位置」を使う必要が無かったので登場しませんでした。

(2) えぇと (1) の回答と本稿があれば、恐らく (2) の回答は必要ないですね…。ポインタ以外の方法を使ってプログラムを書くよりも省メモリで済ませられる場面が多いから、ポインタはメモリの有効利用に寄与するのです。

(3) これも (1) の回答があれば回答の必要は無さそうですね。ポインタという仕組みが無いと、省メモリ化、速度アップ、開発速度の向上ということが絶望的にやりにくくなります。だから必要です。初心者さんが VBAポインタを使わなくても事足りているのは、本稿の 9. で書いた JavaRuby を例に挙げたように、ポインタが不要だから覆い隠されているためです。

 以上。分かんなかったら Twitter で聞いてください。

*1:データが増えたときは arr[5] の 5 を 6 なり 7 なりに書き換えれば済むので else if を書き加えるより手間が減る

KeiKei 2012/06/17 14:29 昔だったら逆で、

そういう理由がある。どうしよう?

ポインタが使える!

という経験をしてきたんですが、特に今のPCのメモリの潤沢さを考えると、入門書での解説は首を捻るのもわかります。
組込ですら今はMB単位になってますからね。

あとは状態遷移を関数へのポインタを用いてすっきり書けるというところでしょうか。

kura-replacekura-replace 2012/06/17 14:54 ですね。僕は無限ループで malloc しまくるコードを書いてしまったときぐらいしか、メモリの上限意識することなんて無かったです。あと、関数ポインタを使ったプログラムの抽象化テクニックは、開発作業の省力化に含んで良いかなと思ってます。

tk_kappatk_kappa 2012/06/17 16:50 わたしは素人なので知らない事だらけです。勉強になります!

kura-replacekura-replace 2012/06/17 17:48 ありがとうございます!

おとなり日記