レキシカルアナライサ レキシカルアナライザは , lex. c と lexstr. c のふたつのファイルて、定義されてい ます。レキシカルアナライザはソースプロ グラムを読み込み , それをトークンにして ーサに渡す役目をもっています。また , yacc を使ってパーサを書くときには , レキ シカルアナライザの名前は必す yylex ( ) て、な ければなりません。 yylex ( ) は , 値としてト ークンのタイプを返します。また , ンの値はグローノヾル変数 yylval にセットし ます。 yylval は YYSTYPE 型の変数て、す。 具体的には cm. y て、 % % union 文て、宣言したメ ンバをもつ構造体になります 0YYSTYPE 型の宣言は , yacc が生成するヘッダファイ ル cm. h に含まれています (List1) 。しかし , このうちレキシカルアナライザが yylval に セットする値は , const, symbol, OP の 3 って、 , それぞれ定数 ( 文字 , 数字 , 文字列 ) , 識別子 , 演算子の種類をセットします。レ キシカルアナライザの本体は lex. c て、定義し ていますが , 文字列を扱う部分は lexstr. c て、 定義しています。 レキシカルアナライザの本体は yylex ( ) て、 す。内容はきわめて単純なものて、 , まず空 白文字 ( スペース , タブ , 改行 ) を読み飛ば し , 次に現れた文字に応じた処理を行いま す。英字か下線ならば lexld(), 数字ならば IexNumber( ) , 文字列ならば IexString( ) を呼び出します。また , ' ( アポストロフィ ) 創刊特別企画 C コンノヾイラの内部を詳解 第 4 回 / レキシカルアナライサと宣言の処理 近藤嘉雪 前回は , Cm コンバイラの文法を定義しました。今回は , ま ず , レキシカルアナライサをつくり , それからテープルやヒ ープ領域の管理などハウスキーピングルーチンをつくり , 最 後に宣言の処理までを一気に作成することにしましよう。 typedef struct 2 : #define MAXSTROBJ 1 : #define MAXSTRPOO し 1 : C 以こよる C コン、イラブログラミング ならば文字定数の処理を自前て、行います。 そのほかの記号の場合には , 演算子の処理 を行います。演算子の場合には , 演算子の タイプをリターンするとともに , 必要に応 じて yy ⅳ al. op に値をセットします。演算子 YYSTYPE 型の中身 に対するリターン値と yylval. op の対応を表 また , # ( シャープ ) ならば , 関数 pre process() を呼び出して , #line プリプロセ ッサ文の処理を行います。 Cm コンパイラ は , プリプロセッサの使用を前提としてい るのて、 , #define や #include などを処理する 必要はありません。しかし , プリプロセッ サを通すと , ソースプログラムの行番号が 1 に示します。 1 : typedef 2 : 3 : 4 : 7 : 8 : List 1 un lOn CONST TREE TYPE Char EXPR Char int YYSTYPE; _const; *type : *symbol; *expr; label; このうち , レキシカルアナライザがセットするのは , const, symbol, op の 3 つのみ List 2 定数トークンの内部表現 : 定数を表す構造体 CONST(struct_const) 2 : 3 : 4 : 5 : 6 : { CONST_CHAR ニ 1 , CONST_I NT, CONST_STRING} typedef enum CONST_TYPE; _const { typedef struct CONST_TYPE type; long va ー ue : CONST : List 3 文字列処理のための定義 (lexstr. c) 50 1500 3 : 4 : 5 : 6 : 7 : 9 : 10 : 1 1 : 12 : 13 : static static static static int Char i nt ー STROBJ; Char Char STROBJ STROBJ strobj strld : *string, ength : strpool CMAXSTRPOO し ] : *poolptr; strobj[MAXSTROBJ] : *objptr; yacc による C コンバイラブログラミング 67
lexstr. c の先頭て、定義されています (List3)o ずれてしまいます。それを補正して , 工ラ パイラの内部て、は同一のトークン CONST 構造体 STROBJ が , 文字列の実体て、 , 3 っ ーメッセージに正しい行番号を表示させる ANT としてまとめています。定数の値を表 ために , Cm コンパイラが # line を処理する必 すために ,cmdef. h の中て℃ ONST 型という のメンバをもっています。メンバ str 工 d は , 構造体を定義しています ( List2 , 表 2 ) 。 文字列の識別番号て、す。 yylval. const. 要があります。 IexID() は , 識別子を読み込んて , キーワ て、 , 文字定数 , 数値定数については , メン value にセットされるのはこの番号て、す。メ ードかどうかを調べます。もし , キーワー ンバ string は文字列本体へのポインタを , バ value にセットする値が , そのまま定数の ドて、あれば , 対応するトークン値を返しま 値となります。 メンバ int は文字列の長さをもっています。 文字列本体は , char 型の配列 strpool に格 す。キーワードてなければ , yylval. symbol 文字列の場合は , 少し複雑な扱いになり ます。文字列の処理を行う関数 IexString( ) 納します。識別番号は , 各文字列を識別す に識別子名へのポインタをセットして , ト ークンエ DENT 工刊 I 刊 R を返します。 は , ファイル lexstr. c て、定義してあります。 るためにユニークな ( 一意的な ) 番号を割り lexNum( ) は , 数値定数を読み込み , その 文字列は , もともと可変長のものなのて、 , 当てる必要があります。これには ,gensym() 直接扱おうとするとやっかいなことになり を利用します。 gensym ( ) は , 呼び出される 値を yylval. ー const にセットして , トークン CONSTANT を返します。ここて定数の扱 たびに一意的な番号を返す関数て、 , misc. c ます。そこて、 , 文字列を直接扱うのて、はな て、定義されています ( List4 ) 。 いについて説明しましよう。 Cm には , 3 種 く , 実体をどこか別の場所にとっておき , 類の定数ーー数値定数 , 文字定数 , 文字列 その実体へのポインタを通して文字列を処 実際のデータ構造は , Fig. 1 のようになり があります。これらの定数は , それぞ 理することにします。 ます。文字列の実体は , 配列 strobj にとら れ int, char, char * 型て、すが , Cm コン 文字列を扱うためのデータ構造は , れます。この配列は , ポインタ objptr て、管 [ 表 2 ] 定数トークンの内部表現 : 構造体 CONST(struct const) の意味 [ 表 1 ] 演算子と y ⅵ ex のリターン値 , yylval.op の関係 定数の種類 メンノヾ v 引 ue の意味 メンノヾ type の値 yylex の yylval.op 演算子 リターン値 の値 文字定数 CONST CHAR 文字コード 数値定数 CONST INT 数値 addop 十 文字例 文字列の識別番号 CONST STRING addop shiftop [ 表 3 ] ヒープ領域を操作する関数 ( heap. c ) shiftop くく 目的 mulop 目言を表す木構造をつくる mulop 式を表す木構造をつくる 文字列を格納する COÆIPOP ヒープ領域をクリアする く COÄÄPOP COÄIPOP COÆIPOP Fig. 1 文字列のテータ構造 eqop 配列 equop strobj [MAXSTROB 」 ] asslgnop asstgnop asslgnop asslgnop asslgnop asslgnop asslgnop asslgnop asslgnop asslgnop asslgnop incdecop 十十 incdecop unÖp unop logor logand 関数 listl, list2, list3 makeNode1, makeNode2, makeNode3 saveldent 「 esetHeap 厄 ngth 6 9 2 string strld 1. 5 6 ← 0bj ptr 配列 strpool [MAXSTRPOOL] 0 \ 0 C 0 m p u t e 「 a \ 0 P00 わ t 「 文字列は , strld で識別する。 strld= l——+hellow ー 5 ーー→ .com puter strld ー strld = 6 ーーー a ″ 68 CMAGAZINE 19 1
Obieot 0 「 ie : 叮〉 5 虻 ) ; *this く ESC く ()w ? [ 〉 51 ' screenLen-l; ln クラス screen を定義する。 これは , データをひとつももっていない は , if ( ( C01 c) く 0 ) C01 (*this く ESC ) く ()w ? 貰〉 51 ' : 貰〉 5h つ ; メソッドだけからなるクラスて、ある。この if ( C01 〉 = screenWid) と解釈される。演算子くが screen 型のオプ ようなクラスは , データがないために複数 C01 screenWid 個のオプジェクトを考えることが無意味に ジェクト ( 実は自分自身 ) への参照を返すの return * this; なる。実際物理的には , このクラスに属す て、 , このように書くことがて、きるわけて、あ るオプジェクトは , ただひとつのコンソー る。なお最初のくは int 型のオプジェクト この関数は , 行数と桁数がともに正しい を , また 2 番目のくは char * 型のオプジェ ル画面しか存在しない。そこて、 , これを , 範囲にあるように調整している。また戻り theScreen と呼ぶことにしよう・ クトを出力するための演算子て、あるから , 値て、用いている this は , このメンバ関数を screen theScreen; それぞれ operator く (int) と operator く (char 呼び出した際のオプジェクトへのポインタ 〃スクリーンオプジェクト * ) を呼び出すことになる。 を保持する特別なポインタ変数てある。し このオプジェクトへの出力は , 演算子くを このクラスにはクラス名の前に ~ をつけた たがってメンバ関数 set は , 自分 ( オプジェ 使って以下のように行う : 名前の目新しいメンバ関数 ~ screen がある。 これは前出のコンストラクタと対をなすデ クト ) 自身への参照を返している。 〃フザー const int BUZAR = Ox07; 関数名 operatorX は演算子 X を再定義す ストラクタ (destructor) と呼ばれるものて、 , る際の名前て、ある。これを演算子の多義化 theScreen く BUZAR; / / フゞーがなる このクラスに属するオプジェクトの寿命が (operator overloading) と呼ぶ。本例て、は , この記法を使った非公開メンバ関数 cur つきて消え去る直前に呼び出される , いわ posl と pos2 を scrPos 型の変数とすると , sor と fKey の本体は , 次のとおりて、ある。 ゆる後始末関数て、ある。 〃カーソル ON/OFF POSI pos2; 一般にスクリーンへの文字列の出力は , posl 十 = POS2; VOid screen : : cursor(logical sw) 公開メンバ演算子くくを用いる : posl = pos2; screen& screen : : operator くく (const char * S) というような書き方がて、きるが , これらは それぞれ POSI. operator= (POS2); POSI. operator 十 =(POS2); =(POS2); posl. operator— の省略形と考えられる。 上て定義したクラス scrAttr と scrPos を 使って , List5 に画面を表すオプジェクトの O; *this く ESC く ()w [ 〉 5 蹙 : " [ 〉 5h" / / 関数キー表示 ON/OFF VOid screen ・ : fKey(logical sw) register int c; while (c * this く c ; return * this; , こて、ポインタ引数 s の型についている修 飾詞 const は , この関数内て、はこの引数が指 すオプジェクトの内容を変更しない ( て、きな い ) ことを宣言している。これによってこの 関数のユーザは , 引数に渡すオプジェクト のコピーを取らなくても安心して利用て、き るようになる。 スクリーンの属性も演算子くくて、指定する (List6)0 また , スクリーン位置もくくて、指 定て、きる (List7) 。 このようにして演算子くくを多義化するこ とによって , 文字列はもとより属性や位置も 同一のメッセージて、スクリーンオプジェク トに渡すことがて、きるようになる。なおこ の演算子の使用例は , 次節を参照されたい 第一特集オプジェクト指向プログラミング 51 *this く ESC く ()w ? " [ 〉 11 " : " [ 〉 1h ' こて List 8 / / 既定幅 1 : const int defWid / / 既定長 2 : const int def し en / / 最小サイズ 3 : const int smallW し 4 : c lass w i ndow { / / ウインドウ 5 : private: / / 左上の位置 scrPos pos; / / 現在の位置 scrPos curPos : / / 桁幅 i nt col W i d : / / 行幅 9 : int ln し en; / / タイトル cha r* t i い e : 1 1 : public: window(scrPos&, int ー defWid, int ニ defLen, window ( ) : / / 出力用メソッド window& operator くく (const char*) : window& operator くく (scrPos&) : scrPos& operator() (scrPos&) : / / スクリーン位置ー > ウインドウ位置 20 : } : 0 0 11 っ乙・ー char*
り , スタックへのコヒ。ーによるオーノヾーへ ッドを削減する場合に有効となる。 射影関数 1 ( ) と c( ) 以外のメンバ関数は , すべてメンバ関数 set を呼び出している : こ修飾 の変数の中身を変更て、きなくなる。したが #define screenWid 80 って実際は , #define screenLen 25 50 CMAGAZINE 1990 1 がってこの方法は , 引数に副作用を与えた ものを使って処理を行うようになる。した こうしたコピーはつくられず , 渡す値その を行う。これに対して , 参照呼びて、渡すと り , そのコピーを使って関数本体て、の処理 す際にその値のコヒ。ーをスタック上につく 値呼び ( call by value ) を用いると , 値を渡 C における通常の引数の受け渡し方法て、ある れる引数の受け渡し方法を指定している。 pos は , 参照呼び ( call by reference) と呼ば 2 番目のコンストラクタの引数 scrPos & のふたつも同じ意味になる。 scrPOS origin(), O); scrPos origin; のふたつは同じ意味になる。また , scrPos tenthLine(10, O); scrPos tenthLine(10); 既定値引数と呼ばれる。たとえば , いられる既定値て、ある。このような引数は は , これらの引数が指定されないときに用 タの仮引数に代入 ( ? ) してあるふたつの 0 呼び出すかを決める。最初のコンストラク ンパイラは引数の型によって , どの関数を は多義化 ( overload ) されているといい , コ の ( メンバ ) 関数が複数あるとき , この関数 つある点に注意されたい このように同名 まずは , コンストラクタ scrPos( ) がふた クラス scrPos の定義は List4 のとおり。 文は必要なくなる。 ンライン展開によって , ほとんどの #define る点が違う。いすれにしてもこの const とイ 型て、あるかどうかの ) 型チェックをしてくれ と書くのと同じてあるが , コンパイラが (int const int screenLen 〃スクリーン長 これは基本的には , int screenWid = 80; int screenLen と同じ意味て、ある。ただ , 子 const をつけて宣言すると , 以後はこれら 22 : } : 1 : 24 : } 〃スクリーン幅 25 ; 〃スクリーン長 このよう ( 25 ; scrPos& scrPos : 1 ) : set(int 1 , int c) く O) ln O; if ((ln NG ニ 0 , OK } : screenLen) List 5 2 : 3 : 4 : 5 : 7 : 10 : 13 : 14 : 17 : 20 : 1 : enum logical { NO, YES. OFF = 0 , ON, class screen { / / スクリー private : void cursor(logical); void fKey(logical); public: ン / / 関数キー 0N/0FF / / カーソル ON/OFF screen() { cursor(0FF) : fKey(0FF) ・ screen() { fKey (ON) ; cursor (ON) ・ private: / / 出力用メソッド ( バイナリ・モード ) screen& operator く (const char* s) { cputs(s) : return *this ・ public: screen& operator く (int c) { putch(c) : / / 出力用メソッド ( テキスト・モード ) screen& operator くく (const char*) : private: screen& updPrnSw(IogicaI&) ; 19 : pub I i c : screen& operator くく (scrAttr&) : screen& operator くく (scrPos&) : r•eturn *this; } List 6 3 : 4 : 5 : 7 : 9 : 20 : 22 : 23 : screen& screen::updPrnSw(logicaI& printed) if (printed) *th i s く e 1 S e printed return *this; screen& screen: :operator くく (scrAttr& attr) = YES; *th i s く E SC く 1 og i ca I P r i nted = NO ; ニ attr. m(); i nt mode if (mode & HIGH_ い GHT) updPrnSw(printed) く if (mode & UNDERLINE) updPrnSw(printed) く ' 4 ' ・ if (mode & B い (K) updPrnSw(printed) く ' 5 ' if (mode & REVERSE) updPrnSw(printed) く ' 7 ' ・ ret urn ( * th i s く ' m ' ) : updPrnSw(printed) く ' 3 ・く C010r + ' 0 ' if (color 〉ニ 0 & & C010r く 8 ) i nt CO I or = attr. c(); List 7 1 : screen& screen: :operator くく (scrpos& POS) 3 : 4 : cprintf("%c[%d;%dH" ESC, (pos. I ( ) ) + 1 , return *this;
2 : 4 : 5 : 7 : 8 : 9 : 10 : 12 : 13 : 14 : 3 : # i ncl ude 6 : #define INCORE-STREAM し ENGTH List 5 ーフ・ン centerbox( int xw, int yw, const char* t= 0 , cell 作 0 ) : / / コンストラクタ list 5 文字列バッファクラスのサンプルプログラム 2 : 3 : sstream. C sample. C List 6 ” sstream. 1 : / / ⅱ st 6 標準入出力ストリー ム cout,cin の付け替え 10000 / / cout 用のパッフアの大きさ Char stringbuf ostream Char stringbuf i stream List 7 cout-buf[ INCORE_STREAM し ENGTH ] cout-sb ( cout-buf, INCORE-STREAMLENGTH ) : cout( &cout-sb ) : cin-buf[] cin-sb( cin-buf ) : cin( &cin-sb, 1 , / / 入力すべき文字列 &cout ) : 2 : 3 : 4 : 7 : 8 : 9 : nn : nn : nn : nn : nn : nn : nn : 1 : / / list 7 window. h の概要 5 : / / ウインドウの抽象クラス 6 : class window { public: window() : ⅵ ndow ( ) : typedef unsigned short cell;/* テキスト VRAM の 1 文字 ( フレームの種類指定 ) * / open( int & int Y ) : ⅵ rtua 1 VO i d active() : virtual VOid get( char* buf, int n ) : virtual int V irtua ー W indOW& operator くく ( const char* S nn : / / フレーム ( 枠 ) を持たないウインドウ ・ public window { nn: class board public: / / コンストラクタ / / テ・イストラクタ / / ( x , Y ) にウイント・ウをオ / / アクティフ・化 / / 文字列入力 ) : / / 文字列出力 board( int xw, int YW ) : / / コンストラクタ ( 大きさ xw. xy のウイント・ウを構築 ) / / テ・イストラクタ board ( ) : open( int x, int Y ) : VO i d get( char* buf. int n ) : int window& operator くく ( const char* s ) : / / ( x , y ) に ) イント・ウをオーフ・ン / / 文字列入力 / / 文字列出力 nn : / / フレームを持っウインドウ nn: class box : public board { public: box( int xw, int yw, const char* cell f=0) : / / コンストラクタ nn: / / フレームを持っウインドウ ( センタリング出力 ) nn: class centerbox : public bOX { public: centerbox() : / / 大きさ (xw,yw) / / 大きさ (xw,yw) タイトル t フトムの種類 f のウイント・ウを構築 / / テ・イストラクタ タイトル t フレームの種類 f のウイント・ウを構築 window& operator くく ( const char* s ) : / / テ・イストラクタ / / 文字列出力 flow を呼び出し , バッフアを空にしてから データを格納する。 istream を用いた入力は , この反対と考え ればよい。すなわち , 次のようになる。 1 : istream 内の処理 メンバ変数 bp が指す strembuf 型変数に snextc メンバ関数などを用いて文字データ 列を獲得し書式の処理 ( たとえば , 文字列 表現を整数値に変換する ) などを行う。 2 : streambuf 内の処理 メン六関数 snextc などによって文字デー タが要求されたとき , メンバ変数 base が指 すバッファ上のデータを返す。もしバッフ アが空のときは , メンバ ( バーチャル ) 関数 underflow を呼び出し , バッフアにデータ をつめてからデータを読み出す。 リバッファクラス stri gbuf の作成 文字列へのフォーマットつき入出力 ( C 言 語の sprintf, sscanf) を行うため , strea mbuf の派生クラスて、ある stringbuf を作成 する。実現の方法として , 「基本クラス (streambuf) のノヾッファリングの機能を , 文字列へのデータ埋め込み / 読み出しに使用 する」 , つまり「文字列領域をバッフアと共 有して streambuf の機能を利用する」方法を とる (List2,3)0 文字列への出力の場合 , 基本クラス (streambuf) の機能によって , メンバ変数 streambuf: : base の孑旨すノヾッファ上に文字 第 2 特集実践 C 十 + プログラミング 81 : underflow が呼び出される。コンストラク が空になったとき , メンバ関数 streambuf: ら , データを取り出してくれる。バッファ streambuf: : base が孑旨すノヾッフア領域か (streambuf) の機能によって , メンバ変数 文字列からの入力の場合 , 基本クラス 情報 (EOF) を返せばよい (List3- ① ) 。 め , メンノヾ関数 overflow は , つねにエラー クラス ( stringbuf ) は出力装置をもたないた overflow が呼び出される。文字列バッファ ばいになったとき , メンバ関数 stringbuf: ・ 列を展開してくれる。そしてバッフアがいっ
られた文字列にいくつの String から参照さ を渡すようにするべきてす。 同、、効率のよい れているカウンタ ref を設けておいて , オプ 一例として , どのくらいたいへんかを C 十十 渡 / 値返しの方法は ジェクトが新しくつくられると 1 に初期化 トランスレータが出力するコードから簡単 し , String のコピーコンストラクタて、は十 しかし人間の欲望はかぎりなく膨らむも にみてみます。 List5 を C 十十トランスレー のて、す。速くて効率のよい値渡し / 値返しの 1 , ディストラクタて、一 1 にし , その値が O に タ Ver. 1.2 に通し , 得られた C コードに見や なったときだけ delete するという方法て、す 方法はないものて、しようか ? よく知られ すくするため多少の修正を加えたのが List6 ている方法に「リファレンスカウント」と (List8, Fig. 5 ) 。こうすると , 毎回メモリの てす。コメントをたくさん追加してあるの 確保 / 解放 / コヒ。ーを行わなくてもすむのて , て、 , だいたいのことはつかめると思います。 いうものがあります。ヒープメモリ上にと たいへんな手間をかけて値渡し / 値返しを実 現しているようすがわかりますね。当然と いえばそれまてて、すが , こんな簡単な例て、 さえも値渡しにコピーコンストラクタを 1 回 , 値返しにも 1 回 , その後の代入に、、 String : : operator = ( ) 〃を 1 回 , リターン前の後始末 にディストラクタが 2 回起動されているのが わかります。 て、は , どうしても値渡し / 値返しを使わな ければならない場合とは , どんな場合て、し ようか。その典型的な例は、、 + 〃 な どの演算子をオーバーロードしたときの戻 り値て、す。ちょっと Basic チックかもしれま せんが , クラス String に文字列をつなぐた めの演算子、、 + 〃を考えてみましよう。 の種の 2 項演算子は , ふつうフレンド関数と して組みます ( List7 ) 。この関数の中て、 ans は , この関数にローカルなオプジェクトてす。 ローカルなオプジェクト ans は , リターン するときには消えてなくなる運命て、すのて、 , String: :operator=( ) のように参照返しは てきません。ては , ans を static String ans; とスタティック宣言してはどうて、しようか ? 残念ながら答えはノーてす。 a = b 十 c 十 d; のような式は , a=(b 十 c) 十 d; と解釈されるのて , ( b + c ) の結果 , すなわち ans 自身に d を連結し , 結果を再び ans に入 れるという矛盾が生じるからてす。演算子 が返すものはひとつの独立したオプジェク トて、ないと , 一般的な「式」のもつ意味が 保てなくなってしまうのてす。 もちろん , 、、 + 〃演算子が受け取る引数は 参照型とし , 無用なコピーコンストラクタ の呼び出しを防止するのはいうまて、もあり ません。 88 CMAGAZINE 1990 1 List list head tail ーコ 想数一ル 仮関テプ 1 0 0 0 0 D2::introduce() 私は D2 の オプジェクト Obj::introduce() 一私は Obj の オプジェクト ー 1 D1::introduce() 私は DI の オプジェクト D3::introduce ( ) 私は D3 の オプジェクト List 7 String operator 十 (const String& a, const String& b) 2 : 3 : / / 文字列をつなぐクラス Str i ng のフレンド関数 4 : / / 連結結果を入れるためのオプジェクト 5 : String ans; 6 : int len if (). str) len + ま strlen(). str); 7 : if (). str) len + ニ strlen(). str); 8 : if (len) { 9 : ans. str 咢 strcpy (new char(len 十 1]. a. str) : strcat(ans. str, b. str) : こは値返しでなければならない return ans; / / List 8 struct StrB0dy { / / いくつの Str i ng から参照されているかを記億するカウンタ int ref; char s い [ 1 ] : / / 文字列本体 2 : 3 : 4 : 5 : class String { 6 : StrBody* body : 7 : 8 : public: String(const char* P) 9 : / / 文字列から Str i ng を作るコンストラクタ
Obieot 0 「 ie 変換には , 演算子 ( ) を用いる : 〃スクリーン位置ー〉ウインドウ位置 scrPos& window : : operator ( ) (scrPos& 00 ) List 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : 16 : } : List 1 : 2 : 3 : 4 : 5 : 6 : 7 : 9 : 14 : 20 : 22 : 23 : 24 : 25 : 26 : 28 : 29 : 30 : } 31 : 32 : { 33 : 34 : } List 1 : 3 : 4 : 5 : 6 : scrPos pos; mouseState st; private: 1 3 1 : c I ass mouse { / / マウス logical leftButton; / / 左ボタンの状態 logical rightButton;// 右ボタンの状態 public: mouse(int X ニ 320 , int mouse() : / / カーソルの表示と消去 void cursor(logical); logical right() { return rightButton; } logical left() { return leftButton; } / / カーソル位置の取得とボタン状態の改訂 scrPos& curPos ( ) : mouseState state() { return st; } / / カーソル状態 / / カーソル位置 = 200 ) : Y return (po マウス POS); 1 4 const mouseBios static REGS reg; = 0X33 : / / 98 のマウス割り込み番号 const GraphBios ニ 0X18 : / / 98 のグラフィック割り込み番号 enum mousescreen { mouseBlue, mouseRed, mouse: :mouse(int X, int y) mouseGreen } ; / / 400 行にセット reg. h. ah = 0X42 : reg. h. ch ニ OxcO; int86(GraphBios, ®, / / マウスの初期化とカーソル状態の設定 reg. X. ax int86(mouseBios, ®, ®) : ®) : (reg. x. (x) ? mouse0ff mouseBad; st int86(mouseBios, ®, ®) : reg. x. bx mouseRed; reg. X. ax / / カーソル表示画面の設定 int86(mouseBios, ®. ®) : reg. x. dx reg. X. CX reg. X. ax / / カーソル位置の設定 mouseBad) return;// 異常 if (state ( ) mouse: mouse ( ) cursor(0FF) : int86 (mouseBios, ®, reg. X. if (state ( ) ! = mouseBad & & state() VOid mouse::cursor(logical SW) 1 5 / / カーソルの表示と消去 ®) : オプジェクト指向の環境において重要な 働きをするデバイスとしてマウスがある。 そこて、この節て、は , このマウスのオプジェ クト化を行う。 最初にマウスの状態を表現するための型 mouseState を定義する (List12) 。 それぞれの値の直観的意味は , mouseBad がマウスの初期化に失敗したこと ( たぶんマ ウスドライバをインストールし忘れていた とき ) , および mouseOff, mouseOn がそ れぞれマウスカーソルが ON, OFF というこ とて、ある。 mouseState を使って定義したク ラス mouse は List13 のとおりて、ある。 このクラスも screen と同様 , これに属す るオプジェクトは物理的にただひとっしか 存在しない。そこて、このオプジェクトを theMouse と呼ぶことにする : mouse theMouse;// マウスオプジェクト 最初にコンストラクタとデストラクタの インプリメントを示す。なお , これ以降は 98 の BIOS に依存したコードになっている (List14)0 コンストラクタは , 98 の BIOS を直接操作してマウスの初期設定を行って いる。またデストラクタはマウスカーソル を消しているだけて、ある。 なお , 共用体 REGS はく dos. h 〉て、定義さ うした型の宣言も C のよう れているが , union REGS reg; て、はなくて , たんに REGS reg ; と書くだけて、よい点に注意されたい マウスカーソルの ON / OFF は , List15 に 示すメンバ関数て、行う。 この関数は , カーソルの状態を変更する 第一特集オプジェクト指向プログラミング 53
トの複製をつくるという意味てこれを「コ ピーコンストラクタ」と呼ぶこともあるよ うてす。 あるクラスにとってコヒ。ーコンストラク タが必要か否かは , プログラマが決めるこ となのて、 , ( 本当は必要なのに ) コピーコン ストラクタがなくても処理系は何も文句を いってきません。工ラーも警告もなくコン パイル / リンクて、きてしまいます。その代わ り , プログラムを走らせているうちに動作 が変になり , 最後は昇天してしまうて、しよ 昇天に至るまて、の筋書きは , 次のような 感じて、す。まず , 値渡しとして渡されたノ ラメータに関して呼ばれた関数側からみる と , 「はじめから初期化されているローカル 変数』て、あり , リターン時には消えてなく なる運命にありますから , その値を煮て食 おうと焼いて食おうと , 呼び出し側にはい っさい影響を与えないはずて、す。 void f3() クラス String にコピーコンストラクタが String c ( " 文字列 A " ) , d ( " 文字列 B " ) : 一般の構造体と同じようにビッ ないとき , 〃ビット幅のコビーではダメ 〃 St r i ng : : operator= ( ) によって代入される ト幅 ( 構造体のバイト数 ) 分のメモリがコヒ。 / / c, d に対してディストラクタが起動される ーされ , パラメータとして渡されます (Fig. 2)。 str こそ別々て、すが , str がポイン (delete) してしまうのて、す。 ンするときに始まります。値渡しされたパ トしている文字列は共有したままて、す。 その後 , 呼び出し側の関数は , 文字列に ラメータは一種のローカル変数なのて、 , リ れて、は , 煮て食うなり・・ ・・というわけには 割り当ててあったメモリが解放されている ターンと同時に寿命がつきますが , String いきません。文字列部分に書き込みを行え などとはつゆ知らず , 陽気に文字列をアク のようにディストラクタをもったクラスの ば , 呼び出し側のオプジェクトまて、変わっ オプジェクトは , 必ずここて、ディストラク セスし続けます。このとき , いったい何を てしまいます。 タが起動され , 呼び出した関数側のオプジ 読み書きしているのてしようか ? 最後に さて本当の悲劇は , この関数からリター 呼び出し側のオプジェクトの寿命がっきた ェクトのもち物て、あるはずの文字列を解放 String aC 文字列つ List 3 String: :String(const String& s) if (). str) str = strcpy( new char[ strlen(). str) + 1 ] , s. str) : e ー se str vo id f2 (Str i ng b) / / b は関数内だけで自由に使えるオプジェクト / / 関数の最後にディストラクタが b を消す void fl() String a ( " 文字列つ : / / コピーコンストラクタによって f2 (a) : / / オプジェクトの複製が作られる渡される List 4 String& String: :operator=(const String& s) / / 先ず自分が持っていた文字列を解放する delete str; if (). str) / / 文字列の複製を作る = strcpy( new char[ strlen(). str) + 1 ] , s. str) : str e ー se str return *this; / / 自分への参照を返す / / a b = c : のような C らしい記述が可能になる String c 代入の後ではニ重に参照され ニ重に解放される 代入の後ではどこからも参照 されなくなる 呼び出した関数 文字列 呼び出された関数 str 文字列 A \ 0 String d String b Str 文字列 B \ 0 構造体 StrB0dy String aC 文字列つ この StrBody はふたつの String \ 0 オプジェクトから参照されて String 1 いる これはひとつ 2 文子列 \ 0 呼び出した関数 fl 呼び出された関数 f2 1 String b \ 0 String 2 1 86 CMAGAZINE 19 1
fprintf や fscanf のような , 書式つきの入出 力を行う関数が用意されている。また , 同様 の書式て、文字列のフォーマット入出力を行 う sprintf,sscanf 関数も用意されている。 もし , sprintf がサポートされていなかっ たらどうだろうか ? fprinf のソースコードが入手て、きない場 合 , 1 から sprintf を作成しなければならな いが , これはたいへんな仕事になる。たと えソースコードが入手て、きる場合て、も , ソ ースを改造し再コンパイルを行う必要が出 てくる。このように , C 言語では「再利用性」 に関してほとんど考慮がなされていない。 C 言語において「再利用性」を高める唯一の方 法は , ライプラリ設計者が将来ユーザが必 要になるかもしれないすべての関数をサポ ートすることだけなのて、ある。しかし , のような方法には本質的なジレンマが存在 する。つまり , 「再利用性」を高める努力が ライプラリのインタフェイスを複雑にして しまい , かえって再利用性を低くしてしま うのて、ある。 C 十十讎吾における した文字列のフォーマット入出力について 用性」をもっことがて、きる。たとえば , 前述 C 十十て、記述されたライプラリは高い「再利 派生クラス , 仮想関数などの機能により , 考えてみよう。 stream. h て、宣言されている。その概要を List1 C 十十の標準入出力ストリームクラスは , ースト丿ームクラス stream. h 標準出力 にこの機能を追加する方法を解説する。 準入出力ストリームクラスを拡張して新た サポートされていないものとし , 既存の標 ポートしているが , ここて、は後者の機能が 字列のフォーマット入出力機能も標準て、サ る。もちろん , sprintf, sscanf にあたる文 に相当する書式っき入出力を可能にしてい h) をサポートし ,C 言語の fprintf や fscanf は , 標準入出力ストリームクラス (stream. C 十十 ( リリース 1.2 ) のクラスライプラリ List 1 1 : / / listl stream. h の概要 2 : 4 : 5 : 6 : 7 : nn : nn : nn : nn : nn : nn : nn : nn : nn : nn : nn : nn : nn : nn : nn : nn : nn : nn : nn : nn : nn : nn : nn : 3 : struct streambuf{ char* char•* char* char* virtual virtual base : pptr; gptr; eptr : int int int snextc ( ) / / ハ・ツフアの先頭アト・レス / / 未使用領域先頭アト・レス / / 使用済領域先頭アト・レス / / ハ・ツフアの終端 + 1 overflow(int c=EOF);// ハ・ツフアを空にする underflow ( ) : / / ハ・ツフアにテ・一タをつめる / / 1 文字入力する { return (gptr>=(pptr-l)) ? underflow() : * + + gptr & 0377 : sputc(int c =EOF) { / / 1 文字出力する { return (eptr<=pptr) ? overfl ow ( c & 0377 ) ( * pptr + + = c & 0377 ) : int allocate() : class ostream { fri end istream; streambuf* bp : streambuf() : streambuf (char* streambuf ( ) : / / まだハ・ツフアにメモリが割り当てられていないなら . / / メモリを獲得しハ・ツフアに結びつける / / コンストラクタ ( ハ・ツフアを割り当てない ) int I);// コンストラクタ ( p : ハ・ツフアの先頭アト・レスいハ・ツフアの大きさ ) 〃ストリームの状態 ( 工ラー、 EOF 等 ) / / ストリームハ・ツファ / / テ・イストラクタ short public: ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& state; operator くく (const char*) : operator くく (i (t) : operator くく (uns igned) : operator くく (unsigned long) : operator くく (long) : operator くく (double) : operator くく (const streanbuf&) : / / 出力関数 ( 文字列 ) / / 出力関数 ( 整数値 ) / / 出力関数 ( 整数値 ) / / 出力関数 ( 整数値 ) / / 出力関数 ( 整数値 ) put(char) : ostream (streambuf* s) : ostream (i nt ostream(int l, char* / / 出力関数 ( 浮動小数点値 ) / / 出力関数 ( ストリームハ・ツファ ) / / 出力関数 ( 文字 ) / / コンストラクタ ( ストリームハ・ツファ s で構築 ) / / コンストラクタ ( ファイルハント・ル fd で構築 ) / / コンストラクタ ( p : ハ・ツファ先頭アト・レス I : ハ・ツフアのサイス・ ) / / テ・イストラクタ ostream ( ) : nn: class istream { friend ostream: streambuf* bp; ostream* Char short pub ⅱ c : istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& istream& tied_to skipws; state; / / ストリームハ・ツファ : / / 対応する出力ストリーム / / スキッフ・←ト・の時非 0 / / ストリームの状態 ( ェラー、 EOF 等 ) operator>> (char*) : operator>> (char&) : operator>> (short&) : operator>> (int&) : operator>> (long&) : operator>> (float&) ; operator>> (doubl (&) : operator>> (streambuf&) : operator>> (whitespace&) get(char*, int, char= ・ Yn ・ get(streambuf& sb, char- get(char& c) : eof() : int / / 入力関数 ( 文字列 ) / / 入力関数 ( 文字 ) 〃入力関数 ( 整数値 ) / / 入力関数 ( 整数値 ) / / 入力関数 ( 整数値 ) / / 入力関数 ( 浮動小数点値 ) / / 入力関数 ( 浮動小数点値 ) / / 入力関数 ( ストリームハ・ツファ ) : / / 入力関数 ( スへ・一スのスキッフ・ ) / / 入力関数 ( 文字列 ) ー ' \ n ・ ) : 〃入力関数 ( ストリームハ・ツファ ) / / 入力関数 ( 文字 ) / / ストリームの状態レホ・一ト ( eof の時非の istream(streambuf* s. int sk=l, ostream* t= の : / / ( ストリームハ・ツファ s で構築スキッフ・←ト・ sk 出力ストリーム t に対応 ) istream(int l, char* P, int sk=l) : / / ( サイスの文字列 p で構築スキッフ・←ト・ sk ) istream(int fd, int sk=l, ostream* t ニ 0 ) : / / ( ファイルハント・ル fd で構築スキッフ・←ト・ sk 出力ストリーム t に対応 ) / / コンストラクタ / / コンストラクタ / / コンストラクタ に示す。 78 CMAGAZINE 19 1
ウンドウ この節て、は , 前節のスクリーン上に描く ウインドウオプジェクトのクラス window を 作製する (List8) 。 ウインドウの各データは次のような意味 をもっている。 1 : 2 : 3 : 5 : 6 : 7 : 8 : 9 : 12 : 20 : 22 : 23 : 24 : 30 : 32 : 33 : } List 1 : 3 : 4 : 5 : 6 : 7 : 8 : LiSt 1 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : LiSt 1 : window: screenWid-colWid-2; if (coI+coIWid + 2 〉 = screenWid) C01 int col = pos. c(); if ((colWid = (w) く smallWL) colWid = smallWL,• (pos pos) : screen し en-lnLen-2; theScreen くく normaICoIor くく (curPos / / カーソルの初期化 theScreen くく ((curPos = pos) . add(-l, の ) くく title; i f ( (t i e = tt l) ! = の / / タイトルを書く for (i ー 2 : i く coIWid; i + + ) theScreen くく theScreen くく curP. OS. newl ine() : theScreen くく borderCoIor くく for ( i ー 0 : i く c 0 1 W i d : i 十十 ) theSc re en くく theScreen くく くく normalColor; for (int j = 0 : j く ln し en; j + + ) { theScreen くく curPos. newl ine ( ) : for (int i ー 2 : i く coIWid; i + + ) theScreen くく theScreen くく borderC010r くく (curPos. set(ln, col)) : / / 枠を描く POS. set()n + 1, COI + l); if (ln+lnLen + 2 > = screen し (n) ln int ln ニ POS. 1 ( ) : if ((lnLen = (l) く smaIIWL) ln し en = smallW し : for (int i ー 2 : i く coIWid; i 十 + ) theScreen くく for (int j ー 2 ; j く lnLen; j + + ) { theScreen くく normalColor くく curPos; (curPos = pos). add(-l, W indow: : window() 1 0 theScreen くく curPos. newline() : = * s 十十 ) wh i 1 e (c int c; window& window: :operator くく (const char* s) 1 1 theScreen くく curPos. newl ine() : List 9 / / 左上位置 pos の設定 :window(scrPos& PO, int cw, int II, char* ttl) / / 標準色 static scrAttr normalCoIor(WHITE, NORMAL) : static scrAttr borderCoIor( い GHT_BLUE, REVERSE) : / / 境界色 POS screen Len curPos title 表題 tT)his IS an object 0 ⅱ■ lnLen 最初はコンストラクタて、ある。 , こて、は ウインドウの枠とタイトルだけを表示する (List9) 。 多義演算子くくが縦横に駆使されている点 に注意されたい 次に , 使用ずみのウインドウを消去する デストラクタを示す (List10)0 実用的なウインドウシステムて、は , コン ストラクタて、オープン前の画面情報をセー プし , デストラクタは消去だけて、なくその 画面情報も再ロードすべきて、あるが , 工ス ケープシーケンスだけて、これを行うのは難 しい。ウインドウへの文字列の出力も , ス クリーンと同様に演算子くくを使用する。 の関数のインプリメントは , ウインドウ幅 などをチェックしていない手抜きて、ある (ListII) 。 ウインドウ内の位置は , 基点 pos からの相 対位置て、表す。次の演算子は , この相対位 置から正しい座標を計算し移動する : window& win dOW : : operator くく (scrPos& po) e ー se theScreen く c; return *this; 1 2 enum mouseState { mouseBad ー 1 , mouse0ff = OFF, 52 scrPos curPos(pos); theScreen くく (curPos 十 = return * this; これとは逆に絶対座標から相対座標への CMAGAZINE 19 1 po) mouseOn ニ 0N } :