BCPL言語での開発経験はなく、また一般的に使われるコンピュータ言語でない。しかし、C言語には、必ず関係するものだ。
C言語だけを知ってポインタはC言語だけの機能と思っていたこともあった。日本語MS-DOSの環境で身近なコンピュータ言語は、C言語とPascal言語ぐらいだったので知りようがなかった。BCPLのことはC言語の本には必ず説明がある。でも、BCPL言語はどういうものか興味があったが確認する方法がなかった。
ポインタが実装されていたことを後で振り返ってわかった。改めてBCPL言語を勉強してみると、気づかされることがある。BCPL言語は、システム記述のためのものと考えられた。C言語もまた、OSを記述するためだったので同じといえる。当然だが、ポインタの用途はC言語と同じ。
BCPL言語とそのコンパイラ
M.Richards
C.Whitby-Strevens
和田英一
共立出版
※原文引用
BCPLのポインタ(pointer)は記憶装置の語のアドレスである。
記憶場所の内容に辿りつくためにポインタを使う。
単項オペレータ@は変数のアドレスを取るのに使う。
LET A , B = 0 , 0
B := @A
Aのアドレスを変数Bにおく。Aは少しも変化せず名前を使ってAにアクセスできる。オペレータ!をBに作用させてAに間接的にアクセスすることもできる。
LET A , B , C = 0 , 0 , 0
B := @A
C := !B
!Bの形は「Bの指しているものへアクセスする」ことを表している。
この作用はAの内容をCへコピーすることである。Bを経て間接的にアクセスすることでAの内容を変えることである。
LET A , B = 0 , 0
B := @A
!B := 5
ここの効果はBが指しているところのAに値5をおくことである。
BCPLでは記憶場所の続く語はアドレスも数として続くように決めてある。そこでBが何語か連続する場所の先頭を指しているならば B + 1 はその二番目、B + 2 はその三番目のように指す。この作用を理解しているとBCPLのベクタを詳しく説明できる。
宣言 LET V = VEC 5
(i) 六語の連続する場所のベクタと
(ii) このベクタの先頭の場所のアドレスで初期化したもうひとつの変数Vを確保する。
(※図 3.1)参照
Vはベクタの最初の要素を指す。
つまり、 V + 1 の与える値はこのベクタの二番目の要素のアドレスである。この要素へアクセスするには、!(V+1) と書かなければならない。簡略形は V!1 になる。
変数Vはコンパイラがポインタとして初期化するところにある。
この値は別の変数にコピーすることもでき、その変数はその結果同じベクタを指すようになる。またパラメータとして手続きに渡すこともできる。
(P36) 3.2 ポインタの使用法
ポインタには、BCPL言語の構造を表すための二つの重要な使い方がある。一つ目は、文字列の値が入っているベクタのポインタだということ。
配列のデータ構造を扱うポインタの例
代入コマンド
VEGETABLE := "carrot"
WRITES(VEGETABLE)
と書くことができる。
文字列をコンパイルする際に、コンパイラは文字列の入る記憶場所の
第一語のアドレスがある。テーブル(table)は初期化しいつまでも割り当ててあるベクタである。テーブルの値はこのベクタの第一要素へのポインタになる。
16進数表現
WRCH( (N&15)!TABLE
'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' )
二つ目は、木構造表現である。
木の各ノードは数語の連続した記憶場所からなる。先頭の語には識別用の情報があり。残りの語には、値やほかのノードへのポインタがある。
(※図 3.2参照)
たとえば、ABC,PQR,XYZ の名前のリストを作成することができる。
コーディング1
LET RNAMELIST() = VALOF
$( LET A = RNAME() // S.NAMEノードへのポインタを返す
UNLESS SYMB =S.COMMA RESULTIS A
NEXTSYMB()
RESULTIS LIST3(S.COMMA, A, RNAMELIST() )
$)
LET LIST3(X , Y , Z) = VALOF
$( LET P = NEWVEC(2) // ベクタを割り当てる関数
P!0, P!1, P!2 := X,Y,Z
RESULTIS P
$)
NEWVECは領域の確保を行う。
(図 3.3)参照
(P49)
ベクタ・ポインタの別の例
関数よびだしNEXT(S)はどんな入力ストリームSにも作用させることができ、次の文字を返す。同様にしてOUT(T , X)は文字Xを出力ストリームTへ出力する。あるストリームSに係わる必要な情報はすべて、Sが指しているベクタに入れてある。このベクタの始めの数項目は手続き値である。
ベクタは図 3.4 のような形になっており、NEXT.SOURCEは S の NEXT を実現する関数となっている。
(図 3.4)参照
ストリームを作り出す関数自身は
コーディング2
LET INPUTFROMTTY = VALOF
$( LET V = NEWVEC (5) // 空領域からベクタ確保
V!0:=NEXTTTY // ある入力ストリームに対する
V!1:=STREAMERROR // NEXTTTYの手続き値
・・・・・
RESULTIS V
$)
となる。したがって S の第0要素に入っている手続き値がストリームSのNEXTを実現する関数を表している。
NEXT はそこで
LET NEXT(S) = (S!0)(S)
と定義してあるし、同様に OUT は
LET OUT(S, X) BE (S!1)(S, X)
定義している。
(P40)手続きパラメータ
BCPLの手続き呼び出しはパラメータの受け渡しに値とりの方法を使っている。そこで F(X) のような呼び出しをすると、F へ渡るのは X の値であってそのアドレスではない。したがって F のなかで X を変更する直接的方法はない。手続きのほうでは、呼び出しの時点でのパラメータの値は、手続き宣言で宣言した対応する仮パラメータに代入される。どんな場合でも手続きの仮パラメータはローカル変数にすぎない。
ポインタでは何が起きるだろうか。
仮パラメータ A をもつ手続き P があるとする。
LET P( A ) BE ・・・
と宣言している。
変数Vが手続きPの外側で
LET V = VEC 30
と宣言している場合を考える。
呼出し P( V )
手続き P に渡す値は変数 V のもつ値、つまり、V へ別の値の代入がなかったとして、ベクタの第0要素へのポインタである。
この値を仮パラメータ A へコピーするからベクタの要素へは 手続きP の中からA!0, A!1, A!2 と書くことができアクセスできる。
手続きの外で宣言したダイナミック変数の内容を変更したければ、それへのポインタを渡さなければならない。それにスタティック変数にそのポインタを置いておくか、そのアドレスをすでにもっている別の変数の値を渡すか、あるいはアドレスを渡すべく @構造を使うかになる。
コーディング3
二つの変数の内容を交換する手続き
LET SWAP(PX, PY) BE
$( // PX と PY は交換すべきふたつの変数を指している
LET TEMP = !PX // X の値
!PX := !PY // Y を X へコピー
!PY := TEMP // はじめの X を Y へコピーする
$)
SWAPを呼ぶには
SWAP( @A, @B)
変数とアドレスを渡す。
(P61) 4.5 APTOVEC
BCPLでベクタを宣言するには LET宣言 を使う。
LET V = VEC 25
ベクタの大きさ(この例では、連続した26語を割り当てる)はコンパイル時に指定しなければならない。そうすることによりコンパイラはこれを含む手続きに必要なスタックフレームの大きさを算出できる。
プログラマは多くの場合、たとえ記憶場所を無駄にするとわかっていてもベクタを充分に大きくとっておかなければならない。多くの処理系では、ダイナミックに決めてベクタをとる機能がある。それは関数APTOVECを使う。
コーディング4
LET APTOVEC(F , N) = VALOF
$( LET V = VEC N // BCPLでは許されない
RESULTIS F(V , N)
$)
BCPLではダイナミックなベクタ割り当てができないのでAPTOVECは通常アセンブリ言語で書いてある。これは、ベクタ V の場所を、F を呼び出すためのスタックフレームのすぐ下のスタック部分にとるだけのものである。したがって、APTOVECを経由して F から出て読んだ手続きに戻ると、ベクタは動的に取り除かれる。
コーディング5
LET START() BE
$(
LET N = READN)
APTOVEC(MAINPROG , N)
$)
AND MAINPROG(V , N) BE ・・・
(P61) 4.6 空き領域管理
ベクタの大きさをダイナミックに定める機能で、領域管理の問題は幾分楽になったけれど時には領域の割り当てをプログラムの手続き構造に基づく制御の流れとまったく無関係にしたいことがある。このようなとき、プログラマは領域の割り当てと放棄の両方を自分で行わないといけない。
このシステムでは様々な大きさのブロックを割り当てるのに、最初に見つけたものをとる方法を使っている。連続する空き領域は合算する。この方法は特に効率がよいわけではないが手続きが小さくなる。
一番外側にあるブロックで、プログラマは適当な大きさのベクタを宣言し INITBLKLIST を呼び出す。
LET START() BE
$( LET FREESTOREVEC = VEC FSVSIZE
INITBLKLIST( FREESTOREVEC , FSVSIZE)
・・・・・
これでベクタが空き領域管理システムに渡る。
このシステムでは FSVSIZE は偶数でなければならない。
この領域からの割り当てには関数 GETBLK を呼ぶ。
V := GETBLK( X + Y )
これは割り当てた領域へのポインタを返す。
このパッケージでは、取り出した各ブロックの第一語はパッケージが使用するために別にしているが、残りは通常のベクタと同様に使える。
領域が不要になったときは、
FREEBLK(V)
を呼んで返す。
16ビットの機械では、パッケージは次のように定義する。
コーディング6
GET "LIBHDR"
GLOBAL $(
BLKLIST : 100; GET : 101; FREEBLK : 102; INITBLKLIST : 103
$)
MANIFEST $(
SIZEBITS = #XFFFE; FREEBIT = 1
$)
LET INITBLKLIST( V , N ) BE
BLKLIST, V!0, V!N := V, N + FREEBIT, 0
LET GETBLK( N ) = VALOF // N は必要なブロックの大きさ
$(1 LET P ,Q = 0, BLKLIST
N := ( N + 1 ) & SIZEBITS // 次の2の倍数へ丸めあげ
$( P := Q
WHILE( !P & FREEBIT ) = 0 DO // 使用ブロックをたどる
TEST !P = 0 THEN RESULTIS 0 // 記憶場所がおわった
ELSE P := P + !P
Q := P // この空き領域の終わりまでたどる
UNTIL ( !Q & FREEBIT ) = 0 DO Q := Q + !Q - FREEBIT
$) REPEATUNTIL Q - P >= N // 充分大きいブロックまで
UNLESS P + N = Q DO // 合致しなければブロック分割
P!N := Q - P - N + FREEBIT
!P := N
RESULTIS P
$)1
LET FREEBLK( P ) BE !P := !P | FREEBIT
ブロックを割り当てる前にパッケージはその大きさが 2 の倍数であることを確認する。第一語にブロックの大きさを入れる。だから割り当てたブロックはどれも一番下のビットが 0 になっている。このビットは領域が返されると 1 (FREEBIT)にセットされる。充分な大きさの空き領域を探すにはパッケージは割り当てたブロックを飛び越えて、空きブロックにたどり着く。それにつづく空きブロックは合併され、合体したブロックが充分な大きさかどうか調べる。確保できなければやり直す。確保できるようであれば、ブロックを分割して割り当てブロックと空きブロックにする。充分な大きさのブロックがとれなければ GETBLK は 0 を返す。