COFFEE BREAK !
変数の宣言と、ついでにポインタ。
これまでのところでは、
int i ;
などとして当たり前のように 変数 i を使用してきましたが、一息ついて、少し説明しておきます。
int i ; というこの一行は、 「変数の宣言」 といいます。「変数 ( 例では i )」 を使う前には、「宣言」 する、というのが約束事です。cgi を作るための perl 言語 などでは、「宣言」 しなくても自由に、いきなり、変数が使えてしまいます。これを「便利」 ということはできますが、C言語に慣れていると少し気持ち悪く感じてしまうものです。「変数の宣言」 には、コンパイラの都合と、プログラマの頭の整理のため、という両面があります。
int i, j, k ; という具合に、「,」(コンマ) を使っていっぺんに宣言することもできます。
int i = 1 ; という具合に、int i ; と、i = 1 ; の2つを同時に行うこともできます(変数の初期化)。横着な方法ですが、便利で理解もしやすい方法です。更に、int i = j ; のようなやりかたも可能です。
前回の例の、2つあるうちの はじめの例で、
int ret ;
while( 1 ) {
ret = MessageBox( ...
となっている部分を、
while( 1 ) {
int ret = MessageBox( ...
としてしまうことも可能だったのです。この場合、変数 ret の 「スコープ (有効範囲)」 の範囲が少し異なってきます。でも、取り上げた例ではどの道、while( 1 ) {} の 「外」 では 変数 ret を使わないので、これら2つの記述の例に限っては、 動作上はかわりはありません。
これが前回の補足です。
これじゃ少し足りないという人は・・・
C 言語での基本的な「変数のタイプ」 ( 「データ型」 あるいは、単に「型」) は、ちいさいほうから、char(8ビット)、short(16)、int(32?)、long(32)、さらに float(32)、double(64) があります。コンパイラ次第では・・・・・ [変数(識別子)の宣言、に関するコラム]
ポインタについて少し触れておきます。
まだ使っていませんが、えーい!行ってしまえ ( Go )!・・・長いのでやっぱり分けないとだめですか?
ポインタは、"pointer" です。英語では、point という単語は小学生でも日常的に使う言葉なのです。"pointer(指すもの)" と見たら、「先生じゃぁこれ、何を指す(pointする)んですか?」 と聞けば、「コンピュータの中のメモリのアドレスを指すんだよ」。こういうことになるから、そこでメモリが分からなかったら 「じゃぁメモリってなんなんだよ・・・」 ということになるし、「ポインタ」自体はぜんぜん難しくないはずです。日本語だと、「ポインタ」 という言葉からはなにも進まず、「ポインタって何?」 から始まるわけです。これだからやっかいなんです。ネイティブな English speaker でもない限り、勉強して英語が得意な人でもやっかいなのは同じことです。
メモリとかアドレスが分からない人は、パソコンという四角い箱をじっくり見てください。この中にメモリがあります。そして、パソコンなんて、我々にしても、パソコンにとっても、メモリを操作することで成り立っているのです。で、C 言語ではポインタが使えるというのは、メモリのアドレスを具体的に指定できて、メモリを操作できるということです。だったらこれ以上有効な武器はありません。一方、アセンブラでも機械語でもメモリは扱えますが、めんどうくさくてやってられないでしょう。おそらくひたすらそれをやっていたら、やがて自分で C 言語のコンパイラを作ってしまいたくなるでしょう。回り道だけどそっちのが手っ取り早い。まさにそうやってできたのが C 言語そのものです。
で、ポインタは、例えば、整数型へのポインタは、
int
*ptr_to_int ; // 使用 「準備」 にあたります。のように宣言すれば使えます。ptr_to_int は、「(int 型への)ポインタ変数」とか、「int * 型」の変数のように呼びます。単にポインタでもかまいません。
なんだ、簡単じゃないか。じゃぁためしに、アドレスを 「50」 にして、なにが入っているか確かめてやろう、と、そう思ったら、 ( メモリの中身を 参照/使用するには、これまた
// #include <windows.h>
int WinMain(.....) {
int
「アドレスの中身」 を知りたいときは、ポインタ参照演算子 : 「
*」(アスタリスク : asterisk ) を頭につければいいことで、上でもそうしています(色に注目してください)。アドレスが指定できても、アドレスの中身を "使えなければ" 意味がありませんね。この例をやろうとすると実はコンパイルはうまくいきます。ところが、間違いなく実行時にエラーが出ます。@ は実行時でもエラーにはなりません。A でエラーを起こし、「落ち」 ます。50 といういい加減なアドレスを「見る」 のがいけない。( だからコードを中途半端にしています )
基本は、宣言時の
* の使い方と、参照時の * の使い方です。同じ * を「違う」というのもなんですが、宣言の構文上の * と、参照を意味する * とでは、意味はやっぱり違うんだと、覚えておきましょう。ただ、int *ptr_to_int ; で、「*ptr_to_int が、ちょうど int にあたるんだ」、と考えれば、「ptr_to_int を参照(*)した結果は、int 型だ」、と理解でき、それは、間違いでも勘違いでもありません。char **pstr ; なら、pstr を "2回繰り返し参照"(**) した結果は、char 型です。ただし、この 「ポインタのポインタ」 の場合は、そのように「理屈」で理解するのには常に骨が折れるものです(かといって骨の折れない工夫なんていうものもありません)。応用的事項としては、
インクリメント・デクリメント・ポインタ同士の引き算(差)、関数ポインタ、クラスオブジェクトへのポインタ(C++)、ポインタのポインタ、配列とポインタ、密接な事項としては、アドレス演算子 &(アンド)、また、キャスト、或いはキャスト演算子 (上例の 「(int *)」 ) があります。キャスト(情報)は単なる、コンパイラに与える情報、またはプログラムの整理 (見易さ) のためなんだ、ということも付け加えておきましょう。 : vs. C++ の「ダイナミックキャスト」。
この、基本的な理屈、はきわめて簡単です。でも、理屈はわかっていても、メモリはシステムが整理また、管理しているものだから、むやみなアドレスを指定してみるわけにもいかないのです。むやみなアドレスを「見」ようとした場合では、システムにより「アクセス違反」とみなされて実行時に強制終了させられます。
このようなことのために、練習しようにもわかりいいネタがないというわけです。上の例はおそらく、誰しもはじめに考え付くことです。でも、エラーを見てたって仕方ない。
※
ptr_to_int = (int *)50 ; にたいして、 *ptr_to_int = 50 ; という、代入も、可能です。もちろん!意味は違います。前者の意味は以下の英文をみてください。後者は、メモリ内のある位置に 50 を放り込みますが(代入しますが)、その、ある位置のアドレスが、ptr_to_int というわけです("ptr_to_int" points to the address)。ptr_to_int = なにか有効なアドレス ; とすることによって、あらかじめ、ポインタ自身になにか正しいアドレスを代入していない以上、この場合も、実行時その代入を処理する時点で、不正なアドレスに数字を入れようとした"カド"で、強制終了されるでしょう。前
: "ptr_to_int" is 50 ;
後 : memset( ptr_to_int , 50 , 1 ) ; // と "ほぼ" 同じ・・・。メモリに数字を入れるというからには、memset() 関数でも同じことができるという結論です。
うまく説明できたとぜんぜん思えません。見ないほうが身のためかもしれません。立ち止まって考えるなどもってのほか。そもそもポインタがややこしい、っていうのは・・・・・そんなに叫ばなくてよいのではなかろうか・・・・・。C 言語で Null Terminated 文字列 (単に文字列) を使うときは、意識せずとも pointer を使います。pointer に文字列の先頭アドレスを代入しつまり初期化、してから その pointer をインクリメントしていくような処理も、単純そのもので、ほんとうの意味で難しくないから、ほんとうに心配ありません。ただ、そのレベルで、いくらポインタを使っていても、あまり上達もしません。(そして、する必要もない。しかも上達ってなに?)。わたくしの経験では、自分の非力さを実感するのは、C 言語の標準関数 、malloc() を使って、いざ自分で 「2次(的)配列」 をつくる必要に迫られたときです。だから、そのときまでは、「不安よさようなら」 。まったく問題ありません。「
ところがこの話には続きがあって、Windows API 関数だけでやってるころは事実あまり問題がなかったのですが、COM オブジェクトを使いはじめたときに、QueryInterface() なる関数をはじめとして、「ポインタのポインタ」 型の引数、が主役に現れはじめたのでした。結局は、&をつかって・・・・(中略)、COM は COM なりの、パラメータの指定の仕方が独特に統一されていて、統一されているからこそ、これもなれればどうってことないのですが、今でもいきなり 「ポインタのポインタ」 型が現れたら、一発で正しいコードが書けるか自問してみるに、自信がもてなくなってしまいました (どうにかはなるだろうという自信はあります)。ということはつまり、「ポインタのポインタ」 型、というのは永遠に難しいのです。つまり、やっぱりポインタは難しいという結論になりました。そんなとき、コンパイラの型チェック機能にたよるのです。コンパイラの型チェック機能がなかったら、世の中バグコードだらけでとんでもないことになるし、一方でプログラマを助けるのに一役かっています。つくづく、コンパイラはありがたいものです。
[コラム]
"p で指定されるアドレス" は、英語で "an address pointed to by p" 。p は pointer なんですもんね、述語は point 。この単純さがとても印象的でした。日本語でこれを対照的にするには、ポインタとかいわないで、「アドレス指定君」 とか、格式ばれば 「指定子」 ・・・。ありえません。
[実験]
有効なアドレスは、どういうときにお目にかかるか。そして、そもそもやっぱり、アドレスを操作できるからってそんなに便利なのか?
[やってみる] ・・・最終的に関数ポインタやクラスのオブジェクトへのポインタなんてものにまで展開していくから、ポインタはすごいんです。やっぱり。
※ どのようなポインタも同じ 32 ビット値です。unsigned で考えて、0〜4294967295 ( 65536*65536-1、2^32-1 : 40億 ) の数字※※ がアドレスにたいして使えれば今のところ十分というわけです。 ( CPU やコンパイラの発展に従い変わり得るかもしれません )
"int 型のポインタ" というのは、ポインタで指すところに入っているもの(Contents pointed to by the pointer)が 整数 (int 型) であるという意味です。ポインタ変数自体はどんなものを示すポインタでも("char *"型も "short *"型も "int *"型も)、32 ビット値で、ポインタ型でない int 型 (32ビット値) となんら変わりがなく、コンパイラ内部の扱いが違うだけです。どの道数字だから、上でも、ptr_to_int = (int *)50 ; としています。 もし、50 というアドレスが有効ならば、ポインタ変数 ptr_to_int なんか使わず、 *(int *)50 の書き方を使って アドレス 50 を直接参照したっていいんです。
※※ 1〜4294967296 と言えたら最後の "-1" は要らないのにね!
ここでのポインタについての具体的な解説はいまいち乏しい気がします。
でもだからこそ Coffee Break です。
[トップ] [インデックス] [前へ] [次へ]