ポインタとメモリと型(構造体)の関係 (3)
前回は、ちょっと高度ですが、非常に処理系依存で、最終的につかうかどうか分からない知識でしたが。
今回と次回で紹介する内容はちょっと高度だけど、
初心者がもう一歩ステップアップするところでは必ずといっていいほど必要になる知識だと思います。
では、いってみましょう~~
void型ポインタ
一般的な入門書ではおそらく扱われていない、
もしくは扱われていたとしてもそういうものがあるという程度で
実際どうやって使うのかまでは書かれていないと思いますが、
C言語には「 void 型ポインタ」というものがあります。
単に「 void 」という記述は、返値の型などでよく見かけると思います。
返値の方で指定すると、何も返さないということを示すものです。
また、引数に書いた場合、何も引数をとらないという意味になります。
すなわち、 void は何もないという意味で用いられています。
では「 void 型」のポインタとは何かといいますと、型がないポインタのことです。
第14回でお話ししましたが、
ポインタは char 型のポインタでも int 型のポインタでもポインタの値そのものに違いはありませんから、
型がなくても値そのものは保持できます。
しかし、変数の型とは解釈です。
ポインタの値があっても型がなければ、
どこからどこまでをどう解釈していいのか分からないため、演算などができません。
では、どこで用いるのかというと、「型による区別をしたくない場合」もしくは「解釈を後で定義したい場合」、
もっと言えば、「抽象的な記述」をしたいときに用います。
(ポインタの持つ値がアドレスですから、アドレスだけあれば十分、
アドレス以外は必要ないという用途に使われるといってもいいのですが、
ちょっと言葉の情報量が少ないような気がします(日本語って難しい))
抽象的な記述って何やねんな、と思うかもしれませんが
(オブジェクト指向を勉強した人はよく分かっているでしょう。
というかそういう人はこんなところ読まないか・・・)
徐々に例などを見ていれば何となく分かってくると思います。
さて、サンプルプログラムなどで紹介する前に、
おそらくC言語初心者でも既に使っていると思われるようなところで
既に使われている例を紹介します。
それは、 malloc です。
動的メモリ確保で出てくるあれです。こいつの書式は以下のようになっています。
void *malloc(size_t NBYTES);
この関数は、指定したサイズのメモリを確保して、そのポインタを返してくれます。
書式を見るとこの返値は void * つまり、 void 型ポインタとなっているのです。
それ故、この関数はどんな型のメモリ領域であってもこの関数一つでまかなうことができるのです。
もし、 int 型ポインタ用、 double 型ポインタ用・・・なんて用意していたらとてもすべてをまかない切れません。
つまり、型による区別をしたくない場合というのはこういう状況のことです。
また、この関数は指定された大きさのメモリ領域を確保するだけで、
実際にどう使うかはそれを呼び出した側が決めるのです。
つまり、解釈を後で定義しているともいえます。
void 型ポインタを利用し、その関数を書いているうちはそのデータの実態をはっきりさせず、
それを利用する側で利用の仕方を決めるようにすることで、応用範囲の広い関数を書くことができます。
これが抽象的な記述というわけです。
これで、何となく型のないポインタのイメージがつかめたでしょうか?
では、抽象的な記述の 一番簡単な例で、データの位置だけが必要で、 実際に計算したりするわけではない場合を取り上げます。
アルゴリズムを実装する上で、データ構造というものが重要になってきます。
よく使われるデータ構造として、隣接リスト、スタック、キュー、ハッシュ、AVL木、等々様々なデータ構造があります。
これらは非常に便利で、いろんなプログラムで使用されます。
しかし、これらを毎回実装していては非常に効率が悪いです。
そこで、こういったデータ構造を扱う関数群をあらかじめ書いておいて、必要となる度にそれを呼び出すようにしようと考えます。
扱うデータが異なっていても、これらの出して入れるという操作自体は同じなので、こういったことも簡単にできそうです。
しかし、実際にやってみようとすると必要ないにもかかわらず「型」が障害になってくるのです。
C言語というのは、プログラミング言語の中で型にシビアな部類に入る言語です。
つまり、事前に型を指定してプログラムを書かないといけないのです。
たとえば、あまり意味のあるプログラムではありませんが、(実際にデータ構造のコードを書くのは面倒なのでさぼってます)
下のように、一つのデータを格納できる構造体、
それにデータを格納する関数と取り出す関数を書いてみます。
struct box{
int a;
};
void pack(struct box *b, int a){
b->a = a;
}
int unpack(struct box *b){
return b->a;
}
さて、この関数は int 型の値を格納するのには使えます。
しかし、ここに double 型の値を格納しようとすると、自動的にキャストされ、データが失われます。
(コンパイラによってはワーニングがでます)
さらに、 box 構造体の中に box 構造体をいれるといったことをやろうとすると、これはたいていコンパイルエラーになります。
データを格納し、取り出すということをやるためには、計算などを用いていないにもかかわらず、エラーです。
まぁ、ちょっとわざとらしい例ですが、これをデータのコピーではなくポインタを格納する様にした場合でも、ワーニングはでます。
なぜかというと、格納する型を指定しているからです。
(int型ポインタで格納するようにするとワーニングはでても、たいてい動作はします。
それは字面では型を書いていますが実際には型を使用しないからです)
そこで、この box という箱には何でも入れられるように書きたいという時に、 void 型ポインタが活躍します。
struct box{
void *a;
};
void pack(struct box *b, void *a){
b->a = a;
}
void *unpack(struct box *b){
return b->a;
}
このように書くと、この box の中には何でも入れられるようになります。
int でも double でも、box の中に box が入っていて、その中にはやっぱり box で・・・っていう格納の仕方もありです。
もちろんエラーやワーニングもでません。
(ただし、この関数を利用する側が取り出しの時などにプログラマの責任で明示的にキャストしないといけませんが)
void 型ポインタが使われていることで、プログラマにとっても型に縛られずに格納できる、ということが一目瞭然です。
また、データのコピーを格納する場合でも、データのポインタと、そのサイズを( sizeof 演算子を使って)引数で渡せるようにすれば、
バイナリコピーを行うことで可能になります。
(その場合、内部で動的メモリ確保をやったりするため、メモリリークしないように気をつけてプログラミングする必要があり面倒です。)
データを格納したり、取り出したりする。今回の例は各データ構造を極端に簡略化したものですが、 この box の中身をもっと複雑にしてあるルールに従って格納、取り出しが出来るようにすれば、 どんなデータも格納できる汎用的なデータ構造のライブラリを書くことが出来ます。
さて、非常に単純で中途半端な例ですが、何となく void 型ポインタの使い道が分かったでしょうか?
これだけでもそこそこ使い道はあるわけですが、次回紹介する関数ポインタというものと組み合わせることで、
さらに抽象的な記述が可能になります。
この void 型ポインタと関数ポインタが分かるようになると、
API やライブラリなどが理解できるようになると思います。
では、また次回~~
C言語では void* を使って「何でも入れられる箱」は作れます。
C++ などではさらに、設計時は「何でも入れられる箱」なんだけど、
使うときは「入れるモノ」を特定したいというわがままは要求に応える仕組みがあります。
それはテンプレートです。(最近はJavaやC#にも組み込まれたみたい)
void* によって何でも入れられるってのは、はじめは便利に思えますが、
使っていく内に取り出すときのキャストが面倒になってきます。
一つのプログラム内で同じデータ構造のインスタンスを複数作って、
それぞれ異なるデータを管理し始めると混乱ヽ(´ー`)ノ
なぜかっていうと、入れるときは安全なキャストなんだけど
(型がはっきりしてなきゃならない→何でもいい は何やっても正しい
(どんな型のポインタもvoid型に変換できる))
取り出すときには、危険が伴う
(何でもいい→型がはっきりしてなきゃならない は実体が分からないのに正しいキャストをしなきゃならない
(間違ったキャストをすると、データを破壊する))。
さらには、入れるときは型は分かってるけど、出すときははっきりとは分からない。
プログラマが「~と処理するから、ここでは~型だ。」って決めなきゃいけない。(だから明示的キャストが必要)。
早い話、コンパイラは面倒みてくれないんです。間違ったキャストしても、コンパイラはエラーを出さない。
しかし、実行するとデータが破壊され動かない。バグバグバグ~~。