演算子 - みる会図書館


検索対象: 月刊 C MAGAZINE 2000年5月号
170件見つかりました。

1. 月刊 C MAGAZINE 2000年5月号

の定義を行えるとはかぎりません。しかし , …旦言をすべて記述しておけば , そのモジ 関数の呼び出しをコンパイルできるわけで ュールを利用する側は , ヘッダファイルを 関数プロトタイプがヘッダファイルの一部 す。 読み込むだけでそのモジュールを正常に使 としてソースファイルの先頭で読み込まれ 一方 , モジュールのソースファイルは , 用できるようになります。そして , モジュ れば , 関数定義の順番に関係なく , 正しく モジュールの利用者に公開したくない内容 ール自体もほかのモジュールを利用してい るでしようから , それらのヘッダファイル 2 次元べクトル演算モジュールヘッダファイル ( vecto 「 2. h ) をインクルードしていることになります。 整理するとモジュールのヘッダファイル の内容は , ( 1 ) ほかのモジュールのヘッダファイル群 ②公開してもよい型・列挙定数・マク ロの定義 ( 3 ) 公開してもよい大域変数・関数の外 部参照宣 ということになります。このように , モジ ュールのヘッダファイルには , モジュール を使うために必要な情報がまとまっている ので , ヘッダファイルのことを " モジュール のインタフェイス (interface) " と呼んだりし ます。 さて , これらのヘッダファイルの要素を よく見ると , ( 1 ) と②はモジュールのソー スファイルでも必要なものです。しかし , いちいちへッダファイルとソースファイル で同じ内容を書くというのもめんどうです し , それらを矛盾のないように常に一致す るように管理するのは困難です。そこで , モジュールヘッダファイルをモジュールソ ースファイルでもインクルードして , その 内容を利用すればよい , ということになり ます。 ( 3 ) の外部参照宣言がモジュールのソー スファイルにインクルードされても問題は ありません。大域変数の外部参照宣言と初 期化付き定義が同じ翻訳単位に存在する場 合 , 外部参照宣言のほうは無視されます。 また , 関数プロトタイプ宣言は , むしろソ ースファイルに読み込んだほうがよいので す。なぜなら , 前述したようにコンパイラ は未定義の関数が出てきた場合に , 関数プ ロトタイプがないと適当に返却値型や引数 の型を予測して関数呼び出しの部分をコン パイルしてしまいます。互いに呼ばれる関 数など , 必ずしも関数呼び出しの前に関数 58 C MAGAZINE 2000 5 2 2 : * * * vec に 0 て 2 . h * * * ー 2 次元べクトル演算モジュールヘッダファイル 3 : 5 : 6 : #if !defined( —VECTOR2D—H— ) 7 : #define -VECTOR2D—H— 8 : / * 2 次元べクトルの型定義 * / 9 : 10 : typedef struct vector2d { 11 : double x, 12 : } vector2d; 13 : 14 : / * 外部参照宣言 * / 15 : extern const vector2d gXUnitVector; / * x ・方向単イ立へ、クトノレ * / 16 : extern const vector2d gYUnitVector,• / * y 方向単イ立べクト丿レ * / 17 : 18 : / * 関数プロトタイプ * / / * 加算 * / 19 : vector2d VAdd ( vector2d a , vector2d b / * 減算 * / 20 : vector2d VSub ( vector2d a , vector2d b 潺 21 : double lnnerproduct( vector2d a, vector2d b ) * 内積 * / / * べクトルの大きさ * / 22 : double Norm( vector2d a 23 : 24 : #endif / * -VECTOR2D-H— * / 2 次元べクトル演算モジュールソースファイル ( vecto 「 2. c ) 3 1 : * * * vector2. c * * * ー 2 次元べクトル演算モジュールソースファイル 3 : 4 : 5 : #include <math. h> 6 : #include "vector2d. h" 7 : 8 : / * 単位べクトル ( 大域の const 定数 ) の定義 * / 9 : const vector2d gXUnitVector = { 1 , 0 10 : const vector2d gYUnitVector = { 0 , 1 11 : 12 : / * 公開する関数の at 土 c なし定義 * / 13 : vector2d VAdd( vector2d a, vector2d b ) / * 加算 * / 14 : { vector2d v; 15 : V. x ま a. x 十 b. x; V ・ y ま a. y 十 b. y; 16 : 17 : return V ー 18 : } 19 : 20 : vector2d vsub( vector2d a, vector2d b ) / * 減算 * / 21 : { 22 : vector2d v ー 23 : Ⅳ . x = a. x b. v. y = a. y ー b ・ y; 24 : return V ー 26 : 27 : double lmerproduct( vector2d a, vector2d b ) / * 内積 * / 28 : { return (). x * b. x) 十 (). y * b. y); 29 : 30 : } 31 : 32 : d0 厄 Norm( vector2d a ) / * べクトルの大きさ * / 33 : { return sqrt( lnnerp て oduct( a, a ) 34 :

2. 月刊 C MAGAZINE 2000年5月号

