ポインタとハンドル
ここですること:
・ポインタの概念を理解する
・実際にポインタを使ってみる
・ハンドルの概念を理解する
・実際にハンドルを使ってみる
(1)ポインタの概念
ポインタというのはメモリのアドレスです。
例えば1000バイトのテキストを読み込んでメモリに保存したいとしましょう。1000バイトのメモリ空間を得るには NewPtr という関数を使います。
この関数を呼び出すと、システムのメモリマネージャはアプリケーションヒープ(アプリケーションメモリ)の中に1000バイト以上の空間を見つけて、その先頭アドレスを関数の返値として戻します。
put NewPtr( 1000 ) into theText
この theText がいわゆるポインタ変数で、メモリブロックの先頭アドレスを持っています。メモリマネージャはアプリケーション毎にメモリのデータベースのようなものを持っていて、今確保した1000バイトが使用中であることをそこに登録します。
以後、その1000バイトはプログラマが好きに使っていい空間です。アプリを終了するまでそのメモリブロックは絶対になくなりませんし、移動もしません。
NewPtr 関数の返値はあくまで確保したメモリブロックの先頭番地ですから、そこから何バイト確保してあるのかはプログラマが覚えておかねばなりません。もしアドレス theText から2000バイトのデータを入れたら、どこかの1000バイトを壊してしまっていることになります。メモリマネージャはメモリブロックの大きさそのものは承知していますが、メモリの読み書きでいちいちチェックを行わないのです。ブロックをはみ出さないよう管理するのはプログラマの仕事です。
さて、こうやって必要なメモリをどんどん確保して行くとアプリケーションヒープはいつか一杯になってしまいます。そこで使用済みのメモリブロックは開放してやらねばなりません。これが DisposPtr です。
DisposPtr theText
これで今の1000バイトは開放されましたから、メモリマネージャがメモリを必要とした時にこの空間を再利用することが出来ます。当然、theText の値は DisposPtr の実行後には何の意味も持ちませんから、これを使ってデータにアクセスすると、デタラメなデータを拾ってきたり、ヨソのデータを壊したりすることになります。
(2)ポインタを使ってみる
実際にポインタを使ってみましょう。文字列を扱おうとするとややこしいので、ここでは数値を扱ってみます。
function test aNum
put NewPtr( 4 ) into thePtr --*1
put aNum * 5 into thePtr@.longintType --*2
put thePtr@.longintType into resultNum --*3
DisposPtr thePtr --*4
return resultNum --*5
end test
put NewPtr( 4 ) into thePtr --*1
数値型のデータを入れるのに必要なサイズのメモリブロックを確保しています。
数値には2バイトを使う int(Cでは short )と、4バイトを使う longInt(Cでは long )とがありますが、ここではlongInt型を入れられるよう4バイトを確保しています。thePtr にはこのメモリブロックの先頭アドレスが入ります。
put aNum * 5 into thePtr@.longintType --*2
引数 aNum の値を5倍して、thePtr の指すメモリブロックに入れています。
thePtr の後に付いている @ は「逆参照」の記号です。この記号を付けることで、「 thePtr の指しているアドレスに」値を入れるよう指示しています。ポインタ経由で値を入れる時には型の指示が必要です。longint 型としてメモリに格納するために .longintType を付けています。(ピリオドに注意)
put thePtr@.longintType into resultNum --*3
逆に thePtr の指すメモリブロックの内容を、変数に読み出しています。記号 @ と .longintType は書き込む時と同様です。
DisposPtr thePtr --*4
不要になったメモリブロックを破棄しています。
これをやっておかないと、ハイパカのメモリが減る一方で、すぐエラーが出てしまいます。
return resultNum --*5
メモリブロックから読み出した値をハイパカに返しています。
引数を5倍して書き込んでいますから、読み出した値は 25 ですね。コンパイル後にメッセージボックスに put test( 5 ) と打ち込むと 25 が表示されます。
ざっとこれがポインタを使ったメモリアクセスの基本型です。書き込み時も読み込み時も変数の型を指定すると言うことは、逆に言うと指定する型を間違えると正常な読み書きが出来ないということでもあります。
変数の型は大きく分けると数値型と文字列型とに分けられ、数値型はその大きさによって int と longInt に分かれます。
int は2バイトの数値を扱う型で、2バイトを使って表すことの出来る数値、つまり -32767 から +32768 までを扱えます。longInt は4バイトの数値を扱う型で、4バイトを使って表すことの出来る数値、つまり -2147483647 から +2147483648 までを扱えます。
CompileIt! では通常の数値計算は longInt で行っています。HyperTalk だけでソーススクリプトを書いているうちは数値の型について考える必要はありませんが、ToolBox を扱うようになると、ToolBox の必要とする型のデータをきっちりと作る必要が出て来ます。
(3)ハンドルの概念
ポインタを使ってメモリブロックの確保/解放を繰り返していると、メモリ内が使い込んだハードディスクのように、「虫食い」状態になってしまいます。
空きメモリが断片化するとメモリが有効利用出来なくなります。例えば文字列変数に1000バイトが必要になった場合は「連続した」1000バイトが必要なのであって、バラバラでは利用出来ないのです。
ポインタ メモリブロック ポインタ メモリブロック
aPtr ------->AAAA XXXXXXXX
bPtr ------->BBBB 虫食い状態 bPtr ------->BBBB
cPtr ------->CCCC → XXXXXXXX
dPtr ------->DDDD dPtr ------->DDDD
ePtr ------->EEEE ePtr ------->EEEE
メモリマネージャはこれをノートンの SpeedDisk のように、デフラグメントすることが出来ます。これを「メモリのコンパクション」と呼びます。
NewPtr などが実行されてメモリブロックを確保しようとした時、空きメモリが足りないと、メモリマネージャはメモリコンパクションを行って、必要な大きさの連続した空きメモリを作ろうとします。
ところが事はそう簡単ではありません。
例えば前の例ではメモリブロックのアドレスを thePtr という変数に入れて使っていますが、もしこのプログラムの実行途中でデフラグメントが行われたら、thePtr の持っているアドレスは全く意味を成さなくなります。
ポインタ メモリブロック ポインタ メモリブロック
XXXXXXXX BBBB
bPtr ------->BBBB デフラグ後? bPtr ------->DDDD
XXXXXXXX → EEEE
dPtr ------->DDDD dPtr ------->FFFF
ePtr ------->EEEE ePtr ------->HHHH
そこで、ポインタ変数が直接メモリブロックのアドレスを持つのではなく、「メモリブロックのアドレスの書いてあるメモリ」のアドレスを持つようにします。
この「メモリブロックのアドレスの書いてあるメモリ」を「マスターポインタ」と呼び、マスターポインタを指すポインタ変数を「ハンドル」と呼びます。
ハンドル マスターポインタ メモリブロック
aHand --------->A ---------------->AAAA
bHand --------->B ---------------->BBBB
cHand --------->C ---------------->CCCC
dHand --------->D ---------------->DDDD
eHand --------->E ---------------->EEEE
メモリマネージャはメモリブロックを移動する時に、必ずマスターポインタの値を更新します。ハンドル変数はメモリブロックのアドレスではなくマスターポインタのアドレスを持っているので、マスターポインタの値さえ更新されていれば、それをたどってメモリブロックにアクセスすることが出来ます。下図はメモリコンパクションの行われた後の様子です。
ハンドル マスターポインタ メモリブロック
xx /------------>BBBB
bHand --------->B --/ /--------->DDDD
xx / /------->EEEE
dHand --------->D-----/ /
eHand --------->E-------/
実際にはメモリマネージャはメモリコンパクション時に「ハンドルで確保したメモリブロック」だけを移動します。「ポインタで確保したメモリブロック」は一切動かしません。
ポインタで確保したメモリブロックが多いほどメモリ内の風通しが悪くなり、実際には(バラバラの)空きメモリがあるのにメモリが足りなくなるという、理不尽な現象が起こり易くなります。メモリブロックを確保する時は出来るだけハンドルを使う、ということは覚えて置いて下さい。
(4)ハンドルを使ってみる
実際にハンドルを使ってみましょう。
ハンドルを使うのはポインタを使うのと大差ありません。ハンドル変数は「メモリブロックのアドレスを持っているメモリ」のアドレスを持っているので、二重に逆参照すればいいだけのことです。
メモリブロックの確保は
put NewHandle( 50 ) into theText
破棄は
DisposHandle theText
そしてハンドル内のデータにアクセスするには、
put theText@@.longintType into theNum
と言うように、@ を2つ使います。
コマンドが違うのとアクセス時に @@ を使う以外は、ポインタと同じです。
function test aNum
put NewHandle( 4 ) into theHand
put aNum * 5 into theHand@@.longintType
put theHand@@.longintType into resultNum
DisposHandle theHand
return resultNum
end test
ポインタにしろハンドルにしろ、「普通の変数と何が違うの?」と思われるかも知れません。通常の計算をするだけなら普通の変数を使っている方がずっとラクですよね。
ポインタやハンドルが必要になるのは ToolBox アクセスです。ToolBox ではほとんどの場合、ポインタやハンドル経由でしかデータを読み書きすることが出来ません。これは後で出てくる「レコード(構造体)」で決定的になります。
Next
CompileIt! Lab.
UDI's HomePage