原因になります。ですから Fig. 3 - ②に示す 複合代入演算子は , 各演算子を@ とする ように , 適切な値で初期化するように したほうがいいでしよう。 変数には , 代入演算子 " = " を用いて , 変数名 = 設定したい値 という代入式を書くことで値を設定できま す。たとえば , 一度宣言された整数変数 i とすれば , 変数 i に整数値 1 を設定すること ができます。この「 i = 1 」のように , 変数な どを演算子で結んだものを式 (expression) と呼びます。変数が値を持つように , 式自 体にも値があります。上の代入式の場合 , 左辺の変数に代入された値 ( つまり 1 ) が式 の値になります。 数値を扱う演算子としては , TabIeI のよ うな演算子があります。四則演算子の + , * , / は加減乗除を計算する演算子です。 % は剰余を求める演算子で演算対象 ( オペラ ンド , operand) は整数です。 整数 1 % 整数 2 とすることで , 整数 1 を整数 2 で割ったとき の余りを計算します。ビット演算子は整数 を表現するビットごとの演算 ( TabIe2 ) を行 います。 & , 匚 ^ はそれぞれふたつの整数 型のオペランドのビットごとの積 , 和 , 排 他的論理和を計算し , ~ は整数型の右オペ ランドのビットごとの否定を求めます。シ フト演算子は左オペランドの整数のビット 表現を右の整数型オペランドで指定された 数だけ左右にずらした値を求めます。くくは 左にずらし , > > は右にずらします。たとえ ば , 8 ビットの整数 a のビットイメージが 00011010 となります。 だとすると , a くく 1 は , 00110100 させます。たとえば , ⅲ t 型変数 i の値を 1 増 格納されている値をひとつだけ増加・減少 ト演算子 " ~ " はそれぞれ , スカラ型変数に インクリメント演算子 " + + " とデクリメン とする代わりに やしたいときは , とを " デクリメント (decrement) する " とい (increment) する " , ーーで値を減少させるこ で値を増加させることを " インクリメント という式の値は ( 3X5 ) で 15 となります。 + + という式の値は ( 4X5 ) で 20 になりますが つまり , i の値が 3 のとき , なります。 加 ( または減少 ) させる前の値 " が式の値に の値になり , 後置形式の場合は " ひとっ増 は " ひとつ増加 ( または減少 ) させた値 " が式 が異なってくることです。前置形式の場合 ( 後置形式 ) こともでき , 両者で式自体の値 の前に置く ( 前置形式 ) ことも後ろに置く いのは , + + 演算子とーー演算子はオペランド と記述できます。気をつけなくてはならな います。 という形をしており , a @= b のように使います。これは , a = a @ b の短縮形です。 式 ; にセミコロン ( ; ) を付けて , 式と式を区分けするために 各式の末尾 と書くことができ , これはひとつの文 ( state ment) という単位となります。 C 言語の処理 は , この文の並びで記述することになりま 12345 びます。数値を直接 , 値を変更できない情報のことを定数と呼 定数 す。 (Fig. 5 ) 。 C 言語の列挙型は内部的には int て列挙型 ( enumeration type) があります ます。また , 整数定数を表す特別な型とし って宣言された定数は " const 定数 " と呼ばれ 数として宣言できます (Fig. 4 ) 。 const を使 すると , その変数を値の変更ができない定 const という予約語で修飾して変数を宣 と書きます。 ひとつ囲んで , 字の定数はシングルクオート (' ) で文字を などと書いたものが定数に当たります。文 Fig. 2 各整数型の最低限表現すべき範囲とメモリ領域の大きさ 40 C MAGAZINE 2000 5 ※ 1 バイト = 8 ビットの場合 ( 2 バイト以上※ ) ( 2 バイト以上※ ) ( 4 バイト以上※ ) sho 「 t の大きさミ int の大きさ坙 long の大きさ (2)sho 「 t, int, long の相対的な大きさ ・ long は -2147483647 ~ 2147483647 , unsigned long は 0 ~ 4294967295 ・ int は -32767 ~ 32767 , unsigned int は 0 ~ 65535 ・ sho 「 t は一 32767 ~ 32767 , unsigned cha 「は 0 ~ 65535 ・ signed char は -127 ~ 127 , unsigned cha 「は 0 ~ 255 ( 1 ) 最低限表現すべき範囲 Fig. 3 変数の宣言の仕方 ( 1 ) 初期化を伴わない場合 く書式 > 変数の型名変数名 : く注意 > ・変数名は , アルファベットと数字 , アンタ、一バーごで定義する。 ただし先頭文字は数字であってはならない ( 2 ) 初期化を伴う場合 く書式 > 変数の型名変数名 = 初期化値 : く例 > int i = 0; charcl charc2 = 1 , double d = 0.1 :

3. 月刊 C MAGAZINE 2000年5月号

0 0 の C 言語クイックマスター ( 初期化を含めた ) 変数宣言を , 型の整数として表現されています。なお , 構造体型と共用体型 struct ComplexNumber { Fig. 5 には示していませんが , 型定義と ( 初 期化を含めた ) 変数宣言を , double 「 e 酬 double image, enum Country { いくつかの情報をまとめてひとつの型と したい場合があります。たとえば , " 個人 " と一緒に行ってもかまいません。なお , 構 America, Japan, UK, USA = 0 造体のサイズは , 各メンバのサイズの合計 を表現するには , 名前 , 年齢 , 性別 , 住 } myCountry; よりも大きくなることがあることに注意し 所 , 電話番号などをまとめて Pers 。 n 型とい というように一緒に行ってもかまいません。 てください。これは , CPU が特定のメモリ う型にできたら便利です。 C 言語では , 複 ところで , Fig. 5- ( 1 ) の例では , " / * " と 位置に変数が配置されているほうが効率よ 数の変数や定数をまとめてひとつの新しい * / " の間に説明が入っていますが , " / * 型にしたものを構造体型 (structure type) と くアクセスできるなどの理由で , メンバの から " * / " まではプログラムには影響を与 間に使用されない領域を挿入して , 各メン 呼びます。構造体型の定義とその変数宣言 えないコメントとして扱われます。わかり バをもっとも効率のよい位置に配置するこ の仕方を Fig. 6 に示します。構造体に収め やすい注釈をコメントとして記述するよう とがあるからです。 にしましよう。なお , " / * " と " * / " を " 入 られたそれぞれの変数はメンバ (member) ときに , 同じ変数のために割り当てられ と呼ばれ , Fig. 6- ( 3 ) のように . 演算子を使 れ子 " にして記述することはできません。つ たメモリ領域を複数の別の目的で使い分け まり , って , たい場合があります。たとえばある変数を , 構造体変数名 . メンバ名 あるときは文字データを格納する変数とし とすることで取り扱うことができます。ま というようには使用できないので注意して て , あるときは実数データを格納する変数 た , Fig. 6 には示していませんが型定義と ください。 Fig. 5 列挙型を使った整数定数 ( 1 リ挙型の定義の仕方 く書式 > enum 列挙タグ名 { 定数名 O = 値 O, 定数名 1 = 値 1 , 定数名 n = 値 n く注意 > = = 値 " は省略可能。その場合は , 定数値は直前の定数値に 1 加え た値となる。いちばん最初の定数で値を省略すると , 0 が指 定されたものとみなされる ・定義する定数は 1 個以上なくてはならない ・ enum 列挙タグ名 " の部分が型名となる く例 > 国別コード enum Country { Ame 「 ica, Japan, USA = 0 Fig. 4 const 定数の定義の仕方 く書式 > const 定数の型名定数名 = 定数値 : く例 > const int i = 0; const cha 「 cl const double d = 0.1 : TabIe 1 数値計算をする演算子の例 種類 四則演算子 十 % ( オペランドは整数 ) 剰余演算子 & , に ^ , ~ ( オペランドは整数 ) ビット演算子 > > , くく ( オペランドは整数 ) シフト演算子 インクリメント演算子 十十 デクリメント演算子 複合代入演算子 演算子 / * 定数値は O となる * / / * 定数値は 1 となる * / / * 定数値は 2 となる * / / * 定数値は 0 となる * / くく = ( 2 ) 列挙型変数の宣 く書式 > enum 列挙タグ名列挙型変数名 , enum 列挙タグ名列挙型変数名 = 定数値 , く注意 > ・列挙型の定数および変数は int 型で表現されている く例 > 前例の enum Count Ⅳ型を使う例 enum Country myCountry = Japan; myCountry = -1 : / * このように列挙型以外の整数値も代入できてしまう ことに注意 * / TabIe 2 A ビット演算 A と B の排他的論理和 A の否定 A と B の積 A と B の和 0 1 1 1 0 0 0 B 0 0 1 1 ・ 0 1 ー 0 1 1 1 0 0 1 0 1 0 1 41 特集 2 C + + プログラマのための C 言語クイックマスター

4. 月刊 C MAGAZINE 2000年5月号

0 0 ・ C 言語クイックマスター 期化を 1 回だけにすることで , この問題を 解決することができます。方法は , 予約語 static を付けて , static 型名局所変数 ; と変数を宣言するというものです。ですか void f( void ) { static int i = 0 ; i + + ; } とすれば , 最初に f が呼び出されたときだけ i が生成・初期化されるので , f を 30 回呼び 出したあとでは , この i は値 30 を持っことに なります。 このように sta ⅱ c を付けて宣言された局所 変数を , 静的局所変数 (static localvariabl e) と呼びます。ここで " 静的 ( sta ⅱ c ) " という 語は , 一般的な局所変数が生成されたり消 去されたりを繰り返すのに対して , " ずっ と存在し続ける " というニュアンスが含ま れています。 さて , プログラムで高度な処理を行うに は , 条件によって処理を分岐させたり , 繰 り返し処理を行うことが必要です。分岐や 繰り返しを行う文が制御文です。制御文を 説明する前に , まず " 条件 " というものを表 現するために真理値と論理演算について解 説しましよう。 真理値と論理演算 真理値とは , 真 (true) または偽 (false) の 値のことで , C 言語では整数で表されてお り , 0 でない整数値を真 , 整数値 0 を偽とし て扱います。 必ず真か偽の値をとる条件を命題 ( p 「 0P0 ならば式全体は真となり . そなり , A が偽のときに式全体 題を表現することが可能になります。 A と 組み合わせることによって , より複雑な命 ~ でない”と 命題を " かっ " , " または " は真理値を返すようになっています。 れていて , これらの演算子で演算した結果 ないかを検査する演算子 ( TabIe3 ) が用意さ て大小関係を比較したり , 等しいか等しく といえます。 C 言語には , スカラ型に対し は , 真または偽の値をとるので命題である は A と B の少なくとも一方が真であれば , 真 A または B うでないときは偽となります。 は A と B が両方とも真のときに真となり , そ A かっ B B を命題とすると , Fig. 20 if 文 く書式 > if ( 制御式 ) 文 1 制御式 偽 文 1 く注意 > ・制御式の値が真の場合だ け文 1 を実行し , 偽の場 合は何も実行しない Fig. 21 if-else 文 < 書式 > if ( 制御式 ) 文 1 else 文 2 となり , A と B が両方とも偽のときのみ偽と なります。 A でない は A が真のときに偽 , A が偽のときに真とな ります。このよラに命題を組み合わせる演 算を論理演算と呼びます。 C 言語には Table 4 のように論理演算を行う演算子が用意さ れており , これによって " ( 整数 i は 0 以上 ) か つ ( 整数 i は 5 以下 ) " という命題は , ( 0 ← i ) & & ( i ← 5 ) と記述できます。 i が 0 以上 5 以下の範囲にあ ればこの式の値は真となり , そうでなけれ ば偽となります。 選択文 制御文の一般形は次のようになっていま す。 予約語 ( 制御式 ) 文 制御式とは条件判断の手がかりになる式 制御式 偽 文 2 文 1 sition ) と呼びます。たとえば , “整数 i は 2 より小さい” TabIe 4 論理演算子 論理演算子 意味 論理和演算子 (" ~ または ~ " ) A と B がともに真のときに式全 A と B の両方もしくは一方が真 A が真のときに式全体は偽と うでなければ偽となる 論理積演算子 A & & B 体は真となり , そうでなけれ ば偽 !A は真となる 論理否定演算子 (" ~ でない ) ※ A と B は算術型と互換性がある型を持つ式 ※式全体の型は int 型で値は真 ( 1 ) か偽 0 となる く注意 > ・制御式の値が真の場合は文 1 を実行し偽の場合は文 2 を 実行する です。制御文は , 選択文・繰り返し文・ジ ャンプ文に分かれています。選択文は制御 式の値によって処理を分岐させる文です。 繰り返し文は文字どおり繰り返し処理を行 う文 , ジャンプ文は処理を強制的に分岐さ せる文です。 選択文から順に説明していくことにしま す。 if 文 (Fig. 20 ) はもっとも基本的な制御 文といえるでしよう。制御式の値が真のと きだけに特定の文を実行させることができ ます。たとえば , " 数値 x が 0 でないときに は a と b を x で割る " という処理は , と書くことができます。 特定の条件が真のときにある処理を実行 し , 偽のときには別の処理を実行するには , 特集 2 C + + プログラマのための C 言語クイックマスター 49

5. 月刊 C MAGAZINE 2000年5月号

解析部 ( List5 ) を順に見ていくことにしま せん。 B が評価されるのは , A が偽である となり , これを実現するコードを生成する す。 expr3() から expr6() まではすでに説明 場合にかぎられます。対して Pascal などで 必要があります。 exprl() では , 仮想マシ は A の値にかかわらず常に B も評価されま ン命令の仕様 (Table 3 ) に基づき , した再帰下降解析の典型といえるもので ( 左オペランドの評価コード ) す。ここではそれ以外の部分について考察 す。 toy では前者 , つまり A の評価結果に応 じて B を評価するかどうかを決定する方式 することにします。 TJMP a=L1 b=TRUE ( 右オペランドの評価コード ) を採用しています。この方式の論理 OR 演 Ⅱ演算子の処理 ( exp 「 1 ( ) ) 算を実現するには , A の評価結果に応じた LABEL a=L1 分岐コードを生成させなければなりませ という中間コードが生成されます。 toy のⅡ演算子は , C 言語と同様に論理 ん。つまり流れとしては , & & 演算子の処理 ( exp 「 2 ( ) ) OR 演算を行う演算子です。ご存じのこと A を評価→ 真であれば , 真をブッシュして LI と思いますが , C 言語をはじめとして多く の言語では論理 OR 演算 , 論理 OR 演算同様 , & & 演算子による論理 に飛ぶ→ A Ⅱ B B を評価 AND 演算についても , toy では C 言語流の において , A が真の場合には B を評価しま 評価法を採用します。つまり , / * * / 演算子の優先順位 * / void expr6 ( int 料 val ue ) int op = 0 ー expr7( lvalue); fo て ( ) { switch ( Lex. type ) { case STAR : OP = —MUL; break; case SLASH : -DIV; break ・ case PERC : op = ー 0 break; default: return ー expr7( lvalue); add-code ( op , 0 , 0 , NULL *lvalue = FALSE; / * 単項演算子の優先順位 * / VOid expr7(int * lvalue) getlex( if( Lex. type = = MINUS expr7 い val ue add—code ( ー INVT , の *lvalue = FALSE; factor い val ue LI : List 5 List 5 void var(void) vardata *vdata ー vdata = (vardata*) Lex. data; if( vdata->vartype & (VARTYPE-ARRAYIVARTYPE—REFER) ) { getlex( if( Lex. type ! = LBRACK ) { ungetlex( add—code ( —PUSH , CONST, 0 , NULL else{ expression( if( Lex. type ! = RBRACK ) e てて 0 て ( 勹がありません″ add—code ( —LOAD , vdata->0ffset , vdata->vartype , NULL void call (void) funcdata * fdata; int n=0 ー ( funcdata* ) Lex. data; fdata getl ex ( if ( Lex. t e ! = LPAR ) 000 , ( 。数呼び出しに ( がありません " if( fdata->narg ) { do { if( fdata → argtype[n] = = ARGTYPE-REFER ) { / * 参照渡し * / int ー va ー ue=TRUE ー exprl(&lvalue); if ( ! ⅳ引 ue ) { error("%s の呼び出し : 第記引数は参照渡しでなければならない” fdata->name, n 十 1 chg—code ( —ADDR else / * 値渡し * / expression( n 十十一 }while( n く fdata->narg & & Lex. type = CO 鳳 getlex( if ( Lex. t e ! = RPAR ) e , , 0 て ( " 数呼び出しが ) で終結していません " if ( fdata->nar ! = n ) e てて。て ( " 関数び出しにおける引数の個数が間違っています " add—code( —CALL, 0 , の fdata); void factor(int *lvalue) int is—lvalue = FALSE ・ = LPAR ⅱ if( Lex. type expression( if( Lex. type ! = RPAR ) error ( ″ ) がありません” else if( Lex. type = = CONST ) add—code ( —PUSH , CONST , Lex. detail . number , NULL ま STRING ) else if( Lex. type add—code ( —PUSH ′ STR I NG , 0 , Lex. detai 匚 string else if( Lex. type = = LOCALVAR ) { var ( is—lvalue = TRUE; else if( Lex ・ type = = FUNC) call(); erro て ( ”式に不正な要素が含まれています” if( ! is-lvalue ) *lvalue = FALSE; getlex( 22 C MAGAZINE 2 側 0 5

6. 月刊 C MAGAZINE 2000年5月号

特集 1 五ロ - 三ロ を となります。 単項演算子の処理 (expr7() ) 単項演算子として , toy では整数の符号 逆転を行う - 演算子を持っています。 - 演算 子に対する中間コード生成は , オペランド の評価コードの後ろに符号逆転のための命 A & & B において , A が偽の場合には B を評価しま という流れを実現してやればよいわけで せん。これを実現するには論理 OR 演算の す。 考え方をそのまま流用できます。つまり , こで生成される実際の中間コードは , A を評価→ ( 左オペランドの評価コード ) 偽であれば , 偽をブッシュし LI に FJMP a=L1 b=TRUE 飛ぶ→ ( 右オペランドの評価コード ) B を評価 LABEL a=L1 TabIe 3 toy の仮想マシン命令および中間コード生成時に用いられる命令 要 仕様 LOAD a = オフセット b=VARTYPE VAR データ fp[a] をブッシュする。ここで fp はフレームの先頭アドレス データ ( fp + a ) [ pop ( 1 ) ] をブッシュする。ただし , pop ( i ) はスタックから i 回ポップした際のデータを意味 _LOAD a = オフセット b=VARTYPE ARRAY する LOAD a = オフセット b=VARTYPE REFER pt 「 =dpCa] をアドレスとして , データ pt 「 [ pop ( i) ] をブッシュする ADDR a = オフセット b=VARTYPE VAR アドレス fp + a をブッシュする ADDR a = オフセット b=VARTYPE ARRAY アドレス fp + a + pop ( 1 ) をブッシュする ADDR a = オフセット b=VARTYPE REFE 日 ptr=dpCa] をアドレスとして , アドレス pt 「 + pop ( i ) をブッシュする p=p0P(2) に対するアサイン。 * p=pop(l) ASGN アドレス PUSH a=CONST b = 整数 整数データとして b をブッシュ PUSH a=STRING p = 文字列の先頭アドレス 文字列データとして p をブッシュ POP スタックポインタ sp をデクリメント。 _JMP p = 命令アドレス p への無条件ジャンプ b=FALSE p = 命令アドレス _TJMP pop(l ) が真であれば p へ飛ぶ b=TRUE p = 命令アドレス pop(l ) が真であれば真値をブッシュし p へ飛ぶ _TJMP b=FALSE p = 命令アドレス pop(l ) が偽であれば p へ飛ぶ _FJ M P b=TRUE p = 命令アドレス pop(l ) が偽であれば真値をブッシュし p へ飛ぶ _FJ M P _CMP 論理演算 pop ( 1 ) = = pop ( 2 ) の結果をブッシュ a=EQEQ _CMP a=NOTEQ 論理演算 pop ( 1 ) ! = p 。 p ( 2 ) の結果をブッシュ CMP 論理演算 pop ( 2 ) く pop ( 1 ) の結果をブッシュ a=LT _CMP 論理演算 pop ( 2 ) く = pop ( 1 ) の結果をブッシュ a=LE _CMP a=GT 論理演算 pop ( 2 ) > pop ( 1 ) の結果をブッシュ CMP a=GE 論理演算 pop ( 2 ) > = pop ( 1 ) の結果をブッシュ ADD pop ( 1 ) + pop ( 2 ) の結果をブッシュ _SUB pop ( 2 ) ー pop ( 1 ) の結果をブッシュ popl ( 1 ) * pop ( 2 ) の結果をブッシュ MUL pop ( 2 ) / pop ( 1 ) の結果をブッシュ DIV pop ( 2 ) % pop ( 1 ) の結果をブッシュ _MOD - pop ( 1 ) の結果をブッシュ INVT pop(a),.. , pop ( 1 ) を順に stdout に出力する PRNT a = テータ数 p = 関数構造体のアドレス p が指す関数を呼び出す _CALL RET 現在の関数をリターンする ラベ丿レ値 a で識別されるラベルを表す。中間コードでのみ用いられる命令およびバラメータ LABEL a = ラベル値 これらのジャンプ命令は , 中間コード生成時に実際の飛び先アドレスが確定していないため , 飛び先とし _JMP,_TJMP,_FJMP てバラメータ a にラベル値が入る。中間コードでのみ用いられる命令およびパラメータ 特集 1 スクリプト言語を作ろう 23

7. 月刊 C MAGAZINE 2000年5月号

特集 1 五ロ - 三ロ を 果となることを確認できるはずです。つま と思われる再帰下降解析 (recursive descen ように構文木から後置記法への変換ルール り , スタック式仮想マシンを想定した場合 tparsing ) と呼ばれる方法に基づいていま はきわめて簡単であり , 構文木を生成する 後置記法から直接に式の評価コードを生成 す。 アルゴリズムを多少変形すれば字句を読み できるわけです。 この手法では , 演算子のプライオリティ ながら直接に後置記法へと変換するアルゴ toy の式解析部の実装では構文木を作ら に応じて木の生成ルーチンを個別に用意し リズムを得ることもできます (List 4 ) 。 ず , 字句を読みながら一気に中間コードを ます。それぞれのルーチンでは , そのルー 以上で , 後置記法への変換が構文木を生 生成するという手法をとっていますが , そ チンの担当とする演算子のオペランドとな 成せずとも行えるということを理解してい の手法を解説する前に , 素直に構文木を生 る部分木を次に高いプライオリティを担当 ただけたと思います。そして , 後置記法か 成するアルゴリズムを見ておきましよう するルーチンに生成させます。この方法を らは直接に式の評価コードを生成すること (List 3 ) 。これは 4 種類の二項演算子 ( + , とれば , 各ルーチンは自身の処理すべき問 ができるため , 後置記法を生成するアルゴ * , / ) のみで構成される式に対して構文木 題のみを扱えばよいことになり , たいへん リズムを少々変形させるだけで , 字句を読 を生成するきわめて簡略化されたプログラ シンプルな構造となるのです。 みながら直接に中間コードを生成するアル ムですが , 字句を読みながら構文木を生成 ゴリズムを導出できるのです。 このように , 構文木を生成するアルゴリ ズムは単純なものです。そして , 前述した するアルゴリズムとしてはもっとも一般的 では , 式の構文規則 ( Fig. 7 ) に基づく式 式の構文規則に基づく式解析部 List 5 / * = = ! = 演算子の優先順位 * / void expr3(int *lvalue) int optype ー expr4(IvaIue); for(; optype = Lex. type ・ switch( Lex. type ⅱ case EQEQ : case NOTEQ: expr4(IvaIue); add—code(—CMP, optype, 0 , NULL); *lvalue = FALSE; break ー defau 比 : return ・ expr. c: 式解析系 #include ” toy. h ” static void exprO (int* ) ,exprl(int* ) expr2(int*) expr3(int*) , expr4(int* ) expr5(int* 5, expr6(int*5,expr7(int*) , factor(int*5, var( ) , call ( void expression(void) 土 n に lvalue=TRUE; exprO(&IvaIue); / * 代入演算子 * / void exprO(int *lvalue) exprl( lvalue); if( Lex. type = = EQUAL ) { if ( ! * ⅳ引 ue ) er て。て ( ”非左辺値への代入” ); chg—code ( —ADDR expression( add—code( —ASGN, 0 , 0 , NULL); *lvalue = FALSE; / * Ⅱ演算子 * / void exp て 1(土nセ * lvalue) 土 n セ LI ー expr2( lvalue); while( Lex. type = = OROR LI = ね b 引 -next( add—code( —TJMP , LI, TRUE, NULL); expr2(lvaIue); 0 , NULL); add—code ( —LABEL , LI , *lvalue = FALSE; / * & & 演算子の優先順位 * / void expr2(int *lvalue) 土 n セ LI ー expr3(IvaIue); while( Lex ・ tYEE = = ANDAND ) ( LI = ー abel —next( add—code( —FJMP , LI, TRUE, NULL); expr3(IvaIue); add—code( -LABEL , も 1 , 0 , NULL); *lvalue = FALSE; / * くく = > > = 演算子の優先順位 * / void expr4(int *lvalue) int optype ー expr5(IvaIue); optype = Lex. type ・ switch( Lex. type ⅱ case LT: case LE: case GT: case GE: expr5(IvaIue); add—code(—CMP, optype, 0 , NULL); *lvalue = FALSE; break; defau 比 : return ー / * + - 演算子の優先順位 * / void expr5(int *lvalue) int op = 0 ー expr6(IvaIue); switch ( Lex 北 e ) { case PLUS : break; op = —ADD; case MINUS : -SUB; break; default: return ー expr6( lvalue); add-code(op, 0 , 0 , NULL); *lvalue = FALSE; 特集 1 スクリプト言語を作ろう 21

8. 月刊 C MAGAZINE 2000年5月号

00 ◇ 五ロ - 三ロ を スコープを最優先した検索を実現できるわ ません。字句定義がシンプルなものであれ オリティが設定されているため , * は + に けです。そして , プロックスコープを抜け ばあるほど , 解析も容易に行えます。つま 優先して結合します。したがって , A + B * た時点で先頭のハッシュ表を削除してやれ り , うまく設計された字句定義はそれ自体 C は A + ( B * C ) と解釈されなければなりま ば , 直前の状態へと戻すことができます。 が字句抽出のアルゴリズムのようなものな せん。式解析部のおもな役割は , 与えられ イメージとしては Fig. 5 のようになるでし ので , 解析部はその定義を単純になぞるだ た式中のオペランド ( 被演算子 ) を , 演算子 ようか。 けで済むということです。ただし , 字句定 のプライオリティに従って正しく結合させ 義が複雑な場合 ( たとえばかなりの先読み ることです。 が必要とされる場合 ) には字句解析部も複 オペランドと演算子の具体的な結合イメ 雑な構造にならざるをえないので , 新しい ージをとらえるため , 式の構造を表す構文 言語を設計する場合にはなるべく単純な字 木と呼ばれる木表現を導入しましよう。な 字句解析そのものは難しい作業ではあり 句定義にしておくことをお薦めします。 こでは簡単にするために二項演算の お , みを考えることにします。構文木のノード は演算子か変数のいずれかで , 演算子の場 合にはオペランドを意味する二本の子ノー ドを持ちます。以上の規則に従って式 A + B * C と A * B + C / D を構文木として表現する と , Fig. 6 のようになります。いったんこ のような形式に変換してしまえば , この木 から同一の式に対する異なる表現を得るこ 字句解析の次の段階として , 抽出された を例にとると , 何らかのアクションは常に とが可能となります。 字句の意味を解釈しながら式や文の構造を 式によって引き起こされます。関数の呼び A * B + C / D の構文木を例にとると , まず , 解析する , 構文解析を行う必要があります。 出しは式であり , 条件分岐やループなどの 左の子→右の子→親 いったん字句抽出を完全に済ませておき , 実行条件を与えているのも式です。よって , の順にノードを置く作業をルートから出発 得られた字句の列を読みながら構文解析を このような言語では , 式の評価順序を制御 して再帰的に行うと , 行うという方法もありますが , toy では構 することで目的とする動作を実現していま A B * C D / + 文解析部が逐次字句解析部を呼び出し , 必 す。式 = ( 状態変化をもたらすもの ) , 文 = のように後置記法 ( 逆ポーランド記法 ) の式 要に応じて字句を得るという方式をとるこ ( 式の評価順序を制御するもの ) , という図 表現が得られます。また , とにします。 式は toy にかぎらず多くの言語に共通する 親→左の子→右の子 構文解析部のおもな役目は , 与えられた ものです。 の順にやはり再帰的に置いていけば , 字句の一連の集合からそれらを言語の文法 まずは A + B * C という式を考えてみまし + * A B / C D に照らし合わせ , 何らかの規則に従った形 よう。たいていの言語では演算子にプライ となり , 前置記法と呼ばれる式表現を得る 式へと変換することです。一部のインタブ Fig. 6 リタでは , 構文解析部と実行部が直接結び 式 A + B *C と A* B + C / D を構文木として表現する 付いているものもありますが , toy では構 文解析部が字句列を解析しながら , 出力と してアセンプリ語コードに相当する中間コ ード列を生成します。 実装のポイント 構文解析 十 十 式解析 C 言語にかぎらず , 大半の言語を構成す る基本要素として式と文があります。 toy 特集 1 スクリプト言語を作ろう 1 9

9. 月刊 C MAGAZINE 2000年5月号

パソコンにかぎらず , どのような種類の コンピュータでも , ・入力装置 キーポード , マウス →ロ ・記憶装置 ハードディスク メモリ , ・制御装置 CPU ・演算装置 CPU 制御装置 ・出力装置 ディスプレイ , プリンタ などの五つの装置 ( ハードウェア ) から構成 されています。これらを「コンピュータの 5 大要素」と呼びます ( Fig. 1)0 入力装置から入力されたデータを演算装 置で演算し , その結果を出力装置に出力し 制御の流れ →データの流れ ます。そのとき使われるデータやプログラ 己憶装置 ムは , 記憶装置に格納されます。これらコ ンピュータ全体の動作タイミングを制御装 置で制御します。 とデータの入出力を行うことと , CPU が内 のプログラムの動作には , データの入力 , ワープロやゲームなど , どんなに複雑に 部でデータの演算を行うことだけなのです 演算 , および出力の三つのことしかないこ 見えるプログラムでも , その基本動作は驚 (Fig. 2 ) 。 とを覚えておいてください。小さなモジュ くほど単純なものです。 CPU がメモリや I/O ハードウェア的に見れば , コンピュータ ールであっても大きなシステム全体であっ ても , プログラムの動作をデータの入力 , 演算 , および出力の三つに分けて考えるこ 主記憶装置と補助記憶装置 とが , ハードウェアを意識してプログラミ ングをする第一歩となります。 記憶装置は , さらに主記憶装置 ( メモリ ) が百 ~ 数十 M バイトなのに対し , ディスク と補助記憶装置 ( ハードディスクなどのデ 装置は数 G バイトもあります。逆に , デー ハードウェアから見た イスク装置 ) のふたつに分類されます。主 タを読み書きする速さは , メモリの数十ナ プログラム実行の仕組み 記憶装置は電気的に記憶するので , データ ノ秒 ( 10 億分の 1 秒 ) に対し , ディスク装置 の読み書きが高速にできますが , 記憶容量 は数十ミリ秒 ( 1000 分の 1 秒 ) の単位です。 パソコンのハードウェアの概要がわかっ が少なく価格も高くなります。補助記憶装 実際にはこれらを使い分けて , 必要なプ たところで , プログラムとハードウェアの 置は磁気的な記憶を行うので , 低速であっ ログラムやデータだけを必要なときにディ 関係を見てみましよう。みなさんが作成し ても記憶容量が大きく安価という特徴があ スク装置からメモリに読み込み , 高速処理 たプログラムは , CPU に与える命令となり ります。多くのパソコンでは , メモリ容量 するような仕組みになっています。 ます。 CPU は , 命令に従ってハードウェア Fig. 1 コンピュータの 5 大要素の関係 出力装置 演算装置 入力装置 ′ 1 1 単 1 . 亠」 = = ロ コラム 1 Fig. 2 ハードウェア的に見たプログラムの動作 入力装置 出力装置 CPU データの入力 チータの出力 データの演算 ァータの入力 データの出力 メモリ 84 C MAGAZINE 20 側 5

10. 月刊 C MAGAZINE 2000年5月号

明した 式 ; という文です。関数の実行部では , 特定の 文に目印を付けるために文の先頭に ラベル名 : 文 は三つの文をひとつにまとめた複合文です。 { a; a + b; 1 * c; } す。たとえば , た文を複合文またはプロックなどと呼びま う方法があります。Ⅱによってまとめられ する方法としてⅡで複数の文をくくるとい た , いくつかの文をまとめてひとつの文に のような文をラベル付き文と呼びます。ま というラベルを付けることができます。 TabIe 3 関係・等号・不等号演算子 と書くことができます。 なお , 仮引数をとらない関数は仮引数リ ストに void と記述します ( Fig. 17 ) 。また 値を返さない関数は返却値の型を void に指 定します (Fig. 18 ) 。その場合 , return 文に は返却値を指定せずに単独で , return, と記述することになります。 自作の関数の返却値型は基本的には自由 に定義していいのですが , main 関数だけは 事情が異なります。 main 関数は i 猷型を返す ように定められており , その返却値に関し ても , 0 は“プログラムが正常に終了した " ことを表す特別な値として定義されている のです。 処理の記述 関数の実行部には文を並べて処理を記述 します。文の種類を Fig. 19 に示します。 式文は「変数宣言と演算子・式」の項で説 失敗します。しかし , 局所変数の生成と初 int a = 0 ; a が b より大きければ真 , a と b が等しければ真 , Fig. 19 文の種類 文 式文 ラベル付き文 複合文 制御文 プロックの最初の部分は ( 関数定義の本体 と同様に ) 宣言部になっていて , 局所変数 を定義したり型を定義することが可能です。 たとえば , のような場合 , double 型の a は外側の int 型 別子を隠します (name hiding) 。つまり次 子は , その外側で導入された同じ綴りの識 と呼びます。プロック内で導入された識別 れた識別子のスコープをプロックスコープ ( scope ) といいます。プロック内で導入さ こういった識別子の有効範囲をスコープ いのです 別子は , そのプロックの外側からは見えな り , プロックのなかで新たに導入された識 ロックの外側からは参照できません。つま 定義された型 , またラベルなどは , そのプ プロックのなかで宣言された局所変数や あと , その値を 1 だけ増加させています。 では , ⅲ t 型整数 a を 1 に初期化して宣言した { int a = 1 ; a + + ; } 関係演算子 の a を " 隠し " ます。 if 文 if-else 文 switch 文 while 文 do 文 fo 「文 return 文 b 「 eak 文 continue 文 goto 文 a 十十・ 十十 a ; double a = 1 .0 ; ジャンプ文 繰り返し文 選択文 れ 0 に初期化されるので , このもくろみは としても , i は関数が呼ばれるごとに生成さ void f( void ) { int i = 0 ; i + + ; } 変数 i に格納しようとして , す。ですから , 関数が呼ばれた回数を整数 終了したら , そのメモリ領域は削除されま 化されます。そしてプロック部分の実行が リ上に領域が確保され , 必要に応じて初期 は , プロック部分が実行される直前にメモ プロックの先頭で宣言された局所変数 れている a は int 型の a ということになります ふたっとも double 型の a で , デクリメントさ ですから , インクリメントされている a は 演算子 例 意味 a く b a く = b a が b より小さいか等し a > = b a が b より大きいか等し a > b a が b より小さければ真 いなら真 , そうでなけ そうでなければ偽 れば偽 いなら真 , そうでなけ そうでなければ偽 れば偽 等号演算子 そうでなければ偽 不等号演算子 真 , そうでなけれは偽 a と b が等しくなければ ※ a と b は算術型と互換性がある型を持つ式※式全体の型は int 型で値は真 ( 1 ) か偽 ( 0 ) となる 48 C MAGAZINE 2000 5