プログラム言語 - math.nagoya-u.ac.jpnaito/lecture/2001_ss/note_2.pdf · sin sqr sqrt succ...

201
II プログラム言語

Upload: tranque

Post on 23-Jul-2018

213 views

Category:

Documents


0 download

TRANSCRIPT

第II部

プログラム言語

数理解析・計算機数学特論 111

第5章 PASCAL 入門

5.1 ここでの目的

ここでは Pascal の文法を中心に解説をするが, 単に Pascal を用いてプログラムが書けるようになることが目的なのではなく, Pascal の言語構造を理解し, アルゴリズムを正確に記述できるようにすることが重要である.またプログラムは, 自分自身だけが読むものではなく他人にも読めるように, わかりやすく簡潔に書くべ

きである. これらのことを念頭において学ぶことを期待したい.

5.2 Pascal とは

プログラム言語 Pascal は1968年に N. Wirth によって開発された言語であり, 2つの主要な目標にそって設計されている. 一つはプログラミングの教育に適した言語にすること. もう一つは, 信頼性が高く, 効率のよいものを実現することである. 教育用に設計された言語という意味は, プログラムの機能や構成要素を論理的に説明するための構成を持っていることであり, 可搬性や書きやすさという要素を削ってでも, プログラムの機能の理解が容易になるような言語構造を持っている.ここでは, Sun Microsystems の SPRACompiler と呼ばれる処理系での拡張を加えながら, 1983年

に承認された ISO による標準 Pascal を元に, 解説を進める. この章の内容の元になったものは, Jensen とWirth による [1] と ISO の規格書に解説を加えた [2] である.

5.3 Pascal をはじめる前に

Pascal によるプログラミングをはじめる前に, 後の混乱を避けるための注意をする. 以下の注意書きは,無用なトラブルを回避するためできる限り守った方がよい.

• 各ファイルには, その中身がわかるような簡潔なファイル名をつけること.• 各プログラム毎にサブディレクトリを作成することが望ましい.• test.p など, 既存のプログラム名に .p を付加したファイル名は避けること.• 不要になったファイルは削除すること.

また, プログラムソースを見やすくするために, 「字下げ」をきちんとすること.

5.4 Pascal のプログラムの書き方・実行の方法

実行コードを作成するには以下の手順で行なう.

1. エディタ (Mule など)を利用して, プログラムソースを書く.2. コンパイラを起動して, 実行コードを作成する.3. 作成した実行コードを実行する.

Id: P1.tex,v 1.5 2001-03-11 17:21:41+09 naito Exp

112 数理解析・計算機数学特論

例えば, Mule を利用して, hello.p というプログラムソースを作成した時, これをコンパイルするには, 以下のようにする.

% pc hello.p -o hello

最後の -o hello という部分は, 実行コードを hello という名前で作成することを指示している. もし, -ohello という部分を省くと, コンパイラは a.out という名前で実行コードを作成する.ここで作成した hello を実行するには

% ./hello

とする. ここで, ./ をわざわざ指定していることに注意せよ1 .

Exercise 5.4.1 test.p というプログラムを

% pc test.p -o test

として, 作成して,

% test

とすると, どのようなことが起こるか. それは何故か?

5.4.1 Pascal の処理系について

講義で利用する Pascal の処理系は SPARCompiler と呼ばれる SUN MicroSystems が作成したものである. この処理系は, ANSI の規格以上の機能を持っているので, ここで作成したプログラムが他の処理系の上で実行できないこともあり得る.この講義では, Pascal の標準仕様内のものと, SPARCompiler 独自のものを区別して利用する. 本来, 標

準仕様の内部で利用することが望ましいが, 種々の理由により, 独自拡張の部分も利用する.

5.5 Pascal の基本的な注意

5.5.1 Pascal Program の基本的な構造

Pascal のプログラムは, コメント(注釈), 頭書き, ブロックから構成されている. 頭書きは, プログラムに名前を与え, プログラムのパラメタを並べて書く. ブロックは, 次の6つのセクションにわかれ, 最後のセクション以外は空でも良いが, セクションは必ず次の順序で書かなければならない.

1. 名札宣言2. 定数定義3. 型定義4. 変数定義5. 手続きと関数の宣言6. 文

それぞれの文は ; で終るか, begin と end で囲まれた文の集まりである複合文で構成されている. ここで, C との違いは, ; は文の分離記号であり, 文の終端記号ではない.

1 UNIX ではカレントディレクトリはデフォールトではコマンドサーチパスには入っていないし, 入れない方が望ましい.

Id: P1.tex,v 1.5 2001-03-11 17:21:41+09 naito Exp

数理解析・計算機数学特論 113

5.5.2 Bachus-Naur formula

Pascal の場合, その構文は Bachus-Naur formula (BNF) と呼ばれるメタ言語によって記述できる. BNFにおいては,

::= | { }

は BNF による形式化のメタ記号に属し, Pascal の記号ではない. {} はその記号の中の0回以上の繰り返しを表す.上に述べたプログラムの構造を BNF を利用して記述してみると

〈プログラム 〉 ::= 〈プログラムの頭書き 〉 〈 ブロック 〉 .

〈プログラムの頭書き 〉 ::= program 〈名前 〉 ( 〈ファイル名 〉 { , 〈ファイル名 〉 } ) ;

〈ファイル名 〉 ::= 〈名前 〉〈名前 〉 ::= 〈英字 〉 { 〈英数字 〉 }〈英数字 〉 ::= 〈英字 〉 | 〈数字 〉〈ブロック 〉 ::=〈名札宣言部 〉 〈定数定義部 〉 〈型宣言部 〉 〈変数宣言部 〉 〈手続きと関数の宣言部 〉 〈文の部 〉

のようになる.

5.5.3 Pascal で利用できる文字

Pascal では, 英文字, 数字, 空白文字(スペース, タブなど), 記号文字, 改行文字などが利用できる. 記号文字には特別な意味があることが多いので, 注意すること. また 標準の Pascal では, 大文字と小文字は区別されない2 . また, Pascal のコンパイラにおいて, 日本語が利用できるといっても, 変数名などに日本語が利用できるわけではないので注意すること.また, 以下のものは全て空白と見なして無視される.

• 空白文字, 改行文字, タブ, 改ページ記号, コメント.

5.5.4 コメント

コメントとは, プログラミングの補助となるようソースプログラム中に書かれた注釈部分のこと. コンパイラは単にこれを無視するので, プログラムには影響を与えない. コメントは, { ではじまり, } で終る. また, { } が利用できないシステムの場合には, (* と *) の対を利用することができる. コメントは入れ子にはできない3 . 即ち, コメントの中にコメントを入れることはできない.

5.5.5 名前

名前とは, 定数, 型, 変数, 手続き, 関数を表すための名称である. 標準的な Pascal においては, 名前の長さには制限はない. また, 名前はどの予約語とも異なった綴りでなくてはならない.

2 今回利用する SPARCompiler では, default では大文字と小文字を区別しているので注意すること. しかしながら, 標準の範囲内で全てを小文字で記述するという考え方をしたほうがよい.

3 SPARCompiler においては, /* , */ の対, " で囲まれた部分もコメントとみなされる. その際, 異なった記号によるコメントは入れ子にできる.

Id: P1.tex,v 1.5 2001-03-11 17:21:41+09 naito Exp

114 数理解析・計算機数学特論

5.5.5.1 特殊記号

特殊記号とは, 英数字以外のうち, Pascal の文法上特殊な意味を持つものである. 標準 Pascal の特殊記号は以下のものである.

〈特殊記号 〉 ::=+| - | * | / | = | < | > | [ | ] | . | , | := | ; | ; | ( | ) | <> | <= | >= | .. |^| 〈予約語 〉

また SPARCompiler においては, 次の特殊記号が定義されている.

~ & | ! # %

したがって, SPRACompiler での特殊記号の定義は次のようになる.

〈特殊記号 〉 ::=+| - | * | / | = | < | > | [ | ] | . | , | := | ; | ; | ( | ) | <> | <= | >= | .. |^|~|&| | | ! |#|%| 〈予約語 〉

5.5.5.2 予約語

以下の名前は予約語と呼ばれ, 特別な意味を持ち, 他の名前として利用できない.

and array begin case const div do downto elsefile for forward function goto if in label main mod nil notof or packed procedure program record repeat set then totype until var while with

また, SPARCompiler においては, 次の名前も予約語となっている.

define extern external module otherwiseprivate public static univ

Pascal においては, 名前の一部に $, _ を使うことできるが, これらの文字を名前の先頭におくことは,system が既に利用している名前と衝突することが多いので, できる限り避けることが望ましい.以下の名前は標準 Pascal において既に使われている名前である.

abs arctan boolean char chr cos dispose eof eoln expfalse get input integer ln maxint new odd ord outputpage pred put read readln real reset rewrite roundsin sqr sqrt succ text true trunc write writeln

また, SPARCompiler においては, 次の名前が既に使われている

FALSE TRUE addr alfa argc argv arshft asl asrassert bell card clock close date discard double exitexpo firstof flush getenv getfile halt in_range index integer16integer32 intset land lastof length linelimit lnot longreal lorlshft lsl lsr max maxchar message min minchar minintnext null open pack random remove return rshft seedshortreal single sizeof stlimit string substr sysclock tab timetrim univ_ptr unpack varying wallclock xor

5.5.5.3 数

Pascal における数は整数と実数4 のデータ型からなる. それぞれは, 次のように定義される.

4 正確には, 「整数と呼ばれるデータ型と実数と呼ばれるデータ型」.

Id: P1.tex,v 1.5 2001-03-11 17:21:41+09 naito Exp

数理解析・計算機数学特論 115

〈数字列 〉 ::= 〈数字 〉 { 〈数字 〉 }〈符号なし整数 〉 ::= 〈数字列 〉〈符号なし実数 〉 ::= 〈符号なし整数 〉 . 〈数字列 〉| 〈符号なし整数 〉 . 〈数字列 〉 E 〈桁移動子 〉 〈符号なし整数 〉 E 〈桁移動子 〉〈符号なし数 〉 ::= 〈符号なし整数 〉 | 〈符号なし実数 〉〈桁移動子 〉 ::= 〈符号なし整数 〉 | 〈符号 〉 〈符号なし整数 〉〈符号 〉 ::= +| -

これらの例としては,

1 100 0.1 5E-3 87.35E+8

などがあり, 87.35E+8 とは, 87.35 を 10(+8) 乗したもののことである.また, SPARCompiler においては, 2 から 36 進数の表現をすることができる. それは, 次のように表現

される

〈拡張された数字列 〉 ::= 〈英数字 〉 { 〈英数字 〉 }〈BASE〉 ::= 〈英数字 〉〈符号なし BASE進数 〉 ::= 〈BASE〉 # 〈拡張された数字列 〉〈符号なし数 〉 ::= 〈符号なし整数 〉 | 〈符号なし BASE進数 〉 | 〈符号なし実数 〉

ここで, 11 から 36 を表すには A から Z を用いる.また符号なし整数は [0, maxint] の範囲内になければならない.

5.5.5.4 文字列

文字の列を ’ で囲んだものを文字列と呼ぶ. 2個以上の文字列要素を含んだ文字列は, 同じ個数の成分を持った文字列型の値を表したものである.

〈文字列 〉 ::= ’ 〈文字 〉 〈文字 〉 ’

また, ’ 自身を文字列に入れるには ’’ とする.

5.5.5.5 定数の定義

定数を定義することは, 定数と同義な名前を定義することである.

〈定数名 〉 ::= 〈名前 〉〈定数 〉 ::= 〈符号なし数 〉 | 〈符号 〉 〈符号なし数 〉 | 〈定数名 〉 | 〈符号 〉 〈定数名 〉 | 〈文字列 〉〈定数定義 〉 ::= 〈名前 〉 = 〈定数 〉

5.5.6 式

Pascal における式とは, 計算規則を表す構成要素である. 式は, その変数が使用時に不定である場合を除いて, ある値を表す. それを式の値と呼ぶ. 式は演算子と被演算子(変数, 定数, 関数)からなる. また, 演算子には優先順位が付けられている.

Id: P1.tex,v 1.5 2001-03-11 17:21:41+09 naito Exp

116 数理解析・計算機数学特論

Example 5.5.1 次のようなものは式である.

x15sin(x+y)x*yx+y-xx = 1.5

式の構文, 演算子, 優先順位とは後ほど述べることにする.

5.5.7 文

文とは, Pascal プログラムの基本的な単位になるもので, アルゴリズム上の動作単位を表す. また, 記号; は文の区切りを表している.

Example 5.5.2 次は文である.

x := y + z

また, 何も書かれていない文(空文)も存在する.

また, begin と end によって文の集まりを一つの文(複合文)にすることができる.

Example 5.5.3 次は一つの複合文である.

begina := b ;c := d ;

end

5.6 最も簡単なプログラム

はじめに最も簡単と思われるプログラムを書いてみよう.プログラムを実行すると画面に何かを表示するものである.

Example 5.6.1 もっとも簡単なプログラムの例

{Program 1Hello World. を出力する.

}program Hello_World (output) ;

beginwriteln(’Hello World.’)

end.

以下では, このプログラムの内容を説明する. (ただし, コメント, 空白行は行数として数えない.)

1行目program Hello_World (output) ;

Id: P2.tex,v 1.3 2001-03-11 17:24:24+09 naito Exp

数理解析・計算機数学特論 117

この行がプログラムの頭書きである. プログラムの名前として, Hello_World とした. また, このプログラム中で利用されるパラメタ(関数)として, この場合出力しか行なわないので, output とだけ指定してある. もし, 入力も行なう時には, input も指定しなくてはならない.

2行目と4行目beginend.

で囲まれた部分が「文」となっている. Pascal においては, 「文の部」の最後の end の後には . をつける

ことに注意しよう. これは, . で文が全て終了することを表している.

3行目writeln(’Hello World.’)

ここで利用された writeln という手続きは, その引数として, 文字列をとり, その文字列を標準出力に出力する5 . その際, 出力した文字列の最後に改行文字を出力する. ここで, この文には ; をつける必要がない

ことに注意. begin と end は文をまとめて複合文にするのであるから, わざわざ文の区切り記号である ;

をつける必要はない. もし, つけたとすると, その後に空文があると理解できる.Cで記述されたプログラムなどは,システムに対して戻り値を与えることができる. しかしながら, Pascal

の言語仕様にはそのようなものは存在しない.

Exercise 5.6.1 Program 1 を真似て, 次のような出力を得るプログラムを書け.

各自の学籍番号 (改行)各自の名前 (改行)何でも好きなこと (改行)

5.7 プログラムの宣言部

Section 5.5.1で, Pascalのプログラムは「頭書き」と「ブロック」からなることを述べた. ここでは,「ブロック」の宣言部がどのようになっているかを見てみよう. BNF による「プログラム」の定義は, 以下のようになっていた.

〈プログラム 〉 ::= 〈プログラムの頭書き 〉 〈 ブロック 〉 .

〈プログラムの頭書き 〉 ::= program 〈名前 〉 ( 〈プログラム引数 〉 { , 〈プログラム引数 〉 } ) ;

〈プログラム引数 〉 ::= 〈名前 〉〈名前 〉 ::= 〈英字 〉 { 〈英数字 〉 }〈英数字 〉 ::= 〈英字 〉 | 〈数字 〉〈ブロック 〉 ::=〈名札宣言部 〉 〈定数定義部 〉 〈型宣言部 〉 〈変数宣言部 〉 〈手続きと関数の宣言部 〉 〈文の部 〉

以下では, 頭書き, 名札宣言部, 定数定義部, 型宣言部, 変数宣言部の文法を述べておく.

5 ここの解説は本当は正しくない. この手続きは「テキスト」ファイルという型の変数に対して動作が定義されているもので, より一般的な記述法がある.

Id: P3.tex,v 1.5 2001-03-11 17:24:47+09 naito Exp

118 数理解析・計算機数学特論

5.7.1 頭書き

頭書きにおける「プログラム名」はプログラム中においては何の意味も持たない. 「プログラム引数」には, Pascal 内部で定義されたもの以外のもの(例えば関数, ファイル名)とプログラムとの結び付きを要求するものの名称を書いておく. それぞれが, どんな型もののであるかは, 標準名称 input と output を除い

て, プログラムブロックの変数宣言部に記述しておかなくてはならない.手続き readln, writeln 等を標準入力・標準出力に利用する時には, それぞれ input, output を記述し

ておかなくてはならない.

5.7.2 名札宣言部

プログラム中には後から参照ができるように, 名札を書くことができる. プログラム中で利用する名札は, 使用する前に名札宣言部において宣言6 をしておかなくてはならない.しかしながら, Pascal においては名札は goto 文の目印として利用されるだけであり, Pascal の構造上,

ほとんど goto 文は利用しないので, 事実上名札は利用しない.名札宣言部は以下のような構文である.

〈名札定義部 〉 ::= 〈空 〉 | label 〈名札 〉 { , 〈名札 〉 } ;

〈名札 〉 ::= 〈数字列 〉

と定義され, 数字列の値は [0, 9999] の範囲内になければならない. さらに, 名札の数字列は, 数値として同じものであれば, 同じ名札とみなす. すなわち, 7 と 007 は同じ名札を表している.

5.7.3 定数定義部

定数定義部の構文は以下の通りである.

〈定数定義部 〉 ::= 〈空 〉 | const 〈名前 〉 = 〈定数 〉 ; { 〈名前 〉 = 〈定数 〉 ; }

ここで定義した名前(定数名)はその定数と同義語になる. また, 定数とは次のように定義されている.

〈定数 〉 ::= 〈符号なし数 〉 | 〈符号 〉 〈符号なし数 〉 | 〈定数名 〉 | 〈符号 〉 〈定数名 〉 | 〈文字列 〉〈定数名 〉 ::= 〈名前 〉

すなわち, 定数としてとり得るものは,(符号がついても良い)数, すでに定義されている定数名, もしくは, 文字列であることがわかる.

Example 5.7.1 次は正しい定数定義である.

const a = 32 ; b = 1.0 ; c = -2.0 ; d = a ; e = -b ; f = ’-----’ ;

5.7.4 型定義部

Pascal のデータ型は, 変数の宣言において直接記述しても良いし, 型名で参照しても良い. Pascal ではすでに定義されている型から新しい型をつくり出す手続きが定義されている. これが型定義である.

6 一般に宣言とは, プログラム中で実際に実行される文ではなく, それ以降に利用する識別子等を定義することをいう.

Id: P3.tex,v 1.5 2001-03-11 17:24:47+09 naito Exp

数理解析・計算機数学特論 119

〈型定義部 〉 ::= type 〈名前 〉 = 〈型 〉 ; { 〈名前 〉 = 〈型 〉 ; }〈型 〉 ::= 〈単純型 〉 | 〈構造つきの型 〉 | 〈ポインタ型 〉

この右辺に現れたものは後ほど定義する. また, Example についても, 後ほど説明する.

5.7.5 変数宣言部

Pascal のプログラム中に現れる全ての変数は必ず変数宣言しなくてはならない. また, 変数はプログラム中で現れる前に, 変数を利用する前に宣言しなくてはならない.

〈変数宣言部 〉 ::= var 〈名前 〉 { , 〈名前 〉 } : 〈型 〉 ; { 〈名前 〉 { , 〈名前 〉 } : 〈型 〉 ; }

Example 5.7.2 以下は正しい変数の宣言である.

var a, b : integer ;c, d : real ;e : Boolean ;f : char ;

これは, 文法的には, 名前と型を結び付けている(結合)と考えられる. この結合は, その名前を従属するブロックで再定義しない限り, その宣言を含むブロック全体で有効である.また, 同一のレベルで, 同一の有効範囲内においては, 単一の名前は一度しか宣言できない.

5.8 変数とは

変数とは, 識別子によって区別された初期化, 代入などが許される記憶領域のことである. Pascal の変数は, 型定義部の定義に述べた通り, 「単純型」, 「構造つきの型」, 「ポインタ型」に分類される. また, 変数定義部の説明で述べた通り, 変数はプログラム中で現れる前に, 変数を利用する前に宣言しなくてはならないし, その名前を従属するブロックで再定義しない限り, その宣言を含むブロック全体で有効である. すなわち, 変数は宣言しなくては利用できなくて, その名前は, より小さなブロックで再定義しない限り有効である. これは, 変数の「記憶クラス」, 「スコープ」, 「寿命」と呼ばれる考え方であり, これに関しては後ほど詳しく述べる.ここでは, 「単純型」のみを扱い, その他の型については後ほど述べることにする.

5.8.1 変数の初期値

Pascal では全ての変数は, 宣言されただけでは値は定まらない. しかも, 標準的な Pascal においては, 変数の宣言と同時に明示的な初期化は行なわれない. したがって, 明示的な初期化の方法としては,

var a,b : integer ;begina := 1 ;b := 1

end ;

といった方法をとらなくてはならない.しかしながら, SPARCompiler においては, 変数の宣言と同時に明示的な初期化を行なうことができる.

Id: P3.tex,v 1.5 2001-03-11 17:24:47+09 naito Exp

120 数理解析・計算機数学特論

var a,b : integer := 1 ;

このように, 宣言と同時に初期化することもできる. しかしながら, 下の例の場合, 変数 a, b の初期化は実行時にただ一度だけ行なわれるが, 上の例の場合は, 文を実行されるたびに値が代入されることに注意.

5.8.2 単純型

Pascal において, 単純型は次のように定義されている.

〈単純型 〉 ::= 〈順序型 〉 | 〈実数型名 〉〈順序型 〉 ::= 〈書下し順序型 〉 | 〈順序型名 〉〈書下し順序型 〉 ::= 〈列挙型 〉 | 〈部分範囲型 〉〈順序型名 〉 ::= 〈型名 〉〈実数型名 〉 ::= 〈型名 〉〈列挙型 〉 ::= ( 〈名前 〉 { , 〈名前 〉 } )

〈部分範囲型 〉 ::= 〈定数 〉 .. 〈定数 〉

5.8.2.1 列挙型

列挙型は名前を列挙することによって, 値の順序集合を定義する. 値はそれらの名前が表し, その名称を列挙した時の並べ方によって, その値の順序が決まる.

Example 5.8.1 次の例は正しい列挙型の定義である.

type color = (white, red, blue, yellow, green, black) ;day = (mon, tue, wed, thr, fri, sat, sun) ;

Example 5.8.2 次の例は間違った列挙型の定義である.

type workday = (mon, tue, wed, thr, fri, sat) ;free = (sat, sun) ;

ここでは, sat の型が曖昧なので, 間違っている. すなわち, sat は workday と free の両方で定義され

ている.

列挙型の値の順序数は, 名前の順序にしたがって 0 から始まる連続した非負整数値への写像によって決まる. したがって, 次に述べる整数型の範囲を越える数の要素を持った列挙型は定義することができない.また, 順序型に対して, 次の関数を適用することができる.

succ 列挙中の後続値.pred 列挙中の先行値.

Example 5.8.1 での例に従えば, succ(white) = red であり, pred(blue) = red である.SPARCompiler においては, firstof など, 順序型に適用可能な関数もいくつか用意されている.

5.8.2.2 標準の型

標準の Pascal においては, 標準の単純型として, 次のものが定義されている.

Id: P3.tex,v 1.5 2001-03-11 17:24:47+09 naito Exp

数理解析・計算機数学特論 121

整数 値は処理系が定義した整数の部分集合. その値は整数.実数 値は処理系が定義した実数の部分集合. その値は実数で表す.論理数 値は名前 true と false によって表示される真理値である.文字 値は処理系が定義した文字集合. それらは文字自身を引用符で囲んで表す.

5.8.2.3 部分範囲型

部分範囲型は, すでに定義されている順序型の部分集合として, その最大値と最小値を指定することによって定義される. 両方の定数の値は同一の順序型でなくてはならない. その順序型のことを, 部分範囲型の親の型という. 最小値は最大値よりも小さいか等しくなくてはならない.

Example 5.8.3 次は正しい部分範囲型の変数の定義である.

var a : 1..10 ;b : -20..30 ;

これらはいずれも整数の部分範囲型の変数として定義されている.

Example 5.8.4 次は正しい部分範囲型の定義である.

type days = (mon,tue,wed,thr,fri,sat,sun) ;workd = mon..fri ;letter = ’a’..’z’ ;

はじめの定義は列挙型の定義であるが, 2番めのものは型 days の部分範囲として定義されている.

部分範囲型として定義された変数に代入する際には, それを越えた範囲の代入を行なうと実行不可能になる可能性がある.

5.8.2.4 整数

整数を表す型は integer であり, それが表す整数の範囲, そのメモリ内での表現方法は処理系ごとに定義されている. 整数型の値の順序数は, その値そのものである. これを整数型と呼ぶ.

5.8.2.5 実数

実数を表す型は real であり, それが表す実数の部分集合, そのメモリ内での表現方法は処理系ごとに定義されている. これを実数型と呼ぶ.

5.8.2.6 論理数

論理数を表す型は Boolean であり, これは標準的に

type Boolean = (false, true) ;

で定義されている. すなわち, 標準名 false と true が定義され, さらに, false < true となる順序が入っ

ている. これを論理型 と呼ぶ.

Id: P3.tex,v 1.5 2001-03-11 17:24:47+09 naito Exp

122 数理解析・計算機数学特論

5.8.2.7 文字

文字を表す型は char であり, それが表す文字集合, そのメモリ内での表現は処理系に依存する. これを文字型と呼ぶ.文字型が表現する文字集合に対しては, 少なくとも, 以下のものを含む.

• 大文字の英字.• 10進の数字.• 空白文字.

それ以外にも, 特殊記号等を含んでいるのが普通である7 . この時, 文字に対する順序数は, 0 から始まる連続した非負整数値に写像される. この写像は次の性質を満たす.

• 数字の 0 から 9 を表現する文字の値は, 数の順に順序付けられ連続.• 大文字の A から Z を表現する文字の値は, (使用可能な時には)アルファベット順であるが, 連続とは限らない.

• 小文字の a から z を表現する文字の値は, (使用可能な時には)アルファベット順であるが, 連続とは限らない.

文字型に対しては, 2つの標準関数がある. これらはいずれも文字集合から自然数の部分集合の間の単射な写像である.

ord 処理系に依存した文字の順序集合において, 文字の順序数を表す.chr chr(i) は順序数 i をもつ文字を表す.

この2つの関数は互いに逆写像の関係にある. しかも, 文字型は順序型であるので, 標準関数 pred, succを利用することができるが,

pred(c) = chr(ord(c)-1)succ(c) = chr(ord(c)+1)

が成り立つ.

5.8.2.8 型の適合性

次の4つのいずれかが成り立つ時, 型 T 1 と T 2 は適合するという.

(a) T 1 と T 2 は同じ型である.(b) T 1 が T 2 の部分範囲型であるか, T 2 が T 1 の部分範囲型であるか, T 1 と T 2 の両方が同じ親の型の

部分範囲型.(c) T 1 と T 2 が基底型の適合する集合型であって, 両方とも詰めありか, 両方とも詰めなしである.(d) T 1 と T 2 が同数の成分を持つ文字列型である.

5.8.3 変数と記憶領域

変数は宣言と同時に対応する記憶領域が確保される. しかしながら, それぞれの型に対して, どれだけの記憶領域が確保されるかは処理系に依存している.

7 通常, よほどのことがない限り, ASCII 文字集合と等価であると思って間違いないだろう.

Id: P3.tex,v 1.5 2001-03-11 17:24:47+09 naito Exp

数理解析・計算機数学特論 123

5.8.3.1 文字型

文字型を表す記憶領域は, 通常その処理系の扱う文字集合を表すのに必要十分な量が利用される. SPAR-Compiler の場合, ASCII 文字集合を利用しているので, 文字型の変数の記憶領域は1バイト=8ビットである.

SPARCompiler において, minchar, maxchar, bell, tab と書かれる文字定数が定義されている. それらの順序数は10進数で

minchar 0,maxchar 255,bell 7,tab 9

である.

5.8.3.2 整数型

整数型を表す記憶領域は, 文字型の表すそれの整数倍であることが普通である. 特に, 処理系の設計上, 1バイト, 2バイト, 4バイトなどの 2n バイトを利用することが多い.

SPARCompiler の場合, 整数型の変数の記憶領域は4バイト=32バイトである. この値は, コンパイル時において -xl オプションを利用すると2バイト=16バイトとなる. このようなコンパイル時における依存性を避けるため, SPARCompiler においては, integer16, integer32 と書かれるそれぞれ16ビット,32ビット長に固定された整数型が定義されている.また, maxint, minint と書かれる整数定数が定義され( maxint は標準 Pascal の範囲で定義されてい

る.)それぞれ, integer 型の表す最大の整数, 最小の整数を表現する.また, Pascal においては, C で利用される「符号なし整数」の型は定義されていないが, 部分範囲型を利

用することにより, 「符号なし整数」型を定義することも可能である.

5.8.3.2.1 整数の内部表現 整数型の変数の内部での表現は, Section 2.3.2 で述べた表現がとられていることが多い.

5.8.3.3 実数型

実数型を表す記憶領域は, 文字型の表すそれの整数倍であることが普通である. 特に, 処理系の設計上, 4バイト, 8バイトなどの 2n バイトを利用することが多い.

SPARCompiler の場合, 実数型 real の変数の記憶領域は8バイト=64ビットである. この値は, コンパイル時において -xl オプションを利用すると4バイト=32ビットとなる. このようなコンパイル時における依存性を避けるため, SPARCompiler においては, single, shortreal, double, longreal と書かれるそれぞれ32ビット, 32ビット, 64ビット, 64ビット長に固定された実数型が定義されている.これらの数を表す表現方法は, 実際には浮動小数点数として表されている. (10を底とする)浮動小数

点数とは, 以下のような型で表現された数のことである.

(0でない1桁の数).<仮数部> × 10^<指数部>

ここで, 任意の 0 でない実数は, このような表示が可能であることに注意せよ. (もちろん, その表示は有限小数と仮定すれば一意的である.)

Id: P3.tex,v 1.5 2001-03-11 17:24:47+09 naito Exp

124 数理解析・計算機数学特論

5.8.3.3.1 実数型の内部表現 実数型の変数の内部での表現は, Section 2.3.2 で述べた表現がとられていることが多い.オーバーフロー, アンダーフローをした数の演算, 比較等の結果がどうなるかは規格では定められていな

い. しかし, それぞれの処理系によって規定されている.

5.8.3.4 列挙型

列挙型の変数の記憶領域は, 整数のそれに準じている.

5.9 いくつかの用語

5.9.1 誤り

プログラムの規則に対する違反には次の2種類がある.

• 宣言のない変数の使用等, コンパイル時に検出できる違反.• 配列の添字の範囲の逸脱等, 実行時でないと検出できない違反.

ここでは, 「誤り」という言葉は, 後者の意味で用いる.

5.9.2 不定

変数や駆動結果に値が与えられていないものを「不定」と呼ぶ. 一般に「不定」といった時には, 変数が値を持っているが, それがどんな値かわからないという意味で利用するが, Pascal における「不定」とはもっと強い意味で, 不定の変数に対して比較 x = x を行なうと誤りとなることを意味する.

5.9.3 処理系依存と処理系定義

Pascal における処理系定義, 処理系依存とは, 言語仕様の中では決めることができず, 各処理系に対して自由度を与えなくてはならない部分を示している.処理系定義とは, 各処理系がその処理系の中では定義が与えられているものを指す. 一方, 処理系依存と

は, 一つの処理系の中でも, その意味を決められないものを表す.処理系定義の例には次のようなものがある.

• 文字列用の文字の値.• 実数型の値.• 文字値の順序数.

この他にも多くの可能性がある.一方, 処理系依存の例は,

• 演算子のオペランドの評価順序.• 関数の実引数の評価順序.• 代入文の左辺と右辺の評価順序.

などがある.

Id: P3.tex,v 1.5 2001-03-11 17:24:47+09 naito Exp

数理解析・計算機数学特論 125

5.10 標準関数と演算子

5.10.1 標準関数

ここまでに述べたもの以外にも, Pascalで標準的に定義されている関数がある. それらについては, Jensen-Wirth の付録参照のこと. また, SPARCompiler 独自の関数も数多く定義されている. これらに関しては,SPARCompiler のマニュアルを参照されたい.

5.10.2 演算子

標準 Pascal における演算子は, その被演算子の型, 結果の型に応じて, 代入演算子, 算術演算子, 関係演算子, 論理演算子, 集合演算子に分類される.全ての二項演算において, 被演算子の評価順序は, 処理系依存である.

5.10.2.1 代入文

代入の演算子 :=を利用した文を代入文と呼ぶ. この被演算子は, その左辺であり, 右辺の値を左辺に代入する. この時, 被演算子の型は, ファイル型を除く全ての型で有効である. 代入文の構造は次で定義される.

〈代入文 〉 ::= 〈変数 〉 := 〈式 〉 | 〈関数名 〉 := 〈式 〉

5.10.2.1.1 代入可能性 次の5つのうちいずれかが成り立つ時, 型 T 2 の値は, 型 T 1 に対して代入可能であるという.

(a) T 1 と T 2 が同じ型で, その型がファイル型の成分として許されている.(b) T 1 が実数型で T 2 が整数型である.(c) T 1 と T 2 が適合する順序型で, T 2 の値が型 T 1 の定める区間内にある.(d) T 1 と T 2 が集合型で, T 2 の値の全ての元が, 型 T 1 の基底型が定める区間内にある.(e) T 1 と T 2 が適合する文字列型である.

型 T 2 が型 T 1 に対して代入可能である時に限り, 型 T 1 を持つ変数に型 T 2 を持つ値を代入することができる.代入とは「値を与える」ことであり, 値が不定なものを代入することは誤りである.

5.10.2.2 否定演算子

否定演算子 not は, 一つの論理型の被演算子を持ち, 被演算子の否定を表す.

〈否定演算子 〉 ::= not

5.10.2.3 乗法演算子

乗法演算子とは次で定義されている.

〈乗法演算子 〉 ::= * | / | div | mod | and

それぞれの演算は次のようなものである.

Id: P3.tex,v 1.5 2001-03-11 17:24:47+09 naito Exp

126 数理解析・計算機数学特論

演算子 操作 被演算子の型 結果の型

* 乗算 実数型, 整数型 実数型, 整数型積集合 任意の集合型 被演算子の型

/ 除算 実数型, 整数型 実数型

div 小数点以下切捨ての除算 整数型 整数型

mod 余り 整数型 整数型

and 論理積 論理型 論理型

ここで, 除算が / と div の2種類があることに注意せよ.

5.10.2.3.1 除算の規定 除算 / においては, x/y の時, y が 0 であれば, それは誤りであり, y が 0 でない時, x/y は, x と y で割った結果である.除算 x div y は, y が 0 であれば誤りであり, そうでなければ, 次の結果を満たす.

abs(i) - abs(j) < abs((i div j) * j) ≤ abs(i)

ここで,標準関数 absは,整数型の引数をとり,その絶対値を整数型として返す. ただし, abs(i) < abs(j)

ならば i div j は 0 である.剰余 i mod j は, j が 0または負の時には誤りである. そうでなければ, i mod j は次の2つの式を満

たす.

• i mod j = j - (k*j) ただし, k は整数.• 0 ≤ i mod j < j

したがって, i ≥ 0, j > 0 の時に限って, (i div j) * i + i mod j = i が成り立つ.

5.10.2.4 加法演算子

加法演算子とは次で定義されている.

〈加法演算子 〉 ::= +| - | or

それぞれの演算は次のようなものである.

演算子 操作 被演算子の型 結果の型

+ 加算 実数型, 整数型 実数型, 整数型和集合 任意の集合型 被演算子の型

- 減算 実数型, 整数型 実数型

差集合 任意の集合型 被演算子の型

or 論理和 論理型 論理型

この時, +, - に関しては, 一つの被演算数しか持たない時には, - は符号の反転を表し, + は恒等変換を

表す.

5.10.2.5 関係演算子

関係演算子とは, 2つの被演算子の間の関係を判定するもので, 次で定義されている.

Id: P3.tex,v 1.5 2001-03-11 17:24:47+09 naito Exp

数理解析・計算機数学特論 127

〈関係演算子 〉 ::= = | <> | < | > | <= | >= | in

それぞれの演算は次のようなものである.

演算子 被演算子の型 結果の型

in 左被演算子:任意の順序型 T 論理型

右被演算子:正規 T 集合型 論理型

=, <> 任意の単純型, ポインタ型, 文字列型, 正規 T 集合型 論理型

<, > 任意の単純型, 文字列型 論理型

<=, >= 任意の単純型, 文字列型, 正規 T 集合型 論理型

演算子 <>, <=, >= はそれぞれ, 等しくない, 等しいか小さい, 等しいか大きいを表す. また, <=, >= が集合型に適用された時には, 集合の包含関係を表す.さらに, p, q が論理式(論理型の値を持つ式)であれば, p=q は論理式の同値性を表す. また, p<=q は, p

ならば q であることを表す. これは, false < true であることから計算される. 論理型の変数に対する<> は排他的論理和と同義である.また, 関係演算子を文字列に適用した場合には, いわゆる辞書式順序によって比較が行なわれる.

5.10.2.6 算術演算子と型変換

算術演算子とは, 乗法演算子, 加法演算子のうち, 被演算子として, 整数型または実数型を持つものである. この時, 被演算子として, 整数型もしくは実数型となっているものに対しては, その演算結果は, 両方の被演算子が整数型であれば, 整数型となり, そうでなければ実数型となる. また, 実数型の変数と整数型の変数の算術演算を行なう時には, 整数型の変数は, 実数型の値へと自動的に変換を受ける. この型変換は,Pascal における自動型変換の唯一の例である. その他の型の間の変換は, 明示的に変換関数を用いなければならない.整数型の処理系定義の値 maxint は, 次の条件を満足する.

• 区間 [-maxint, maxint] の中の値は, 全て整数型で表すことができる.• その区間内の2つの整数値に対する2項算術演算は, その結果も区間内に収まるならば, 整数計算における正しい数学的演算を行なう.

• この区間内の2つの整数値に対する2項関係演算は, 整数計算における正しい数学的演算を行なう.

この規則に当てはまらないものに関しては, 正しい演算が行われるか, 誤りとなるかは処理系による.

5.10.2.6.1 変換関数

trunc(x) 式 x の値は実数型. 結果の型は整数型である. trunc(x) の結果は, x が正または0であれば,0 ≤ x - trunc(x) < 1 を, そうでなければ, −1 < x + trunc(x) ≤ 0 を満たす. そのような値が存在しなければ, 誤りである. 例えば, trunc(3.5) = 3, trunc(-3.5) = −3 である.

round(x) 式 x の値は実数型. 結果の型は整数型である. round(x) の結果は, x が正または0であれば,round(x) は trunc(x+0.5) と等価. そうでなければ, round(x) は trunc(x-0.5) と等価である. そのような値が存在しなければ, 誤りである. 例えば, round(3.5) = 4 である.

ここで, round(-3.5) を考えてみよう. 実数型の値の与え方は, 処理系定義であるので, 実際に 3.5 の値が3.4999 · · · となってしまうかも知れない. このようなことがありうるので, 注意しなくてはならない.

Id: P3.tex,v 1.5 2001-03-11 17:24:47+09 naito Exp

128 数理解析・計算機数学特論

5.10.3 演算子の優先順位と式

ここで定義した演算子に対しては, その優先順位が定められ, それによって「式」が定義される. 演算子の優先順位は, 否定演算子, 乗法演算子, 加法演算子, 関係演算子の順に低くなり, 優先順位を持つ演算子の列は, 左から右に向かって実行される. この場合, 被演算子(オペランド)の評価順序は処理系依存なので,注意しなくてはならない. (一般に, 式の出現ごとにも異なることがありうる.)これを構文として定義すると, 以下のようになる.

〈符号なし定数 〉 ::= 〈符号なし数 〉 | 〈文字列 〉 | 〈定数名 〉 | nil〈因子 〉 ::= 〈変数 〉 | 〈符号なし定数 〉 | 〈関数呼びだし 〉 | 〈集合 〉 | ( 〈式 〉 ) | not 〈因子 〉〈集合 〉 ::= [ 〈要素の並び 〉 ]

〈要素の並び 〉 ::= 〈要素 〉 { , 〈要素 〉 }| 〈空 〉〈要素 〉 ::= 〈式 〉 | 〈式 〉 .. 〈式 〉〈項 〉 ::= 〈因子 〉 | 〈項 〉 〈乗法演算子 〉 〈因子 〉〈単純式 〉 ::= 〈項 〉 | 〈単純式 〉 〈加法演算子 〉 〈項 〉 | 〈加法演算子 〉 〈項 〉〈式 〉 ::= 〈単純式 〉 | 〈単純式 〉 〈関係演算子 〉 〈単純式 〉

ここで, 集合の構成要素となる式は, 全てその集合の基底の型と同一でなくてはならない. [] は空集合を表し, [x..y] は, 区間 x から y の全ての要素からなる集合を表す.

Example 5.10.1 因子とは,

x15(x+y+z)not p[red,black,green][1..10]

などである. また, 項とは

x*yp or q(x<=y) and (y<z)

などである. 単純式とは

x+y-xi*j-1

などである. 式とは

x = 1.5p<=q(i<j)=(j<k)c in h

である.

この定義により, もっとも優先順位が高いのが not であり, 優先順位がもっとも低いのが関係演算子であることがわかる. もちろん, () を用いることにより, 先に優先順位の低い式を実行することができる.この定義によると, 式を評価する際には全ての単純式が左から右に向かって評価されることになる.

Example 5.10.2 次のような場合を考えてみよう.

Id: P3.tex,v 1.5 2001-03-11 17:24:47+09 naito Exp

数理解析・計算機数学特論 129

a := 1 ; b := 2 ; c := 3 ; d := 4 ;

この時,

(a > b) and (c < d)

を左から右に評価すると, a > b を評価して, その値が false であることがわかる. したがって, and の

第2被演算子が何であっても, この式の値は false であることがわかる.

【重要な注意】 Pascal においては, 二項演算子の評価順序は規定されていない. その意味は, 書かれている順序で評価しても良いし, 逆順に評価しても良いし, 並列に同時に評価しても良い. それどころか, 出現するたびに評価順序も異なることがある.一方 C においては, 式の評価順序は確定していて, さらに, 式の値が定まった時点で式の評価を終る. こ

れを短絡評価と呼ぶ. Pascal においては最後まで式の評価が行なわれるかどうかは不確定である.また, C とは優先順位が異なることに注意せよ. 例えば, 上と同じことを C では,

a > b && c < d

と書けるが, Pascal では, and の優先順位が >, < よりも高いので, このままでは文法エラーとなる.

5.10.4 非標準の演算子

SPARCompiler では, 次の演算子が定義されている8 .

演算子 操作 被演算子の型 結果の型

~ bitwise not 整数型 整数型

& bitwise and 整数型 整数型

| bitwise or 整数型 整数型

! bitwise or 整数型 整数型

ここで, bitwise not は否定演算子, bitwise and は乗法演算子, bitwise or は加法演算子として定義されている

5.10.5 非標準の関数など

SPARCompiler では, 次の関数が定義されている9 .

関数名 動作

asl 整数型の変数の左算術シフト

asr 整数型の変数の右算術シフト

lsl 整数型の変数の左論理シフト

lsr 整数型の変数の右論理シフト

8 その他にも2つほどあるのだが.9 これ以外にもいくつかあるので, それらは SPARCompiler のマニュアルを参照.

Id: P3.tex,v 1.5 2001-03-11 17:24:47+09 naito Exp

130 数理解析・計算機数学特論

5.10.5.1 シフト

整数型の変数に対するシフトとは, 次のようなものである. 前に述べた通り, 整数型の変数は2進数で表現され, 先頭のビットは符号を表している. その変数を左(右)に 1 シフトするとは, ビット列として各ビットを左(右)に 1 ずらすことをいう. したがって, 符号ビットを無視すれば, 左に 1 シフトするとは,その整数に 2 を掛けることに等しく, 右に 1 シフトするとは, 2 で割った商を求めることに等しい.しかしながら, 先頭に符号ビットがついているので, それらの扱いが問題となるため, 算術シフト, 論理シ

フトの2種類がある. 算術シフトとは, 符号ビットを保存したシフトである.

Example 5.10.3 以下は, それぞれの16ビット整数を右に 1 算術シフトした結果である.

シフトする前 シフトした後

1111 1111 1111 1000 1111 1111 1111 1100

0000 0000 0000 1000 0000 0000 0000 0100

同様に算術左シフトとは, 符号を保存して左に 1 シフトする.

Example 5.10.4 以下は, それぞれの16ビット整数を左に 1 算術シフトした結果である.

シフトする前 シフトした後

1111 1111 1111 1000 1111 1111 1111 0000

0000 0000 0000 1000 0000 0000 0001 0000

一方, 論理シフトとは, 符号ビットも含めてシフトする方法である.

Example 5.10.5 以下は, それぞれの16ビット整数を右に 1 論理シフトした結果である.

シフトする前 シフトした後

1111 1111 1111 1000 0111 1111 1111 1100

1000 0000 0000 1000 0100 0000 0000 0100

Example 5.10.6 以下は, それぞれの16ビット整数を左に 1 論理シフトした結果である.

シフトする前 シフトした後

1111 1111 1111 1000 1111 1111 1111 0000

0100 0000 0000 1000 1000 0000 0001 0000

5.11 いくつかのプログラム

ここまででは, 変数の値を表示する方法は述べていなかった. それをするためには, writeln 手続きを使う.

Example 5.11.1 その利用法は, 次の通りである.

Id: P3.tex,v 1.5 2001-03-11 17:24:47+09 naito Exp

数理解析・計算機数学特論 131

program print_example (output) ;vara : char ;b,c : integer ;x : real ;

begina := ’a’ ;b := 1 ;c := 2 ;x := 1.0 ;writeln(’a = ’,a) ;writeln(’b = ’,b,’ c = ’,c) ;writeln(’b div c = ’,b div c) ;writeln(’b / c = ’,b/c) ;writeln(’x = ’,x)

end.

writeln 手続きの出力の形式については, SPARCompiler の writeln の項を参照せよ. 特に実数の出力の形式には注意しなくてはならない.

Exercise 5.11.1 色々な型の演算の値を出力するプログラムを書け.

Exercise 5.11.2 integer 型で表現される最大の数に 1 を加えたらどうなるかを考察せよ.

Exercise 5.11.3 正の浮動小数点数の小数点以下を四捨五入した値を求めるプログラムを書け.

Exercise 5.11.4 正の浮動小数点数の小数点以下第2桁めを四捨五入した値を求めるプログラムを書け.この際, writeln が浮動小数点数をどのように表示するかを確かめよ. 一般に, 出力関数がどのような仕様になっているかは処理系依存なので, 浮動小数点数を利用する前に, その辺を確かめておく必要がある.

Exercise 5.11.5 AND, OR, NOT から XOR を作れ.

5.12 文

文とはアルゴリズム上の動作を表す. 文は実行されるものである.

〈文 〉 ::= { 〈ラベル 〉 : } 〈単純文 〉 | 〈構造文 〉

5.12.1 単純文

単純文とは, 他の文を含まないものであり, 空文とは字句を含まない, すなわち, 何もしない文である.

〈単純文 〉 ::= 〈空文 〉 | 〈代入文 〉 | 〈手続き呼びだし文 〉 | 〈goto 文 〉〈空文 〉 ::=

代入文は前に解説した. 手続き呼びだし文に関しては, 後で説明する.

5.12.1.1 goto 文

goto 文の構文は以下の通りである.

Id: P4.tex,v 1.4 2001-03-11 17:35:37+09 naito Exp

132 数理解析・計算機数学特論

〈goto 文 〉 ::= goto 〈ラベル 〉

goto 文は, プログラム中にラベルで指定された他の部分への制御の移動を行う. ラベルとそれを参照する goto 文との間にはある条件が成り立たなければならない. しかし, Pascal において, goto 文はプログラムの自然な流れを壊さなければならないような, 異常な状態の処理にのみ用い, それ以外で goto 文を利用しなければならないようなことはあり得ない. もし, そのようなことがおこれば, アルゴリズムをもう一度見直した方がよい. このような理由で, ここでは goto 文に関する解説を行わない. 詳しくは [1, 解説書Section 4] を参照.

5.12.2 構造文

構造文は, 以下の構文を持つ.

〈構造文 〉 ::= 〈複合文 〉 | 〈条件文 〉 | 〈繰り返し文 〉 | 〈with 文 〉〈文の列 〉 ::= 〈文 〉 { ; 〈文 〉 }

ここで, 文の列は, そこに含まれる文を文面上の順序にしたがって実行する. ただし, goto 文の実行によって変更された場合を除く.

5.12.2.1 複合文

複合文とは, 文の列の実行を指定する.

〈複合文 〉 ::= begin 〈文の列 〉 end

Example 5.12.1 次は複合文である.

begin z := x ; x := y end

5.12.2.2 条件文

条件文は以下の構文を持つ.

〈条件文 〉 ::= 〈if 文 〉 | 〈case 文 〉

5.12.2.3 if 文

if 文とは, 論理式の値によって制御を分岐させるものである.

〈if 文 〉 ::= if 〈論理式 〉 then 〈文 〉 { 〈else 部 〉 }〈else 部 〉 ::= else 〈文 〉

ここで, if 文の「論理式」の値が true であれば, if 文の「文」が実行される. 「論理式」の値が false の場合には, if 文の「文」は実行されず, else 部があればその「文」が実行される.

else 部を持たない if 文の直後には字句 else は現れてはならない. これは,

Id: P4.tex,v 1.4 2001-03-11 17:35:37+09 naito Exp

数理解析・計算機数学特論 133

if e1 then if e2 then s1 else s2

if e1 thenbegin if e2 then s2 else s2 end

と解釈するという意味である.

Example 5.12.2 以下は正しい if 文の例である.

if x <> 1.5 then z := x + y else z := 1.5 ;

if x <> 1.5 then z:= x + y

if x <> 1.5 thenif y <> 2.0 thenz:= x + y

else z := yelse z := x

5.12.2.4 case 文

case 文とは, そこに現れた「選択定数」の値によって, 選択肢を実行する分岐文である.

〈case 文 〉 ::= case 〈選択式 〉 of 〈選択肢 〉 { ; 〈選択肢 〉 } end〈選択肢 〉 ::= 〈選択定数並び 〉 : 〈文 〉〈選択式 〉 ::= 〈式 〉〈選択定数並び 〉 ::= 〈選択定数 〉 { , 〈選択定数 〉 }〈選択定数 〉 ::= 〈定数 〉

Example 5.12.3 次が case 文の例である.

case x of1: y := x ;2: z := x ;3,4: w := x

end

ここで, case 文に入る時には, 「選択式」の値が「選択定数」のいずれかに一致しなくてはならない. そうでなければ誤りである.

5.12.3 繰り返し文

繰り返し文とは, 指定の条件の下に, 文を繰り返し実行することを指定する.

〈繰り返し文 〉 ::= 〈repeat 文 〉 | 〈while 文 〉 | 〈for 文 〉

Id: P4.tex,v 1.4 2001-03-11 17:35:37+09 naito Exp

134 数理解析・計算機数学特論

5.12.3.1 repeat 文

〈repeat 文 〉 ::= repeat 〈文の列 〉 until 〈論理式 〉

repeat 文の文の列は, 文の列の実行の後に論理式が true となるまで繰り返し実行される. 論理式が文の列の実行の後に評価されるので, 少なくとも1回は文の列は実行される.

Example 5.12.4 次のプログラムは, 1 から 10 までの和をとる文である.

program sum ;var i,j : integer ;

begini := 0 ; j := 1 ;repeati := i + j ;j := j+1 ;

until j = 11 ;end.

5.12.3.2 while 文

〈while 文 〉 ::= while 〈論理式 〉 do 〈文 〉

while 文

while b do body

beginif b thenrepeat bodyuntil not(b)

end

と等価である. すなわち, 論理式が最初に評価され, true であれば, 文が実行され, false ならば実行はされない. したがって, 「文の列」は一度も実行されないことがある.

Example 5.12.5 次のプログラムは, 1 から 10 までの和をとる文である.

program sum ;var i,j : integer ;

begini := 0 ; j := 1 ;while j <= 10 do begini := i + j ;j := j+1 ;

endend.

5.12.3.3 for 文

for 文とは, 制御変数が表す変数に一連の値を与えながら文を繰り返し実行することを指定する.

Id: P4.tex,v 1.4 2001-03-11 17:35:37+09 naito Exp

数理解析・計算機数学特論 135

〈for 文 〉 ::= for 〈制御変数 〉 := 〈初期値 〉 { to | downto } 〈終値 〉 do 〈文 〉〈制御変数 〉 ::= 〈純変数 〉〈初期値 〉 ::= 〈式 〉〈終値 〉 ::= 〈式 〉〈純変数 〉 ::= 〈変数名 〉

for 文は, 以下の制限を持つ.

• 制御変数はその for 文を直接に含むブロック内の変数宣言部で宣言されたものを名称とする変数でなければならない.

• 制御変数の型は順序型でなければならない. 初期値, 終値はその型に適合しなければならない.• for 文の文が1回でも実行されるなら, 初期値と終値は制御変数の型に対して代入可能でなければならない.

また, for 文の実行後, その制御変数の値は不定となる. ただし, goto 文で抜け出した場合は除く.for 文の内部の文 S で制御変数 V に対して, つぎに挙げることをしてはならない.

• S が代入文であり, V に値を代入する.• S が for 文であり, V は S の制御変数である.• S が関数の実引数である.• S が手続き read または readln の実引数である.

これらの制約を除けば,

for v := e1 to e2 do body

は, 順序型の変数 v を初期値 e1 から, 終値 e2 まで, 関数 succ を利用して一つずつ動かしながら文 body

を実行する. 特に, 上の制約を除き, 次の文と等価である.

begint1 := e1 ;t2 := e2 ;if t1 <= t2 thenbeginv := t1 ;repeatbody ;v := succ(v) ;

until v > t2end ;

また,

for v := e1 downto e2 do body

は, 順序型の変数 v を初期値 e1 から, 終値 e2 まで, 関数 pred を利用して一つずつ動かしながら文 body

を実行する.

Example 5.12.6 次のプログラムは, 1 から 10 までの和をとる文である.

program sum ;var i,j : integer ;

begini := 0 ;for j:=1 to 10 doi := i + j ;

end.

Id: P4.tex,v 1.4 2001-03-11 17:35:37+09 naito Exp

136 数理解析・計算機数学特論

Example 5.12.7 次のプログラムは, 1 から 10 までの和をとる文である.

program sum ;var i,j : integer ;

begini := 0 ;for j:=10 downto 1 doi := i + j ;

end.

このように, for 文では, 制御変数を 1 刻みにしか動かせないので, それ以外の時には while 文, またはrepeat 文を利用することとなる.

5.12.3.4 with 文

with 文に関しては, 後ほど述べる.

5.12.4 演習問題

ここの演習問題は, ここまでに学んだ繰り返し文や条件文を使って書くことができる.はじめに演習問題をやるために必要な関数について注意しておく.

5.12.4.1 数値を入力する

標準入力から整数数値を入力するためには, readln を使う.

readln(v)

利用法は, 以下の通り.

Example 5.12.8 標準入力から整数を読んで, それを出力する.

var n : integer ;beginreadln(n) ;writeln(n)

end.

5.12.4.2 乱数の発生

random という(SPARCompiler に付属の)関数は real 値の乱数を発生させる. 通常, 次のようにして使う.

Example 5.12.9 乱数を2つ発生させる.

var n : integer ;r, s : real ;

beginn := seed(wallclock) ;r := random(n) ;s := random(n) ;

end.

Id: P4.tex,v 1.4 2001-03-11 17:35:37+09 naito Exp

数理解析・計算機数学特論 137

(SPARCompiler に付属の)関数 seed の部分は, 乱数を初期化する部分で, 現在の時刻から決まる数を使って初期化をしている. また, random の引数 n は特に意味はないが, 文法上必要なものである. ここで,発生する乱数は 0 から 1 の間の数が出てくる.

Exercise 5.12.1 repeat 文, while 文, for 文を利用して, 2 から N 迄の偶数の和を計算して出力するプロ

グラムを書け.

Exercise 5.12.2 摂氏と華氏の温度対応は, ◦C = (5/9)(◦F − 32) である. 華氏の温度(整数)を入力して, 摂氏の温度を印字するプログラムを書け. その際, 摂氏の温度として, 小数点以下切捨て, 小数点以下四捨五入, 小数点以下第2桁めを四捨五入の3種類を書け.

Exercise 5.12.3 非負の integer 型の数値を入力して, それを2進数で表示するプログラムを書け.

Exercise 5.12.4 0 から 1 までの乱数を十分沢山発生させ, その値が 0.5 未満の確率を表示するプログラムを書け.

Exercise 5.12.5 for 文

for v := e1 downto e2 do body

と等価な repeat 文を作れ.

5.13 構造つきの型

構造つきの型は, 構造型, レコード型, 集合型, ファイル型に分類される.

〈構造型 〉 ::= 〈書き下し構造型 〉 | 〈構造型名 〉〈構造型名 〉 ::= 〈型名 〉〈書き下し構造型 〉 ::= { packed } 〈詰めなし構造型 〉〈詰めなし構造型 〉 ::= 〈配列型 〉 | 〈レコード型 〉 | 〈集合型 〉 | 〈ファイル型 〉

書き下し構造型に packed がある場合には, その型は「詰めあり」と呼ばれる. 構造型に対する詰めありの指定は, 値のデータ領域を節約するように処理系に指示する働きがあり, その型の変数に対する演算などが高速に行なわれる可能性がある10 .

5.13.1 配列型

配列 (array) 型は, 添字型で示される値から個々の成分への写像としての構造を持つ. 各成分は配列型の成分型の型表記で示される型を持つ.

〈配列型 〉 ::= array [ 〈添字型 〉 { , 〈添字型 〉 } ] of 〈成分型 〉〈添字型 〉 ::= 〈順序型 〉〈成分型 〉 ::= 〈型 〉

Example 5.13.1 例えば, integer 型の添字 0 から 9 までを持つ配列型の変数 digit は次のように定義

される.

var digit : array [0..9] of integer ;10 SPARCompiler では, packed には何も効果がない.

Id: P5.tex,v 1.3 2001-03-11 17:33:43+09 naito Exp

138 数理解析・計算機数学特論

この時, 添字型 0..9 は整数型の部分範囲型として決めている.

digit[0] digit[1] digit[2] digit[3] digit[4] digit[5] digit[6] digit[7] digit[8] digit[9]

その他にも, 次のようなものも定義できる.

array [Boolean] of color

ここで, 型名 color は, 正当に定義された型であるとしている.

Pascal においては, 配列が定義されている添字以外への参照は誤りとなる. すなわち,

var digit : array [0..9] of integer ;

と定義されている配列 digit に対して, digit[10] を参照することは出来ない.二つ以上の「添字型」の列を指定する配列型は, 次のような配列型の省略記法である.

• 「添字型」はもとの配列型の最初の「添字型」である.• 「成分型」はもとの指定の2番め以降の「添字型」の列を持ち, もとの指定と同じ「成分型」を持つ配列型である. この「成分型」は, もとの配列型が詰めありの時に限り, 詰めありとなる.

次の2つの例は, それぞれ同じ配列型の異なる表現法を表す.

Example 5.13.2  

array [Boolean] of array [1..10] of array [size] of realarray [Boolean] of array [1..10,size] of realarray [Boolean,1..10] of array [size] of realarray [Boolean,1..10,size] of real

Example 5.13.3  

packed array [1..10,1..8] of Boolean ;packed array [1..10] of packed array [1..8] of Boolean ;

Example 5.13.4  

var str : array [0..1][0..9] of integer ;

str[i] は, 10 個の要素を持つ配列となる.

str[0] [0] str[0] [1] str[0] [2] str[0] [3] str[0] [4] str[0] [5] str[0] [6] str[0] [7]

str[1] [0] str[1] [1] str[1] [2] str[1] [3] str[1] [4] str[1] [5] str[1] [6] str[1] [7]

str[0] [8] str[0] [9]

str[1] [8] str[1] [9]

配列型の変数の値は次のようにして定義される. 配列型の添字型の値の最小値を m, 最大値を n とした時,k = ord(n)−ord(m)+1 とおき, 配列型の値は, 成分型の値の k 個の組である. それを, v = (v1, · · · , vk)と書いた時, 配列型の変数 v の値は v であり, v[i] は vord(i)−ord(m)+1

を表す. このような, 配列型の成分を表す変数を添字つき変数と呼ぶ.

〈成分変数 〉 ::= 〈添字つき変数 〉 | 〈フィールド表記 〉〈添字つき変数 〉 ::= 〈配列変数 〉 [ 〈添字式 〉 { , 〈添字式 〉 } ]

〈添字式 〉 ::= 〈式 〉〈配列変数 〉 ::= 〈変数 〉

配列型の変数の値は, その成分が全て定義されている時に限り, 決まっている.これらの配列の定義は, コンパイル時に配列の大きさが決定できることが必要であり, 実行時に配列の大

きさを変更したりすることはできない.2つ以上の添字を持つ配列(多次元配列)の添字の評価順序は処理系依存である.

Id: P5.tex,v 1.3 2001-03-11 17:33:43+09 naito Exp

数理解析・計算機数学特論 139

5.13.1.1 配列型の例

ここでは, real 型の配列をベクトルと思い, その和を計算してみよう.

program sum_of_vector ;vara,b,c : array[1..3] of real ;i : integer ;

begina[1] := 1.0 ; a[2] := 1.0 ; a[3] := 2.0 ;b[1] := 2.0 ; b[2] := 0.0 ; b[3] := 3.0 ;for i:=1 to 3 doc[i] := a[i] + b[i] ;

end.

5.13.2 文字列型

文字列型とは, 次のような詰めありの配列型である.

• 「添字型」は最小値が 1 で, 最大値が 1 より大きいような部分範囲型.• 「成分型」は文字型.

文字列型は, その値をテキストファイルに書くことができる, 関係演算子が利用できるという意味で, 通常の配列型とは異なった性格を持つ.

5.13.2.1 文字列の例

ここでは, 文字列型の変数に文字を代入して, 出力をしてみる.

program output_of_string (output) ;vars : packed array [1..30] of char ;i : integer ;

beginfor i := 1 to 30 dos[i] := chr(ord(’A’) + (i-1)) ;

writeln(s) ;end.

この例で, 30 まで値を与えなかった時, SPARCompiler は値が与えられた範囲のみを出力する. また,SPARCompiler では, 次のような特別な型が用意されている.

型名 内容

alfa 長さ 10 の文字列型string 長さ 80 の文字列型varying 可変長の文字列型,

varying[u] と定義した時, 長さ u の文字列型を表す.u は 0 から 65535 まで.

ただし, これらの型で指定した文字列を writeln で出力する時には, それぞれの型で結果が異なることがあるので注意すること.

Id: P5.tex,v 1.3 2001-03-11 17:33:43+09 naito Exp

140 数理解析・計算機数学特論

varname1 : packed array [1..25] of char ;name2 : packed array [76..100] of char ;name3 : alfa ;name4 : string ;name5 : varying [25] of char ;name6 : varying [25] of char ;

beginname1 := ’a’ ;name2 := ’a’ ;name3 := ’a’ ;name4 := ’a’ ;name5 := ’a’ ;name6 := ’a’ ;writeln(name1, ’ and ’, name2) ;writeln(name3, ’ and ’, name4) ;writeln(name4, ’ and ’, name6)

end.

5.13.2.2 配列と文字列の初期化

SPRACompiler では, 配列, 文字列を宣言と同時に初期化できる.

varint : array [1..10] of integer = [maxint,1,-375,5,20] ;ch : packed array [1..10] of char = ’ABCDEF’ ;int2 : array [1..*] of integer = [maxint,1,-375,5,20] ;ch2 : packed array [1..*] of char = ’ABCDEF’ ;

ここで, * が利用されている例では, 初期化が同時に行なわれているので, 必要最小限の量の配列が自動的に確保される.

varint : array [1..100] of integer = [50 of 1,50 of 2] ;

この例では, 最初の 50 個に 1 が, 次の 50 個に 2 が入る.

varint : array [1..100] of integer = [* of 1] ;int2 : array [1..10, 1..10] of integer = [[* of 1], [3 of 8], [10 of 88],] ;

二つめの例では, 第1列の全てに 1 が, 第2列の最初の3つに 8 が, 第3列の全てに 88 が入る.このように, SPARCompiler では, 配列を宣言と同時に初期化する場合には, 配列の大きさをわざわざ書

かなくても済むのだが, やはりコンパイル時に配列の大きさが決定されていることに注意. 決して, 実行時に配列の大きさを変更できているわけではない.

5.13.3 集合型

集合型は, ある基底の型のベキ集合としての構造を持つ. 集合型の各々の値は, 基底型の値を元とする集合である.

〈集合型 〉 ::= set of 〈基底型 〉〈基底型 〉 ::= 〈順序型 〉

次は, 集合型の例である.

Id: P5.tex,v 1.3 2001-03-11 17:33:43+09 naito Exp

数理解析・計算機数学特論 141

set of char ;set of (club, diamond, heart, spade) ;

各順序型 S に対して, 正規 T 集合型と呼ばれる集合型が存在する. もし, S が部分範囲型であれば, T は

その親の型であり, そうでなければ, T は S である. 正規 T 集合型とは,

A : set of 1..5 ;B : set of 2..9 ;

に対して, A and B が定義できるようにした仮想的な型のことである.

5.13.3.1 集合型の例

program set_operation ;typedays = (mon,tue,wed,thr,fri,sat,sun) ;week = set of days ;

varwk,work,free : week ;d : days ;

beginwork := [] ;free := [] ;wk := [mon..sun] ;d := sat ;free := [d] + free + [sun]

end.

この例では, 曜日の列挙型 days と, その集合型 week を定義している. さらに, week 型の変数 wk, work,free を定め, 最終的に, free は要素 sat と sun を持つ集合型となっている.

5.13.3.2 集合型の内部表現

集合型の変数は, 内部では次のように表現されている. 例えば,

T : set of T0

となっているとしよう. この時, T0 が N(T0) 個の要素を持つとすると, T の型の変数のメモリ領域は,少なくとも N(T0) ビット必要である. 実際には, 中途半端なビット数のメモリ領域は利用できないので,SPARCompiler では, 少なくとも16ビット, さらに, 16ビットの倍数のメモリ領域が利用され, その最大は256ビットである.

5.13.4 レコード型

レコード型とは一つの型の中に必ずしも同一の型ではない決まった数の成分を持つような型のことであ

る. それぞれの成分をフィールド (field) と呼ぶ.

Id: P5.tex,v 1.3 2001-03-11 17:33:43+09 naito Exp

142 数理解析・計算機数学特論

〈レコード型 〉 ::= record 〈フィールド並び 〉 end〈フィールド並び 〉 ::= { 〈固定部 〉 { ; 〈可変部 〉 }| 〈可変部 〉 }{ ; }〈固定部 〉 ::= 〈レコード要素 〉 { ; 〈レコード要素 〉 }〈レコード要素 〉 ::= 〈名称並び 〉 : 〈型表記 〉〈可変部 〉 ::= case 〈可変要素選択子 〉 of 〈可変要素 〉 { ; 〈可変要素 〉 }〈可変要素選択子 〉 ::= { 〈タグフィールド 〉 : } 〈タグ型 〉〈タグフィールド 〉 ::= 〈名称 〉〈可変要素 〉 ::= 〈選択定数並び 〉 : ( 〈フィールド並び 〉 )

〈タグ型 〉 ::= 〈順序型名 〉〈選択定数並び 〉 ::= 〈選択定数 〉 { , 〈選択定数 〉 }〈選択定数 〉 ::= 〈定数 〉

固定部も可変部も持たないフィールド並びは, 成分を持たず, 値は空値ただ1種類を持つ. このようなフィールド並びは空であるという.

Example 5.13.5 例えば, 固定部のみからなるレコード型は次のようなものである.

recordyear : 0..3000 ;month : 1..12 ;day : 1..31

end

これは, フィールドとして, year, month, day を持ち, それぞれが integer 型の部分範囲型として定義さ

れている.m 個の成分を持つ空でないフィールド並びの値は, 次の m 組である. ただし, Vi はそのフィールド並び

の i 番めの成分である.(V1, · · · , Vm)

可変部を直接に含むフィールド並びは, その可変部に対応した一つの成分を持つ. この成分の値と構造は,その可変部によって定義される.

Example 5.13.6 可変部を含むレコード型は次のようなものである.

typemonth_type = 1..12 ;days_type = record

year : 0..3000 ;case days : month_type of

1,3,5,7,8,10,12 : (days_1 : 1..31) ;4,6,9,11 : (days_2 : 1..30) ;2 : (days_3 : 1..28)

end

可変部の値は, 可変部の選択子の値 k と可変部の有効な可変要素のフィールド並びの値 Xk によって決ま

る (k,Xk) である.

Example 5.13.7 固定部と可変部を持つレコード型の例は次のようなものである.

Id: P5.tex,v 1.3 2001-03-11 17:33:43+09 naito Exp

数理解析・計算機数学特論 143

recordx, y : real ;area : real ;case shape ofrectangle : (side1, side2 : real ; skew : angle) ;circle : (diamiter : real)

end

この例では, 「可変要素選択子」で「タグフィールド」を利用していない. この時, x をこの型の変数とすると, x.side1 を利用すると, それは 可変部が rectangle であるとみなされる. 可変部が rectangle

の時, diamiter は有効ではない.

レコード型の変数の各フィールドの値は, そのフィールド名を変数名のあとに . を使ってつなぐことによ

り得ることができる.

〈フィールド表記 〉 ::= 〈レコード変数 〉 . 〈フィールド指定部 〉 | 〈フィールド表記名 〉〈レコード変数 〉 ::= 〈変数 〉〈フィールド指定部 〉 ::= 〈フィールド名 〉〈フィールド名 〉 ::= 〈名称 〉

ここで, 可変部の選択子が不定であれば, 可変部のどの可変要素も有効ではない. また, フィールド名は変数名とは別の名前空間に属するので, 次のような名前の与え方は間違いではない.

a : recorda : real ;b : real ;

end ;b : integer ;

Example 5.13.8 この例は, 固定部と可変部を持つレコード型を定義し, そのフィールドに値を代入している.

Id: P5.tex,v 1.3 2001-03-11 17:33:43+09 naito Exp

144 数理解析・計算機数学特論

program record_test ;typeb_year = 1..4 ;m_year = (m1,m2) ;d_year = (d1,d2,d3) ;b_or_m_or_d = (bachler,master,doctor) ;person = record

name : recordfirst, family : string ;

end ;id : string ;case kind : b_or_m_or_d ofbachler : (gakunen_b : b_year) ;master : (gakunen_m : m_year ;

boss_m : string) ;doctor : (gakunen_d : d_year ;

boss_d : string)end ;

varp : array [1..100] of person ;

beginp[1].name.first := ’Masashi’ ;p[1].name.family := ’Kubo’ ;p[1].id := ’9400001’ ;p[1].kind := doctor ;p[1].gakunen_d := d3 ;p[1].boss_d := ’Prof. Ihara’

end.

また, 可変部の指定で, 次のようなものは誤りである.

recordcase digit : integer of1 : (S1 : array[1..1] of char) ;10 : (S2 : array[1..2] of char) ;100 : (S3 : array[1..3] of char)

end

これは, タグフィールドがとびとびの値しかとっていない.

5.13.4.1 レコード型の変数の内部表現

レコード型の変数はメモリ内で, 可変部がどのような状態であっても, それを格納するために十分なメモリ領域が使用される. しかしながら, メモリ内でフィールド並びの順にならんでいるとは限らない.

5.13.4.2 with 文

with 文は次のような構文を持つ.

〈with 文 〉 ::= with 〈レコード変数 〉 { , 〈レコード変数 〉 } do 〈文 〉

これは, 指定したレコード変数に対して, そのフィールド名を指定するだけで, 各レコード変数のフィールドをアクセスするための構文である.

Example 5.13.9 次の代入は Example 5.13.8 の例の代入と等価である.

Id: P5.tex,v 1.3 2001-03-11 17:33:43+09 naito Exp

数理解析・計算機数学特論 145

with p[1],name dobeginfirst := ’Masashi’ ;family := ’Kubo’ ;id := ’9400001’ ;kind := doctor ;gakunen_d := d3 ;boss_d := ’Prof. Ihara’

end

with 文中において, レコード変数を変更してはならない.

5.13.5 ファイル型

ファイル型は, 指定された成分型の列を, 列中の現在の位置, 及び, 列が検査中か生成中かを示すモードも含めて表現するものである.

〈ファイル型 〉 ::= file of 〈成分型 〉

ただし, 成分型として次のようなものは許されない.

• ファイル型.• 成分の型表記の中にファイル型の成分型として許されないものがある構造型.

Example 5.13.10 次のようなものはファイル型の例となる.

file of realfile of vector

要素が文字であるファイルをテキストファイルといい, そのために標準の型 text を

type text = file of char ;

と定義する.

5.13.6 ポインタ型

ポインタ型とは, nil 値, もしくは指示値の値を持つ変数である. 指示値とは, ポインタ型の被指示型の変数を指示する. nil は指示値ではない.

〈ポインタ型 〉 ::= 〈書き下しポインタ型 〉 | 〈ポインタ型名 〉〈書き下しポインタ型 〉 ::= ^ 〈被指示型 〉〈被指示型 〉 ::= 〈型名 〉

5.13.6.1 被指示変数

被指示変数は, そのポインタ変数の値が指示する変数を表す.

〈被指示変数 〉 ::= 〈ポインタ変数 〉 ^〈ポインタ変数 〉 ::= 〈変数 〉

Id: P5.tex,v 1.3 2001-03-11 17:33:43+09 naito Exp

146 数理解析・計算機数学特論

5.13.6.2 バッファ変数

ファイル変数はファイル型の変数であり, バッファ変数はファイル変数が表す変数と結合された変数である. テキストファイルのバッファ変数は文字型である. それ以外の場合には, ファイル変数の持つ成分型である.

〈バッファ変数 〉 ::= 〈ファイル変数 〉 ^〈ファイル変数 〉 ::= 〈変数 〉

5.13.7 配列型・レコード型変数の代入

配列型, レコード型変数は, それらの間に代入可能性があれば, 次のようにして代入をすることができる.

program a_test (output) ;type

ax = array [1..10] of integer ;bx = record

a : integer ;b : real ;

end ;

vara0,a1 : ax ;b0,b1 : bx ;i : integer ;

beginfor i:=1 to 10 dobegin

a0[i] := i ;end;b0.a := 100 ;b0.b := 0.125 ;a1 := a0 ;b1 := b0

end.

5.13.8 演習問題

Exercise 5.13.1 integer 型の数値を入力して, それを次のような平衡3進展開を行なうプログラムを書け. ここで, 整数 n の平衡3進展開とは, n =

∑tk3k, tk = −1, 0, 1 と展開することで, プログラムの出力

には, −1 を n, 0 を z, 1 を p と表せ. 例えば, 5 = 9 − 3 − 1 であるので, pnn と表すことができる.

Exercise 5.13.2 2 × 2 行列の和と積を計算するプログラムを書け.

Exercise 5.13.3 集合 A = {1, 2, 3}, B = {2, 3, 4} の和集合 A∪B, 積集合 A∩B, 差集合 A−B, B −A,対称差 (A − B) ∪ (B − A) を求めるプログラムを書け.

Exercise 5.13.4 複素数を表すレコード型の変数を定義し, その和, 差, 積を計算するプログラムを書け.

5.14 関数と手続き

プログラムは一連の計算のステップによって記述されるが, それらの中では, いくつかの部分的な仕事に分割し, それぞれをプログラム部分として定義することが可能である.

Id: P6.tex,v 1.5 2001-03-15 15:43:57+09 naito Exp

数理解析・計算機数学特論 147

このようなプログラムの部分的な仕事を手続きもしくは関数としてはっきりとした部分として定義する

ことができる.

5.14.1 手続き

手続き宣言はプログラムの一部分を定義するものであり, 手続き呼びだし文によって, それらを実行することができる.この宣言は, プログラムと同一の形式であり, プログラムの頭書きの変わりに手続きの頭書きを利用する.

〈手続き宣言 〉 ::= 〈手続き見出し 〉 ; 〈指令 〉 | 〈手続き表示 〉 ; 〈手続きブロック 〉| 〈手続き見出し 〉 ; 〈手続きブロック 〉〈手続き見出し 〉 ::= procedure 〈名前 〉 ( { 〈仮パラメタ節 〉 { ; 〈仮パラメタ節 〉 }} )

〈手続き表示 〉 ::= procedure 〈手続き名 〉〈手続き名 〉 ::= 〈名前 〉〈手続きブロック 〉 ::= 〈ブロック 〉〈指令 〉 ::= 〈英字 〉 { 〈英字 〉 | 〈数字 〉 }〈仮パラメタ節 〉 ::= 〈パラメタ群 〉 | var 〈パラメタ群 〉 | 〈関数引数仕様 〉 | 〈手続き引数仕様 〉〈パラメタ群 〉 ::= 〈名前 〉 { , 〈名前 〉 } : 〈型名 〉〈手続き引数仕様 〉 ::= 〈手続き見出し 〉〈関数引数仕様 〉 ::= 〈関数見出し 〉

手続きを呼び出す場合には, その手続きは呼び出す以前に宣言されていなければならない. 従って, 2つの手続きもしくは関数をお互いに呼びあう場合などには問題が生じる. その場合に利用されるのが, 指令forward である. 指令 forward のついた手続き宣言は, その手続きブロックが後方にあってもよいが,どこかに手続きブロックが存在しなければならない.

5.14.1.1 手続きブロックでの宣言

手続きブロック内で宣言されたラベル, 定数, 型, 変数は, そのブロック内でのみ有効であり, そのブロック外からは参照できない. このようなものを, 局所的に定義されているという. 例えば, 局所的に定義された変数は局所変数と呼ぶ. また, 外のブロックと同じ名前で宣言されたものがある場合には, より内部で定義されたものが有効となる. これを(変数の)隠蔽と呼ぶ.局所変数はプログラムの実行中には, その局所変数を定義するブロックを駆動中の時のみメモリ内に存

在する. それ以外の時には, その局所変数はメモリ内には存在しない.ただし, SPARCompiler の static をつけて宣言された局所変数は, そのブロックの駆動中以外でもメ

モリ内には存在しているが, 外部のブロックからは見ることができない. このような, その変数がどこのブロックから参照可能かの概念を, 変数のスコープと呼ぶ. また, どのような時に変数がメモリ内にあるかの概念を変数の寿命と呼ぶ.

5.14.1.2 手続きの仮パラメタ節と呼びだし

手続きの仮パラメタ節中で定義された変数は, 局所変数と同様に扱うことができる. 実際に, 手続き呼びだし文において書かれた変数を実引数と呼ぶ.

Id: P6.tex,v 1.5 2001-03-15 15:43:57+09 naito Exp

148 数理解析・計算機数学特論

〈手続き呼びだし文 〉 ::= 〈手続き名 〉 〈実引数並び 〉〈実引数並び 〉 ::= ( 〈実引数 〉 { , 〈実引数 〉 } )

〈実引数 〉 ::= 〈式 〉 | 〈変数 〉 | 〈手続き名 〉 | 〈関数名 〉

ただし, 手続き read, readln, write, writeln の場合には, 少々定義が異なる.仮パラメタとしては, 単に変数が書かれた時(値引数), var が指定されている時(変数引数), 手続

き名が指定されている時(手続き引数), 関数名が指定されている時(関数引数)によって, 呼び出しの方法が異なる.

5.14.1.2.1 値引数 値引数の場合には, 仮パラメタ節に記述された型と同値な型を持つ値であることが必要である. この場合, 手続きにはその値のみが渡される. これを値呼びだし (call by value) と呼ぶ.

5.14.1.2.2 変数引数 変数引数の場合には, 仮パラメタ節に記述された型と同値な型を持つ変数であることが必要である. この場合, 手続き中ではその変数が参照されている. (具体的には, 変数のアドレスが渡される.)これを参照呼びだし (call by address) と呼ぶ.

5.14.1.2.3 手続き引数 この場合には, 実引数の手続きは手続きを呼び出したブロックで定義されていなければならない. 手続き中では, その実引数を表している.

5.14.1.2.4 関数引数 この場合には, 実引数の関数は手続きを呼び出したブロックで定義されていなければならない. 手続き中では, その実引数を表している.

5.14.1.3 手続きの副作用

参照呼び出しで渡した変数は, 手続き内ではもとの変数を参照しているので, 手続き内で変更をした場合にも, もとの変数が変更されている. これを手続きの副作用と呼ぶ.

5.14.2 関数

関数は式の評価で利用するために, 単一のスカラ値, もしくはポインタ値を計算するプログラムの一部分である. 関数宣言は関数を定義するものであり, 関数呼びだし文によって, それらを実行することができる.この宣言は, プログラムと同一の形式であり, プログラムの頭書きの変わりに関数の頭書きを利用する.

〈関数宣言 〉 ::=〈関数見出し 〉 ; 〈指令 〉 | 〈関数表示 〉 ; 〈関数ブロック 〉 | 〈関数見出し 〉 ; 〈関数ブロック 〉〈関数見出し 〉 ::= function 〈名前 〉 ( { 〈仮パラメタ節 〉 { ; 〈仮パラメタ節 〉 }} )

〈関数表示 〉 ::= function 〈関数名 〉〈関数名 〉 ::= 〈名前 〉 : 〈結果型 〉〈関数ブロック 〉 ::= 〈ブロック 〉〈結果型 〉 ::= 〈単純型名 〉 | 〈ポインタ型名 〉

この定義を見ればわかるように, ほとんど手続きと同じであるが, その値を返すことだけが異なる.結果の型としては, 単純型及びポインタ型以外を返すことはできない.関数に関することは, 値を返すこと以外は全て手続きと同じである.

Id: P6.tex,v 1.5 2001-03-15 15:43:57+09 naito Exp

数理解析・計算機数学特論 149

〈関数呼びだし文 〉 ::= 〈関数名 〉 〈実引数並び 〉

5.14.3 手続きと関数の例

Example 5.14.1 以下の手続きと, それを呼び出しているプログラムは, 与えられた2つの integer 型の

変数のうち小さくない方を表示するものである.

program print_max (output) ;varx,y : integer ;

procedure print_max(x,y : integer) ;varz : integer ;

beginif (x < y) thenwriteln(y)

elsewriteln(x) ;

end ;

beginx := 1 ;y := 2 ;print_max(x,y)

end.

Example 5.14.2 この例では, 2つの integer 型の変数の和を返す関数を記述している.

program print_sum (output) ;varx,y : integer ;

function return_max(x,y : integer) : integer ;beginreturn_max := x + y ;

end ;

beginx := 1 ;y := 2 ;writeln(return_max(x,y))

end.

Example 5.14.3 この例は, 配列をベクトルと思い, それの長さを返し, さらに, それに直交するベクトルを一つ求めている11 .

11 本当はちょっと問題のある計算である.

Id: P6.tex,v 1.5 2001-03-15 15:43:57+09 naito Exp

150 数理解析・計算機数学特論

program vector (output) ;typevector = array [1..2] of real ;

varx,y : vector ;r : real ;

function get_norm_and_normal (a : vector ; var b : vector) : real ;beginb[1] := a[2] ;b[2] := (-1)*a[1] ;get_norm_and_normal := sqrt(a[1]*a[1] + a[2]*a[2]) ;

end ;

beginx[1] := 1.0 ;x[2] := 0.0 ;r := get_norm_and_normal(x,y) ;writeln(’length of (’,x[1],’,’,x[2],’) = ’,r) ;writeln(’normal of (’,x[1],’,’,x[2],’) = (’,y[1],’,’,y[2],’)’) ;

end.

このように, 関数の結果型とはなり得ないものを求めるには, 関数もしくは手続きの副作用を利用することができる.

Example 5.14.4 この例は, 異なった関数に対して, f(x) + g(y/2) の値を求める関数である.

program sum_of_function_values (output) ;varx,y : real ;

function sin_ft(a : real) : real ;begin

sin_ft := sin(a) ;end ;

function cos_ft(a : real) : real ;begin

cos_ft := cos(a) ;end ;

procedure ch(var a : real) ;begin

a := a/2.0 ;end;

function sum_of_function_values (a,b : real;procedure ch (var a : real) ;function f(a : real) : real ;function g(a : real) : real ) : real ;begin

ch(b) ;sum_of_function_values := (f(a) + g(b)) ;

end ;

beginx := 0.0 ;y := 1.0 ;writeln(x,y,sum_of_function_values(x,y,ch,sin_ft,cos_ft)) ;

end.

Example 5.14.5 この例は, 上の例と同じ結果をもたらすが, forward を利用している.

Id: P6.tex,v 1.5 2001-03-15 15:43:57+09 naito Exp

数理解析・計算機数学特論 151

program sum_of_function_values (output) ;varx,y : real ;

procedure ch(var a : real) ; forward ;

function sin_ft(a : real) : real ;begin

sin_ft := sin(a) ;end ;

function cos_ft(a : real) : real ;begin

cos_ft := cos(a) ;end ;

function sum_of_function_values (a,b : real;

function f(a : real) : real ;function g(a : real) : real ) : real ;begin

ch(b) ;sum_of_function_values := (f(a) + g(b)) ;

end ;

procedure ch ;begin

a := a/2.0 ;end;

beginx := 0.0 ;y := 1.0 ;writeln(x,y,sum_of_function_values(x,y,sin_ft,cos_ft)) ;

end.

5.14.4 再帰的関数呼びだし

前の section で解説した関数, 手続きは, それ自身をその内部で呼び出すことができる. これを再帰的関数呼び出し (recursive function call) と呼ぶ.再帰的関数呼び出しを利用すると, 帰納的に定義されたものを計算することが容易になる. 数学的には,

再帰で計算できるものは必ず再帰を利用しなくても計算できることが証明されているが, 再帰で計算するとプログラムが簡潔になるという利点がある. 一方, 関数, 手続き等を呼び出す際には, 多くの処理系において呼び出しの手順として時間がかかることが多い. したがって, 再帰には時間がかかることが多い.

Example 5.14.6 次は, 帰納的に定義された数列 an+1 = an + 2, a1 = 1 の an を求める関数である.

function recursive_function(n : integer) : integer ;beginif (n = 1) thenrecursive_function := 1

elserecursive_function := recursive_function(n-1) + 2 ;

end ;

再帰を用いたアルゴリズムは, 必ず再帰を用いなくても実現できる. 再帰を用いると, メモリ内のスタック(stack) 領域を大量に消費する. したがって, あらかじめどのくらいの回数の再帰が行われるかを見積もり,余りに回数が多い場合には, 再帰以外の方法を考えた方が良い場合もある.

Id: P6.tex,v 1.5 2001-03-15 15:43:57+09 naito Exp

152 数理解析・計算機数学特論

5.14.5 整合配列パラメータ

Pascal の大きな欠点として, サイズの異なる配列は異なった型となることが挙げられる. したがって,Example 5.14.3 の2次元ベクトルの長さを求める関数を, 3次元ベクトルの長さを求める関数にそのままでは利用できない. そのために ISO 規約で処理系に任意で採用することが認められているものとして, 整合配列パラメータがある.

Example 5.14.7 任意のサイズのベクトルの長さを与える関数 get_normを整合配列を利用して実現する.

program vector (output) ;type

Positive = 1..maxint ;varx : array [1..2] of real ;y : array [1..3] of real ;r : real ;

function get_norm (var a : array [A..B : Positive] of real) : real ;varn : real ;i : Positive ;

beginn := 0.0 ;for i:=A to B dobeginn := n + a[i]*a[i] ;

end ;get_norm := sqrt(n) ;

end ;

beginx[1] := 1.0 ;x[2] := 0.0 ;(* この呼び出しは正しい *)r := get_norm(x) ;writeln(’length of (’,x[1],’,’,x[2],’) = ’,r) ;(* この呼び出しは正しい *)y[1] := 1.0 ;y[2] := 0.0 ;y[3] := 0.0 ;r := get_norm(y) ;writeln(’length of (’,y[1],’,’,y[2],’,’,y[3],’) = ’,r) ;

end.

Example 5.14.7 の関数仮引数 array [A..B : Positive] of real が整合配列を表し, 関数呼び出しの時点で A, B が実引数の配列範囲の値と置き換えられる.しかし, 整合配列は「任意」と定められているため, これに対応しない処理系も多く存在する.

5.14.6 演習問題

Exercise 5.14.1 二つの複素数を与えた時, その和, 差, 積等を計算するプログラムを手続きもしくは関数を利用して書け.

Exercise 5.14.2 Fibonacci 数列を計算するプログラムを書け.

Exercise 5.14.3 与えられた整数係数 n × n 行列( n はプログラム中で指定して良い)の行列式を計算

するプログラムを書け.

Id: P7.tex,v 1.3 2001-03-11 17:32:25+09 naito Exp

数理解析・計算機数学特論 153

5.15 ファイル入出力

ファイルへの入出力を行なうには, ファイル型変数と, read, write 手続きを利用する.ファイル変数を宣言すると, その要素の型を持ったバッファ変数が自動的に定義される. それをファイル

変数 f に対して f^ と表す. バッファ変数は, ファイルに対して既存の要素を読みだしたり, 書き出したりすることができる窓であると理解する. この窓は, ファイル演算によって自動的に移動する.

5.15.1 ファイル処理の関数と演算子

バッファ変数 f^ がファイル f の終りを通過すれば, 標準の論理関数 eof (f) は true を返し, そうでなければ false となる.ファイル演算子には標準的には次のものがある.

reset(f,filename) 読み込みを開始するために, バッファ変数を初期化する. すなわち, f の最初の要素に f^ をセットする. f が空でなければ eof(f) は false となる. f が空であれば f^ は不定となり,eof(f) は true となる.

rewrite(f,filename) ファイル f の書換えをするには, reset(f) の代わりにこれを用いる. このとき,eof(f) は true となる.

get(f) バッファ変数を次の要素に進める. すなわち, 次の要素の値をバッファ変数に代入する. そのような要素がなければ, バッファ変数の値は不定となり, eof(f) は true となる.

put(f) ファイルにバッファ変数の値を付け加える. これは, 実行の直前で eof(f) が true の時に限り定義される. その結果は eof(f) が true となり, バッファ変数の値は不定となる.

実際には, get(f), put(f) の代わりに手続き read, write を用いる. それぞれは, 次と等価である.

read(f,x): x := f^ ; get(f)

write(f,x): f^ := x ; put(f)

Example 5.15.1 この例は, integer からなるファイルを作成し, そこからデータを読み出している.

varf : file of integer ;i, n : integer ;

beginrewrite(f,’integer_file’) ;for i:=1 to 10 dowrite(f,i) ;

close(f) ;reset(f,’integer_file’) ;while (not(eof(f))) dobeginread(f,i) ;writeln(i) ;

end ;close(f)

end.

利用したファイルは利用後にその利用が終ったことを OS に対して知らせることが必要になることがおおい. そのために利用するのが close(f) という手続きで, この手続き自身は SPARCompiler で定義されてるものだが, 多くの処理系で対応するものが存在する. また, open 手続きというのもあり, reset, rewriteよりも細かいファイルの制御が可能になる.

Pascal は OS との関係については, その仕様には全く含まれないので, このような OS に関係する部分に関しては, 処理系定義となることが多い.

Id: P7.tex,v 1.3 2001-03-11 17:32:25+09 naito Exp

154 数理解析・計算機数学特論

5.15.2 テキストファイル

テキストファイルに対しては, writeln, readln, eoln が利用できる.

eoln(f) テキストファイル f が現在の行の終りに達したかどうかを判定する. true ならば, バッファ変数は行末に位置している.

writeln(f,x) テキストファイル f に対して, x を出力し, 現在の行を終了する. すなわち, 行末文字を印字する.

readln(f,x) テキストファイル f に対して, x を読み, 次の行の先頭まで読みとばす. バッファ変数は次の行の最初の文字となる.

ここで, ファイル変数が省略された場合には, writeln は標準出力に対して, readln は標準入力に対して行なわれる. また, SPARCompiler では標準エラー出力は errout と書かれる.

5.15.2.1 read, readln

手続き read, readln は,

read(file,v1,v2,v3)

といった書き方ができる. この場合, ファイルから, v1, v2, v3 を一度に読むことを指示している.

5.15.2.2 write, writeln

手続き write, writeln は,

write(file,v1,v2,v3)

といった書き方ができる. この場合, ファイルに v1, v2, v3 を書き出すことを指示している.また, これをテキストファイルに適用する場合には, その引数として,

e:A:B

といった型をとれる. ここで, A, B は書き出す長さを指定する部分であり, その default の値は処理系定義である.e はファイルに書き出す値を表した式であり, その型は, 整数型, 実数型, 文字型, 論理型, 文字列型でな

くてはならない.

5.15.2.2.1 文字型の時 文字型の時,

e:A

という型をとり, ファイルに書き出される表現は

• (A− 1) 個の空白,• e の文字値

である.

5.15.2.2.2 文字列型の時 文字列型の時,

e:A

という型をとり, e のあらわす文字列を, つぎのようにして書く. ここで, L をその文字列の長さとする.

1. A > L の時.

Id: P7.tex,v 1.3 2001-03-11 17:32:25+09 naito Exp

数理解析・計算機数学特論 155

• (A− L)個の空白.• e を順に並べたもの.

2. A ≤ L の時.

• e を順に A 番めまで並べたもの.

5.15.2.2.3 論理型の時 論理型の時,

e:A

という型をとり, e の値を文字列 true もしくは false を利用して書かれる. その文字列は, A を利用して書かれる. この時, 文字列が大文字になるか, 小文字になるかは処理系定義である.

5.15.2.2.4 整数型の時 整数型の時,

e:A

という型をとり, e の10進表現がファイルに書かれ, A に関しては, 次のようになる.

1. A ≥ IntDigit +1 の時.

• (A− IntDigit− 1) 個の空白,• e が負の時, 符号 -, それ以外の時空白• IntDigit 桁の abs(e) の10進表現

2. A < IntDigit +1 の時.

• e が負の時, 符号 -, それ以外の時空白• IntDigit 桁の abs(e) の10進表現

ここで, IntDigit は, その絶対値を10進表現するために必要な最小の桁数.

5.15.2.2.5 実数型の時 実数型の時には, 浮動小数点表現と固定小数点表現がある.

5.15.2.2.5.1 浮動小数点表現

e:A

という型をとり, e の浮動小数点表現がファイルに書かれ, A に関しては, 次のようになる.

• 符号. e <0 かつ, eWritten > 0 の時 -, それ以外の時空白,• eWritten の10進表現の第1桁めの数字,• 文字 .,• eWritten の10進表現の第2桁め以降の DecPlaces 個の数字,• 処理系定義の指数部表現 e または E,• ExpValue の符号, ExpValue < 0 の -, それ以外の時 +,• ExpValue の ExpDigit 桁の10進表現. (必要なら先頭に文字 0 が並ぶ).

ここで, ExpDigit は処理系定義の値. eWritten, DecPlace, ExpValue は, 次のように定義されている.

• 整数 X に対して, TenPower(X) を 10 の X 乗の値 (実数).• 実数 X に対して, RealSize(X) を, 次のような整数 Z: TenPower(Z-1) ≤ abs(X) < TenPower(z).• 実数 X, 整数 Y に対して, Truncate(X,Y) を X の小数点以下 Y 桁より下を切捨てた実数の値.

とする. この時, ActWidth を

if A >= ExpDigit + 6 then ActWidth := Aelse ActWidth := ExpDigit + 6 ;

Id: P7.tex,v 1.3 2001-03-11 17:32:25+09 naito Exp

156 数理解析・計算機数学特論

で定義する. これで, eWritten, DecPlace, ExpValue は,

DecPlace := ActWidth - ExpDigit - 5 ;if e = 0.0 thenbegineWritten := 0.0 ; Expvalue = 0

endelsebegineWritten := abs(e) ;ExpValue := RealSize(eWritten) - 1 ;eWritten := eWritten / TenPower(ExpValue) ;eWritten := eWritten + 0.5 * TenPower(-DecPlace) ;if eWritten >= 10.0 thenbegineWritten := eWritten / 10.0 ;ExpValue := ExpValue + 1 ;

end ;eWritten := Truncate(eWritten,DecPlace) ;

end.

Example 5.15.2 例えば, 10.255 という値を writeln(x:9)で表示すると, 1.03e+10 となる. ExpDigit= 2 とすると, ActWidth = 9, DecPlace = 2, eWritten = 1.0300, ExpValue = 1 となる. 9 は全部の

出力する文字数を表し, 符号(1桁), 小数点の記号(1桁), e(1桁), 指数部(符号を含む)(3桁)の桁数を引いた残りの桁 9-1-1-1-3 = 3 桁の仮数部を出力する. その際に, 仮数部の 3 + 1 = 4 桁めを四

捨五入している.

5.15.2.2.5.2 固定小数点表現

e:A:B

という型をとり, e の固定小数点表現がファイルに書かれ, A に関しては, 次のようになる.

• A ≥ MinNumChar なら (A− MinNumChar) 個の空白,• 符号. e <0 かつ, eWritten > 0 の時 -,• eWritten の10進表現の最初の IntDigit 個の数字,• 文字 .,• eWritten の10進表現のその次の B 個の数字.

ここで, ExpDigit は処理系定義の値. eWritten は, 次のように定義されている. TenPower, RealSize,Truncate は前と同じ. これで, eWritten は,

if e = 0.0 thenbegineWritten := 0.0

endelsebegineWritten := abs(e) ;eWritten := eWritten + 0.5 * TenPower(-B) ;eWritten := Truncate(eWritten,B) ;

end.

であり, IntDigit は,

if RealSize(eWritten) < 1 thenIntDigit = 1

elseIntDigit = RealSize(eWritten) ;

MinNumChar は,

Id: P7.tex,v 1.3 2001-03-11 17:32:25+09 naito Exp

数理解析・計算機数学特論 157

MinNumChar := IntDigit + B + 1 ;if (e < 0.0) and (eWritten > 0) thenMinNumChar := MinNumChar + 1 ;

Example 5.15.3 例えば, 10.255という値を writeln(x:3:1)で表示すると, 10.3となる. ここの x:3:1

の最初の 3 は, 全部の桁数を表し, 後ろの 1 は小数点以下の桁数を表している. ExpDigit = 2 とすれば,eWritten = 10.3, IntDigit = 2, MinNumChr = 4 となる. おおよそ, 上から 3+1=4 桁めを四捨五入して

いる. 実際には, writeln(10.25:3:1) は 10.2 を出力し, writeln(10.250000000000001:3:1) は 10.3

を出力する.

Remark 5.15.1 SPARCompiler では, writeln 手続きの桁指定子に関してはもっと多くの自由度がある.詳しくは SPARCompiler のマニュアルを参照.

5.15.3 演習問題

Exercise 5.15.1 標準入力から入力されたファイルの行数を印字するプログラムを書け.

Exercise 5.15.2 標準入力から入力されたファイルの行数, 単語の数, 文字数を印字するプログラムを書け.

Exercise 5.15.3 標準入力から入力された1文字が英字でなければ, そのまま印字し, 大文字の英字なら小文字に, 小文字の英字なら大文字を印字するプログラムを書け. ただし, 改行だけが入力された場合には,終了するようにしなさい. また, 入力された文字は ASCII コードで表現されていることに注意せよ.

5.16 いくつかのデータ構造

レコード型を利用すると, アルゴリズムの実現に役立つリスト (list), ツリー (tree) といったデータ構造を実現することができる.これらのデータ構造は, そのデータ量があらかじめわかっているならば, ポインタを利用せずに実現でき

るが, データ量がアプリオリにはわからない時にはポインタを使わざるをえない. ここでは, これらのデータ構造が, どのようなものかを見ていこう.

5.16.1 リスト

リストとは, データが一列につながったものである. 各データは次のデータへのポインタを持ち, 最後のデータが持つ次のデータへのポインタは何も指し示していないという形で実現できる. リストになったデータを操作するには, ポインタを動かせば良い.具体的には, 次のような形式になっている.

5.16.1.1 ツリー

ツリーとは, 各データが1つ以上の他のデータへのポインタを持ったものである. 各データは他のデータへのポインタを持ち, 最後のデータが持つ他のデータへのポインタは何も指し示していないという形で実現できる.具体的には, 次のような形式になっている.

Id: P8.tex,v 1.2 2000/04/21 02:17:03 naito Exp

158 数理解析・計算機数学特論

5.16.1.2 自己参照

レコード型は, それ自身を参照することができる. これを利用して, リストやツリーといったデータ構造を実現することができる.例えば, リストを実現するには, 次のような方法を利用する.

Example 5.16.1 整数と, 次のデータへのポインタを持った構造体は以下のように定義できる.

typelink = ^data ;data = record

str : integer ;next : link ;

end ;

このように定義したレコード型を利用して, リストを実現することができる.

Example 5.16.2 Example 5.16.1 で定義した構造体を初期化する. 即ち, 一番始めのデータには何も入れない. はじめに, このようにして定義した自己参照レコード型の変数を確保するために, 手続き new を行な

う. 次に必要なことは, 一番最後のデータ(最初は一番はじめのデータと同じ)にデータを入力したら, もう一つデータを持ってきて, それを初期化することである.

Id: P8.tex,v 1.2 2000/04/21 02:17:03 naito Exp

数理解析・計算機数学特論 159

program list (output) ;type

link = ^data ;data = record

str : integer ;next : link ;

end ;

varfirst, p, q : link ;i : integer ;

beginnew(first) ;first^.next := nil ;for i:= 1 to 10 dobegin

new(p) ;p^.next := first ;p^.str := i ;first := p ;

end ;q := p ;while(q^.next <> nil) dobegin

writeln(q^.str) ;q := q^.next ;

end;end.

このようにして作ったリスト形式のデータを一番最初のデータから順にアクセスするためには, 最初のデータを指し示すポインタを作成して, next が次のデータを指ししていることを利用して, ループを使ってアクセスすれば良い.

Example 5.16.3 ここでは, リストの途中にデータを挿入するための手順を示している.

Id: P8.tex,v 1.2 2000/04/21 02:17:03 naito Exp

160 数理解析・計算機数学特論

program list (output) ;type

link = ^data ;data = record

str : integer ;next : link ;

end ;

varfirst, p, q, r : link ;i : integer ;

beginnew(first) ;first^.next := nil ;for i:= 1 to 10 dobegin

new(p) ;p^.next := first ;p^.str := i ;first := p ;

end ;q := p ;while(q^.next <> nil) dobegin

writeln(q^.str) ;q := q^.next ;

end;writeln ;new(r) ;r^.str := 11 ;q := p ;q := p^.next ;r^.next := q^.next^.next ;q^.next := r ;q := p ;while(q^.next <> nil) dobegin

writeln(q^.str) ;q := q^.next ;

end;end.

この例では, 前から2番めに新しいデータを挿入している.

p

p

p^.next

r^.next

r

ここで確保したメモリ領域は, 必要がなくなったら, dispose 手続きで領域を開放する.

Example 5.16.4 各データが2つのポインタを持ったツリーを実現するには, 次のようなデータ形式を利用すれば良い.

Id: P8.tex,v 1.2 2000/04/21 02:17:03 naito Exp

数理解析・計算機数学特論 161

typelink = ^data ;data = record

str : integer ;lnext : link ;rnext : link ;

end;

5.16.2 演習問題

Exercise 5.16.1 標準入力から, 空白で区切られた学籍番号, ID, 氏名の組を読み込み, それらをレコード型の配列として格納し, 標準出力に以下のフォーマット (format) (書式)で出力するプログラムを書け.

学籍番号: xxxxxID : xxxxx氏名 : xxxxx

この際, 入力するデータの数は 100 以下と仮定して良い.

Exercise 5.16.2 Exercise 5.16.1 の問題をリスト形式で書け. この時は, 入力するデータの数はアプリオリにはわからない.

5.17 標準関数

ここでは, 今までに述べなかった Pascal の標準関数を書いておく.

5.17.1 ファイル処理

page(f) テキストファイル f の次の行を新しいページの先頭から印刷するように, 印刷装置に指示する.

5.17.2 算術関数

abs(x) x の絶対値を計算する. x は整数型か実数型. 結果の型は x と同じ.sqr(x) x の2乗を計算する. x は整数型か実数型. 結果の型は x と同じ.sqrt(x) x の平方根を計算する. x は整数型か実数型. 結果の型は実数型.sin(x) sin(x) を計算する. x は整数型か実数型. 結果の型は実数型.cos(x) cos(x) を計算する. x は整数型か実数型. 結果の型は実数型.arctan(x) arctan(x) を計算する. x は整数型か実数型. 結果の型は実数型.exp(x) exp(x) を計算する. x は整数型か実数型. 結果の型は実数型.ln(x) loge(x) を計算する. x は整数型か実数型. 結果の型は実数型.

5.17.3 論理関数

odd(x) x は整数型. x が奇数ならば結果は真. そうでなければ偽.

Id: P-etc.tex,v 1.4 2001-03-11 17:33:14+09 naito Exp

162 数理解析・計算機数学特論

5.18 その他の注意

5.18.1 日本語の文字列について

SPARCompiler 他の処理系において, JIS コードからなる日本語を含む文字列を利用しようとすると, コンパイラが通らない時がある. これは, 次のような事情による. Pascal においては, ’ が, C においては "

が文字列の始まりと終りを表している. 一方,「是」という文字は JIS コードでは 0x4027 と書けているので, 文字列 ’是’ は, 完全に日本語化されていない Pacal コンパイラでは @’ と解釈されエラーとなる. 同様に C でも, そのコードに 0x22 を含む漢字はエラーとなることがある.これを避けるには, コンパイラに通すプログラムコードを EUC 漢字コードに直すと良い. UNIX におい

て EUC 漢字コードに変換するには, プログラム nkf を利用する. nkf は引数として与えられたファイル

を, オプションとして与えられた漢字コードに変換して標準出力に出力する. したがって, 次のようにすれば良いことがわかる.

% nkf -e filename > eucfile

ここで, オプション -e は EUC 漢字コードに変換することを指示している.

5.18.2 ランタイム・エラーを起こした時

ここでは, 実行時のエラーを起こした時の対処法を述べておく. SPARCompiler Pascal では, 実行時にエラーを検出すると, 自動的に dbx と呼ばれるデバッカが起動する.

Example 5.18.1 次のようなプログラムを実行してみる.

program test (input,output) ;vari : integer ;

beginread(i) ;

end.

このプログラムで, integer 型以外の入力を行なうと, 次のような出力が行なわれ, プログラムの実行が停止する.

standard input : Bad data found on integer read

*** ./a.out terminated by signal 5: trace/breakpoint trap*** Traceback being written to a.out.trace

これは, a.out というプログラムを実行した時に, 予期しないデータ・フォーマットの入力が行なわれたので, 実行時エラーを検出して, 自動的にデバッガが起動されている.もし, この状態で止まってしまったら, quit と入力すればよい. その後, dbx のトレース・ファイル (実

行したプログラム名に .trace をつけた名前のファイル)を消去する必要がある.

References

[1] K. Jensen and N. Wirth. PASCAL(原書第4版). 培風館, 1993.

[2] ISO. Pascal の標準化 –ISO 規格全訳とその解説–. 共立出版, 1984.

[3] B. W. Kernighan and P. J. Plauger. プログラム書法(第2版). 共立出版, 1982.

Id: Pascal.tex,v 1.1 2001-03-20 15:36:26+09 naito Exp

数理解析・計算機数学特論 163

[4] B. W. Kernighan and P. J. Plauger. ソフトウェア作法. 共立出版, 1982.

[5] B. W. Kernighan and P. J. Plauger. Software Tools in Pascal. Addison-Wesley, 1981.

[6] B. W. Kernighan and P. J. Plauger. プログラミング作法. アスキー出版局, 2000.

[7] A. R. Feuer and N. Gehani. Ada, C, Pascal. 工学社, 1981.

[8] S. McConnel. Code Complete. アスキー出版局, 1994.

[9] E. Post. Real programmers don’t use Pascal.http://www.mit.edu/people/rjbarbel/Humor/Computers/real.programmers, 1982.

[10] N. Wirth. アルゴリズム+データ構造=プログラム. 日本コンピュータ協会, 1979.

[11] N. Wirth. アルゴリズムとデータ構造. 近代科学社, 1990.

[12] 日本規格協会. JISハンドブック(情報処理-プログラム言語編). 日本規格協会.

Id: Pascal.tex,v 1.1 2001-03-20 15:36:26+09 naito Exp

数理解析・計算機数学特論 165

第6章 C 言語入門

6.1 ここでの目的

ここでは C 言語の文法を中心に解説をするが, 単に C 言語を用いてプログラムが書けるようになることが目的なのではなく, C 言語の言語構造を理解し, C 言語の特徴である移植性の高いプログラムを書けるようにすることが重要である.またプログラムは, 自分自身だけが読むものではなく他人にも読めるように, わかりやすく簡潔に書くべ

きである. これらのことを念頭において学ぶことを期待したい.

6.2 C 言語とは

プログラム言語 C は B. Kernighan と D. Ritchie によって1972年に開発された [1]. 元々は, UNIXオペレーティングシステムを記述するために開発された言語で, システムを記述する能力や可搬性に優れるという特徴を持つ. そのため, C 言語を理解するには, オペレーティングシステムの知識, ハードウェアの知識などが必要であり, 初学者には敷居の高い言語であることは事実である.しかしながら, その移植性の高さとシステム記述能力の高さにより, UNIX をはじめとする各種のシステ

ム上のアプリケーションは, 現在でも C で記述されているものが多い1 . C 言語の文法は機械にとっては理解しやすい形式を持っていて, その分だけプログラマにとっては難解な部分も多い. しかし, 機械にとって理解しやすいということは, 処理系の記述が易しいということであり, 現在ではほとんどすべての OS 上で C 言語処理系が存在している.この章では, ANSI 規格の C 言語を [2] の内容にそって解説を行う. なお, C 言語の標準規格は ISO/IEC

9899-1990 であり, その規格書は ANSI から入手可能である. また, ANSI C はそのまま JIS X3010-1993となっているので, 日本語訳は JIS ハンドブック [3] で入手可能である. ANSI C の Rationale (基本概念)部分は [4, 5] で入手可能である.

6.3 C 言語をはじめる前に

C 言語によるプログラミングをはじめる前に, 後の混乱を避けるための注意をする. 以下の注意書きは,無用なトラブルを回避するためできる限り守った方がよい.

• 各ファイルには, その中身がわかるような簡潔なファイル名をつけること.• 各プログラム毎にサブディレクトリを作成することが望ましい.• test.c など, 既存のプログラム名に .c を付加したファイル名は避けること.• 不要になったファイルは削除すること.

多くのプログラムソースを書いた時に, それぞれのファイルがどのような内容のものかがわからなくなることが良くある.

1 最近では JAVA などの言語も流行であるが, JAVA は C を元にした仕様を持ち, クラスライブラリによって, システム仕様などを吸収している部分が多く, コンピュータの理解のためには C の方が望ましいと考えられている.

Id: C1.tex,v 1.8 2001-03-17 13:30:57+09 naito Exp

166 数理解析・計算機数学特論

また, プログラムソースを見やすくするために, 「字下げ」をきちんとすること. Mule で XXX.c という

ファイルを編集する際の「字下げ」の方法は各行頭で Tab����を押すことにより行なう. これだけで Mule が状況に応じて適当に処理してくれる2 .

6.4 C のプログラムの書き方・実行の方法

講義で利用する処理系は gcc とよばれるコンパイラである. 実行コードを作成するには以下の手順で行なう.

1. エディタ (Mule など)を利用して, プログラムソースを書く.2. コンパイラを起動して, 実行コードを作成する.3. 作成した実行コードを実行する.

例えば, Mule を利用して, hello.c というプログラムソースを作成した時, これをコンパイルするには, 以下のようにする.

% gcc hello.c -o hello

最後の -o hello という部分は, 実行コードを hello という名前で作成することを指示している. もし, -ohello という部分を省くと, コンパイラは a.out という名前で実行コード3 を作成する.ここで作成した hello を実行するには

% ./hello

とする. ここで, ./ をわざわざ指定していることに注意せよ4 .

Exercise 6.4.1 test.c というプログラムを

% gcc test.c -o test

として, 作成して,

% test

とすると, どのようなことが起こるか. それは何故か?

より高度なプログラムを書く場合には, 複数のファイルからなるプログラムを作成することがある. そのような場合には, 一度にコンパイルすることはできないので, それぞれのファイルをコンパイルして, リンクと呼ばれる操作で実行コードを作成する.

6.5 C 言語の基本的な注意

6.5.1 C の基本的な構造

C のプログラムは, コメント(注釈) (comment), プリプロセッサ命令 (preprosessing), 文 (state-ment), 関数 (function) の集まりで構成されている. それぞれの文は, ; で終るか, { } で囲まれた文の集

まりである複文 (compound statement) で構成されている. 関数自身もまた文である. 実際には, mainという名前の特別な関数がはじめに実行され, そこに記述されている順序にしたがって実行される.

2 これは mule の “c-mode” の特徴である.3 多くの UNIX 上の処理系では, コンパイラが出力する実行コードのデフォールトの名前は a.out になる. これは, Assembra

Output の略. 最近の LINUX, FreeBSD では elf という名前になるものがある. これら実行形式の名前の違いは, 実行形式の違いでもある.

4 UNIX ではカレントディレクトリはデフォールトではコマンドサーチパスには入っていないし, 入れない方が望ましい.

Id: C1.tex,v 1.8 2001-03-17 13:30:57+09 naito Exp

数理解析・計算機数学特論 167

6.5.2 C で利用できる文字

C では, 英文字, 数字, 空白文字(スペース, タブなど), 記号文字, 改行文字などが利用できる. 記号文字には特別な意味があることが多いので, 注意すること. また C では, 大文字と小文字は区別される. C のコンパイラにおいて, 日本語が利用できるといっても, 変数名などに日本語が利用できるわけではないので注意すること. 日本語が利用できるのは, 文字列に日本語が利用できるという程度の意味であり, 今回利用する処理系では, このコードは EUC でなければならない.また, 以下のものは全て空白と見なして無視される.

• 空白文字, 改行文字, タブ, 改ページ記号, コメント.

行末に \ を書くと, 行の連結を表し, 1行として扱われる.

6.5.2.1 行

C 言語では「行」という概念は存在しない. 改行文字は空白文字と見なされるが, 改行文字の直前に / が

ある場合には, 行の連結を表し, 処理系によって / と改行文字の連続は一つの空白文字に置き換えられる.

6.5.3 コメント

コメント (comment) とは, プログラミングの補助となるようソースプログラム中に書かれた注釈部分のこと. コンパイラは単にこれを無視するので, プログラムには影響を与えない. コメントは, /* ではじ

まり, */ で終り, 入れ子にはできない. 即ち, コメントの中にコメントを入れることはできない.また, 文字定数, 文字列の中にはコメントを書くことはできない.

6.5.4 トークン

トークン (token) とは, 空白やコメントによって区切られた文字の列で, コンパイラが認識する最小の単位である. トークンは以下のいずれかに分類される.

• 演算子 (+ - など)• デリミタ(区切り子) ({ } ( ) ; など)• 定数(整数, 浮動小数点数, 文字定数)• 文字列リテラル• 識別子• キーワード

6.5.4.1 定数

C 言語における定数 (constant) は整数定数, 浮動小数点定数, 文字定数, 列挙定数に分類される. 整数定数は, 8進数, 10進数, 16進数による表現が可能である. それぞれを区別するには, 以下の規約による.

• 16進数:0x で始まり, 後に 0~9, a~f がいくつか続く. a~f と x は大文字でも良い.• 10進数:0以外ではじまり, 後に 0~9 がいくつか続く.• 8進数 :0 で始まり, 後に 0~7 がいくつか続く.

0x10 を 0x0010 と書いても良い.また, 整数定数に u または U をつけると, 符号なしの数を表し, l または L をつけると “長い” 整数

(long) を表す.浮動小数点定数は, 以下の形をしている.

Id: C1.tex,v 1.8 2001-03-17 13:30:57+09 naito Exp

168 数理解析・計算機数学特論

整数部 . 小数部 e 指数 接尾子

“e 指数” 部分は省略することができる. また, 指数の記号 e は E を用いても良い. 接尾子は以下のいずれか.

• f または F: float 型• 接尾子なし: double 型• l または L: long double 型

文字定数とは, ’ で括られた文字の列である. 例えば a という文字を表すには ’a’ と書く.以下の特別な文字を表す以外には, 文字定数は一文字でなくてはならない.

• \n 改行

• \r 復帰

• \f 改ページ

• \t 水平タブ

• \v 垂直タブ

• \b 後退

• \a ベル

• \? ?

• \’ ’

• \" "

• \\ \

• \ooo 8進数で ooo. 3桁以下• \xhh 16進数で hh. 2桁以下

このような特別な文字のことをエスケープ文字 (escape charactor) と呼ぶ.

6.5.4.2 文字列リテラル

文字列定数とも呼ばれ, " で囲まれた文字の列である. 文字列リテラル5 (string literal) が記憶域に格納される時には, 末尾に \0 が付けられる. また, 隣接する2つ以上の文字列リテラルは連結される. 例えば "abc" "ABC" は "abcABC" となる.また,

"abc""ABC"

は "abcABC" に連結される.

6.5.4.3 識別子

識別子 (identifier) とは, 変数, 関数などに付けられる名前のことである. ここで与えられた名前にしたがって, コンパイラはそれぞれを区別する.識別子として使える文字は, 英文字, 数字, _ であって, 数字を先頭にすることはできない. また, C 言語

の規約によって, 31 文字までは区別され, 大文字と小文字は区別される6 . 即ち, 32 文字目以後が異なるような2つの名前は区別されるとは限らない.

5 「リテラル」 (“literal”) とは, 「文字通りの」という意味である.6 正確には,• 内部識別子またはマクロ名においては意味のある先頭の文字数は 31 文字であり, 大文字と小文字が区別される.• 外部識別子においては意味のある先頭の文字数を 6 文字に制限して良く, 大文字と小文字の区別を無視しても良い.

というのが ANSI の規格 [3, X3010 6.1.2, p. 1856] である. しかし, 最近の処理系で外部識別子が6文字に制限されたり, 大文字と小文字の区別を無視するようなものは見当たらない.

Id: C1.tex,v 1.8 2001-03-17 13:30:57+09 naito Exp

数理解析・計算機数学特論 169

また, C 言語には名前空間, スコープという概念があり, 同じ名前を与えても, 違うものを示していることがあるので注意すること. これについては後ほど解説する.

6.5.4.4 キーワード

以下の単語はキーワード (keyword) と呼ばれ, 特別な意味を持ち, 識別子としては利用できない.char double int long enum float shortunsigned signed void typedef auto register externstatic volatile union struct const sizeof ifelse switch case default break return forwhile do continue goto

6.5.5 文

文 (statement) とは, C 言語のプログラムの基本的な単位になるもので, それぞれの文は ; によって

終了する. 一つの文が複数行にわたっても良い.

Example 6.5.1 次の各行は全て文である.

int n ;printf("Hello World.\n") ;x + y ;a = b ;;

最後の行は空文と呼ばれる.

また, {, } によって文の集まりを一つの文(複文 (compound statement))にすることができる.

Example 6.5.2 次は一つの複文である.

{a = b ;c = d ;

}

6.5.6 式

式 (expression) とは, 計算を行なう最小単位のことで, それぞれの式は値を持つ. その値は, 次のようにして決まる.

• 計算式の場合は, その結果.• 代入式の場合は, その左辺の値.• 関数の場合は, その戻り値.• 比較の場合, 真ならば 1, 偽ならば 0.

式に ; をつけることにより, 文にできる. それを式文 (expression statement) と呼ぶ.

Example 6.5.3 次のようなものは式文である.a ; /* 値は a */x + y ; /* 値は x + y */a = b ; /* 値は代入された a の値 */printf("Hello World.\n") ; /* 値はこの関数の戻り値 */a = b = c ; /* 値は代入された a の値 */a = b, c = d ; /* 値は最後に代入された c の値 */a < b ; /* 値は真ならば 1, 偽ならば 0 */

Id: C1.tex,v 1.8 2001-03-17 13:30:57+09 naito Exp

170 数理解析・計算機数学特論

6.6 用語

6.6.1 処理系とその動作

C において, 処理系 (implementation) とは, 特定の環境中の特定のオプションの下で, 特定の実行環境用のプログラムに翻訳を行うソフトウェア群を指し, 処理系依存 (implementation-defined behavior)とは, その処理系ごとにどのように振舞いかが規定されているものである. 一方, 不定(または未定義)(undefined behavior) とは, 同じ処理系であっても, その振舞いが規定されていないものを指す. 特に,最適化(オプティマイザ (optimizer))の指定によって振舞いが変わることが多い. 未規定 (unspecifiedbehavior) とは, 規格がその動作を一切指定しないものを指す. この他に, 処理系依存の項目の一部として,文化圏固有動作 (locale-specific behavior) がある. これは, 処理系そのものに依存するのではなく, 処理系動作またはプログラム動作中に与えられた文化圏情報 (locale) ごとに, 処理系が明示的に動作を規定するものである.

Example 6.6.1 未規定の動作の例としては, 関数の実引数の評価順序. 不定(未定義)の動作の例としては, 整数演算のオーバフローに対する動作. 処理系定義の例としては, 符号付き整数を右シフトした場合の最上位ビットの伝播. 文化圏固有動作の例としては, islower 関数が26個の英小文字以外の文字に関して, 真を返すかどうかがある.

ANSI の規格にしたがった処理系とは, ANSI の規格で規定されているすべての動作を受理しなくてはならない.

6.6.2 その他

ANSI 規格で定められているその他の用語として, バイト, 文字, オブジェクトがある. バイト (byte) とは, 実行環境中の基本文字集合の任意の要素を保持するために十分な大きさを持つデータ記憶領域の基本単位と定められ, 1バイトのビット数は処理系依存, バイトは連続するビット列からなる. 文字 (character)とは, 1バイトに収まるビット表現. オブジェクト (object) とは, その内容によって, 値を表現できる実行環境中の記憶領域. ビットフィールド以外のオブジェクトは, 連続する一つ以上のバイトの列からなる. また, オブジェクトを参照する場合, オブジェクトは特定の型を持っていると解釈して良い.

6.7 最も簡単なプログラム

はじめに最も簡単と思われるプログラムを書いてみよう.プログラムを実行すると画面に何かを表示するものである.

Example 6.7.1 もっとも簡単なプログラムの例

/* Program 1* Hello World. を出力する.*/

#include <stdio.h>

int main(int argc, char **argv){

printf("Hello World.\n") ;return 0 ;

}

Id: C2.tex,v 1.4 2001-03-15 20:32:05+09 naito Exp

数理解析・計算機数学特論 171

以下では, このプログラムの内容を説明する. (ただし, コメント, 空白行は行数として数えない.)

1行目#include <stdio.h>

# ではじまる行はプリプロセッサと呼ばれるものによって処理される.C コンパイラは, 実際には以下の手順によって実行される.

1. プリプロセッサによる前処理.2. コンパイラによるオブジェクト・コードの作成.3. リンカによるオブジェクト・コードとライブラリの結合.

C 言語のプログラム中に, # ではじまる行があらわれると, プリプロセッサはその文法にしたがって, コードを書き換える. 実際, #include という指示は, これに続くトークンで指示されたファイルを, その位置に挿入する命令である.

C 言語では, 原則として全ての関数は, 定義されたり, 利用される前に宣言されなくてはならない. そこで, 標準的な関数(このプログラムでは printf)を使うためには, その宣言が書かれているファイル(ここでは stdio.h)を挿入することによって, その関数の宣言をする. このような(標準関数の)宣言が書かれているファイルのことをヘッダ・ファイルと呼ぶ.また, ヘッダ・ファイルの挿入には

#include <XXXX.h>#include "XXXX.h"

の2つの書き方がある. コンパイラの実装によって決まる標準的な場所7 にあるファイルを挿入するには前

者の方法を使い, カレント・ディレクトリにあるファイルを挿入するには後者の方法を使う.どのような関数が, どのヘッダ・ファイルで宣言されているかはオンライン・マニュアルを見ればわかる.

2行目int main(int argc, char **argv)

これは main という関数の定義である. この関数の本体は3行目の { と6行目の } に囲まれた部分である.この部分は次の3つの部分に分解される.

• int

• main

• (int argc, char **argv)

はじめの int は, この関数の戻り値が int 型であることを示す. 関数の戻り値が書かれていない時には,コンパイラは int であると解釈する.次の main は関数の識別子である.ここで使われている (int argc, char **argv)に関しては後に議論するので,取りあえずここでは「お

約束」としておこう. ここには, (存在すれば)その関数の引数が書かれる. 引数をとらない場合にも ()

または (void) と書かなくてはならない.プログラムが開始されるときには, その時点で呼び出される関数の名前は main でなければならない. す

なわち, main という名前を持つ関数が, プログラム開始時点で最初に呼び出され実行される.

7 これは, コンパイル時のオプションで変更可能

Id: C2.tex,v 1.4 2001-03-15 20:32:05+09 naito Exp

172 数理解析・計算機数学特論

4行目printf("Hello World.\n") ;

ここで利用された printf という関数は, その引数として, 文字列リテラルをとり, その文字列リテラルを標準出力に出力する.8 ここで, 最後の ; によって, この一行が文になっていることを注意せよ.本来, この関数には戻り値が存在するが, ここではその戻り値は利用していない.

5行目return 0 ;

return という文は次の形でなくてはならない.

return 式 ;

式の部分には, どのような式を書いても良い. ここでは, 単に 0 という式を書いている.この文は, main 関数の戻り値を与えている. main 関数が終了した時点でプログラムの終了処理が行わ

れ, main 関数の戻り値はプログラムを実行したシェルに返される9 .

Exercise 6.7.1 Example 6.7.1 を真似て, 次のような出力を得るプログラムを書け.

各自の学籍番号 (改行)各自の名前 (改行)何でも好きなこと (改行)

Exercise 6.7.2 printf という関数の戻り値は, 出力した文字数である. main の戻り値として, 出力した文字数を返すように Example 6.7.1 を変更せよ. ただし, 変数を用いてはならない. シェルに戻された戻り値は, csh の場合は

% echo $status

を実行することで得ることができる。

6.8 変数とは

変数 (variable) とは, 識別子によって区別された初期化, 代入などが許される記憶領域のことである. C言語の変数には, 多くの型があり, それぞれの型によって, どれだけの記憶領域が確保されるかが異なる. また, C 言語の変数には記憶クラス, スコープ, 寿命, リンケージなどの概念があるが, それらについては関数,分割コンパイルの後で述べる. ここでは, 変数の宣言, 型などについて考える.

6.8.1 変数の宣言

C 言語では全ての変数は使う前に宣言しておかなくてはならない. 宣言は変数の性質を告げるためのもので,

int step ;int lower, upper ;float x ;

のように, 型の名前と変数(の識別子)の名前のリストからなる.8 ここの解説は本当は正しくない。この関数はもっと多くの引数をとり、最初の引数も文字列リテラルである必要はない.9 main 関数が明示的な戻り値を持たない場合には, シェル(ホスト環境)に返される値は不定となる.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

数理解析・計算機数学特論 173

これらの宣言を色々な場所に書くことで, それらの変数の意味が変わるが, ここでは, 次のように, どのブロックにも含まれず, すべての手続きの前に書くことにする. (下の例を参照.)

#include <stdio.h>

int step ;int lower, upper ;float x ;

int main(int argc, char **argv){

}

6.8.2 変数の初期値

C では変数は, 定義されただけでは値は定まらない(と考えた方が良い)10 . そのため, (必要なら)その変数を使う前に初期化を明示的に行なう必要がある.変数の初期化の方法には2通りある.

int a=0, b ;

int main(int argc, char **argv) {b = 0 ;

}

このように, 宣言と同時に初期化することもできる. a の初期化は実行時にただ一度だけ行なわれるが,b の場合は, この文を実行されるたびに b に 0 が代入される.

C においては, 変数の宣言と定義は異なり, 宣言だけではメモリ領域が確保されない. これに関しては,extern 宣言を参照.

6.8.3 変数の型

C で定義されている変数の型は以下の通りである.11

変数の型 型の名前

char 文字型

short int 短い整数型 short と書いても良い

int 整数型

long int 長い整数型 long と書いても良い

float (単精度)浮動小数点型

double 倍精度浮動小数点型

long double 長い倍精度浮動小数点型

void 何もない型

enum 列挙型

また, char, short int, int, long int に対しては, unsigned を前につけると, それぞれ符号無しの型を表し, signed をつけるとそれぞれ符号つきの型を表す. 何もつけない時は, short, int, long は符号つき10 [2, 2.4] によれば, 次のように書かれている: 外部変数, 静的変数はゼロに初期化される. 明示的な初期化式がない自動変数は不定(ゴミ)の値を持つ. (External and static variables are initialized to zero by default. Automatic variables for which thereis no explicit initializer have undefined (i.e., garbage) values.)11 void, enum, long double は ANSI の規格ではじめて定義された. Kernighan-Ritchie の初版 [1] では定義されていない.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

174 数理解析・計算機数学特論

であると解釈される. しかし, char に関しては, どちらになるかは処理系依存である. 例えば, 今回使用する gcc の場合は char は signed char である.

6.8.3.1 const 修飾子

変数の型に const という修飾子をつけると, 初期化はできるが, プログラム中で変更のできない定数として扱うことができる. 例えば, 次のように宣言する.

const int a=0 ;float const b=1.0 ;

const 宣言をした変数を変更した時の振る舞いは不定である12 .

Remark 6.8.1 この remark は非常に高度で面倒な内容を含んでいるので, 興味のない人は無視してもよい. また, ここでのプログラム断片は表示を少なくするため, あまりきれいな形にはなっていない.実は const 宣言をした変数の扱いが非常に厄介で, 以下のようなプログラム断片を調べてみよう.

int n ;const int cn ;

cn = n ; n = cn ;

この中で代入が許されるのはどの場合かを考えてみる. 当然 n = cn は許される. しかし, cn = n は gcc の場合には, assignment of read-only variable ‘cn’ という警告が出される. Sun の C コンパイラでは, left operand

must be modifiable lvalue というエラーとなる.しかし, 次の例はどうだろうか?

char *cp ;const char *ccp ;

ccp = cp ; cp = ccp ;

こちらは ccp = cp が許され, cp = cpp の代入では, gcc では assignment discards qualifiers from pointer

target type という警告が出される. これでは何を言っているかわからないので, Sun の C コンパイラに通してみると, assignment type mismatch: pointer to char "=" pointer to const char という警告が出る.まず, cpp = cp が許される理由を考えてみよう. 実は, const char * という型指定は, 「const char へのポイン

タ」という意味であり, ccp 自身を const 宣言しているのではなく, ccp が指し示すオブジェクトが const と言っているのである (cf. [2, A.8.6.1]). したがって, 次のような例は警告対象となる.

const char *ccp="abc" ;*ccp =’b’ ;

しかし,

char cp[4] ;const char *ccp="abc" ;

ccp = cp ; *cp =’b’ ;

のように, 一旦 const 修飾子がついていないオブジェクトを経由して, const 宣言を行ったオブジェクトへのアクセスを行うことは, 文法上問題は発生しない. しかし, const 修飾子は, 「読み出し専用」のメモリ領域にオブジェクトを配置して良いことをコンパイラに知らせるという役目も持ち, そのような場合も含めて, この例の結果は不定であると考えるべきである. なお, const ポインタを宣言するには, char *const ccp とする. すなわち,

char *const ccp="abc" ;

とすれば, ccp = cp といった代入が許されなくなる.また, cp = cpp が許されない理由は, 型の適合性の問題にある. 単純代入が許される条件の一つとして, 次の条件が

規定されている. (cf. [3, X3010 6.3.16.1, p. 1890])

• 両オペランドが適合する型の修飾版または非修飾版へのポインタであり, かつ左オペランドで指される型が右オペランドで指される型の修飾子をすべて持つ.

12 より正しくは, 「const 修飾型を持つオブジェクトを, 非 const 修飾型の左辺値を使って変更しようとした場合, その動作は未定義とする.」というのが ANSI の規定.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

数理解析・計算機数学特論 175

cp = cpp は右オペランドで指される型の修飾子 const を左オペランドで指される型が持たないため, この条件に違反し, 他の単純代入の条件にも合致しないため, 文法エラーとなる.さらに, 次のような例もある. (cf. [6, p. 48])

int foo (const char **p) {}int main(int argc, char **argv){ foo(argv) ; }

この例では, gcc の警告は passing arg 1 of ‘foo’ from incompatible pointer type となる. これは, 関数 foo

の仮引数 const char **p が「 const 修飾された char 型変数へのポインタのポインタ」であり, 実引数 argv は「char 型変数へのポインタのポインタ」である. そこで, ANSI 規格 6.3.2.2 を見てみよう. (cf. [3, X3010 6.3.2.2,p.1876]) そこには, 関数呼び出しの制約として, 「各実引数は, 対応する仮引数の型の非修飾版を持つオブジェクトにその値を代入できる型を持たなければならない」と書かれている. つまり, 引数を渡すと代入が行われ, 実引数と仮引数は代入が許される関係になければならないということである. したがって,

int foo (const char *p) {}int main(int argc, char **argv){char *q ;foo(q) ;

}

という例であれば, p = q という代入が行われることに相当し, 上で述べた単純代入の規約を満たす. しかし, constchar ** の例では, 仮引数 p は「const char * へのポインタ」であり, 実引数 argv は「char * へのポインタ」であるため, 単純代入の規約を満たさない.なお, char * を仮引数とする多くの標準ライブラリ関数(例えば, strcpy など)は, 仮引数として const char *

を宣言している. これは, 関数内で明示的に仮引数の指し示す値を変更しないための措置である.

6.8.3.2 変数と記憶領域

変数は宣言と同時に対応する記憶領域が確保される. しかしながら, それぞれの型に対して, どれだけの記憶領域が確保されるかは処理系依存である.それぞれの型がどれだけの記憶領域をとるかを調べるには, sizeof 演算子を使う. sizeof 演算子の利

用法は以下の通りである.

sizeof (型) ;sizeof オブジェクト ;

ここで, その結果として得られる値は, 符号なし整数で表現され13 , その意味は, char 型の何倍の記憶領域が確保されるかを表す. 即ち, sizeof(char) の結果は処理系によらず 1 である.それでは, char 型がどれだけの記憶領域を使うかを知るには, どのようにすれば良いのだろうか. それ

には, C 言語の標準的なヘッダ・ファイルを見れば良い. 実際, limits.h に定義されている CHAR BIT と

いうマクロ14 の値が char 型のビット数である. Sun Sparc Station の C コンパイラの場合,

#define CHAR_BIT 0x8

となっているので, char 型は8ビット(1バイト)であることがわかる.15 また, int 型の長さは, その計算機の自然な長さであると定義されている.それぞれの変数が記憶領域に確保される時, 宣言した順序で記憶領域内に確保されるという保証はない.

また, 多くの処理系では, int, long はワード境界にアロケートされる.

Example 6.8.1 int が2バイト, long が4バイトの処理系で,

char c ;int n ;long l ;char d ;13 正確には size t 型で表現される. size t 型がどの型に対応するかは処理系依存である.14 マクロの意味は後日解説する.15 ANSI の規格書によれば, char 型の占めるビット幅を1バイトと定義している.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

176 数理解析・計算機数学特論

と変数を定義した場合, 下の図のいずれのメモリ配置をとるかは処理系や最適化に依存する. これ以外の取り方をする可能性もある.

16 bits

c (padding)nl

d

16 bits

c dnl

16 bits

c nn(続き)l

d

(a) (b) (c)

この中で (c) のメモリ・アロケーションはアライメント (alignment, 境界調整) に適合していない環境が多いため, ほとんどこのようなアロケーションは行われない.

Example 6.8.2 変数に値を代入する操作とは, 変数に対して与えられたメモリに数値を書き込むことに他ならない. 例えば, (int が16ビットの場合)

int n ;n = 1 ;

とすることは,

16 bits

n 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

16 bits

n =⇒ または16 bits

n 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0

8 bits

と値を代入することになる. 下のように上位バイトと下位バイトを入れ替えて数値を表現する処理系 (CPU)を big endian, 上のように数値を表現する処理系 (CPU)を little endianと呼び, 8080, Z80などの Intel社の CPU は big endian になっていることが多く, 68000, SPARC などの CPU は little endian になっている.

6.8.3.3 文字型と整数型

文字型 (character type) とはその名の通り, (1)文字を変数として扱う型である. 例えば,

char c ;c = ’a’ ;

とすると, 変数 c には a という文字が代入される. 文字型では, その中身を文字の持つコードの値として扱う. したがって, 文字型変数の実体は “整数”と思って良い. (しかしながら, char 型を文字として扱う時には, 常に正の数値として扱う.)その意味で, char は整数型 (integer type) (short, long なども含む)の一部として考えると都合が良いことが多い.

Example 6.8.3 例えば, ASCII コード体系の処理系で,

char c ;c = ’a’ ;

とすると, c には16進整数値 0x60 が入り, 整数値 0x60 として計算や比較が行われる. したがって,

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

数理解析・計算機数学特論 177

char c, d ;c = ’a’ ; d = ’A’ ;

の時, c + d は16進数値 0x40 + 0x60 = 0xA0 となる. また,

char c ;c = 0x60 ;

とすると, c には ’a’ が代入されたこととなる.

それでは, 整数型がどれだけの記憶領域を使うかを考えてみよう. C では, short, int, long などの記憶領域については, 全て処理系に依存していると定義されているが, 次の関係だけは C の定義にある.

char ≤ short ≤ int ≤ long

ここで, char ≤ short という意味は, short 型の記憶領域は char 型のそれよりも短くないという意味で

ある. また, short, int 型は最低16ビット, long 型は最低32ビットが保証されている.実際, それぞれの整数型の長さ(sizeof の返す値)を見てみると, 今回利用する処理系では, 次のように

なる.

short 2int 4long 4

これで, char 型が1バイトであることを使うと, int は4バイトであることがわかる.

6.8.3.3.1 整数の内部表現 整数型の変数の内部での表現は, Section 2.3.2 で述べた表現がとられていることが多い. ANSI 規格では整数型の内部表現の具体的な方法については何も規定していない.

6.8.3.4 浮動小数点型

浮動小数点型 (floating type) についても, C では以下のことしか定義されていない.

float ≤ double ≤ long double

実際, それぞれの浮動小数点型の長さ(sizeof の返す値)を見てみると, 今回利用する処理系では, 次のようになる.

float 4double 8long double 8

これで, char 型が1バイトであることを使うと, float は4バイトであることがわかる.(10を底とする)浮動小数点数とは, 以下のような型で表現された数のことである.

(0でない1桁の数).<仮数部> × 10^<指数部>

ここで, 任意の 0 でない実数は, このような表示が可能であることに注意せよ. (もちろん, その表示は有限小数と仮定すれば一意的である.)

6.8.3.4.1 浮動小数点数の内部表現 浮動小数点数の変数の内部での表現は, Section 2.3.2 で述べた表現がとられていることが多い. ANSI 規格では整数型の内部表現の具体的な方法については何も規定していない.オーバーフロー, アンダーフローをした数の演算, 比較等の結果がどうなるかは規格では定められていな

い. しかし, それぞれの処理系によって規定されている.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

178 数理解析・計算機数学特論

6.8.3.5 列挙定数

列挙定数 (enumeration constant) とは, 次のようなものである.

enum day {sun, mon, tue, wed, thu, fri, sat} ;

この時, eum 型の変数は int として扱われる. 即ち, 上の例では,

sun <-> 0mon <-> 1

などというように対応づけが行なわれて処理される.

6.8.4 変換

C のプログラムで演算を行なう時には, 数多くの型の変換が行なわれてから演算が実行される.

6.8.4.1 整数への格上げ

汎整数型(char, short, int, long, このような型を integral type と呼ぶ.)に対して, 演算を行なう時には, 整数への格上げ (Integral Promotion) (または汎整数拡張)と呼ばれる操作が行なわれることがある.それは, 以下のように定義されている16 ¿ char, short は, 符号つきも符号なしも, 整数が使える式で使っ

て良い. この時, 元の型の全ての値が int で表現できる時には, その値は int に変換される. int で表現

できない時には unsigned int に変換される. char または short 型の変数が unsigned int にしか変

換できないという状況は, short と int が同じバイト幅である時に起こり, この時, unsigned short は

unsigned int に変換されるという意味である. long, unsigned long については規定されていない.

6.8.4.2 符号拡張

C では char は符号つきか符号無しかを規定していない. この時, 最上位ビットが 1 であるような char

を int に変換する時の振舞いは処理系依存である. 例えば, 最上位ビットが 1 として負の数に変換される(これを符号拡張と呼ぶ)こともあれば, 0 として正の数に変換されることもある.例えば, char が1バイト, int が4バイトのときに, char が signed char である時には, 符号拡張され,

int に変換され計算される. この時, 前に述べた2進数の表現がとられ, 負の数は2の補数表現がとられている時, 負の signed char 型の変数は, 上位ビットに 1 が埋められ, signed int と扱われる. 一方, charが unsigned char の場合は, 常に 0 が埋められる.したがって, 0x7F を越える char 型の変数を扱う時には, 必ず unsigned char とし, より広い整数への

変換がある時には, 符号なしで受けなくてはいけない17 . 例えば, char が signed char の時, char a =

0x80 とすると, (int)a は 0xFFFFFF80 となるが, char が unsigned の時には, 0x80 のままである.

6.8.4.3 符号拡張と整数への格上げの演算への影響

符号拡張と整数への格上げは, char 型の変数同士の演算の場合に大きな影響をおよぼす. CPU の演算レジスタ長よりも短いメモリサイズを持つ変数に対する演算を行う場合, 何らかの形で演算レジスタ長に合

16 [1] の定義によれば, 「符号なし型はより広い符号なし型に変換される」とされているので注意すること.17 C の定義によれば, 「標準文字セットのすべての文字は正の値を持つ」となっている.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

数理解析・計算機数学特論 179

う値(ビットパターン)に変換を行ってから演算を行う必要がある. 符号拡張・整数への格上げは, 演算レジスタ長に値を合わせる変換と理解して良い.

Example 6.8.4 標準演算レジスタ長が16ビット, 1バイトが8ビットである処理系を考えよう. すなわち, int は2バイト長である. さらに, char は signed char であり, 符号拡張を行う処理系であるとする.この時, 次の演算結果はどうなるだろうか?

char c=0x70, d = 0x80 ;if (d < c) printf("d < c\n") ;else printf("d >= c\n") ;

通常であれば, 0x70 < 0x80 であるので, d < c が成り立つはずである. しかし, この結果は d >= c と

なる. これは, char が符号付きであり, 比較 < の演算で整数への格上げが行われるため, 符号拡張を受け,比較の段階で2つの演算レジスタに格納されている値は, d に対応するものは, 0xFF80, c に対応するものは 0x0070 であり, 0xFF80 は負の数と判断されるためである.この結果を正しく判断させるためには,

unsigned char c=0x70, d = 0x80 ;if (d < c) printf("d < c\n") ;else printf("d >= c\n") ;

としなければならない. すなわち, 文字型変数を「符号なし」と明示的に指定し, 符号拡張の影響を排除しなければならない.

6.8.4.4 整数への変換

任意の整数が符号つき型に変換される時, その数が新しい型で表現可能ならば, その値は不変になるが,そうでない時の結果は処理系依存である.

6.8.4.5 整数と浮動小数点数

浮動小数点数を汎整数に変換する時には, 小数部は無視される. また, 結果として得られる整数が目的の型で表現できない時の振舞いは不定である.逆に, 整数を浮動小数点数に変換する時には, その結果が表現可能な範囲にある時でも, 正確に表現がで

きない時には, 一番近い大きな数か小さな数のどちらかに変換される.

6.8.4.6 浮動小数点数

浮動小数点数がより精度の高い浮動小数点数に変換される時には, その値は不定である.逆に精度の高いものが低いものに変換される時には, その結果が表現可能な範囲にある時でも, 正確に表

現ができない時には, 一番近い大きな数か小さな数のどちらかに変換される.

6.8.4.7 算術変換

算術変換 (arithmetic conversion) とは, 算術演算が行なわれている時に, 被演算数の型を揃え, その結果も同じ型にするという操作である.ほとんどの演算において, この変換が行なわれる. その手順は, 以下の通りである. (条件に一致した最

初の変換が行なわれる).

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

180 数理解析・計算機数学特論

1. いずれかの被演算数が long double ならば, 他も long double にする.2. いずれかの被演算数が double ならば, 他も double にする.3. いずれかの被演算数が float ならば, 他も float にする.4. 上のいずれも一致しない時には, 整数への格上げを行なって, 以下の変換を行なう.

(a) いずれかの被演算数が unsigned long ならば, 他も unsigned long にする.(b) いずれかの被演算数が long で, 他が unsigned int である時には, 次を調べる.

i. long が unsigned int の全ての数を表現できれば, unsigned int の被演算数は long int に

する.ii. そうでない時には, 全ての被演算数は unsigned long に変換される.

(c) いずれかの被演算数が long ならば, 他も long にする.(d) いずれかの被演算数が unsigned int ならば, 他も unsigned int にする.(e) 上のいずれかも当てはまらない時には, 被演算数を int として計算する.

要するに, 「算術演算で異なる型の値を指定すると, 型変換が行われる. 変換は情報が欠落しない限り, 実数, 高精度, 符号付きの方向で行われる」ということである18 .

Example 6.8.5 例えば, long と unsigned int の和は, int = long の場合と, int < long の場合とで,結果が異なる.

unsigned int a = 1U ;long b = -1L ;

a > b ; /* long = int の時, 正しくない. long > int の時正しい. */

これを正確に判定するには,

unsigned int a = 1U ;long b = -1L ;

(long)a > b ;

とする. (後述の Section 6.8.6 参照.)

Example 6.8.6 int が2バイトである時,

unsigned int a = 256 ;

(a * a * 1L) == (a * (a * 1L)) ;/* この式は正しくない. */

ということが起こる.

Example 6.8.7 次の例は, 符号拡張, 算術変換などの例である.

18 この文章は [6, p. 53] から引用.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

数理解析・計算機数学特論 181

int a,b,c ;char n ;double x ;

a = 1 ; b = 2 ;x = a/b ; /* x の値は 0 である. */x = 1/b ; /* x の値は 0 である. */x = 1.0/b ; /* x の値は 0.5 になる. 算術変換. */

a = -7 ; b = 2 ;c = a/b ; /* この値が -3 となるか -4 となるかは処理系依存. */

n = 1 ;a = n ; /* sizeof(int) = 4, char が符号付きの時,

* a = 0xFFFFFF01 となるか (符号拡張)* a = 0x00000001 となるかは処理系依存. */

この値を正しく計算するには, 後述する型変換を使う必要がある.

6.8.4.8 K&R での算術変換

K&R の C 言語, すなわち [1] で定義されている, traditional C と ANSI C とでは, 算術変換の方法が全く異なる. K&R では,

まず, char または short 型の任意の被演算数が int に変換され,float 型は double に変換される.次に, どちらかの被演算数が double なら他方も double に変換さ

れ, それが結果の型となる.そうでなく, 一方の被演算数が long なら他方も long に変換され,それが結果の型となる.そうでなく, 一方の被演算数が unsigned なら他方も unsigned に

変換され, それが結果の型となる.そうでないときには, 両方の被演算数が int でなければならず, それが結果の型となる.

と書かれている. ANSI C では「値を保存」する方向に変換が行われるが, K&R C では「unsigned を保

存」する方向に変換が行われる.

Example 6.8.8 次のコード19 は, ANSI C と K&R C では異なった結果を出力する20 .

if (-1 < (unsigned char)1)printf("-1 is less than (unsigned char)1: ANSI\n") ;

elseprintf("-1 is NOT less than (unsigned char)1: K&R\n") ;

比較の段階で, ANSI C の場合は (unsigned char)1 が (int)1 に格上げされ, int として比較される.K&R C の場合には, -1 が unsigned int に変換されたビットパターンとして比較される21 . unsigned

char が unsigned int の場合には, K&R でも ANSI でもともに unsigned int としての比較が行われ

るため, -1 < (unsigned int)1 は「偽」の値を返す.

19 [6] からの引用20 実は, K&R の規約では, unsigned char は存在しない. unsigned, short, long は int につく限定詞と定義されている. しかし, 古い ANSI 規格ではない処理系の多くで unsigned char が利用できる.21 しかし, 正しくは K&R C には unsigned char は規定されていない.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

182 数理解析・計算機数学特論

6.8.5 演算

6.8.5.1 2項算術演算

2項算術演算とは, 通常の足し算, 引き算, かけ算, 割算, Modulo である. 2項算術演算の項の評価順序は不定であるので注意すること.

6.8.5.1.1 加法演算子 加法演算子には +, - がある. これらは, 左から右に作用する. 被演算数が算術的22 (即ち, 整数や浮動小数点数)であれば, 算術変換が適用される.

6.8.5.1.2 乗法演算子 乗法演算子には *, /, % がある. これらは, 左から右に作用する. *(かけ算), /(割算)23 は, 被演算数が算術的でなくてはならない. % (余りを出す)は, 被演算数は汎整数でなくてはならない. これらの演算には, 算術変換が適用される. 即ち, 整数同士の割算の結果は, 再び汎整数となり,その商が求められる./, % の第2被演算数が 0 で無い場合には, (a/b)*b+a%b が a に等しいということが常に保証され, 両方

の被演算数が非負の場合には, あまりが非負で, 除数よりも小さい. そうでないときには, あまりの絶対値が除数の絶対値よりも小さいことが保証される.すなわち, どちらか片方の被演算数が負の時には, 除算(/ または %)を行ってはいけない. この場合除

算を行うと, 結果は処理系依存となる.

6.8.5.2 単項算術演算子

ここでは, インクリメントのみを扱う. ここで述べる2種類の演算子は, 汎整数かポインタに対してのみ適用できる.

6.8.5.2.1 前置インクリメント演算子 式の前に ++ もしくは -- がついている式もまた, 式になる. これは, その値が使われる前に 1 だけ増やされる(++ の場合).

6.8.5.2.2 後置インクリメント演算子 式の後に ++ もしくは -- がついている式もまた, 式になる. これは, その値が使われた後に 1 だけ増やされる(++ の場合).

Example 6.8.9 インクリメントの例は以下のものである.

int a ;

a = 1 ;a++ ; /* この値を表示させると, 1 を表示したあとに, 1 増分される. */a = 1 ;++a ; /* この値を表示させると, 1 増分した後に, 1 を表示する. */a = 1 ;--a++ ; /* これはエラーである. */

次のような計算を考える.

int x, y ;

x = 1 ;y = (x++) - (x++) ;

22 加法演算子は, 後述するポインタにも作用する.23 もちろん, /, % の第2被演算数は 0 であってはならない. /, % の第2被演算数が 0 の場合には結果は不定となる. 一般には「 0除算による例外割り込み」が発生する.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

数理解析・計算機数学特論 183

この結果は, 次の3通りが考えられる.

1. y = 0 これは, 先に - を実行し, それからインクリメントをした.2. y = -1 これは, 1 - 2 を実行した.3. y = 1 これは, 2 - 1 を実行した.

このように2項演算と各項の評価をどの順序で行なうかは, 不定であるので注意すること24 .

Example 6.8.10 次のような式を考えよう.

x+++y ;

この式は, x[+[++y と x++[+[y と2通りに解釈できるが, C の構文解析では, 最大一致法をとるという規約があり, そのため, 構文に合致する最大のトークンである x++ を採用し, x++[+[y と解釈される.

x+++++y

は x++[+[++y という解釈が可能であるが, C の構文解析パーサには x++[++[+[y と解釈することが求めら

れている.

6.8.5.3 代入演算子

次のような代入を考える.

a += 1 ;a -= 1 ;a *= 1 ;a /= 1 ;

これらは, それぞれ, a の値を 1 加えた(減らした, 掛けた, 割った)値を再び, a に代入する演算である.

Example 6.8.11 代入の例は以下のものである.

int a ;

a = 1 ;a += 1 ; /* a の値は 2 となる. */

その他にも, %=, <<=, >>=, &=, ^=, |= がある. もちろん, = も代入である(= を単純代入と呼び, それ以外の代入演算子を複合代入と呼ぶ).

6.8.5.4 単項演算子

単項ビット演算には, ~, ! がある.~ は1の補数をとるための演算子で, 被演算数は整数でなければならない. この時, 整数の格上げが行な

われる. 1の補数とはビット反転のことである. すなわち, 以下のような操作を行う.

1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0

↓ ~

0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1

! は論理否定をとるもので, 被演算数は算術型かポインタである. 被演算数が 0 であれば, 結果は 1 となり, そうでなければ, 0 である.24 このような評価式は ANSI 規約 [3, X3010 6.3, p.1873] の「式」の規定で, 「直前の副作用完了点から次の副作用完了点までの間に, 式の評価によってオブジェクトに格納された値を変更する回数は高々1回でなければならない. さらに, 変更前の値は, 格納される値を決定するためだけにアクセスしなければならない. 」とある. したがって, y = (x++) - (x++) が不定であるだけでなく, i= ++i + 1 も不定であることに注意しよう.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

184 数理解析・計算機数学特論

6.8.5.5 2項ビット演算

2項ビット演算には, &, |, ^, <<, >> がある. これらの演算の被演算数は汎整数型でなくてはならない.被演算数に対して, 通常の(格上げ等を含む)算術変換が行われる. 汎整数型に関しては, 内部表現を定めていないが, 通常の2進表現と考えてビット演算を行った結果と理解してよい.& はビットごとの AND をとる演算子, | はビットごとの OR をとる演算子, ^ はビットごとの XOR を

とる演算子である.

1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0

1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1

↓ &

1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0

1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

↓ |

1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 1

1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0

1 0 1 0 1 0 1 0 1 0 1 0 1 1 1 1

↓ ^

0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1

<<, >> はそれぞれ, ビットごとの左シフト, 右シフトをとる演算子である. 被演算数は汎整数でなくてはならない. また, 整数への格上げが行なわれる. 結果は, 格上げを受けた左被演算数の型である.E1 << E2 の値は, E1 を左に E2 だけシフトしたものである. オーバーフローがない場合には 2E2 をか

けることに等しい. E1 >> E2 の値は, E1 を右に E2 だけシフトしたものである. E1 が符号なし, または負でない時には 2E2 で割ることに等しい. そうでない時には, 結果は処理系依存である.E2 が負である時には, 結果は不定である.また, 左にシフトした時には, 右には 0 がつめられる. 符号なし数を右にシフトした時には, 左には 0 が

つめられるが, 負の数を右にシフトした時には, 左には, 1 がつめられる(算術シフト)か 0 がつめられる(論理シフト)かは処理系に依存する.

1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0

↓ <<1

0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 0

0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1

↓ >>1

0 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0

このように正の数のシフトには何の問題も生じない.オブジェクトが signed の場合

1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0

↓ >>1 (算術シフト)1 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1

1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0

↓ >>1 (論理シフト)0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1

負の数のシフトで, どちらが起きるかは処理系依存

Example 6.8.12 <<, >> の演算の例は, 以下の通りである.

signed char a ;unsigned char b ;

a = -0x02 ; b = 0x02 ;a << 1 ; /* 結果は int で FFFFFFFC */a >> 1 ; /* 算術 shift の時, 結果は int で FFFFFFFF, 論理 shift なら 7FFFFFFF */b << 6 ; /* 結果は int で 128 */a << 6 ; /* 結果は int で FFFFFF80 */

ここで, 負の数のシフトは, 整数への格上げを受け, 符号拡張も受けていることに注意せよ.

signed char a = -0x02

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

数理解析・計算機数学特論 185

1 1 1 1 1 1 1 0

↙ 符号拡張と格上げ ↘ 格上げ1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0

↓ >>1 (算術シフト) ↓ >>1 (論理シフト) ↓ >>1

1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1

0xFFFF 0x8FFF 0x0077

6.8.5.6 論理演算

論理演算には, 比較(関係演算子), 等値演算子, AND, OR がある. これらの演算子は全て, 左から右に適用される.比較には, <, >, <=, >= があり, それが正しければ, int 型の 1, そうでなければ int 型の 0 が返される.

以下のようにして使う.

式1 < 式2

比較演算子, 等値演算子は被演算数が算術型の時には, 通常の算術変換を行う. 比較演算子は被演算数が算術型の時, 算術変換をした後, その型に適合した数値としての比較を行う.等値演算子は, == で表される.

式1 == 式2

その結果が正しければ(式1と式2が等しい) 1, そうでなければ 0 となる. ただし, == は内部表現で判定しているので,

0xFFFF == 65535 ;0xFFFFFFFF == -1 ; /* int が4バイトで, 2の補数表現の時 */

はともに int 型の 1 となる.等値演算子の否定は, != で表される.

式1 != 式2

その結果が正しければ(式1と式2の値が等しくない)int 型の 1, そうでなければ int 型の 0 となる.論理 AND 演算子は && で,

式1 && 式2

であって, 被演算数の両方が非零の時 int 型の 1 を返し, そうでない時 int 型の 0 を返す. また, && で評価されるのは, もっとも左の式であり, それが 0 であれば, その式の値は 0 である. そうでなければ, 右の被演算数が評価される. それが 0 であれば, 式の値は 0 となり, そうでない時に 1 となる.論理 OR 演算子は || で,

式1 || 式2

であって, 被演算数のどちらかが非零の時 1 を返し, そうでない時 0 を返す.論理 AND, OR 演算子の結果は int である.

Example 6.8.13 論理 AND, OR 演算子の例は以下の通りである.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

186 数理解析・計算機数学特論

int a=0,b=1,c=2,d=3 ;

(a < b)&&(c < d) ;/* これは a < b && c < d でも同じだが, *//* 関係をはっきりさせるために, 括弧をつけた方がよい. *//* この式の評価は, (1) && (1) となるので, 1 が値となる */

(a > b)||(c > d) ;/* この式の評価は, (0) || (0) となるので, 0 が値となる */

a && (c < d) ;/* この式の評価は, (0) && (1) となるので, 0 が値となる */

b || (c > d) ;/* この式の評価は, (1) || (0) となるので, 1 が値となる */

(!(a < b))&&(!(c < d)) ;/* この式の評価は, (0) && (0) となるので, 0 が値となる */

ここで, 注意しなくてはならないのは, 3, 4番めの式である. 次の例を見よう.

Example 6.8.14 論理 AND, OR 演算子の変な例は以下の通りである.

int a=0,b=1,c=2,d=3 ;

a && (c++ < d) ;

はじめの式は a = 0 であるので, && は右の式の評価は行なわない. したがって, この式の後に c の値を

求めると, c = 2 のままである.

int a=0,b=1,c=2,d=3 ;

b && (c++ < d) ;

この場合は c++ が実行されるが, その順序は, 先にインクリメントが実行されるので, b = 1, (c++ < d)

= 0 となり, 結果は 0 となる.

int a=0,b=1,c=2,d=3 ;

b || (c++ > d) ;

この場合は, b = 1 であるので, || は右の式の評価は行なわない.

このように C では, 評価が全て行なわれる前に式の値が決定することがあり, その時点で式の評価が終了するので, 注意しなくてはならない.

6.8.5.7 3項演算子

3項演算子 ? : は条件演算子とも呼ばれる演算子である.

式1 ? 式2 : 式3

まず, すべての副作用を含めて, 式1が評価され, それが 0 でなければ結果は式2の値となり, そうでなければ式3の値となる. この時評価されるのは式2または式3のいずれかである. 式2, 式3の被演算数が算術型であれば, 通常の算術変換が行われて, それらは共通の型となり, それが結果の型となる.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

数理解析・計算機数学特論 187

3項演算子は if-else 文で書くと長くなる場合に, それを短く表現する手法として使われるが, すべての条件分岐を表現できるわけではないことに注意.

Example 6.8.15 2つの変数の小さくない方の値を代入する.

max = (a > b) ? a : b ;

Example 6.8.16 例えば, 次の if-else 文を考えよう.

if (n == 1) printf("The list has %d item%s\n", n, "") ;else printf("The list has %d item%s\n", n, "s") ;

これは, n が 1 であれば item と出力し, n が 2 以上であれば items と出力する. これは, 3項演算子を使って, 出力を1行にまとめることが出来る.

printf("The list has %d item%s\n", n, n==1 ? "" : "s") ;

しかし, 3項演算子を余りに多用すると, かえって分かりにくいプログラムになる.

6.8.5.8 コンマ演算子

式1 , 式2

コンマ , で区切られた式は左から右に評価され, 式1の値は捨てられる. 結果の型と値は式2の型と値である. 式1の被演算数の評価に伴うすべての副作用は, 式2の評価を始める前に完了している.これまでに出てきた

int a, b ;

はコンマ演算子を使う例である.

6.8.5.9 演算子の優先順位と結合規則

これら演算子の優先順位, 結合規則は, 次の通りである. 上ほど優先順位が高い.

演算子 結合規則

( ) [ ] -> . 左から右

! ~ ++ -- + - * & (type) sizeof 右から左

* / % 左から右

+ - 左から右

<< >> 左から右

< <= > >= 左から右

== != 左から右

& 左から右

^ 左から右

| 左から右

&& 左から右

|| 左から右

?: 右から左

= += -= *= /+ %= &= ^= |= <<= >>= 右から左

, 左から右

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

188 数理解析・計算機数学特論

ただし, 単項の + - * は二項のそれよりも上である.最も注意しなければならないのは, &, | と == の優先順位である.

Example 6.8.17 int 型の変数 n が偶数か奇数かを判定するために,

if (n & 1 == 0)

としたとしよう. プログラマは,

if ((n & 1) == 0)

の意味で書いているかもしれないが, 実際には

if (n & (1 == 0))

と解釈される.

Example 6.8.18 括弧は不要な場合であっても, 括弧を書くことにより, 意味が明快になる場合がある. この例は, int 型の変数 year で示される西暦の年号が, うるう年かどうかを判定する.

leap_year = year % 4 == 0 && year % 100 != 0 || year % 400 != 0 ;

うるう年は year が 4 で割りきれ, 100 で割りきれないとき, または, 400 で割りきれないときであるが,

leap_year = ((year%4 == 0) && (year%100 != 0)) || (year%400 != 0) ;

と書いた方が意味が明快になる.

結合規則は聞きなれない言葉であるが, 以下のような意味を持つ.

Example 6.8.19 = の結合規則は「右から左」であるので,

a = b = c ;

は, b = c の代入が行われ, その結果の値(この場合は代入された b の値)が a に代入される. すなわち,a = (b = c) という意味となる. ただし, (a = b) = c は (a=b) が左辺値とならないので, 文法エラーとなる.

Example 6.8.20 == の結合規則は「左から右」であるので,

a == b == c ;

は, 比較 a == b が行われ, その結果の値と c との比較が行われる. すなわち, (a == b) == c という意味

となる. この場合は, (a == b) == c は文法上正しい構文である.

Example 6.8.21 式 a < b < c の意味は数学的な不等式ではなく, もし, b が a よりも大きければ, 1 とc を比較し, そうでなければ, 0 と c を比較していることに注意.

Example 6.8.22 数学的には b �= 0 の時, ab/b = a が成り立つが,

a/b*b ;a*b/b ;

は異なった演算規則が適用される. 乗法演算子の結合規則は「左から右」であるので, a/b*b は (a/b)*b,a*b/b は (a*b)/b と計算される. すなわち, a/b*b は a/b の値を評価し, その結果と b の値との積を求

める.したがって, a, b がともに整数型の時, a/b は商を計算するため, a/b*b は a と等しくなるとは限らない.

また, a, b がともに整数型で, 非負であれば, 式 a*b の値がその型の範囲内に収まるとき a*b/b は a と等

しくなるが, a*b の値がその型の範囲内に収まる保証はない. そのような場合, a*b/b は a と等しくなる保

証はない.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

数理解析・計算機数学特論 189

6.8.6 型変換

異なる型の被演算数が式に現れると, いくつかの規則にしたがって, 明示的もしくは暗黙に共通の型への変換が行なわれる.前のセクションで述べた変換がその代表的なものであるが, ここでは, それ以外に明示的に型変換 (cast)

を行なう方法を述べる.それは, 次のような方法である.

(型名) 式

この方法によって, 式はその前に書いた型名で示された型に変換される. その際の規則は, Section 6.8.4で示した通りである.

Example 6.8.23 次のような例がある.

char a ;int b ;

a = 0x01 ;b = (int) a ;

この場合は, char がより広い型の int に変換されている. しかし, この型変換は整数への格上げそのものである. このように, 整数への格上げがあっても, 明示的に型変換をすることが望ましい.

次のような例もある.

Example 6.8.24

int a,b ;double x ;

a = 1 ;b = 2 ;x = (double)a/b ;

この場合型変換を行なわないと, x の値は 0 となるが, 型変換を行なっているので, x の値は 0.5 となる.演算子 ( ) の優先順位は最も高いところにあるので, (double)a/b は a/b を型変換するのではなく, a を型変換していることに注意. a/b を double に型変換するときには (double)(a/b) とする.

6.8.7 左辺値

左辺値 (lvalue, left value の略) は, オブジェクトを参照し, 変更できる式のことである. 左辺値でないものに代入しようとすると, 文法エラーとなる. C の定義によれば, オブジェクトとは, 名前つきのメモリ領域のことであり, 左辺値はオブジェクトを参照する式であるとされている.例えば, 変数は左辺値となり得る. しかし例えば, a++ などの演算を受けたものは左辺値にはなり得ない.

どのようなものが, 左辺値となり得るか, なり得ないかは [2, A5] に定義されている.

6.9 いくつかのプログラム

ここまででは, 変数の値を表示する方法は述べていなかった. それをするためには, printf 関数を使う.

Example 6.9.1 その利用法は, 次の通りである.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

190 数理解析・計算機数学特論

char a ;int b ;long c ;float x ;double y ;long double z ;

printf("a = %c\n", a) ;printf("b = %d, c = %ld\n",b, c) ;printf("x = %f\n", x) ;printf("y = %e\n", y) ;printf("z = %Lf\n", x) ;

このように, printf 関数を用いると, %d などと, 変数を対応させ, 表示させることができる.

6.9.1 printf 関数の利用法

printf 関数で第2引数にある文字列リテラル中に %d と書かれた部分は, 変換指定と呼ばれ, 変換指定の出現順に, その後にある引数の値が変換指定の指定にしたがって表示される.

printf("b = %d, c = %d\n",b c) ;

とした場合には, b[=[ の後に b の値が %d にしたがって表示され, ,[c[=[ の後に c の値が %d にしたがっ

て表示される.変換指定の書式は % の後に続く次のもので次の順序で指定される.

• 変換指定の意味を修飾する0個以上のフラグ. フラグ文字と意味は以下の通り.

- 変換結果を左詰めにする. これがないと変換結果は右詰めになる.+ 符号付きの変換結果を常に + または - ではじめる. これがないと, 非負の値には符号はつかない.空白 符号付きの変換結果が符号で始まらない場合, または結果の文字数が 0 の場合, 変換結果の前に空白をつける. 「空白」と + がともに指定されたときには「空白」は無視される.

# o 変換に関しては先頭に 0 をつける. x または X 変換に関しては先頭に 0x または 0X をつける. e,E, f, F, g, G 変換に関しては, 小数点文字の後に数値が続かないときにでも, 小数点文字を表示する. g, G 変換に関しては, 後ろに続く 0 を結果から取り除かない. それ以外に関しては不定.

0 d, i, o, u, x, X, e, E, f, g, G 変換に関しては, 0 をフィールド幅に左詰め込みに利用する.

• 省略可能なフィールド幅. 値を変換した結果がこの文字数よりも少ないときには空白を詰め込む.• 省略が可能な精度. 精度の形式は . の後に10進整数を指定する.

– d, i, o, u, x, X 変換に関しては, 出力すべき最小の桁数.– e, E, f 変換に関しては, 小数点文字の後に出力すべき桁数.– g, G 変換に関しては, 最大の有効桁数.– s 変換に関しては, 文字列から書き出すことの出来る最大の文字数.

• 省略が可能な h, l, L のいずれか.

h d, i, o, u, x, X 変換の場合には, 対応する実引数が short または unsigned short であることを

示す.l d, i, o, u, x, X 変換の場合には, 対応する実引数が long または unsigned long であることを示す.L e, E, f, g, G 変換の場合には, 対応する実引数が long double であることを示す.

• 変換形式を示す文字.

d, i int 型の実引数を [-]dddd 形式で10進表記する.o, u, x, X unsigned int 型の実引数を, 符号なし8進 (o), 符号なし10進 (u), 符号なし16進 (x,

X) 表記する. x の時には文字 abcdef を用い, X の時には文字 ABCDEF を用いる.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

数理解析・計算機数学特論 191

f double 型の実引数を [-] dddd.dddd の10進表記にする. 精度が省略されたときには 6 であると解釈する. また, 最終桁は適切な桁数への丸めを行う.

e double 型の実引数を [-] d.ddde + dd または, [-] d.ddde - dd の10進表記にする. 精度が省略されたときには 6 であると解釈する. また, 仮数部の最終桁は適切な桁数への丸めを行う. E 変換の場合には, 指数を表す文字を e ではなく, E を用いる.

g, G double 型の実引数を有効桁数を指定する精度に従い, f または e 形式で変換する. (G の場合はE 形式) 変換の結果得られる値の指数が −4 より小さい, または精度以上の場合には, e または E

形式を用いる.c int 型の実引数を unsigned char 型に変換し, その結果の文字を出力する.s 実引数は文字型の配列へのポインタでなければならない. 配列内の文字を文字列終端まで表示する.p 実引数は void へのポインタでなければならない. そのポインタの値を処理系定義の方法で表示可能文字に変換する.

% 文字 % を出力する. 対応する実引数はない. 変換指定全体が %% でなければならない.

したがって, 以下のように利用することが出来る. (詳しくはオンライン・マニュアル man -s 3S printf

を参照.)

char a = ’a’ ;int b = -1 ;long c = 10L ;unsigned int d = 2U ;char s[3] = "ab" ;double x = 1.0e-4 ;double y = 1.0e-5 ;long double z = 1.0L ;

printf("a = %c\n", a) ;printf("a = %x\n", a) ;printf("b = %d, c = %ld\n",b, c) ;printf("b = %d, c = %+ld\n",b, c) ;printf("b = % .3d, c = % .3ld\n",b, c) ;printf("b = %0.3d, c = %0.3ld\n",b, c) ;printf("c = %3ld\n",c) ;printf("d = %u\n", d) ;printf("d = %0.5u\n", d) ;printf("d = %X\n", d) ;printf("d = %.3x\n", d) ;printf("d = %#.3x\n", d) ;printf("s = %p\n", (void *)s) ;printf("x = %f\n", x) ;printf("y = %e\n", y) ;printf("x = %G\n", x) ;printf("y = %g\n", y) ;printf("z = %LE\n", z) ;

a[=[aa[=[61b[=[-1,[c[=[10b[=[-1,[c[=[+10b[=[-001,[c[=[[010b[=[-001,[c[=[010c[=[[10d[=[2d[=[00002d[=[2d[=[002d[=[0x002s[=[effff9c8x[=[0.000100y[=[1.000000e-05x[=[0.0001y[=[1e-05z[=[1.000000E+00

6.9.2 プログラムの演習

Exercise 6.9.1 色々な型の演算の値を出力するプログラムを書け. 例えば, 次のようなものである.

#include <stdio.h>

int a,b ;int main(int argc, char **argv){printf("%d + %d = %d\n", a, b, a + b);return 0 ;

}

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

192 数理解析・計算機数学特論

Exercise 6.9.2 次のプログラムの出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>int x=2, y, z ;int main(int argc, char **argv){y = z = x ;printf("x=%d,y=%d,z=%d\n",x,y,z) ;x = y == z ;printf("x=%d,y=%d,z=%d\n",x,y,z) ;x = (y == z) ;printf("x=%d,y=%d,z=%d\n",x,y,z) ;return 0 ;

}

ヒント: =, == の意味と結合規則を考えよ. = と == は入力ミスをおかしやすく, バグに直結するミスであることに注意.

Exercise 6.9.3 次のプログラムの出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>int x, y, z ;int main(int argc, char **argv){x = y = z = 1 ;++x || ++ y && ++z ;printf("x=%d,y=%d,z=%d\n",x,y,z) ;

x = y = z = 1 ;++x && ++ y || ++z ;printf("x=%d,y=%d,z=%d\n",x,y,z) ;

x = y = z = 1 ;++x && ++ y && ++z ;printf("x=%d,y=%d,z=%d\n",x,y,z) ;

x = y = z = -1 ;++x && ++ y || ++z ;printf("x=%d,y=%d,z=%d\n",x,y,z) ;

x = y = z = -1 ;++x || ++ y && ++z ;printf("x=%d,y=%d,z=%d\n",x,y,z) ;

x = y = z = -1 ;++x && ++ y && ++z ;printf("x=%d,y=%d,z=%d\n",x,y,z) ;return 0 ;

}

ヒント: &&, ||, ++ の意味と評価順序を考えよ. これは, 変数の値によっては, 評価が行われない部分があり, このようなコードを書いてはいけない典型的な例である.

Exercise 6.9.4 次のプログラムの出力結果がなぜそのようになるかを考えよ.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

数理解析・計算機数学特論 193

#include <stdio.h>int x, y, z ;int main(int argc, char **argv){x = 3 ; y = 2 ; z = 1 ;printf("%d\n", x | y & z) ;printf("%d\n", x | y & ~z) ;printf("%d\n", x ^ y & ~z) ;printf("%d\n", x & y && z) ;

x = 1 ; y = -1 ;printf("%d\n", !x | x) ;printf("%d\n", ~x | x) ;printf("%d\n", x ^ x) ;x <<= 3 ; printf("x = %d\n", x) ;y <<= 3 ; printf("y = %d\n", y) ;y >>= 3 ; printf("y = %d\n", y) ;return 0 ;

}

この中には, 処理系依存になっているものがある. どれが処理系依存かを考えよ.

Exercise 6.9.5 次のプログラムの出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>

char c ;unsigned char n ;double d ;float f ;long l ;int i ;

int main(int argc, char **argv){c = 0x7F ; printf("c=%X\n",c) ;c = 0x80 ; printf("c=%X\n",c) ;

n = 0x7F ; printf("n=%X\n",n) ;n = 0x80 ; printf("n=%X\n",n) ;

i = l = f = d = 100/3 ;printf("i=%8d\tl=%.8ld\tf=%.8f\td=%.8f\n", i,l,f,d) ;

d = f = l = i = 100/3 ;printf("i=%8d\tl=%.8ld\tf=%.8f\td=%.8f\n", i,l,f,d) ;

i = l = f = d = 100/3 ;printf("i=%8d\tl=%.8ld\tf=%.8f\td=%.8f\n", i,l,f,d) ;

d = f = l = i = (float)100/3 ;printf("i=%8d\tl=%.8ld\tf=%.8f\td=%.8f\n", i,l,f,d) ;

i = l = f = d = (float)100/3 ;printf("i=%8d\tl=%.8ld\tf=%.8f\td=%.8f\n", i,l,f,d) ;

i = l = f = d = (double)100/3 ;printf("i=%8d\tl=%.8ld\tf=%.8f\td=%.8f\n", i,l,f,d) ;

i = l = f = d = 100.0/3 ;printf("i=%8d\tl=%.8ld\tf=%.8f\td=%.8f\n", i,l,f,d) ;return 0 ;

}

ヒント:算術変換が各所に含まれている.

Id: C3.tex,v 1.17 2001-03-20 15:15:41+09 naito Exp

194 数理解析・計算機数学特論

Exercise 6.9.6 short, int, unsigned int, long のそれぞれの型の変数が何バイトの記憶領域をとるかを表示するプログラムを書け.

Exercise 6.9.7 int, long のそれぞれの型で表現される最大の数に 1 を加えたらどうなるかを考察せよ.

Exercise 6.9.8 正の浮動小数点数の小数点以下を四捨五入した値を求めるプログラムを書け.

Exercise 6.9.9 AND, OR, NOT から XOR を作れ.

Exercise 6.9.10 Example 6.8.6 はどうしてかを考察せよ.

Exercise 6.9.11 int が16ビット, long が32ビットの時, -1L < 1U, -1L > 1UL となる. 何故か?

6.10 文

6.10.1 制御文

制御文とは, プログラムの流れを制御する構造で, 以下のものがある.

• 繰り返し文• 条件文• ラベルつき文• ジャンプ文ここでは繰り返し文と条件文のみを扱う.ジャンプ文の中でも goto 文は BASIC などの言語では多用されるが, C 言語においては, 殆んど必要な

い(はず).繰り返し文には, for 文, while 文, do–while 文がある. 条件文には, if 文, if–else 文, if–else if

文, switch 文がある. ジャンプ文の中で, C で使われるものは break 文, continue 文, return 文がある.

6.10.2 繰り返し文

繰り返し文とは, ある条件の元に文(複文)を繰り返すための制御文である.

6.10.2.1 for 文

繰り返し文の代表例としては, for 文がある. for 文の構文は次の通りである.

for(式1;式2;式3) 文

ここで, 式1から式3はどれを省いても良い. 式2は, 算術型もしくはポインタでなくてはならない.このような for 文は, 次のように制御される.

1. 式1が最初に評価される.2. 式2は各繰り返しの前に評価される. もし, 式2の結果が 0 となると, for は終りとなる.3. 繰り返しの部分が終る毎に, 式3が評価される.

ここで, 各式の副作用は, 評価の直後に完了する.

Example 6.10.1 この例は, 0 から 9 までの整数を順に印字するものである.

Id: C4.tex,v 1.6 2001-03-15 20:35:03+09 naito Exp

数理解析・計算機数学特論 195

#include <stdio.h>int i ;

int main(int argc, char **argv){for(i=0;i<10;i++) {printf("%d\n",i) ;

}return 0 ;

}

#include <stdio.h>int i ;

int main(int argc, char **argv){for(i=0;i<10;i++)

printf("%d\n",i) ;return 0 ;

}

この例では, はじめに i = 0 が実行され, i < 10 である間は, { } の部分が実行される. 各繰り返しが終ると, i++ が実行される. i が 9 になると, 繰り返しは実行されるが, それが終了した後, i++ が実行され,i は 10 となる. したがって, i < 10 が 1 となり, 繰り返しの文は実行されずに, for は終る.

Example 6.10.2 この例は, 式1から式3が省かれたもので, 無限ループを実現している.

#include <stdio.h>int main(int argc, char **argv){for(;;) ;return 0 ;

}

式2が省かれた場合は, 常にその値が non-zero と認識される.

Example 6.10.3 この例は, 1 から 10 までの和を計算している.

#include <stdio.h>int i, j ;

int main(int argc, char **argv){for(i=0,j=0;i<10;i++)j += i+1 ;

return 0 ;}

ここで, コンマ演算子が2個所に利用されている. 特に, for 文の第1式のコンマ演算子の利用法に注意.

Exercise 6.10.1 for 文を利用して, 2 から 10 までの偶数の和を計算して印字するプログラムを書け.

Remark 6.10.1 1 から 10 までの和を計算するプログラムでは, for 文を使ったものでも, いろいろな書き方が可能である.

j = 0 ;for(i=0;i<10;i++)j += i+1 ;

上の例の書き方よりも, この方が望ましい. なぜなら, j=0 と初期化を行う部分が独立し, プログラムの意図が明確になっている.

j = 0 ;for(i=1;i<=10;i++)j += i ;

Id: C4.tex,v 1.6 2001-03-15 20:35:03+09 naito Exp

196 数理解析・計算機数学特論

確かに問題なく動作するのだが, C では境界判定条件(この例では i <= 10 )では, i <= 10 と書くよ

りも i < 10 と書く方が(すなわち, 直前の例の方が)標準的である. 可能な限り標準的な C の書き方を身につける方が良い.

j = 0 ;for(i=10;i>0;i--)j += i ;

普通に考えれば i の値をインクリメントするに決まっている. 間違いなく動くのだが, このような無理な書き方はやめた方が良い. バグの元となる.

j = 0 ;for(i=11;--i>0;j+=i) ;

もうここまでいくと, 何をやっているのかわからない. 絶対ダメ!

6.10.2.2 while 文

while 文は以下のような構文を持つ.

while(式)文

ここで, 式は省くことはできない. ここで, 式は算術型もしくはポインタでなくてはならない.このような while 文は, 次のように制御される.

1. 式が最初に評価される.2. 式が 0 でない限り, 繰り返しの文が実行される.

ここで, 式の副作用は, 繰り返しのはじまる前に完了する.

Example 6.10.4 この例は, 0 から 9 までの整数を順に印字するものである.

#include <stdio.h>int i=0;

int main(int argc, char **argv){while (i < 10) {printf("%d\n",i++) ;

}return 0 ;

}

#include <stdio.h>int i=0;

int main(int argc, char **argv){while (i < 10)

printf("%d\n",i++) ;return 0 ;

}

この例では, はじめに i = 0 が実行され, i < 10 である間は, { } の部分が実行される. 各繰り返し中では, i++ が実行されているので, 繰り返し毎に i は 1 だけ増加する. i が 9 になると, 繰り返しは実行されるが, 繰り返しの文中で, i++ が実行され, i は 10 となる. したがって, i < 10 が 1 となり, 繰り返しの文は実行されずに, while は終る.

Example 6.10.5 この例は, 無限ループ25 を実現している.

25 無限ループはウインドウシステムでのアプリケーションの作成に利用される. 前に述べた for を使った無限ループか, こちらの例かどちらかの利用にとどめておいた方が良い. 無限ループの実現方法はいくらでも考え付くが, これら2つのどちらかであれば, Cを少しでも知っているプログラマなら, 見ただけで無限ループと理解可能である.

Id: C4.tex,v 1.6 2001-03-15 20:35:03+09 naito Exp

数理解析・計算機数学特論 197

#include <stdio.h>int i ;

int main(int argc, char **argv){while(1) ;return 0 ;

}

Example 6.10.6 この例は, 1 から 10 までの整数の和を計算するプログラムである.

#include <stdio.h>int i=0,j=0;

int main(int argc, char **argv){while (i < 10)j += ++i ;

return 0 ;}

ここでは和をとる直前に前置インクリメント演算子を利用していることに注意.

Exercise 6.10.2 while 文を利用して, 2 から 10 までの偶数の和を計算して印字するプログラムを書け.

Remark 6.10.2 1 から 10 までの和を計算するプログラムでは, while 文を使ったものでも, いろいろな書き方が可能である.

i = j = 0 ;while(i < 10) {i += 1 ;j += i ;

}

上の例の書き方よりも, この方が望ましいかもしれない. なぜなら, j に i の値をインクリメントする際

に, i がインクリメントされているというプログラムの意図が明確になっている.

j = 0 ; i = 1 ;while(i <= 10)j += i ;

確かに問題なく動作するのだが, C では境界判定条件(この例では i <= 10 )では, i <= 10 と書くよ

りも i < 10 と書く方が(すなわち, 直前の例の方が)標準的26 である. 可能な限り標準的な C の書き方を身につける方が良い.

j = 0 ; i = 10 ;while(i > 0)j += i ;

普通に考えれば i の値をインクリメントするに決まっている. 間違いなく動くのだが, このような無理な書き方はやめた方が良い. バグの元となる27 .

結局, 1 から 10 までの和をとるプログラムは for 文を用いた方が自然であることがわかる. じゃあ, while文はどんな時に使うかって?それは, 境界条件があらかじめ決まっていないときには for 文よりもきれい

になります.26 でも, この辺は難しいところかもしれない.27 でも, 前の for 文を使ってデクリメントする例よりはよほどマシ.

Id: C4.tex,v 1.6 2001-03-15 20:35:03+09 naito Exp

198 数理解析・計算機数学特論

Example 6.10.7 この例は, 標準入力から EOF(ファイル終端)が検出されるまで, 1文字づつをよみ, それを標準出力に書き出す.

#include <stdio.h>int c ;int main(int argc, char **argv){while((c = getchar())!= EOF)printf("%c",c) ;

return 0 ;}

このプログラムでは, c = getchar() により, 標準入力から1文字を読み, 読んだ文字が EOF でない間

は, その文字を標準出力に1文字づつ書き出している28 .このプログラムを getchar.c とし, これを実行形式にコンパイルしたコマンドを a.out としたとき,

./a.out < getchar.c

を実行してみよ.

Remark 6.10.3 上の例で

while(c = getchar() != EOF)

とすると, 正しく動作しない. 代入演算子と等値演算子の優先順位は, 等値演算子の方が上である.

6.10.2.3 do 文

do 文は以下のような構文を持つ.

do文

while(式) ;

ここで, 式は省くことはできない. ここで, 式は算術型もしくはポインタでなくてはならない.このような do 文は, 次のように制御される.

1. はじめに繰り返しの文が実行される.2. それが終るたびに式が評価される.3. 式が 0 でない限り, 繰り返しの文が実行される.

ここで, 式の副作用は, 各繰り返しの後に完了する.

Example 6.10.8 この例は, 0 から 9 までの整数を順に印字するものである.

#include <stdio.h>int i=0;

int main(int argc, char **argv){do {printf("%d\n",i++) ;

} while (i < 10) ;}

#include <stdio.h>int i=0;

int main(int argc, char **argv){do

printf("%d\n",i++) ;while (i < 10) ;

}

28 char c ではなく, int c としている理由は, Section 6.19 を参照.

Id: C4.tex,v 1.6 2001-03-15 20:35:03+09 naito Exp

数理解析・計算機数学特論 199

この例では, はじめに i = 0 が実行される. 繰り返しの部分は, 1回目は無条件に実行され, その後に i<10

が評価される. 各繰り返し中では, i++ が実行されているので, 繰り返し毎に i は 1 だけ増加する. i が 9になると, 繰り返しは実行されるが, 繰り返しの文中で, i++ が実行され, 繰り返しが終ると, i は 10 となる. したがって, i < 10 が 1 となり, 繰り返しの文は実行されずに, do は終る.

do は while とは異なり, 少なくとも1回は繰り返しの文が実行される.

Example 6.10.9 この例は, 1 から 10 までの整数の和を計算するプログラムである.

#include <stdio.h>int i=0,j=0;

int main(int argc, char **argv){doj += ++i ;

while(i < 10) ;}

ここでは和をとる直前に前置インクリメント演算子を利用していることに注意.

Exercise 6.10.3 do 文を利用して, 2 から 10 までの偶数の和を計算して印字するプログラムを書け.

やはり 1 から 10 までの和を計算するプログラムを, do 文を使っていろいろな書き方が可能である. しかし, これまでに見てきたように, わざわざプログラムを難しく書く必要はない.do 文は C では余り多用される構造ではない. なぜなら, ループ終了条件が一番最後にあり, ひとめでは

ループの構造がわかりにくいからである. 一般には,「必ず一度はループに入る」ということを明示的に示したいときに用いるのだが, ループ全体が短く, ひとめでループの構造がわかるときだけに使うのが良い.do 文は, while 文と break 文の組合わせで同等なものを実現することが出来る.

6.10.3 ラベル文

はじめにラベル文の構造を見ておこう. ラベル文は以下のいずれかの構造を持つ.

識別子 : 文cast 定数式 : 文default : 文

case, default ラベルは switch 文で用いられる. case ラベルの定数式は整数式でなければならない.

6.10.4 ジャンプ文

ジャンプ文とは, 無条件または条件付きで, プログラム制御を他の部分に移すために用いられる. ジャンプ先には, ラベル文で定義されたラベルを指定できる場合がある.

6.10.4.1 goto 文

goto 文は以下のような構文を持つ.

goto ラベル

このラベルは, goto 文がある関数内にあるラベルでなくてはならない.

Id: C4.tex,v 1.6 2001-03-15 20:35:03+09 naito Exp

200 数理解析・計算機数学特論

goto 文はプログラムの流れを混乱させることが多いため, C 言語ではほとんど用いられない. もし goto

文を使うようなことがあれば, プログラム全体をもう一度見直して書き直した方が良い.

6.10.5 return 文

return 文は以下のような構文を持つ.

return 式

関数実行中に return 文に出会うと, プログラム制御は直ちに関数の実行をやめ, 関数を呼び出した部分に戻る. この時, 関数が戻り値を持つと定義されているときには, (式)で指定した値を関数の戻り値とする. void 型の戻り値を持つ関数には, 式を持つ return 文は使えない. また, void 型以外の戻り値を持つ関数で, 式を指定しない return 文によって関数を終了したときの戻り値は不定となる.

Example 6.10.10 以下の例はプログラム呼び出しの引数が無い場合に, プログラムをすぐに終了するものである.

int main(int argc, char **argv){if (argc < 1) return 1 ;return 0 ;

}

6.10.6 break 文

break 文は以下のような構文を持ち, switch 文と繰り返し文の中にだけおくことが出来る.

break

break 文に出会うと, break 文を含む最小の文の実行を終了させることが出来る. 制御は文が終了した次の文に移る.

Example 6.10.11

int i = 0 ;while(i < 20) {if (i == 10) break ; /* break 文1*/while(i < 20) {

i += 1 ;if (i == 11) {i += 1 ;break ; /* break 文2*/

}}/* break 文2の抜け出し先はここ */

}i += 1 ;/* break 文1の抜け出し先はここ */

6.10.7 continue 文

continue 文は以下の構文を持ち, 繰り返し文の中にだけおくことが出来る.

Id: C4.tex,v 1.6 2001-03-15 20:35:03+09 naito Exp

数理解析・計算機数学特論 201

continue ;

continue 文に出会うと, それを含んだ最小のループを直ちに実行させる. この時, while 文, do 文の場合は, 評価に移り, for 文の場合には, 第三式の評価に移る.

Example 6.10.12 continue 文を用いると, 1 から 10 までの偶数の和を計算するものを以下のように書くことが出来る.

int i = 0, j = 0 ;while(i++ < 10) {if (i&1) continue ;j += ++i ;

}

i&1 は i が奇数の時に 1 となる. したがって, i が偶数の時には, continue 文が実行される.

Example 6.10.13 continue 文と break 文を用いると, 1 から 10 までの偶数の和を計算するものを以下のように書くことが出来る.

int i = 0, j = 0 ;while(++i) {if (i&1) continue ;j += i ;if (i == 10) break ;

}

Example 6.10.14 次の例は, 0 から 9 までの整数を順に印字する.

int n=0 ;for(;;) {printf("%d\n", n) ;if (++n == 10) break ;

}

Remark 6.10.4 しかし, この3つの Example のようなコードを書くのはやめよう. あくまで, break 文と continue 文の例として書いたに過ぎない.

6.10.8 条件文

条件文とは, 条件によって実行を分岐させるための制御文である.

6.10.8.1 if 文

最も簡単な条件文は if 文である. if 文は以下のような構文を持つ.

if (式)文

ここで, 式は省くことはできない. ここで, 式は算術型もしくはポインタでなくてはならない.このような if 文は, 次のように制御される.

• 式が 0 でなければ, 文が実行される.

Id: C4.tex,v 1.6 2001-03-15 20:35:03+09 naito Exp

202 数理解析・計算機数学特論

6.10.8.2 if–else 文

if–else 文は以下のような構文を持つ.

if (式)文1

else文2

ここで, 式は省くことはできない. ここで, 式は算術型もしくはポインタでなくてはならない.このような if–else 文は, 次のように制御される.

• 式が 0 でなければ, 文1が実行される.• 式が 0 であれば, 文2が実行される.

if–else は繰り返して使うことができ, 次のような形になる.

if (式1)文1

else if (式2)文2

else if (式3)文3

else文4

この形では, 式1から順番に式が評価される. 式1が 0 であれば, 式2を評価するという順番で, 条件式が評価されていく.

Example 6.10.15 次の例は n が 0 か正か負かを判定している.

int n;

if (n == 0)printf("n is equal 0\n") ;

else if (n < 0)printf("n is negative\n") ;

elseprintf("n is positive\n") ;

Remark 6.10.5 上の Exampleの最初の if (n == 0)は if (!n)と書くこともできるが,後の if--else

との整合性を考えると, ここは n == 0 と書いた方が良い.C の標準関数の中で, int 型の変数が 英小文字であるかどうかを判定するための関数として, islower

関数がある. c が英小文字であるときには islower(c) は 1 となり, そうでないときには 0 となる. このような関数を利用するときには,

if (islower(c))

と書けば(そのままこの文章を英語で読めば)非常にわかりやすい.しかし, C の標準関数の中で, 2つの文字型の配列(文字列)が同一であるかどうかを判定する関数とし

て, strcmp がある. char *s, *t に対して, strcmp(s,t) はその辞書式順序に従う差を与える. すなわち,s, t の指し示す文字列が同一内容であるときに 0 を返すため,

if (strcmp(s,t) == 0)

によって, s, t が等しいときの分岐を表すことになる.条件式を簡潔に書くことは, プログラムを見易くするために重要なことであるが, 条件式を余りに簡潔に

しすぎると思わぬバグを産む可能性がある.

Id: C4.tex,v 1.6 2001-03-15 20:35:03+09 naito Exp

数理解析・計算機数学特論 203

if–else 文には曖昧な構文がある.

Example 6.10.16 次の例は else がどちらの if に結び付いているかわかりにくくなった例である.

if (n > 0)if (a > b)z = a ;

elsez = b ;

ここで, 文法的には else は2番めの if に結び付いている.このような表現を避けるため, if–else で制御される文は {} をつけて複文にすることが望ましい.

if (n > 0) {if (a > b) z = a ;}

else z = b ;

if (n > 0) {if (a > b) z = a ;else z = b ;

}

6.10.8.3 switch 文

switch 文は以下のような構文を持つ.

switch (式) {case 定数式: 文case 定数式: 文default: 文

}

ここで, 式は省くことはできない. 式は整数型でなくてはならない. default はなくても良い. 一つの switch に許される default は高々一つ. case の定数式で重複するものがあってはならない. 一つのswitch 文には少なくとも 257 個の case ラベルを用いることが出来る. また, switch は入れ子にできる.このような switch 文は, 次のように制御される.

• 式の結果に応じて, 定数式で表されたどれかの式に制御が移る.• もし, defalut が存在し, 式の結果が定数式のどれとも一致しない時には, default が実行されるが,default が存在しない時には, どれも実行されない.

ここで, 式は, 副作用も含めて, 全て最初に評価され, 整数への格上げを受けた定数式と比較される.switch で重要なことは, 次のことである.

• マッチした定数式に対応する文が実行された後, 制御は次の case ラベルもしくは default ラベルを

持つ文に移され, 無条件に実行される.

Example 6.10.17 この例では, n の値によって, 結果を印字しようとしているが, 期待通りには動かない.

int n ;

switch (n) {case 1: printf("n は 1 だよ\n") ;case 0: printf("n は 0 だよ\n") ;case -1: printf("n は -1 だよ\n") ;default: printf("n は 1, 0, -1 のどれでもない\n") ;

}

これは, n = 1 の時,

Id: C4.tex,v 1.6 2001-03-15 20:35:03+09 naito Exp

204 数理解析・計算機数学特論

n は 1 だよn は 0 だよn は -1 だよn は 1, 0, -1 のどれでもない

という結果をうち出す.

このように switch 文の中で, 一つの case ラベルを実行し, 次の case ラベルに制御が移ってしまうこと

を “Fall Throught” と呼ぶが, Fall Throught を使う仕様はバグの原因となる.このようなことを避けるためには, break 文を使う.

Example 6.10.18 上の Example を改良した.

int n ;

switch (n) {default: printf("n は 1, 0, -1 のどれでもない\n") ;

break ;case 1: printf("n は 1 だよ\n") ;

break ;case 0: printf("n は 0 だよ\n") ;

break ;case -1: printf("n は -1 だよ\n") ;

break ;}

この改良により, この switch 文ではどれか一つのラベルに対する文しか実行しなくなる. 出来ればdefault ラベルを持つ文は一番最後につけよう. その方がわかりやすい.

6.10.9 演習問題

ここの演習問題は, ここまでに学んだ繰り返し文や条件文を使って書くことができる.はじめに演習問題をやるために必要な関数について注意しておく.

6.10.9.1 必要な関数と簡単な例

ここで述べた関数については, 詳しくはオンライン・マニュアルを参照すること.29

6.10.9.1.1 getchar 標準入力から1文字を入力するための関数は, getchar である.

int getchar()

利用法は, 以下の通り.

Example 6.10.19 標準入力から1文字を読んで,それを出力する. プログラムを終了するには, Control��������+ D�

を入力する.

int c ;

while ((c = getchar()) != EOF) {printf("%c",c) ;

}

ここで, getchar の戻り値として, int 型の変数で受けていることに注意. char 型ではない. また, EOFとは, ファイルの終りを表す特別な文字である.29 オンライン・マニュアルで, 期待のものが出てこない時には, man -s 3s getchar などと -s 3s を入れてみると良い.

Id: C4.tex,v 1.6 2001-03-15 20:35:03+09 naito Exp

数理解析・計算機数学特論 205

6.10.9.1.2 数値を入力する 多くの C 言語の教科書には,「標準入力から整数数値を入力するためには,scanf を使う. 」と書いてある. これは大きな間違い!少なくともこの段階で利用できるものだけを使うとすると, 標準入力から1文字を読み, それを数値に変

換することを考えよう. もし, 文字集合が ASCII であることが仮定できれば,

int c, n ;c = getchar() ;if (isdigit(c)) n = c - ’0’ ;

とすれば, 標準入力から読んだ文字が 0 から 9 であるときには, その数値を n に代入することが出来る.文字集合を仮定しないのであれば,

int c, n ;c = getchar() ;switch(c) {case ’0’: n = 0 ; break ;case ’1’: n = 1 ; break ;case ’2’: n = 2 ; break ;case ’3’: n = 3 ; break ;case ’4’: n = 4 ; break ;case ’5’: n = 5 ; break ;case ’6’: n = 6 ; break ;case ’7’: n = 7 ; break ;case ’8’: n = 8 ; break ;case ’9’: n = 9 ; break ;}

とするしかないであろう. 文字列を使えば, もう少しエレガントに操作することが出来る.

6.10.9.1.3 乱数の発生 random() という関数は 0 から 231 − 1 の乱数を発生させる. 通常, 次のようにして使う.

Example 6.10.20 乱数を2つ発生させる.

long n,m ;

srandom((unsigned int)time(NULL)) ;n = random() ;m = random() ;

srandom の部分は, 乱数を初期化する部分で, 現在の時刻から決まる数を使って初期化をしている.

6.10.9.2 演習問題

Exercise 6.10.4 摂氏と華氏の温度対応は, ◦C = (5/9)(◦F − 32) である. 華氏の温度(整数)を入力して, 摂氏の温度を印字するプログラムを書け. その際, 摂氏の温度として, 小数点以下切捨て, 小数点以下四捨五入, 小数点以下第2桁めを四捨五入の3種類を書け.

Exercise 6.10.5 標準入力から入力された1文字が英字でなければ, そのまま印字し, 大文字の英字なら小文字に, 小文字の英字なら大文字を印字するプログラムを書け. ただし, 改行だけが入力された場合には,終了するようにしなさい. また, 入力された文字は ASCII コードで表現されていることに注意せよ.

Exercise 6.10.6 unsigned long 型の数値を入力して, それを2進数で表示するプログラムを書け.

Exercise 6.10.7 0 から 28 − 1 までの乱数を十分沢山発生させ, その値が 27 以下の確率を表示するプロ

グラムを書け.

Id: C4.tex,v 1.6 2001-03-15 20:35:03+09 naito Exp

206 数理解析・計算機数学特論

Exercise 6.10.8 次のコードの最後の比較は, どうなるかを考察せよ.

double x ;

x = 0.1 ;if (0.5 == 5*x) {print ("0.5 と 5 * 0.1 は等しい\n") ;

}else {print ("0.5 と 5 * 0.1 は等しくない\n") ;

}

Exercise 6.10.9 画面に

次のうちの誰が好きですか?1. 本上まなみ2. 桜井淳子3. 戸田菜穂4. 中山美穂5. 今井美樹6. 上原多香子7. 若村麻由美8. 葉月里緒菜9. 松雪泰子0. どれでもない

と表示して, 0 ~ 9 迄を入力した時には, 何か好きなことを表示して, それ以外の時には, もう一度質問を表示するプログラムを書け. (当たり前なのだが, 別に葉月里緒菜などにする必要はない.)

Exercise 6.10.10 次のプログラムの出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>int x ;int main(int argc, char **argv){x = 0 ;if (x == 0) { printf("if part\n") ; }else { printf("else part\n") ; }

if (x) { printf("if part\n") ; }else { printf("else part\n") ; }

if (!x) { printf("if part\n") ; }else { printf("else part\n") ; }

x = 1 ;if (x&1) { printf("if part\n") ; }else { printf("else part\n") ; }

if (x&1==0) { printf("if part\n") ; }else { printf("else part\n") ; }return 0 ;

}

6.11 関数とは

関数とは, ある一定の処理をさせるための部分的なプログラムのことである.これまでに利用してきた, printf, getchar, random などは, 全てあらかじめ組み込まれた関数の例であ

る. このような例のように, C 言語では多くの標準的な関数が用意されている. 一方, 我々が作るプログラ

Id: C5.tex,v 1.17 2001-03-24 20:00:39+09 naito Exp

数理解析・計算機数学特論 207

ムにおいても, 関数を利用することによって, プログラムの構造をわかりやすくできる利点がある. C 言語の標準的な関数にはどのようなものがあるかは, [2, B] に書かれている.

6.11.1 関数の定義

6.11.1.1 関数の定義と例

C 言語では関数は0個もしくは1個以上の引数(ひきすう) (parameter) を持ち, 0個もしくは1個の戻り値(もどりち) (return value) を持つ. 引数, 戻り値は, 数学における関数の独立変数, 関数の値に相当すると考えられるが, C 言語の関数は, 引数が関数を呼び出した後に変化することも多い.30

関数は, 以下の形をしている.

<戻り値の型> <関数の識別子> ( <関数の引数> )関数の本体となる複文

関数の引数として書かれた変数(これを仮引数 (parameter) と呼ぶ)の識別子は, その関数内のみで有効な識別子である.(Section 6.11.1.4 参照.)また, 関数を呼び出したときの引数を実引数 (argument) と呼ぶ.

Example 6.11.1 仮引数として int 型の引数を2つ持ち, それらの和を int 型で返す関数は以下のよう

に書かれる.

int sum(int n, int m){return n + m ;

}

このように, 関数の戻り値を指定するには return 文を用いる.

return 文に出会うと関数の実行は終了し, その後の部分は実行されない.

Example 6.11.2 次は, 2つの int 型の引数の小さくない方の値を返す関数.

int max(int n, int m){if (n < m) return m ;return n ;

}

int max(int n, int m){return (n < m) ? m : n ;

}

関数の戻り値の型を省いた時には int であると解釈される. (cf. [3, X 3010 6.7.1, p. 1922], [2, A10]) したがって, 戻り値を持たない関数の場合, 戻り値の型は void であると明示的に宣言しなくてはならない.関数の戻り値の型として配列をとることは出来ない.また, 引数を持たない関数も作ることができる.

Example 6.11.3 この例は, 実際には余り意味がない.

void only_print(void){printf("Hello\n") ;return ;

}

30 このようなことを副作用と呼ぶ.

Id: C5.tex,v 1.17 2001-03-24 20:00:39+09 naito Exp

208 数理解析・計算機数学特論

文法上は only_print() としても良いが, 明示的に only_print(void) とした方が良い.

一方, printf などは, その場合により引数の数が異なる関数の例である. このような関数を可変引数を持つ関数と呼ぶ.31

Example 6.11.4 このプログラムでは, 関数の引数の識別子(仮引数)と, 関数を呼び出している部分で使われている変数(実引数)の識別子が同じものになっているが, それぞれの実体が違うことに注意. (→Section 6.12).

#include <stdio.h>int a,b,c ;

int sum(int b, int c){return b + c ;

}

int main(int argc, char **argv){a = 0 ; b = 1 ;c = sum(a,b) ;return c ;

}

main もまた関数になっていることに注意. また, 関数はプログラムファイル中の(他の関数内でなければ)どこに書いても良い. さらに, Section 6.13 で述べるように, C では一つのプログラムを複数のファイルに分割することができ, 関数全体が一つのファイル内にあれば, 呼び出される関数が他のファイルにあっても良い.

6.11.1.2 関数の引数について

関数の引数は, どのような型を持っても良い. 関数の引数として用いられた識別子を持つ変数は, その関数内でのみ有効であることは, 前に述べた. ここでは, 関数に渡される引数がどのような型変換を受けるかを解説する.

C 言語においては数の累乗は演算子では定義されていないので, 関数を呼び出すことになる. 累乗を計算する C 言語の標準的な関数としては pow がある. この関数は, 以下のように定義されている.32

double pow(double x, double y)

これは, x の y 乗の値を返す関数である. ここで, 問題となるのは, 全ての変数が double で宣言してある

ことである. この関数を次のように呼び出したらどうなるだろうか?

int n=2,m=3 ;

pow(n,m) ;pow(3,5) ;

第一の呼び出しは, int 型の変数を用いて呼び出しているし, 第二の呼び出しは, 整数の定数を用いて呼び出している.このように, 関数の定義の異なる型の変数や定数を用いて関数を呼び出す時には, 暗黙の型変換(integer

promotion など)が行なわれる. 実際, int 型の変数は double に変換される. (もちろん, 整数から浮動小数点数への変換の規則が用いられる.)31 可変引数の関数は後ほど述べる.32 man 3 pow 参照.

Id: C5.tex,v 1.17 2001-03-24 20:00:39+09 naito Exp

数理解析・計算機数学特論 209

Example 6.11.2 で書いた, 2つの int 型の変数の小さくない方の値を返す関数を以下のように使ってみ

よう.

double x = 2.5, y = 0.3 ;

int max(int n, int m){return (n < m) ? m : n ;

}

int main(int argc, char **argv){printf("%f\n", max(x,y)) ;return 0 ;

}

これでは全く正しい結果が得られない. 実際には, 2つの数の大きさを比較して小さくない方の値を表示させるには,

#define max(A,B) (A < B) ? B : Aprintf("%f\n", max(1.0, 2.0)) ;

とした方が良い.

Remark 6.11.1 関数の実引数の評価順序は不定であるので注意すること.

int v ;func(v++,v++) ;

とすると, 実引数としてどのような値が渡されるかはわからない.

Remark 6.11.2 演算においては, 演算の優先順位と結合規則は, 式の構造のみを決定するだけで, その評価順序は不定であることに注意しよう. たとえば,

f() + g() * h()

としても, g と h の呼出しが先に行われる保証はない. Example 6.8.22 の例, a/b*b および a*b/b ではど

のような順序で式 a, b の評価が行われても, その優先順位と結合規則にしたがって計算が行われていると理解すれば十分であったが, 関数呼出しのように副作用がある場合には, その副作用が完全に終了33 するの

は式全体の評価が終了した時点であることに注意しなければならない.

6.11.1.3 関数のプロトタイプ宣言とヘッダファイル

Section 6.11.1.1 では, 関数の戻り値の型を省略した時には int であると述べた. また, 関数を書く場所はどこでも良いと書いた. このことで, 次のような2つの問題が起きる.例えば, double 型の引数と戻り値を持つような関数 test fn を作り, 以下のようにしたとする.

33 式の評価において, 副作用が完全に終了する場所を, 副作用完了点と呼ぶ. この場合, 副作用完了点は式全体の評価が終わった点である.

Id: C5.tex,v 1.17 2001-03-24 20:00:39+09 naito Exp

210 数理解析・計算機数学特論

int main(int argc, char **argv){int n ;n = test_fn(n) ;

}

double test_fn(double a){...}

これは, 正常にコンパイルできるだろうか?まず, 次のような問題があることに気が付く.

• main 関数内にある識別子 test fn が, その時点で何を示すかわからない.

C コンパイラは識別子 test fn が実引数を伴っていることにより, 関数であることを認識する. 関数シンボル(識別子)の解決は, 最終的にはリンカ(Section 6.13 参照)で行うので, この問題は本質的な問題にはならない.もう一つの問題は, 次のようなところにある. ANSI 規格 [3, X 3010 6.7.1, p. 1922] および K&R [2,

A10] によると,「関数定義の宣言子において省略されている型は int とみなす」とされている. すなわち,戻り値の型や仮引数の型が省略されると, コンパイラはそれを int とみなして処理を進める. したがって,コンパイラは以下のような処理を行う.

• コンパイラが main 内で test fn を呼び出す時には, test fn の戻り値を int と解釈している. 同時に test fn の仮引数の型が int であると仮定して, 処理を進める.

• 次に, test fn の部分をコンパイルする時には, 戻り値や仮引数の型が double となっているので, 矛盾がありエラーとなる.

この様な, 呼び出しと定義の部分の矛盾を避けるために, 関数のプロトタイプ (prototype) 宣言を行なう必要がある. プロトタイプ宣言とは, 関数の持つ引数の型と戻り値の型のみを書いた文を, その関数が利用される前に書いておくことである.上の例の場合には

extern double test_fn(double) ;

という一行を, main の前に書けば良い. 複数の引数を持つような関数のプロトタイプ宣言は

extern double test_fn(double, double) ;

などと書く. すなわち, 全体としては

extern double test_fn(double) ;int main(int argc, char **argv){int n ;

n = test_fn(n) ;}

double test_fn(double a){...}

となる. ここで使われた extern は記憶クラス指定子 (storage class specifier) の一つであり, 詳しくはsection 6.12 で議論する.

Id: C5.tex,v 1.17 2001-03-24 20:00:39+09 naito Exp

数理解析・計算機数学特論 211

Remark 6.11.3 上の例で用いた関数プロトタイプ宣言は

int main(int argc, char **argv){extern double test_fn(double) ;int n ;

n = test_fn(n) ;}

double test_fn(double a){...}

と書くことも可能である. この場合, 関数 test fn のプロトタイプが有効なのは, main 関数の内部に限られる.

C 言語のプログラムを書く際に, #include <stdio.h> などということを書いた. ここで使われたヘッダファイル stdio.h には, いくつかの関数(printf など)のプロトタイプ宣言などが書かれている. そのような意味で, 標準的な関数を利用する際には, それぞれのプロトタイプ宣言を含むヘッダファイルを#include でインクルードしなくてはならない.

6.11.1.4 関数内の局所変数

関数内で局所的にしか利用できない変数を作ることができる. このような変数を定義するには, 関数のブロック内に変数の定義をすることで, 局所変数の定義となる.局所変数は static 宣言をしない限り34 関数が呼び出されるごとに変数領域が確保され, しかるべき初

期化を受ける35 .また, 局所変数の識別子は, その関数内でのみ有効である. 局所変数の識別子と, 関数引数の識別子は重

なってはいけない. 即ち, 関数内で局所的に有効な変数は, 関数引数と局所変数である.

Example 6.11.5 この例では, sum, main の両方の関数内で局所変数を定義している.

#include <stdio.h>extern int sum(int, int) ;

int main(int argc, char **argv){int a,b,c ;

a = 0 ; b = 1 ;c = sum(a,b) ;return c ;

}

int sum(int b, int c){int a ;

a = b + c ;return a ;

}

ここで, main の変数 a,b,c と, sum の変数 a,b,c は異なるものであることに注意.34 static に関しては後述する. static も記憶クラス指定子の一つである.35 明示的に初期化をしない限り, 初期値は不定である.

Id: C5.tex,v 1.17 2001-03-24 20:00:39+09 naito Exp

212 数理解析・計算機数学特論

また, どの関数にも含まれない部分で定義された変数(このようなものを大域変数と呼ぶ.)は, そのファイル中の定義(宣言)以後はどこでも有効であるが, 大域変数と局所変数もしくは関数引数の識別子が重なった時には, その識別子は関数内では局所変数のものと見倣される36 .

Example 6.11.6 この例では, sum という関数内で局所変数を定義し, 一方大域変数も利用している.

#include <stdio.h>int a,b,c ;extern int sum(int, int) ;

int main(int argc, char **argv){a = 0 ; b = 1 ;c = sum(a,b) ;return c ;

}

int sum(int b, int c){int a ;

a = b + c ;return a ;

}

ここで, sum の中では, 変数 a,b,c は, 大域変数ではなく, 局所変数, 関数引数で定義されたものが見えていることに注意せよ.

局所変数に関しては, Section 6.12 で詳しく議論する.

Exercise 6.11.1 次のプログラムの出力結果がなぜそうなるかを考えよ.

#include <stdio.h>

int a = 1, b = 2, c = 3 ;extern int foo(void) ;

int main(int argc, char **argv){int a = 0, b = 1 ;

printf("a = %d, b = %d, c = %d\n", a,b,c) ;foo() ;foo() ;printf("a = %d, b = %d, c = %d\n", a,b,c) ;return 0 ;

}

int foo(void){int a = 2, b = 0 ;

printf("a = %d, b = %d, c = %d\n", a,b,c) ;a += 1 ;printf("a = %d, b = %d, c = %d\n", a,b,c) ;return ;

}

6.11.1.5 main 関数の引数

main 関数の引数は ANSI には厳密には来ていされていないが, 通常は2つの引数をとり,36 このようなことを, 変数のスコープと呼び, Section 6.12.2.3 で詳しく述べる.

Id: C5.tex,v 1.17 2001-03-24 20:00:39+09 naito Exp

数理解析・計算機数学特論 213

int main(int argc, char **argv)

となる37 . これらの引数については Section 6.14.3 で述べる.

6.11.1.6 ライブラリのリンク

標準関数を利用した場合, その関数の実体はどこにあるのかを考えてみる.printf などは, 何も問題なくコンパイルでき, 実行できるが, pow などの数学関数を使い, 通常のように

コンパイルすると,

ld: Undefined symbol_pow

collect2: ld returned 2 exit status

という error が出る. これは, 関数 pow の実体が探せないということで, その実体のありかを指定しなくてはならない. このように関数の実体が集まって, あらかじめ用意されているものをライブラリと呼び,libXXXX.a などというファイルになっている. そこで, libXXXX.a を使うには, 次のようにしてコンパイルする.

% gcc a.c -lXXXX -o a

-l の後に書いたのは, libXXXX.a の XXXX の部分である.pow などの数学関数は, libm.a にあるので,

% gcc a.c -lm -o a

とすれば, 正常にコンパイルできる. ライブラリのリンクに関しては Section 6.13 で詳しく議論する.

6.11.1.7 関数呼び出しの実際

関数が呼び出された時に, プログラムはどのように動作しているかを考えてみよう.プログラム中で関数が呼び出された時には, 次のような手続きが行なわれて, 関数が呼び出される.

• 関数の引数として与えられた変数の値が実際の引数として扱われる.• その引数の値をスタックに積み, 関数のコード部分へ制御を移す. ただし, 複数の引数がある時に, どの順序でスタックに積むかは処理系によってことなる.

• その際に, 関数内のローカルな変数38 はスタック内に確保される.

ここで重要なことは, 関数に渡されるものは変数の値であって, 変数そのものではないことである. このような関数の呼び出しを call by value (値渡し)と呼ぶ.

Pascal の var を利用した変数の渡し方 (call by address(参照呼び出し)) と同様なことを C 言語で実現するには, ポインタが必要となる.

6.11.2 再帰的関数呼びだし

関数はそれ自身をその内部で呼び出すことができる. これを再帰的関数呼び出し (recursive function call)と呼ぶ. 再帰的関数呼び出しを利用すると, 帰納的に定義されたものを計算することが容易になる. 数学的には, 再帰で計算できるものは必ず再帰を利用しなくても計算できることが証明されているが, 再帰で計算するとプログラムが簡潔になるという利点がある. 一方, 関数, 手続き等を呼び出す際には, 多くの処理系において呼び出しの手順として時間がかかることが多い. したがって, 再帰には時間がかかることが多い.

37 システムによっては, より多くの引数(たとえば, 環境変数など)を取ることもある.38 Section 6.12 で述べる.

Id: C5.tex,v 1.17 2001-03-24 20:00:39+09 naito Exp

214 数理解析・計算機数学特論

Example 6.11.7 次は, 帰納的に定義された数列 an+1 = an + 2, a0 = 0 の an を求める関数である.

int recursive_function(unsigned int n){if (n == 0) return 0 ;return recursive_function(n-1) + 2 ;

}

再帰的関数呼び出しの欠点は, 関数呼び出し手続きに時間が掛ること, メモリ領域としてスタック領域を大量に消費する可能性があることなどの欠点を持つ. 数学的に再帰的な定義があるからと言って, 安易に再帰呼び出しとして実現するのは必ずしも望ましくない. 単純な繰り返しを利用して書けるものをわざわざ再帰で書くのは避けるべきである. (cf. [7, Section 3.2])

6.11.3 可変引数を持つ関数

printf に代表される, 可変個の引数を持つ関数の定義方法と性質について述べておこう. ここでは, 後に解説する「ポインタ」や「文字列」を利用している.関数引数が “...” で終る時には, その関数はパラメータより多い引数をつけて呼んで良い. この余分な

変数を参照するには, ヘッダ stdarg.h で定義されるva_arg マクロを使う必要がある39 . また, 可変個の引数を持つ関数は, 少なくとも一つの名前つきパラメータを持たなくてはならない. さらに, 名前なし引数をそのまま他の関数に渡すことはできない.

Example 6.11.8 ここでは具体的に “%s” のみを許す printf に似た関数を書いてみよう.

#include <stdarg.h>int test_va(char *fmt,...){va_list ap ;char *p,*sval ;

va_start(ap,fmt) ;for(p=fmt;*p;p++) {if ( *p != ’%’ ) {

putchar(*p) ;continue ;

}switch (*++p) {case ’s’:

for(sval = va_arg(ap,char *);*sval;sval++) putchar(*sval) ;}

}va_end(ap) ;

}

この例では, test_va("test %s",str) として, 文字列を標準出力に出力する関数を実現している.関数内では, 最初に va_start マクロを利用して, 先頭の名前つき引数と va_list とを結び付けている.

引数 fmt が % でない文字の場合には, そのまま出力を行ない, continue 文により, ループを実行させる.一方, 読んだ文字が % であり, その後の文字が s の場合には, 対応する文字列(引数)を va_arg によって

探し, その文字列を表示する. 最後には, va_end により, 可変引数リストをクリーン・アップしている.この例では, 可変引数リストは char * であることが仮定されているので, それ以外の引数を代入すると

エラーとなる.

上の例では, 最初の引数である文字列によって, 可変引数リストがいくつからなるかを知ることができるが,

39 これらのマクロに関しては, [2, B7] 参照.

Id: C5.tex,v 1.17 2001-03-24 20:00:39+09 naito Exp

数理解析・計算機数学特論 215

一般に可変引数を持つ関数を作成する場合には, このようにすることができないことがある. そのような場合には, 可変引数リストを NULL で終了 (NULL terminate) することが必要になるかも知れない.

6.11.4 関数のエラー処理

ここでは, 関数のエラー処理の方法を C の標準関数である atoi 関数を例にとって考察してみよう.atoi 関数とは, 一つの文字列(char 型へのポインタ)を引数にとり, その数が10進数を表す文字列,

すなわち, 先頭以外には空白を含まない, 0 から 9 までの文字と, 先頭の符号文字だけで構成された文字列の時には, その文字列に対応する int 型10進整数値を返し, それ以外の時には int 型の 0 を返す関数である. この場合, 実引数の文字列が10進整数を表さない文字列の時と, 10進整数の 0 を表す文字列の時

に, 結果だけを見ているだけでは区別がつかない. これを区別するための方法が, 関数のエラー処理である.関数のエラー処理を行うために, C の標準ヘッダの中に errno.h というファイルがあり, 標準関数には

strerror がある (strerror のプロトライプを含むヘッダは string.h). errno.h では大域的な変数 int

errno が定義され, 関数に何らかのエラーが発生した時には, errno に 0 以外の数値をセットすることで,呼出し側の関数にエラー発生を伝えることが出来る.

Example 6.11.9 実引数に10進数を表さない文字列リテラルを指定して, atoi 関数を呼出し, エラーを検出する例.

#include <stdio.h>#include <stdlib.h>#include <errno.h>int main(int argc, char **argv){int n ;

n = atoi("x") ;printf("errno = %d\n", errno) ;if (errno) printf("Error: %s\n", strerror(errno)) ;else printf("Integer = %d\n",n) ;return 0 ;

}

実は, atoi 関数の C の規約における定義では, この場合に, errno に 0 以外の値をセットする義務はないので, このプログラムをそのまま実行すると, else 部の出力が得られる. しかし, 可能であれば, この場合に errno に 0 以外の数値をセットするような関数のコードを書くことが望ましい.

errno の数値の意味は, UNIX の場合 /usr/include/sys/errno.h にシステムに依存して値が定義され,strerror 関数はその値に対応するエラーメッセージを表す文字列(文字型へのポインタ)を返す関数で

ある. 望ましいエラー処理の例としては, Example 6.11.9 のコードの実行結果として, Solaris 2.6 の場合には, errno として, EINVAL (値は 22) を返し, 結果として Invalied argiment という出力をするのが良い.

6.11.5 演習問題

Exercise 6.11.2 次のような関数を書け.

int mul(int n, int m)

mul は n と m の積を返す. ただし, mul 関数の中で n*m を計算してはならない.

Exercise 6.11.3 次のような関数を書け.

int div(int n, int m)

Id: C5.tex,v 1.17 2001-03-24 20:00:39+09 naito Exp

216 数理解析・計算機数学特論

div は n を m で割った商を返す. ただし, div 関数の中で n/m を計算してはならない. また, m が非正の場合には, errno に EINVAL に対応する値を返す.

Exercise 6.11.4 次のような関数を書け.

int mod(int n, int m)

mod は n を m で割った余りを返す. ただし, div 関数の中で n%m を計算してはならない. また, m が非正の場合には, errno に EINVAL に対応する値を返す.

Exercise 6.11.5 次のような関数を書け.

int pow_int(int n, int m)

pow int は n の m 乗を返す. ただし, 負の数の累乗の時には, errno に EINVAL に対応する値を返す.

Exercise 6.11.6 次のような関数を書け.

double pow_d(double n, int m)

pow d は n の m 乗を返す. ただし, 負の数の累乗の時には, errno に EINVAL に対応する値を返す.

Exercise 6.11.7 次のような関数を書け.

int is_upper(int c)

c が数字であれば 0 以外, そうでなければ 0 を返す. ただし, ASCII コード体系であると仮定して良い.

Exercise 6.11.8 摂氏の温度(整数)に対して, 華氏の温度を求める関数を書け.

Exercise 6.11.9 unsigned int 型の変数 x のビット位置 p から n ビットを反転し, 他のビットはそのままにしたものを返す関数 invert bit(x,p,n) を書け. ただし, 最下位ビットをビット位置 0 とする.

Exercise 6.11.10 整数 x の値を右に n ビット回転する関数 rot right(x,n) を書け.

Exercise 6.11.11 2つの正の整数の最大公約数を求める関数を書け.

Exercise 6.11.12 再帰的関数呼び出しを用いて, 2つの正の整数の最大公約数を求める関数を書け.

Exercise 6.11.13 再帰的関数呼び出しを用いて, n! を求めるプログラムを書け. また, 再帰を使わない方法を考えよ.

Exercise 6.11.14 再帰的関数呼び出しを用いて, n×n 行列の行列式を求めるプログラムを書け. (注意:この関数を作るためには, 配列を必要とする.)

Exercise 6.11.15 フィボナッチ数列, すなわち, F0 = 0, F1 = 1, Fn+2 = Fn+1 + Fn を満たす数列 {Fn}を再帰的関数呼び出しを用いて求める関数を安易に書くと,

int fib(unsigned int n){if (n == 0) return 0 ;if (n == 1) return 1 ;return fib(n-1) + fib(n-2) ;

}

となる. この関数では計算効率が悪いことが容易にわかるが, その理由を説明し, 効率よく計算できるように, 単純な繰り返しを利用して関数を書き直せ.

Exercise 6.11.16 次のような printf 関数の類似の関数を作れ. (ただし, 関数内部で printf を利用し

ても良い.)通常の printf の記述子の他に, b として, 引数の2進数による表示を行なう. ただし, フィールド幅の指定子はサポートしなくても良い.

Id: C6.tex,v 1.12 2001-03-19 21:28:13+09 naito Exp

数理解析・計算機数学特論 217

6.12 識別子

C の識別子や関数に対する重要な概念として, スコープ, 寿命, リンケージがある. ここでは, それらに関する解説を行い, C で書かれたプログラムが実行されるときに, 変数がメモリ上にどのように配置されるか, 関数呼び出しの手続きとは何かを考えていこう.

6.12.1 識別子とは

これまでにも「識別子」という言葉を何度も利用してきたが, ここで,もう一度正しく識別子 (identifier)を定義しなおそう.

6.12.1.1 識別子の分類

識別子は, オブジェクト(つまり変数), 関数または, 次のいずれかを表す(後で定義するものも含まれる).

• 構造体, 共用体, 列挙体のタグまたはメンバー,• 型定義名,• ラベル名,• マクロ名,• マクロ仮引数.

6.12.1.2 名前空間

識別子は次の4つの分類ごとに別の名前空間 (name space) に属する.

• ラベル名.• 構造体, 共用体, 列挙体のタグ名.• 構造体, 共用体のメンバー名.• それ以外のすべて. (これを通常の識別子と呼ぶ)

すなわち, 同じスコープを持つ識別子でも, 属する名前空間が異なるものは区別される40 . 具体的な例はSection 6.18.4 参照.

6.12.2 基本概念

変数や関数の定義に関わる基本的な概念として, 「宣言」, 「定義」, 「翻訳単位」, 「スコープ」, 「寿命」, 「リンケージ」を説明していくが, はじめにそれらの用語の意味をきちんと定義しておこう.

6.12.2.1 定義と宣言

これまでは, オブジェクトや関数の「定義」・「宣言」という言葉を曖昧なまま利用してきた. ここで, それらの言葉を正しく理解しよう. 宣言 (declaration) とは, 識別子の組の解釈および属性を指定することである. 識別子によって名前付けられたオブジェクトまたは関数のために記憶域の確保を引き起こす宣言

40 確かに処理系が識別子がどの名前空間に属するかを区別するのは易しい. しまし, 違う名前空間に属する, 同じ文字列からなる識別子をむやみやたらに多用すると, プログラマにとっては混乱の元になり, 自分自身の書いたコードでさえ, 何が書いてあるかわからなくなるので, そのようなことはやってはいけない.

Id: C6.tex,v 1.12 2001-03-19 21:28:13+09 naito Exp

218 数理解析・計算機数学特論

を定義 (definition) という41 .つまり, オブジェクトや関数の「定義」とは「宣言」の中に含まれている. オブジェクトの「宣言」を行

う場合には, オブジェクトは型 (type)とともに宣言されなくてはならない. また, 必要であれば記憶クラス指定子や型修飾子を伴って宣言される. 関数の「宣言」を行う場合には, 関数は戻り値の型, 仮引数の型とともに宣言されなくてはならない42 . また, 必要であれば記憶クラス指定子を伴って宣言される.このように「定義」と「宣言」を定義すると, どれが識別子の「定義」で, どれが「宣言」かわからなく

なるのだが, 一つのオブジェクトや関数に対して「定義」はただ一度だけである. 関数の「定義」は関数本体とともに宣言された時に行われる. それ以外のものはすべて「定義」を伴わない「宣言」である. オブジェクトの「定義」は通常 extern 指定子を伴わない「宣言」で行われる43 . extern 宣言の詳細については後に解説する.なお, 変数の宣言と同時に初期化を行うことが出来るが, 初期化を行うと, その宣言は定義とみなされる.

Example 6.12.1

extern int sum(int, int) ; <==== これは宣言(定義にはならない)extern int x ; <==== これは宣言(定義になるかどうかは,

他のプログラムファイルに依存する)int main(int argc, char **argv) <==== これは定義{int a ; <==== これは定義

....}

int sum(int a, int b) <==== これは定義{int c ; <==== これは定義

}

Example 6.12.2

extern int x = 1 ;

と書くと, 「extern 宣言と初期化を同時にしているけどいい?」なんて警告が出される. 初期化をする場合には, extern 宣言は書かない方が良い. (というよりも, extern 宣言の趣旨とは矛盾する.)

Example 6.12.3 2つのファイルからなるプログラムで以下のようなことをすると, リンク時にエラーとなる. (分割コンパイルに関しては, Section 6.13 を参照.)

/* file1.c */int x = 1 ;

/* file2.c */extern int x = 1 ;

これは x という識別子が2ヶ所で定義されていることがエラーの原因となる.

41 ANSI 規格書によれば, ファイルスコープのオブジェクトを, 初期化子を使わず, 記憶クラス指定子なしか static で宣言する場合を, 仮宣言 (tetative definition) と呼んでいる. これは, オブジェクトコードのリンク時に最終的にリンケージが決定されることを意味している. (cf. [3, X 3010 6.7.2, p. 1924])42 正しくは, traditional な形式の宣言も許されている. すなわち, 仮引数の型を伴わない宣言も許されるが, バグを引き起こす原因となる.# 何でこんなのを許す仕様を残しておいたのだろう?

43 つまり, これまでオブジェクトを宣言してきたものは, すべてオブジェクトの定義となっている. オブジェクトの宣言にすべてextern をつけてもエラーとはならない. リンク時にそれらの宣言のうちのいずれか一つを「定義」とみなす. これが「リンケージ」というやつ.# 要するに, extern 宣言とはきちんと使わなければ, 「メチャメチャ」になるものの典型的なものである.

Id: C6.tex,v 1.12 2001-03-19 21:28:13+09 naito Exp

数理解析・計算機数学特論 219

6.12.2.2 翻訳単位

C において翻訳単位とは, プログラムのファイル1個づつを指す44 . C ではプログララムを複数のファイルに分割し, ファイルごとにコンパイルを行い, リンカでそれぞれのオブジェクトコードを結び付けることが出来る.

6.12.2.3 スコープ

オブジェクトや関数のスコープ (scope, 日本語では有効範囲という) とは, そのオブジェクトや関数の識別子をプログラム中のどこから見えるか(可視 (visible) かどうか)を示す概念である. スコープには次の4種類がある.

1. ファイル・スコープ2. 関数・スコープ3. ブロック・スコープ,4. 関数プロトタイプ・スコープ.

スコープは, 変数を宣言する場所から決定される. 変数を宣言できる場所は以下のいずれかである.

1. どの関数にも含まれない部分. 正しい言い方では「どのブロックにも, 仮引数ならびよりも外側にあらわれる時」. この場合は「ファイル・スコープ」となる.スコープは翻訳単位の終了, すなわちファイルの終了によって終了する.

2. 「関数・スコープ」となる場合は2通りあり,

• 関数定義の仮引数.• 関数の先頭部分.

この場合, スコープは関数ブロックの終了によって終了する.3. ブロックに入った直後45 . これは「ブロック・スコープ」となり, 対応する } の出現で終了する46 .4. 関数プロトタイプ宣言の仮引数. この場合, 「関数プロトタイプ・スコープ」となり, スコープはその宣言内のみとなる.

Example 6.12.4 これらの実際の例は以下の通りである.

#include <stdio.h>int i ; /* ファイルスコープ */extern int sum(int a, int b) ; /* 関数プロトタイプスコープ */

int main(int argc,char **argv) /* 関数スコープ */{int j ; /* 関数スコープ */{int k ; /* ブロックスコープ */k = 0 ;

}return 0 ;

}int l ; /* ファイルスコープ */

44 より正確には, プリプロセッシングを終了した翻訳フェーズにおけるファイルが翻訳単位となる.45 これは余り利用しないが, 有効に利用できる場合がある. 変数を関数内で極めて局所化したい場合に利用することがある. これは, デバック (debug) を行う場合などで利用することがある. 恒久的なコードでこの手法を用いると, 思わぬ変数の隠蔽が起きる可能性があるので, デバック時などの一時的なコードにのみ用いる方がよいだろう.46 正確には, 上の「関数・スコープ」は「ブロック・スコープ」の一部であり, ANSI 規格に定める「関数・スコープ」とは, goto文にあらわれるラベル名だけが適用対象であり, このラベル名は関数内のどこからでも参照可能である. なお, ラベル名の識別子は構文の出現とともに暗黙に宣言される.このノートでは「関数・スコープ」と「ブロック・スコープ」を便宜上区別しよう.

Id: C6.tex,v 1.12 2001-03-19 21:28:13+09 naito Exp

220 数理解析・計算機数学特論

識別子は(ラベル名を除いて)すべて宣言以後でないと可視でないことに注意する. したがって, Example6.12.4 の例の識別子 l のスコープを, ファイル全体にしたい場合には, 以下の例のいずれかに書き直す必要がある.

Example 6.12.5 Example 6.12.4 の識別子 l のスコープをファイル全体にする.

#include <stdio.h>int i, l ;

int main(int argc,char **argv){int j ;

j = 0 ;{int k ;k = 0 ;

}return 0 ;

}

#include <stdio.h>int i ;extern int l ;int main(int argc,char **argv){int j ;

j = 0 ;{

int k ;k = 0 ;

}return 0 ;

}int l ;

右の例では, l の定義の位置を変えずに, extern 宣言を用いてスコープを拡大した. しかし, このような例は「奇妙な書き方」であり, わざわざこのようなことをする必要はない47 .

Example 6.12.6 文法上許されない変数宣言の例.

int main(int argc,char **argv){int j ;j = 0 ;int k ; /* この宣言は文法上許されない */k = 0 ;return 0 ;

}

この例における int k の宣言は文の後に書かれているため, 文法エラーとなる. 複文内で宣言は文のならびの前に書かなければならない.

6.12.2.3.1 識別子の隠蔽 次のような例では, 識別子の可視性はどうなるのであろうか?

int i ;int main(int argc, char **argv){

int i ;

{int i ;

}}

このようにプログラム中に同じ名前空間に属する識別子が複数あり, プログラム中のある点において, それらのうちのいくつかが可視であるとき, その点において見えている識別子は, スコープの最も小さいものとなる. したがって他の識別子は見えなくなる. これを識別子の隠蔽と呼ぶ.

int i ; <= この識別子の示すオブジェクトを i0 としよう/* この点では識別子 i は, オブジェクト i0 を参照する */

47 要するに, Example 6.12.4 の int l の宣言は, 文法上可能というだけのことである.

Id: C6.tex,v 1.12 2001-03-19 21:28:13+09 naito Exp

数理解析・計算機数学特論 221

int main(int argc, char **argv){

int i ; <= この識別子の示すオブジェクトを i1 としよう

/* この点では識別子 i は, オブジェクト i1 を参照する */{

int i ; <= この識別子の示すオブジェクトを i2 としよう/* この点では識別子 i は, オブジェクト i2 を参照する */

}/* この点では識別子 i は, オブジェクト i1 を参照する */

}/* この点では識別子 i は, オブジェクト i0 を参照する */

6.12.2.3.2 関数名のスコープ 関数名は通常はファイル・スコープを持つが, 次のような例では関数宣言がブロックスコープとなる.

int main(int argc,char **argv){int a ;extern int foo(int) ;

foo(a) ;return 0 ;

}

double foo(int a){...}

この場合, 関数 foo の関数プロトタイプ宣言は, main 関数内の関数スコープとなる.

6.12.2.4 寿命

オブジェクトの寿命とは, 正しくは記憶域期間 (storage duration) とは, プログラム実行状態において,そのオブジェクトの記憶域が存在する期間を指す. C における記憶域期間は静的 (static) と自動 (auto)の2種類がある. 寿命という概念は記憶域と関わる概念であるので, 識別子に対する概念ではなく,オブジェクトに対する概念である.オブジェクトが静的であるとは, プログラム実行の開始から終了までの期間, そのオブジェクトの記憶域

が記憶領域内に存在することをいう. オブジェクトが自動であるとは, プログラム実行中のある期間にのみ, そのオブジェクトの記憶域が記憶領域内に存在することをいう.オブジェクトが静的かどうかは, その宣言方法に依存する. ファイルスコープを持つと宣言されたオブ

ジェクトは, 必ず静的である. 一方, ブロックスコープと関数スコープを持つと宣言されたオブジェクトは,デフォールトでは自動であり, その記憶域は, その関数の実行開始から実行終了までで, 記憶領域内に存在し, その関数の実行期間以外は記憶領域内には存在しない. ブロックスコープと関数スコープを持つオブジェクトを静的にするには, 記憶クラス指定子 static をつけて宣言する.なお, 関数プロトタイプ宣言内で宣言された仮引数宣言は, 定義ではないため, 寿命とは無関係である.

Example 6.12.7 オブジェクトの寿命を見てみよう.

Id: C6.tex,v 1.12 2001-03-19 21:28:13+09 naito Exp

222 数理解析・計算機数学特論

int j ;static int n ;int add(int x){

int i ;static int l ;.....{

int k ;.....sub(k) ;.....

}}

int sub(int y){

int m ;static int s ;.....

}

この例で j, l, n, s は静的であり, x, i, k, y, m は自動である. さらに, x, i, k の記憶域存在期間は, 関数add が実行されている間であり, y, m の記憶域存在期間は, 関数 sub が実行されている間である.この例では, add から sub が呼び出されているので, add から呼び出された sub が実行されている間は,

add 内の自動変数の記憶域は存在している.

6.12.2.4.1 オブジェクトの初期化 オブジェクトの初期化の手続きは, 寿命と関連している. オブジェクトが明示的に初期化宣言48 されていない場合, 静的オブジェクトはプログラム実行開始時に記憶領域がビットパターン 0 で初期化される. 自動オブジェクトは記憶領域確保時に初期化は行われない.static 宣言されて, 明示的に初期化宣言がされているオブジェクトは, プログラム実行開始時にただ1

度だけ, その値により初期化が行われる. (cf. Example 6.12.10)

6.12.2.5 リンケージ

リンケージ (linkage, 結合) とは, 異なる有効範囲または同じ有効範囲を持って2回以上宣言された識別子を, 同じオブジェクトまたは関数を参照できるようにする操作(概念)である49 . リンケージは

• 外部リンケージ (external linkage),• 内部リンケージ (internal linkage),• 無結合 (no linkage)

の3種類に分類される. 外部リンケージを持つ同じ名前の識別子がプログラム内に複数回現れた場合には,それらは同じオブジェクトまたは関数を表し, 内部リンケージを持つ同じ名前の識別子が一つの翻訳単位(プログラムファイル)中に複数回現れた場合には, それらは同じオブジェクトまたは関数を表す. 無結合を持つ同じ名前の識別子は, それぞれが一意に決まる実体を持つ.すなわち, 同じ名前の識別子が異なったプログラムファイル中に現れ, それらが内部リンケージを持てば,

それらは別々の実体を表し, 同じ名前の識別子が同じファイル中にあっても, それらが無結合であれば, それらは別々の実体を表す.識別子の宣言で記憶クラス指定子 extern または static を指定することにより, リンケージを変える

ことが出来る. そのルールは以下の通りである.

48 初期化宣言とは, オブジェクトの定義とともに初期化を行うこと.49 C の識別子の概念の中で, もっともわかりにくいのがリンケージであり, その定義はほとんどメチャクチャと思える.

Id: C6.tex,v 1.12 2001-03-19 21:28:13+09 naito Exp

数理解析・計算機数学特論 223

1. オブジェクトまたは関数のファイルスコープの識別子の宣言が static を含む場合, 内部リンケージを持つ.

2. オブジェクトまたは関数のファイルスコープの識別子の宣言が extern を含む場合, ファイルスコープで宣言された可視であるその識別子の宣言と同じリンケージを持つ. ファイルスコープで宣言された可視であるその識別子の宣言が無い場合には, 外部リンケージを持つ.

3. 関数の識別子が記憶クラス指定子を持たない場合には, extern を宣言したかのようにリンケージを決定する.

4. オブジェクトの識別子がファイルスコープを持ち, 記憶クラス指定子を持たない場合には外部リンケージを持つ.

5. オブジェクトまたは関数以外を宣言する識別子, 関数仮引数を宣言する識別子, extern を持たないブロックスコープ(または関数スコープ)のオブジェクトを宣言する識別子は, 無結合となる.

さて, こんなことを書かれて一発でわかるわけがないので, いくつかの例を見ていこう.

6.12.2.5.1 プログラムが単一のファイルからなる場合 まず, 外部リンケージは, プログラムが複数の翻訳単位(プログラムファイル)からなる場合にのみ関係する. プログラムが単一のプログラムファイルからなる場合には, 外部リンケージと内部リンケージは, この場合には同一の意味になる.この場合に上の規約を要約すると,

• 関数の場合には, extern をつけても static をつけても, 意味は変らない. すなわち, 単一ファイル内に同じ識別子をもつ関数があれば, 同じものとみなされる.

• オブジェクトの場合には,

– オブジェクトが extern を持つとき, そのオブジェクトと同じ識別子をもつ, ファイルスコープのオブジェクトがあれば, それと同じものとみなされる.

– それ以外, すなわち static か, 何も記憶クラス指定子を持たないときには, 無結合となる. つまり, 同じ識別子を持つ他のオブジェクトとは別のものとなる.

Example 6.12.8 単一のプログラムファイルからプログラムが構成されていると仮定する. 関数 foo と

変数 l のリンケージに注意.

#include <stdio.h>int i ;int l = 2 ;static int k ;int k ;

extern void foo(void) ;

int main(int argc,char **argv){int i ;extern int l ;

printf("l = %d\n", l) ;foo() ;printf("l = %d\n", l) ;return 0 ;

}

void foo(void){l = 0 ;return ;

}

Id: C6.tex,v 1.12 2001-03-19 21:28:13+09 naito Exp

224 数理解析・計算機数学特論

関数 foo はファイル中で2度宣言されているが, 4行めの extern があってもなくても, ともに外部リンケージとなり, 同じ実体を示す. 仮に, foo の関数プロトタイプ宣言(4行め)が main 関数ブロック内に

あっても, 結果は変らない. foo の関数プロトタイプ宣言(4行め)が main 関数ブロック内にあると, プロトライプ宣言の可視性の問題により, 他の関数内からの foo の呼び出しには問題を生じる(次の Exampleを参照).変数 l はファイル中で2度宣言されているが, main 関数内の宣言において extern 宣言されているため,

3行めの宣言(定義)と結合し, 結果として l はファイルスコープを持つ. つまり, このプログラムはコンパイル可能であり, 実行すると,

l = 2l = 0

という出力を得る.その他のオブジェクト, すなわち, ファイルスコープの i と関数スコープの i は無結合であり, それぞれ

は異なる実体を持つ.オブジェクト k の static を含む宣言は内部リンケージを持つ仮定義であり, 次の宣言 int k と記憶

クラスが矛盾し, 動作は不定となることに注意. これを extern int k とすれば, 正しい宣言となり, k はstatic 記憶クラスに属することになる.

Example 6.12.9 関数プロトタイプ宣言の可視性に問題を生じた例.

#include <stdio.h>extern void bar(void) ;

int main(int argc,char **argv){extern void foo(void) ;foo() ;return 0 ;

}

int bar(void){foo() ;

}

void foo(void){return ;

}

この例では, foo の関数プロトタイプ宣言は, bar からは可視ではないため, foo の呼び出しに警告が生じる.

これら2つの例は, あくまでリンケージの例で無理やり作ったものであり, 通常は, 関数プロトタイプ宣言はファイルスコープで行い, ブロックスコープ(関数スコープ)のオブジェクトを extern 宣言したりはし

ない.

6.12.2.5.2 プログラムが複数のファイルからなる場合 元々, リンケージとはプログラムが複数のファイルからなる場合に, それぞれのファイルで宣言された識別子を結び付けるために考えられた概念である.ここでは, 関数とオブジェクトの識別子に関して別々に考えよう50

50 それ以外の識別子は無結合なので, 考慮する必要はない.

Id: C6.tex,v 1.12 2001-03-19 21:28:13+09 naito Exp

数理解析・計算機数学特論 225

6.12.2.5.2.1 関数のリンケージ まず, 関数の識別子は決して無結合にはならないことに注意しよう. そして, 関数の識別子は extern をつけてもつけなくても, 基本的には外部リンケージを持つという事実に注意する.次のような2つのファイルからなるプログラムを考えよう. この部分では左のファイルを file1.c, 右の

ファイルを file2.c とする.

int main(int argc, char **argv){foo() ;return ;

}

void foo(void){return ;

}

この場合, file1.cで呼び出している関数 fooの実体は, file2.cに書かれている関数 foo なのだろうか?

答えは YES である. なぜなら, file2.c の関数 foo の宣言は外部リンケージを持ち, 外部リンケージとはファイルを跨がって識別子を結び付ける操作である. もちろん, file1.c には foo の関数プロトタイプ

宣言がないので, まずいことがあるのは事実である. また, file1.c における識別子 foo は関数であるこ

とがわかっているので, その宣言がファイル内に存在しなくてもかまわない.それでは, 次の例は?

extern void foo(void) ;int main(int argc, char **argv){foo() ;return ;

}

void foo(void){return ;

}

今度は, file1.c に foo の関数プロトライプ宣言を入れた. この場合にも, この関数プロトタイプ宣言は外部リンケージを持ち, file2.c の関数 foo の宣言も外部リンケージを持ち, 外部リンケージとはファイルを跨がって識別子を結び付ける操作であるので, この2つの識別子の宣言は同じ実体を表すことになる. ここで, file1.c の関数プロトタイプ宣言では extern がなくても良い. もちろん, 通常は関数プロトタイプ宣言を書くのが望ましく, その場合には, 「関数プロトタイプ宣言」であることを明示する意味でも, externをつけた方が良い.ところが, 次の例はどうなるだろうか?

int main(int argc, char **argv){foo() ;return ;

}

static void foo(void){return ;

}

この例では, file2.c で関数 foo を static 宣言している. したがって, file2.c の関数 foo は内部リン

ケージとなり, file1.c の関数 foo の呼び出しは, file2.c の関数 foo を呼び出すわけではない51 .すると2つの疑問が生じる.

1. static で定義した関数の関数プロトタイプ宣言はどうするの?

2. 関数の static 宣言って一体何に使うの?

51 file1.c で呼び出される関数 foo がどのようになるかは, この2つのプログラムファイルのオブジェクトコードをリンクするまではわからない. このことについては, Section 6.13 で詳細に議論する.

Id: C6.tex,v 1.12 2001-03-19 21:28:13+09 naito Exp

226 数理解析・計算機数学特論

まず,「static で定義した関数の関数プロトタイプ宣言はどうするの?」という疑問に対する答えは,「関数プロトタイプ宣言にも extern なしで static をつける」というのが答えです. static をつけても内部リンケージは残るので, これで問題は解決.「関数の static 宣言って一体何に使うの?」ってのに対する答えは何通りも考えられる. 「プログラ

ムを複数のプログラマで開発する際に, 自分の担当するファイル中だけで利用したい関数は内部リンケージにしておく」というのが, 最もよくある通常の答え.

6.12.2.5.2.2 オブジェクトのリンケージ オブジェクトに関しては, ファイルスコープを持つものだけを考えると, リンケージに関しては, 関数とほとんど同一となる. この場合にはオブジェクトは無結合にはならない. そして, ファイルスコープの識別子は extern をつけてもつけなくても, 外部リンケージを持つという事実に注意する. すると, 関数とリンケージの扱いが同一になる.次のような2つのファイルからなるプログラムを考えよう. この部分では左のファイルを file1.c, 右の

ファイルを file2.c とする.

int i ;int main(int argc, char **argv){i = 0 ;foo() ;return ;

}

int i ;void foo(void){i = 1 ;return ;

}

この場合, file1.c で利用している変数 i と, file2.c で利用してる変数 i は同一の実体を持つ. なぜなら, file1.c, file2.c のそれぞれのオブジェクト i の宣言は外部リンケージを持ち, 外部リンケージとはファイルを跨がって識別子を結び付ける操作であるため, その実体は同じものとなる.もちろん, どれか一つの宣言を除いて, 他の宣言には extern をつけるのが望ましい. すなわち,

int i ;int main(int argc, char **argv){i = 0 ;foo() ;return ;

}

extern int i ;void foo(void){i = 1 ;return ;

}

とするのが良い. したがって, 次のような初期化は明らかな文法エラーとなる.

int i = 0 ;int main(int argc, char **argv){i = 0 ;foo() ;return ;

}

extern int i = 1 ;void foo(void){i = 1 ;return ;

}

つまり, 同一の実体を表すオブジェクトを2ヶ所で初期化宣言することは出来ない.オブジェクトの場合も, やはり static 宣言は, 他のプログラムファイルからそのオブジェクトを隠蔽す

るために用いられる. すなわち,

Id: C6.tex,v 1.12 2001-03-19 21:28:13+09 naito Exp

数理解析・計算機数学特論 227

int i = 0 ;int main(int argc, char **argv){i = 0 ;foo() ;return ;

}

static int i = 1 ;void foo(void){i = 1 ;return ;

}

とすることで, file2.c の i は内部リンケージとなり, file1.c の i とは異なった実体を持つ. この場合でも file1.c の i は外部リンケージを持っている.

Remark 6.12.1 次の2つのプログラムで変数 i の宣言に注意.

int i ;int i ;

int main(int argc,char **argv){int i ;

return 0 ;}

int i ;

int main(int argc,char **argv){int i ;int i ;

return 0 ;}

左のプログラムは文法エラーではない. なぜならファイルスコープの2つの宣言 int i はともに外部リン

ケージを持ち, この2つの識別子は同じ実体を表している.しかし, 右のプログラムは文法エラーとなる. ブロックスコープ(関数スコープ)の2つの宣言 int i

はともに無結合であるため, 同じ識別子で同じスコープを持つ2つのオブジェクトが存在する.

6.12.2.6 内部静的変数の利用法

内部リンケージまたは無結合である静的変数(オブジェクト)は, 簡単に内部静的変数と呼ばれることがある.ファイルスコープを持つ内部静的変数の利用方法は, 上に述べた通り, 他のファイルからのオブジェクト

の隠蔽であったが, ブロックスコープ(関数スコープ)を持つ内部静的変数は, ブロック外への変数の隠蔽という効果の他に重要な役割を果たす.すなわち, 関数内で定義した変数に static をつけて宣言すると, その変数に対する寿命は大域的となる.

しかし, スコープは static をつけない時と同じである. しまも, static 変数の初期化は, それがはじめて利用される時にただ一度だけ行われる. この静的な宣言は, 関数のカウンタ, フラグなどに用いる.

Example 6.12.10 この例で, 変数 i は i+=1 以外に値を変える操作がないとする.

int function(){static int i = 0 ;i += 1 ;.....

}

この時, i はこの関数が呼び出された回数を保持している.

Id: C6.tex,v 1.12 2001-03-19 21:28:13+09 naito Exp

228 数理解析・計算機数学特論

6.12.3 演習問題

Exercise 6.12.1 次のプログラムの出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>int i=0 ;int main(){auto int i=1 ;printf("i=%d\n",i) ;{int i=2 ;printf("i=%d\n",i) ;{

i += 1 ;printf("i=%d\n",i) ;

}printf("i=%d\n",i) ;

}printf("i=%d\n",i) ;

}

Exercise 6.12.2 次のプログラムの出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>int i=0 ;int main(){int i=1 ;func_1(i) ;printf("1: i=%d\n",i) ;func_1(i) ;printf("1: i=%d\n",i) ;

}

int func_1(int n){int i=0 ;

i += 1 ; n += 1;printf("2: i=%d\n",i) ;

}

また, 関数 sub_function 内で定義された変数 i を static int i = 0 と定義するとどうなるかを考察

せよ.

6.13 コンパイルとリンク

C 言語で書かれたプログラムを実行形式に翻訳する手順は, 次のステップに分解される.

1. プログラムファイル中に書かれたマクロ定義などの処理を行うプリプロセッサ (preprosessor52 .2. プログラムファイルをオブジェクトコード (object code) と呼ばれる, 機械が認識可能な命令の列に置き換えるコンパイル (compile). コンパイルを行う一連の処理系をコンパイラ (compiler) と呼ぶ.このステップでは, プログラムテキストを解析して, 中間言語に翻訳し, 中間言語からアセンブラコード(命令のニーモニックで書かれた言語)に翻訳する. さらに, アセンブラコードをオブジェクトコードに変換するアセンブラの3ステップからなることが多い.

52 プリプロセッサの終了時のプログラムコードを出力するには, gcc -E file.c とすれば良い. ここでは, マクロ定義等が展開された後の, コンパイラにかかる直前のプログラムを得ることが出来る.

Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp

数理解析・計算機数学特論 229

3. 複数のオブジェクトコードと標準関数などのオブジェクトコードの集まりである, ライブラリとを結合して, 実行形式を出力するリンク (link). リンクを行うプログラムをリンカ (linker) と呼ぶ.

link

file2.ofile1.o

compilecompile

exec code

library

file1.c file2.c

単一のプログラムファイルから実行形式を作成するための手順

% gcc file.c -o target

というコマンドは, これらの一連の操作を一度に行わせる命令である. 以下では, 複数のプログラムファイルからなるプログラムを, オブジェクトコードの作成, リンクの手順に分けて, そのためのコマンドと, それらの役割を見ていこう.

6.13.1 オブジェクトコード

6.13.1.1 オブジェクトコードの作成

file1.c というプログラムファイルからオブジェクトコードを作成するには,

% gcc -c file1.c

というコマンドを利用する. これによってオブジェクトコード file1.o が生成される.オブジェクトコードの作成は, アセンブラコードの作成とアセンブラコードの翻訳という2段階にわか

れる. プログラムファイルからアセンブラコードを出力させるためには,

% gcc -S file1.c

とすれば, file1.s というアセンブラコードを作成させることもできる. もちろん, 実行形式の作成のために必要なステップは, オブジェクトコードの作成だけである.

6.13.1.2 オブジェクトコードの中身

ここでは,オブジェクトコードには何が書かれるかを調べるために,以下の2つのファイル(左を file1.c,右を file2.c とする)を利用しよう.

Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp

230 数理解析・計算機数学特論

#include <stdio.h>int k = 0 ;extern int i ;extern int add(int) ;

int main(int argc,char **argv){int j ;static int l = 0 ;

i = j = 0 ;add(j) ;printf("%d\n", i) ;printf("%d\n", j) ;return 0 ;

}

extern int k ;extern int i ;static int l = 0 ;

int add(int j){i += 1 ; l += 1 ; k += 1 ;return j + 1 ;

}

static void foo(void){k = 0 ; l = 0 ;return ;

}

このプログラムコードをコマンド gcc -S file1.c, gcc -S file2.c を用いて得たアセンブラコードは

以下のようになる53 .

############## file1.c のアセンブラコード.file "file1.c"

gcc2_compiled.:.global k

.section ".data".align 4.type k,#object.size k,4

k:.uaword 0.align 4.type l.3,#object.size l.3,4

l.3:.uaword 0

.section ".rodata".align 8

.LLC0:.asciz "%d\n"

.section ".text".align 4.global main.type main,#function.proc 04

main:!#PROLOGUE# 0save %sp, -120, %sp!#PROLOGUE# 1st %i0, [%fp+68]st %i1, [%fp+72]sethi %hi(i), %o1or %o1, %lo(i), %o0st %g0, [%fp-20]st %g0, [%o0]ld [%fp-20], %o0call add, 0nopsethi %hi(i), %o0or %o0, %lo(i), %o1sethi %hi(.LLC0), %o2or %o2, %lo(.LLC0), %o0ld [%o1], %o1call printf, 0nop

53 これは, SunOS 5.6 上の gcc version 2.95.1 を用いて作成したアセンブラコードで, アセンブラコード, オブジェクトコードの出力は, 処理系(環境, OS, コンパイラ等)に依存する.

Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp

数理解析・計算機数学特論 231

sethi %hi(.LLC0), %o1or %o1, %lo(.LLC0), %o0ld [%fp-20], %o1call printf, 0nopmov 0, %i0b .LL2nop

.LL2:retrestore

.LLfe1:.size main,.LLfe1-main.ident "GCC: (GNU) 2.95.1 19990816 (release)"

############## file2.c のアセンブラコード.file "file2.c"

gcc2_compiled.:.section ".data"

.align 4

.type l,#object

.size l,4l:

.uaword 0.section ".text"

.align 4

.global add

.type add,#function

.proc 04add:

!#PROLOGUE# 0save %sp, -112, %sp!#PROLOGUE# 1st %i0, [%fp+68]sethi %hi(i), %o1or %o1, %lo(i), %o0sethi %hi(i), %o2or %o2, %lo(i), %o1ld [%o1], %o2add %o2, 1, %o1st %o1, [%o0]sethi %hi(l), %o1or %o1, %lo(l), %o0sethi %hi(l), %o2or %o2, %lo(l), %o1ld [%o1], %o2add %o2, 1, %o1st %o1, [%o0]sethi %hi(k), %o1or %o1, %lo(k), %o0sethi %hi(k), %o2or %o2, %lo(k), %o1ld [%o1], %o2add %o2, 1, %o1st %o1, [%o0]ld [%fp+68], %o1add %o1, 1, %o0mov %o0, %i0b .LL2nop

.LL2:retrestore

.LLfe1:.size add,.LLfe1-add.align 4.type foo,#function.proc 020

Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp

232 数理解析・計算機数学特論

foo:!#PROLOGUE# 0save %sp, -112, %sp!#PROLOGUE# 1sethi %hi(k), %o1or %o1, %lo(k), %o0st %g0, [%o0]sethi %hi(l), %o1or %o1, %lo(l), %o0st %g0, [%o0]b .LL3nop

.LL3:retrestore

.LLfe2:.size foo,.LLfe2-foo.ident "GCC: (GNU) 2.95.1 19990816 (release)"

ここで, プログラムファイル file1.c と file2.c の中で利用している関数, オブジェクトと, アセンブラコード中の記述の対応を見ていこう. ここで, gcc -c file1.c, gcc -c file2.c によって出力したオ

ブジェクトコードは, 通常の人間が理解できる形式ではないし, アセンブラコードも良くわからないので,UNIX のコマンド nm によってオブジェクトコードのシンボルテーブル (symbol table) を出力させ, このテーブルと, アセンブラコード, プログラムファイルを比較してみよう.

file1.o:[Index] Value Size Type Bind Other Shname Name[9] | 0| 0|SECT |LOCL |0 |.comment |[2] | 0| 0|SECT |LOCL |0 |.text |[3] | 0| 0|SECT |LOCL |0 |.data |[4] | 0| 0|SECT |LOCL |0 |.bss |[7] | 0| 0|SECT |LOCL |0 |.rodata |[8] | 0| 0|NOTY |LOCL |0 |ABS |*ABS*[13] | 0| 0|NOTY |GLOB |0 |UNDEF |add[1] | 0| 0|FILE |LOCL |0 |ABS |file1.c[5] | 0| 0|NOTY |LOCL |0 |.text |gcc2_compiled.[12] | 0| 0|NOTY |GLOB |0 |UNDEF |i[10] | 0| 4|OBJT |GLOB |0 |.data |k[6] | 4| 4|OBJT |LOCL |0 |.data |l.3[11] | 0| 108|FUNC |GLOB |0 |.text |main[14] | 0| 0|NOTY |GLOB |0 |UNDEF |printf

file2.o:[Index] Value Size Type Bind Other Shname Name[2] | 0| 0|SECT |LOCL |0 |.text |[3] | 0| 0|SECT |LOCL |0 |.data |[4] | 0| 0|SECT |LOCL |0 |.bss |[9] | 0| 0|SECT |LOCL |0 |.comment |[8] | 0| 0|NOTY |LOCL |0 |ABS |*ABS*[10] | 0| 120|FUNC |GLOB |0 |.text |add[1] | 0| 0|FILE |LOCL |0 |ABS |file2.c[7] | 120| 44|FUNC |LOCL |0 |.text |foo[5] | 0| 0|NOTY |LOCL |0 |.text |gcc2_compiled.[11] | 0| 0|NOTY |GLOB |0 |UNDEF |i[12] | 0| 0|NOTY |GLOB |0 |UNDEF |k[6] | 0| 4|OBJT |LOCL |0 |.data |l

コマンド nm の出力の最右行にある “Name” は, シンボル名と呼ばれ, “Bind” で示されるスコープを持つ.また, “Shndx” で UNDEF とされたシンボルは, リンク時にその配置が決定されることを示す.

関数 file1.c の中では, 関数 printf, add が利用されているが, これらの関数は, アセンブラコード中で,call printf, call add という形で, サブルーチン呼び出しとして書かれていることに注意しよう.また, file1.c で定義されている関数 main と, file2.c で定義されている関数 add は, 外部リンケージを持つので, それぞれのオブジェクトコード中で, Type が FUNC, Bind が GLOB と定義されている

Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp

数理解析・計算機数学特論 233

54 . しかし, file1.o では, 関数 add と, ライブラリ関数 printf は UNDEF とされ, それらの場所はリンク時に解決が行われるものとして処理されている.file2.c の関数 foo は static 宣言され, 内部リンケージとなっているので, file2.o 中で, Type がFUNC, Bind が LOCL と定義されている.

オブジェクト file1.c, file2.c で利用されているオブジェクトは, 次の5つに分類できる.

内部自動変数 file1.c の main 関数内の j が該当する.アセンブラコード内では, 明示的なラベル(行頭から書かれていて, : がついている識別子)には表れず, %hi(i), %lo(i) 等として参照されている. したがって, シンボルテーブルにもこのオブジェクトは表れない.

内部静的変数 file1.c 中の main 関数内の l が該当する.シンボルテーブルでは, LOCL な OBJT (オブジェクト)とされ, その “Size” が4バイトであると明示されている55 .

外部リンケージを持ち初期化宣言されている変数 file1.c の k が該当する.シンボルテーブルでは, GLOB な OBJT とされ, 4バイトであることがわかる.

外部リンケージを持ち初期化宣言されていない変数 file1.c と file2.c の i が該当する. これら2つの識別子は外部リンケージで同じオブジェクトを指していることに注意.シンボルテーブルでは, GLOB な NOTY (No Type) とされ, その配置は UNDEF となっている56 .

内部リンケージを持つ大域変数 file2.c の l が該当する.シンボルテーブルでは, LOCL な OBJT とされ, 4バイトであることがわかる57 .

なお,シンボルテーブル中の関数に対する “Size”の値は, その関数の実行コードサイズをあらわし, “Value”はオブジェクトコード中における先頭からのバイト数を表している58 .このように, オブジェクトコードは, 未解決なシンボル名を含む, 環境に依存した命令の列やオブジェク

トの配置情報を含んだデータである. これらの未解決シンボルとオブジェクトの配置は, 複数のオブジェクトコードの結合を行うリンカによって解決される.

6.13.2 リンク

プログラムファイルからコンパイラを利用して生成したオブジェクトコードを結合して, シンボルを解決して, オブジェクトを配置することによって, 実行可能コードを作成することが出来る. この操作をリンク (link) とよぶ.だが, ちょっと待った!Section 6.13.1.2 での例を見ればわかるように, file1.o と file2.o を結合した

だけでは, シンボル printf が解決できない. 関数 printf は C の標準ライブラリ関数であるため, この関数の実体を含むライブラリ (library) もついでに結合しておかなければ, すべてのシンボルを解決し, その実体を明らかにすることが出来ない. つまり, リンクとは必要であれば, ライブラリも結合するという操作を含むことになる.

54 アセンブラコード中で, text セクション内で global と定義されていることに対応する.55 アセンブラコード内(file1.s 内)で, data セクション内に定義され, 0 で初期化されていることに相当している.56 アセンブラコード中でも, 内部自動変数と同じ扱いを受けている.57 オブジェクトコード(アセンブラコード)中での扱いは, 内部静的変数とほとんど同じ扱いであることに注意. したがって, C プログラムのレベルでは static の意味が多少異なるが, 生成するオブジェクトコードレベルになると, 内部リンケージを持つファイルスコープの変数と, 内部静的変数は全く同じ扱いになることに注意しよう.# だから, 同じ static という記憶クラス指定子を持つ.

58 正確には, そのオブジェクトコード中の “text” セグメントの先頭からの「オフセット」と呼ばれる値である.

Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp

234 数理解析・計算機数学特論

6.13.2.1 実行形式の作成

Section 6.13.1.2 での例で作成した2つのオブジェクトコード file1.o と file2.o, さらに, C の標準ライブラリをリンクするには,

gcc file1.o file2.o -o target

とする. ここで, -o target に書かれた target が, リンカが出力する実行形式のファイル名となる.でも, この命令では「標準ライブラリ」を指定していないが?通常のリンカは「標準ライブラリ」を必

ず結合するようになっているため, 明示的に標準ライブラリを指定しなくても良い. 標準関数ライブラリはUNIX の場合, 通常 /usr/lib/libc.a というファイルである.しかし, C の標準関数の中には, 「数学関数」と呼ばれる関数群があり59 , これらの実体は標準関数には

入っていない. 数学関数ライブラリは UNIX の場合には通常は /usr/lib/libm.a であり, 数学関数ライブラリ60 を必要とする場合には,

gcc file1.o file2.o -o target -lm

のように, -lm というオプションをリンカに渡す必要がある61 . 最後に, なぜ -lm オプションを最後につ

けるかという理由を考えてみよう. 例えば, file1.o は関数 sin の呼び出しを含むとき,

gcc -lm file1.o file2.o -o target

としても, リンカは正しく動作する. しかし,

Undefined first referencedsymbol in filesin file1.o

ld: fatal: Symbol referencing errors.

というエラーを出力するだろう. これは, 先に libm.a のシンボルが評価され, file1.o にある未解決シンボル sin の解決が出来なくなっていることを表している. したがって, ライブラリの指定は一番最後にしなくてはならない. また, 同様にライブラリの指定の順序によっては, シンボルの解決が出来ない場合がありうるので注意が必要である.

6.13.2.2 動的リンクライブラリ

C で作成したプログラムをコンパイル(リンク)すると, 必ず標準ライブラリがリンクされてしまう. 標準ライブラリの大きさは,

• Sun Microsystems の Solaris 2.6 (SunOS 5.6 Generic 105181-05 sparc SUNW) で, 約 1.6 MB,• Sun Microsystems の SunOS 4.1.4 (SunOS 4.1.4-JL 1) で, 約 670 KB,• FreeBSD 4.2-Release (4.2-RELEASE FreeBSD) で, 約 1.1 MB

と非常に巨大なファイルである. すべてのプログラムの実行コードにこの大きさのライブラリがリンクされると, 巨大なディスクスペースが必要となる.

59 例えば, 三角関数の値を求める sin や, 対数関数 log がある.60 なぜ数学関数ライブラリのリンクを明示的に指定しなければいけないのだろうか?数学関数ライブラリは, ユーザの目的によっては, その精度や速度に問題がある可能性が否定できない. 標準的な数学関数ライブラリの場合には, 精度と速度が適切になるようなコードから生成されていることが多く, より高い精度や, より高速な実行を求める場合には, 必要に応じて, 異なった数学関数ライブラリを用いることが考えられる. そのため, 数学関数ライブラリが標準関数ライブラリから独立していると考えられる.しかし, Darwin (MacOS X) の Public Beta Version では, 数学関数は標準関数ライブラリに組み込まれていた. Darwin の元

となった NeXTSTEP でどのような構成になっていたかは, 私は良く知らないが, まあ, Darwin では数値計算はするなということなのだろう.61 -l の後に空白なしに指定した文字を XXXX とすると, リンカは指定されたディレクトリから libXXXX.a という名前のライブラリを探し, それをリンクする. 指定されたディレクトリとは, 通常は /usr/lib であり, それ以外のディレクトリもライブラリの検索対象としたい場合には, -L/usr/local/lib のように -L オプションで明示的にディレクトリを指定する必要がある.

Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp

数理解析・計算機数学特論 235

そのため, 最近の UNIX システムや Windows, MacOS 等では, 動的リンクライブラリ (DynamicLinking Library) を用いて, 標準ライブラリなどをプログラム実行時にリンクするという方法をとっている.動的リンクライブラリを用いるもう一つの長所として, もし, 標準ライブラリなどにバグがあった場合,

プログラムを再リンクすることなく, 動的リンクライブラリだけを入れ替えることにより, バグを解消可能となる. しかしながら, 動的リンクライブラリを用いると, プログラムの実行時でのライブラリのリンクの時間だけ実行時間が大きくなるという欠点があり, 現状では, 標準的なライブラリに関しては動的リンクライブラリを, いくつかの特殊な(そのプログラムだけで利用するものなど)ライブラリでは, 静的リンク(static link) (リンク時にライブラリをリンクしてしまう方法)を用いるという使い分けをしている.

6.13.2.2.1 ライブラリの作成方法 オブジェクトコードを静的リンクライブラリとしてまとめる(アー

カイブ (archive) するという)時には, コマンド ar を用いて,

ar -q libx.a file2.o file3.o

とすれば, file2.o, file3.o を libx.a にアーカイブでき, リンク時に -lx オプションで静的にリンクで

きる62 . また, オブジェクトコードを動的リンクライブラリにアーカイブするときには,

gcc -o libx.so -G file2.o file1.o

とすればよい63 64 .

6.13.2.3 インターポジショニング

インターポジショニング (interpositioning) とは, ライブラリ中で定義されている関数を自前の関数で置き換えてしまうことを指す. C では, 標準関数の識別子名は予約 (reserve) されているが, その識別子を使ってはいけないという意味ではない65 . 例えば, C の標準関数 islower を考えてみよう. 次のようなプログラムを書いたら何が起こるかということである66 .

#include <stdio.h>extern int islower(int) ;

int main(int argc, char **argv){int c=’a’ ;

if (islower(c)) printf("%c is lower character\n",c) ;else printf("%c is not lower character\n",c) ;return 0 ;

}

int islower(int c){if ((c >= ’A’)&&(c <= ’Z’)) return 1 ;return 0 ;

}

62 静的リンクライブラリの拡張子 .a は archive の略であるのは明らかだろう.63 動的リンクライブラリの拡張子 .so は shared object の略である. また, あるプログラムがどのような動的リンクライブラリを用いているかは, ldd コマンドで知ることが出来る. ldd /usr/bin/cp としてみるとよい.64 しかし, Solaris 2.xの動的リンクライブラリには少々面倒なところがあり, 実行時の動的リンクライブラリの検索パスを指定するために, プログラムのリンク時に -R オプションにより明示的に動的リンクライブラリを指定するか, シェルの環境変数 LD LIBRARY PATH

で動的リンクライブラリのあるディレクトリを指定する必要がある. SunOS 4.x などでは, 動的リンクライブラリのリンクキャッシュld.so があり, そこに動的リンクライブラリのハッシュテーブルを構成できた. 個人的にはこちらの方が好みなのだが, ld.so をつぶしてしまうと悲惨なことが起きるという欠点がある.# 実際, 私は ld.so を間違って消してしまった経験がある.

65 [6] にも書かれている通り, これは「警告」対象とはならない. せめて警告くらいはしてくれる仕様にして欲しいのは誰でも思うことなのだが...66 islower は ctype.h で宣言されている.

Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp

236 数理解析・計算機数学特論

当然, “a is not lower character” という出力を得る. これだけであれば, 正しく動作するのだが, もし,他の標準関数で islower 関数を利用している関数67 を利用したらどうなるのだろうか?この場合には, 標準ライブラリの islower ではなく, このプログラムファイル中にある islower が利用される. ということは, 悲惨な結末を向かえることになるのは明らかである.このように, 標準関数内で定義されているシンボル名を関数名に利用してはいけない.

6.13.3 メモリ配置

ここでは, C で書かれたプログラムが実行される場合に, 各種のオブジェクトがどのようにメモリ上に配置されていくかを調べてみよう. これによって,関数呼び出しの場合の引数の評価, 実行時のエラー (Section6.21.3 参照) の意味が明確になってくる.

6.13.3.1 オブジェクトのメモリ配置

UNIX では実行形式のファイルが呼び出されると, mmap システムコールにより, 実行形式のファイルが主記憶上に配置され, そのエントリポイントに処理が移ることによって, 実行形式が実行される. ここでは,実行形式が主記憶上に配置されたときのメモリ配置を見てみよう.簡単な例として, 次のプログラムコードを考えてみよう.

int i, j = 0 ;static int k, l = 1 ;int main(int argc, char **argv){int n, m = 1 ;static int s, t = 0 ;

n = l + j ;return 0 ;

}

このコードをコンパイルして, 実行形式を作成し, そのシンボルテーブル(一部省略)を見てみると68 ,

a.out:[Index] Value Size Type Bind Other Shname Name[61] | 67200| 116|FUNC |GLOB |0 |.text |_start[73] | 133784| 4|OBJT |WEAK |0 |.bss |environ[62] | 133780| 4|OBJT |GLOB |0 |.bss |i[63] | 133688| 4|OBJT |GLOB |0 |.data |j[50] | 133776| 0|OBJT |LOCL |0 |.bss |k[47] | 133692| 4|OBJT |LOCL |0 |.data |l[79] | 67600| 72|FUNC |GLOB |0 |.text |main[33] | 133728| 24|OBJT |LOCL |0 |.bss |object.11[48] | 133760| 0|OBJT |LOCL |0 |.bss |s.3[49] | 133696| 4|OBJT |LOCL |0 |.data |t.4

となる. 前にも述べた通り, 変数 i, j, k, l, s, t がシンボルテーブル上に表れ, 変数 n, m は内部自動変数であるので, シンボルテーブルには表れない.ここで, UNIX のメモリ管理のセグメント (segment) という概念が必要となる. UNIX におけるセグメ

ント69 とは, 実行形式に割り当てられた主記憶の区切りのことであり, UNIX では

67 FreeBSD 4.2 RELEASE のライブラリ群のソースコード (/usr/src/lib 以下) を見てみると, libc/net/inet network.c などで利用されている.68 これは, nm -s a.out により生成した.69 MS-DOS におけるセグメントとは異なるので注意すること. MS-DOS におけるセグメントとは, 80286 CPU のアドレス管理方法に依存したもので, 16 ビットアドレス管理で管理可能なメモリの区切りを指す. ちなみに 80286 CPU は 20 ビットアドレス線を持ち, 上位から 16 ビットと下位から 16 ビットのオフセットとセグメントによるメモリ管理を行っていた.

Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp

数理解析・計算機数学特論 237

• text セグメント.実行可能形式のコマンド列を格納する部分.

• data セグメント.実行可能形式の大域的なオブジェクトのうち, 明示的な初期化が与えられたオブジェクトが格納される部分.

• bss セグメント.実行可能形式の大域的なオブジェクトのうち, 明示的な初期化が与えられていないオブジェクトが格納される部分.

• stack セグメント.実行中に自動変数や関数呼び出し, 動的なメモリ割り当てで利用する部分.

の4つに分けられる. これらのセグメントは, 次の図のように割り当てられるのが普通である.

text セグメント

data セグメント

bss セグメント

stack セグメント

スタック

ヒープ

命令の列

jlt

iks

n (main 呼出し時に)m (main 呼出し時に)

text セグメント, data セグメントは, 主記憶への配置時に, 実行ファイル中にある値で埋め尽くされる.bss セグメントは実行開始

時に 0 でクリアされる.stack セグメントは, 関数呼び出しに伴い, 上位メモリから順に利用される.(スタックの利用)また, 動的メモリ割り当てのうち

alloca関数では,スタックの利用が可能である.実行中の動的メモリ割り当

て (malloc 関数の呼出しなど) では, stack セグメントが下位メモリから順に

利用される. (ヒープ領域の利用)

size コマンドで, a.out の各セグメントの大きさを調べると,

% size a.out2288 + 360 + 68 = 2716

となり, text セグメントが 2288 バイト, data セグメントが 360 バイト, bss セグメントが 68 バイトで

あることがわかる.

Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp

238 数理解析・計算機数学特論

仮にプログラム内部でこれらのセグメントを越えてメモリのアクセスを行う70 と, “segmentation fault”という実行時エラーを発生して, プログラムの実行が停止する.stack セグメントは, 次に述べる関数呼び出し手順の中で利用され, stack セグメントにどれだけの大き

さが割り当てられるかは, 実行形式を呼び出したシェルの環境に依存する. stack セグメントの大きさは

limit コマンドで表示される値

cputime unlimitedfilesize unlimiteddatasize 2097148 kbytesstacksize 8192 kbytescoredumpsize 0 kbytesvmemoryuse unlimiteddescriptors 64

で知ることが出来る.

6.13.4 関数呼び出しの手順

Section 6.11.1.7 では, 関数呼出しを行った場合のプログラムの動作の様子を考察し, 関数への実引数は「値渡し」が行われることを述べた. ここでは, それがメモリ内で何をしていることになるのかを考察してみよう.

6.13.4.1 関数実引数の渡し方

ここでは, 次のようなプログラムを例としよう.

extern int add(int, int) ;

int main(int argc, char **argv){int n, m ;

n = 1 ; m = 2 ;n = add(n,m) ;return 0 ;

}

int add(int a, int b){return a + b ;

}

このプログラム中で, 関数 add を呼び出す直前, 呼び出した後, add の終了時のメモリ内の様子は, 以下の通りとなる.【注意】n, m は内部自動変数なので, メモリはすべてスタックセグメントが用いられる.

70 これは, 「ポインタ」を用いると容易に実現できる.

Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp

数理解析・計算機数学特論 239

呼出し前 呼び出した後 関数終了直前main の n, 値 1main の m, 値 2

main の n, 値 1main の m, 値 2

add の戻り値を格納する部分add の a, 値 1add の a, 値 2

値のコピー

値のコピー

main の n, 値 1main の m, 値 2

値 3add の a, 値 1add の a, 値 2

main に戻ってきたとき 関数呼出し終了main の n, 値 1main の m, 値 2戻り値 3

main の n, 値 3main の m, 値 2

この図のように, 関数呼出しを行うと, 関数の実引数はスタック内に新しい記憶領域が確保され, そこへ実引数の値がコピーされる. したがって,

extern int add(int) ;

int main(int argc, char **argv){int n ;

n = 1 ; add(n) ;return 0 ;

}

int add(int a) ;{return ++a ;

}

というプログラムでは,

呼出し前 呼び出した後 関数終了直前main の n, 値 1 main の n, 値 1

add の戻り値を格納する部分add の a, 値 1

main の n, 値 1値 2

add の a, 値 2

main に戻ってきたとき 関数呼出し終了main の n, 値 1戻り値 2

main の n, 値 1

となり, n の値が変化しない理由は明らかとなる. しかし, ファイルスコープのオブジェクトは静的であり,データセグメントまたは bss セグメントに格納されるため, それらを関数内で変更すると, その変更は永続的となる.

extern int add(int) ;int k = 0 ;int main(int argc, char **argv){int n = 1 ;

n = add(n) ;return 0 ;

}

int add(int a) ;{int b = 1 ;

Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp

240 数理解析・計算機数学特論

b += 1 ; k += 1 ;return ++a ;

}

というプログラムの場合には,

呼出し前 呼び出した後 return 文実行直前main の n, 値 1

k, 値 0

main の n, 値 1add の戻り値を格納する部分

add の a, 値 1add の b, 値 1

k, 値 0

main の n, 値 1add の戻り値を格納する部分

add の a, 値 1add の b, 値 2

k, 値 1

関数終了直前 main に戻ってきたとき 関数呼出し終了main の n, 値 1戻り値 2

add の a, 値 2add の b, 値 2

k, 値 1

main の n, 値 1戻り値 2

k, 値 1

main の n, 値 2

k, 値 1

となり, k の値は変更されている. (k はデータセグメントに格納されている.)

Remark 6.13.1 関数呼出しの時には, ここで説明したものよりも多くのデータがスタックに積まれる. 関数呼出しの時には, その時点でのレジスタ情報, 関数終了時にプログラム制御が戻るべきテキストセグメント内のアドレス(プログラム・カウンタ)など, 多くの情報がスタックに積まれ, その後に戻り値領域, 関数実引数領域が確保される.また, 関数実引数がスタック上に積まれる順序は処理系依存である. 実際には, オペレーティングシステ

ムとライブラリ, 処理系などで整合性のある渡し方が行われる71 .

6.13.4.2 再帰的関数呼出しの様子

次に再帰的関数呼出しを行う時の様子を見てみよう. Example 6.11.7 で用いた, 帰納的に定義された数列 an+1 = an + 2, a0 = 0 の an を求める関数を利用しよう.

extern int func(unsigned int) ;

int main(int argc, char **argv){recursive_function(2) ;return 0 ;

}

int func(unsigned int n){if (n == 0) return 0 ;return func(n-1) + 2 ;

}

この関数の呼出しは以下のように行われることがわかる.

71 多くの処理系では後ろに書かれた実引数が先にスタックに積まれることが多い. また, Pascal, Fortran などの言語では, スタックに積まれる順序が指定されていて, それらで書かれたライブラリを使う場合には, 処理系依存のオプションを利用することにより,スタックに実引数を積む順序を指定できることが多い.

Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp

数理解析・計算機数学特論 241

呼出し前 func(2) 呼び出し後 func(1) 呼び出し後(func(2) の戻り値)

(func(2) の実引数) 2(func(2) の戻り値)

(func(2) の実引数) 2(func(1) の戻り値)

(func(1) の実引数) 1

func(0) 呼出し後 func(0) 終了 func(1) 終了(func(2) の戻り値)

(func(2) の実引数) 2(func(1) の戻り値)

(func(1) の実引数) 1(func(0) の戻り値)

(func(0) の実引数) 0

(func(2) の戻り値)(func(2) の実引数) 2(func(1) の戻り値)

(func(1) の実引数) 1(func(0) の戻り値) 0

(func(2) の戻り値)(func(2) の実引数) 2(func(1) の戻り値) 2

func(2) 終了 関数呼出し 終了(func(2) の戻り値) 4

各 func() 終了時には,

return func(n-1) + 2

が行われ, 直前の戻り値に 2 を加えたものがその関数の戻り値となる. このことから, 再帰的な関数呼出しがスタックを順に利用していることがわかる.

6.13.4.2.1 再帰的関数呼出しでスタックをあふれさせる さて, 再帰的関数呼出しを実行して, スタック領域が使い尽くされていくことを実感するために, 以下のような「荒っぽい」ことをしてみよう.上で利用した関数 func を大量に呼び出して, スタックセグメントが使い尽くされると何が起こるだろう

か?まず, 以下のプログラムを実行してみよう.

#include <stdio.h>extern int func(unsigned int) ;

int main(int argc, char **argv){func(10) ;return 0 ;

}

int func(unsigned int n){char c ;printf("n = %3d, c = %p\n",n,&c) ;if (n == 0) return 0 ;return func(n-1) + 2 ;

}

ここで, c = %p の出力は, &c, すなわち, c のアドレスを出力する. したがって, この値はその時点でのおおよそのスタックの先頭のアドレスを示していることになる. 実行結果は,

n = 10, c = effff9a7n = 9, c = effff92fn = 8, c = effff8b7n = 7, c = effff83fn = 6, c = effff7c7n = 5, c = effff74fn = 4, c = effff6d7n = 3, c = effff65fn = 2, c = effff5e7n = 1, c = effff56fn = 0, c = effff4f7

Id: C6-1.tex,v 1.2 2001-03-19 12:45:11+09 naito Exp

242 数理解析・計算機数学特論

となり, 1回の呼出しで120バイトのスタックを利用していることがわかる. そこで, スタックセグメントを小さくするために,

limit stacksize 1

とする. これにより, スタックセグメントは1Kバイトに制限される. そのうえ, func(100) を呼び出してみる. この結果はシステムに依存するが, 多くの場合, 途中で Segmentation fault というエラーを出し

て実行が停止する. これは, スタックセグメントが足らなくなって, 実行が停止する例となっている.

6.13.5 演習問題

Exercise 6.13.1 次の2つのファイルからなるプログラムをコンパイル・リンクし, その出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>extern int sub_function(void) ;int i=0 ;int j=0 ;int main(){sub_function() ;printf("i=%d, j=%d\n",i,j) ;return 0 ;

}

extern int i ;static int j ;int sub_function(){i += 1 ; j += 1 ;return 0 ;

}

Exercise 6.13.2 次の2つのファイルからなるプログラムをコンパイル・リンクし, その出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>extern int sub_function(void) ;int i=0 ;int j=0 ;int main(){sub_function() ;printf("i=%d, j=%d\n",i,j) ;return 0 ;

}

extern int i ;extern int j ;int sub_function(){i += 1 ; j += 1 ;return 0 ;

}

6.14 配列とポインタ(その1)

6.14.1 配列

配列 (array) とは, 特定の型の変数をひとまとまりにして, 利用できる構造である.

6.14.1.1 配列の定義と宣言

配列を利用するには, 配列としての宣言をしなくてはならない. 例えば, int 型の 10 個の配列 digit は

次のように宣言される.

int digit[10] ;

Id: C7.tex,v 1.23 2001-03-21 19:23:29+09 naito Exp

数理解析・計算機数学特論 243

この時, 識別子の名前は digit であり, 「演算子」 [ ] は配列宣言演算子と呼ばれ, 配列の要素数を表す.配列の定義において, 配列の要素数を表すものは 0 より大きい値を持つ整数定数式でなければならない. したがって, 配列の要素の数は, unsigned long で表せる範囲内であれば良いことがわかる. また, 任意の型,及びその派生型に対して配列を定義することができる.上のようにして定義された配列の要素は digit[0], ..., digit[9] のように [ ] の中(添字)に整数

式を代入することで要素を参照することが出来る. 注意すべきことは, 添字は必ず 0 から始まり, 定義された添字の範囲を越えて配列の参照を行った場合の動作は不定となる72 .

digit[0] digit[1] digit[2] digit[3] digit[4] digit[5] digit[6] digit[7] digit[8] digit[9]

配列の各要素は, メモリ内では連続した部分にアロケートされる.配列の宣言では,

extern int digit[] ;

という宣言が可能であり, これは, プログラム中の他のどこかで定義される配列を表し, この配列の定義においてはじめて配列のサイズ(要素数)が決定される. このように, 配列の要素数が決まっていなかったり,(後の配列の初期化で述べるように)配列要素の成分のすべてが決定できないような配列を, 不完全な配列と呼ぶ.

Example 6.14.1 int 型の要素数が 10 個の配列に値を代入し, その後, それらの値を表示させる.

#include <stdio.h>int main(int argc, char **argv){int i ;int digit[10] ;

for(i=0;i<10;i++) digit[i] = i ;for(i=0;i<10;i++) printf("digit[%d] = %d\n", i, digit[i]) ;return 0 ;

}

この時, 上の図でいえば,

0 1 2 3 4 5 6 7 8 9

と値が代入されたこととなり, i が 8 の時, digit[i] で digit の8番目の要素を参照できる.

6.14.1.2 配列の初期化

配列を宣言と同時に初期化することができ, この時に配列が定義される. 配列を初期化子で初期化するには,

int digit[10] = {0,1,2,3,4,5,6,7,8,9} ;

とする. これで, digit[i] = i と初期化できたことになる. また,

int digit[] = {0,1,2,3,4} ;

と初期化すると, 自動的に5個の要素を持った配列として digit が定義される. したがって, この場合にdigit[8] を参照してはならない. 一方,

int digit[10] = {0,1,2,3,4} ;

72 他のオブジェクトを参照してしまうかも知れないし, 実行時エラーをおこすかもしれない.

Id: C7.tex,v 1.23 2001-03-21 19:23:29+09 naito Exp

244 数理解析・計算機数学特論

とすると, digit は10個の要素を持った配列として定義されるが, 6番目以後の要素は初期化されない.したがって, この場合に digit[8] を参照することが出来る.

Remark 6.14.1 配列の要素数を越えて配列の要素に参照を行った場合には, どのようなことが起るかは不定である. たとえば,

int vec[2] ;vec[2] = 1 ;

とした場合には, 実行時エラーとなることもあれば, 他のオブジェクトの指し示す領域にアクセスする可能性もある. したがって, 配列の要素にアクセスする場合に, それが正しいアクセスかどうかを管理するのはプログラマの責任である.

Remark 6.14.2 配列の宣言では, 明示的な初期化を行っている場合を除いて, 配列の要素数として, 非負の整数型の値を持つ式を用いなければならない. しかし,

int i ;int vec[i] ;

という宣言は文法的にエラーとなるわけではなく, 実際に配列に対応するメモリを確保できない可能性がある.

6.14.2 ポインタ

ポインタ (pointer) とは, 他の変数のアドレスを持つ変数である. ポインタとして定義したオブジェクトの中身は, 記憶領域上のアドレスに他ならないので, ポインタはどのような型のオブジェクトのアドレスも格納できるように思えるのだが, C では, どのような型のオブジェクトのアドレスを持つポインタかを明示的に指定して宣言しなければならない.例えば, char 型の変数のアドレスを持つポインタ p を作るには, 以下のように行なう.

char *p ;

この定義により, 変数 p は char 型のオブジェクトを指し示すことが出来る. すなわち, p には char 型のオ

ブジェクトのアドレスを代入することができる. 実際に char 型の変数 c のアドレスを p に代入するには,

char c ;char *p ;p = &c ; ✲

p c

とする. 変数に & をつけると, そのアドレスを示す. & はアドレス演算子と呼ばれ, 被演算数は, ビット・フィールド, register と宣言されたものを参照する左辺値, 関数型であってはならない. また, 単項の * は

間接演算子と呼ばれ. 単項の * つけた変数が指し示すアドレスを返す. 単項 * をポインタに適用すると,そのポインタの指すオブジェクトがアクセスできる.

Example 6.14.2 極めて人為的だが, この例はポインタの利用法を的確に示している.

int x = 1, y = 2 ;int *ip ;

ip = &x ; /* ip は x を指す. すなわち, ip の中には x のアドレスが入っている. */y = *ip ; /* y は 1 となる. *ip は ip の指し示す先の値を表す.

この時点では, ip は x を指し示している. */*ip = 0 ; /* x は 0 となる. *ip は左辺値となりうる.

ip は x を指し示しているので, x の値を変えている. */

この例のように, ポインタを介して, 変数の値を受け渡すことができる. ここで, *ip の値は ip = &x に

よって x を指し示し, *ip = 1 で x の値が 1 となったことに注意.

Id: C7.tex,v 1.23 2001-03-21 19:23:29+09 naito Exp

数理解析・計算機数学特論 245

Example 6.14.3 この例では, ip の内容 (アドレス)が iq にコピーされる73 .

int *ip, *iq ;iq = ip ;

この例と

int *ip, *iq ;*iq = *ip ;

とは全く意味が異なる. こちらの例では, ip が指し示している変数の値が iq が指し示している変数の値

に代入される.したがって,

int *ip, *iq ;int p=1, q=2 ;ip = &p ; iq = &q ;iq = ip ;

とすると, iq は p を指し示めし, q は値 2 を持つが,

int *ip, *iq ;int p=1, q=2 ;ip = &p ; iq = &q ;*iq = *ip ;

とすると, iq は q を指し示し, q は値 1 を持つことになる.

6.14.2.1 ポインタの演算

まず, 次のようなことはできるだろうか.

int *ip ;ip += 1 ;

ip それ自身は, アドレスを指している. これを行うと, ip の値(指し示すアドレス)が1増えるのではなく, ip の指し示しているアドレス自身が int 型の分だけ増加する. もし, int が4バイトを占めていれば,ip は4バイト分増加する74 .また, 次のようにすると, x の値を 1 だけ増やすことができる.

int *ip ;int x ;ip = &x ;*ip += 1 ;

これは, ip が x を指し示していることを考えれば, 当たり前である.

6.14.2.2 配列とポインタ

「C では配列とポインタは強い関係を持ち, ほぼ同様に扱っても良い.」と, C のどのような教科書を見ても書いてある. この言葉は半分は正しく, 半分は間違っている. まず, この言葉の意味を明確にし, 配列とポインタを同様に扱ってもよい文脈を明らかにしよう.例えば,

int a[10] ;

73 もともと iq が指し示していたアドレスの中身にはなんら変化はないことに注意.74 ポインタをインクリメントしたとき, どれだけのバイト数移動するかは, そのポインタが指し示す型に依存する.

Id: C7.tex,v 1.23 2001-03-21 19:23:29+09 naito Exp

246 数理解析・計算機数学特論

は, int 型の要素数 10 の配列を定義しているが, 識別子 a が何を表しているかを考えてみよう. 配列の識別子は, その配列の先頭のアドレスを表している. すなわち, 次の2つのコードは同じものである75 .

int *pa ;pa = &a[0] ;

int *pa ;pa = a ;

左のプログラムでは, &a[0] はオブジェクト a[0] のアドレスを表し76 , 右のプログラムでは, a が配列の先頭要素 a[0] のアドレスを表している. したがって, いずれのプログラムでも, pa は配列 a の先頭を指

し示すこととなる.この時, ポインタの演算により, pa+i は a[0] から int 型変数 i 個分先を指し示すことになり, すなわ

ち, pa+i は a[i] を指し示していることとなる. したがって, *(pa+i) は a[i] を参照することとなる.

a[0] a[4] a[5]a[1] a[2] a[3]

pa

pa=a pa+1

もっと極端なことを書けば,

int a[3] = {1,2,3} ;printf("%d, %d\n", a[2], 2[a]) ;

はともに正しい構文であり, 配列 X[Y] という構文はは常に *(X+Y) と変換される.ここまででは, 配列とポインタは完全に等価であり, どちらで記述しても, 相互に書き換えが可能なよう

に思える. しかし, 注意すべきことは, ポインタはアドレスを格納する変数であるため, int *pa などとい

う(仮)定義において, 確保される記憶領域は, オブジェクトのアドレスを格納するために十分な程度の領域に過ぎない77 . しかし, 配列として int a[10] と定義すると, 記憶領域上に連続した int 型10個分の

領域が確保され78 , その領域は定義が実行された時点で確定したアドレスである. したがって, pa は左辺値であるが, a は左辺値にはなり得ない. すなわち, pa = a, pa++ は意味のある演算であるが, a = pa, a++は正しくない. しかし,

int *pa, a[10] ;pa = a ;printf("%d\n",pa[2]) ;

などは意味のある文である. すなわち, pa[2] は *(pa+2) に変換され, pa = a により, pa は a[0] を指し

示すため, *(pa+2) は a[2] に他ならない. ただし, 元々ポインタと等価になっている配列の先頭アドレスをポインタに代入して参照することは, 配列の要素数を越えてアクセスを行ってしまう元となり, バグになる危険性を秘めている.

Remark 6.14.3 このように配列とポインタはある意味では似ているのだが, その識別子の持つ意味が異なる. したがって, 次のような2つのファイルによるプログラムはエラーにはならないが正常には実行されない.

75 どのような場合にでも同じオブジェクトコードを生成する.76 演算子 & と [] の優先順位に注意.77 Solaris 2.6 では4バイト(32ビット)である.78 Solaris 2.6 の gcc 2.95.1 では40バイトである.

Id: C7.tex,v 1.23 2001-03-21 19:23:29+09 naito Exp

数理解析・計算機数学特論 247

int a[3] ={1,2,3} ;

extern void foo(void) ;int main(int argc, char **argv){foo() ;return ;

}

#include <stdio.h>extern int *a ;void foo(void){printf("%d\n", *(a+1)) ;return ;

}

Remark 6.14.4 配列以外を指すポインタに対して加減を行なっても, 意味のある結果が得られるとは限らない. 例えば

int *p ;int a, b ;p = &a;p++;

としたとき, p が b を指していることは期待できない.

Example 6.14.4 int 型の要素数10個の配列 a の各要素に値 0 を代入する4つの方法. ともに

int a[10], *p, i ;

と定義されていると仮定する.

方法1

for(i=0;i<10;i++) a[i] = 0 ;

方法2

p = a ;for(i=0;i<10;i++) p[i] = 0 ;

方法3

p = a ;for(i=0;i<10;i++) *(p+i) = 0 ;

方法4

p = a ;for(i=0;i<10;i++) *p++ = 0 ;

さて, これらの4つの方法のうちどれが一番お好みだろうか?これら4つの例はプログラムの書き方は異なるが, 生成するコードは全く同一と考えて良い.

Example 6.14.5 double 型の要素数3の配列のコピーを行う例.

int main(int argc, char **argv){int i ;double a[3] = {1.0, 2.0, 1.0}, b[3] ;for(i=0;i<3;i++) b[i] = a[i] ;return 0 ;

}

このコピーを

b = a ;

で行うことはできない.

Id: C7.tex,v 1.23 2001-03-21 19:23:29+09 naito Exp

248 数理解析・計算機数学特論

Example 6.14.6 double 型の要素数2の配列を R2 上のベクトルと思い, それに直交するベクトルを一

つ求める.

int main(int argc, char **argv){int i ;double a[2] = {1.0, 2.0}, b[2] ;

b[0] = -a[1] ; b[1] = a[0] ;return 0 ;

}

6.14.2.3 汎用データ・ポインタ

ここまでは, char, int などのデータの型が決まったものに対するポインタを考えてきた. しかしながら,どのような型に対しても利用できる汎用データ・ポインタを利用することで, さらに広範囲に利用できる関数などを作ることができる.汎用データ・ポインタは void 型へのポインタとして定義される.

Example 6.14.7 この例の関数は, どのような型の配列であっても, 配列の次の要素をかえす関数である.

void * next_member(void *a, int size, int len, int member){if (member >= len) return NULL ;return (a+(member+1)*size) ;

}

ここで, ポインタをインクリメントする際に, size を掛けていることに注意. ここで, a は対象となる配列の先頭を指し示すポインタであり, size は配列の要素の型のバイト数, len は配列の要素数, member は対象となる配列の要素の添字の番号である. この関数を利用すると,

int a[3] = {0,1,2} ;double x[3] = {0.0, 1.0, 2.0} ;char c[3] = {’a’, ’b’, ’c’} ;int *pa ;double *px ;char *pc ;

pa = (int *)next_member(a,sizeof(int), sizeof(a)/sizeof(int), 1) ;printf("%d\n", *pa) ;px = (double *)next_member(x,sizeof(double), sizeof(x)/sizeof(double), 1) ;printf("%f\n", *px) ;pc = (char *)next_member(c,sizeof(char), sizeof(c)/sizeof(char), 1) ;printf("%c\n", *pc) ;

として, 実際に次の要素を出力することができる. ここで, sizeof 演算子を用いて,

sizeof(a)/sizeof(int)

によって配列の要素数を得ていることに注意. これと等価な

sizeof(a)/sizeof(a[0])

でもよい. 後者の方が a の指し示す型を変えたときの可搬性が高く安全である.

void 型へのポインタをインクリメントすると, その変数が指し示すアドレスは 1 だけ増加する. この部分だけを見ると, char 型へのポインタと同じであるが, どのような型の変数をも指し示すことができるようになっているのが汎用データポインタである.

Id: C7.tex,v 1.23 2001-03-21 19:23:29+09 naito Exp

数理解析・計算機数学特論 249

6.14.3 文字列

文字列リテラルは, 文字の配列として認識される. そこで, 文字列を変数として表すには char 型の配列

を用いることとなる. 文字列リテラルを文字の配列として変数域に格納する時には, 文字列の終端がわかるように, 配列の最後に \0 (数値 0 )が挿入される. したがって, 文字列リテラルを表現するために必要な文字配列の長さは, 文字数+1である.char 型の配列に限り, 以下のような初期化の方法が認められている79 .

char amessage[] = "This is a test." ;char *pmessage = "This is a test." ;

しかし, この2つの変数には大きな違いがある. pmessage はポインタであるので, それが指し示すアドレスを変更できるが, amessage はそれ自身配列であるので, その中身は変更できるが, そのアドレスは変更できない. pmessage の定義の場合, “This is a test.” という文字列は, 静的メモリ領域のどこかに確保され, pmessage はそのメモリの先頭のアドレスを値に持つ.

Example 6.14.8 文字列が配列またはポインタであることを利用すると, 文字列のコピーは配列またはポインタを利用して行なうことができる.

char t[10], s[10] ;while(*s++ = *t++) ;

これは, 文字列 t を s にコピーしている.なお, よくやるミスなのだが, この例を用いた完全なプログラムは,

int main(int argc, char **argv) {char *t="abcdefghi", s[10] ;while(*s++ = *t++) ;return 0 ;

}

であり,

int main(int argc, char **argv) {char *t="abcdefghi", *s ;while(*s++ = *t++) ;return 0 ;

}

ではない. 下の例ではコピー先の文字列に対応する十分な記憶領域が確保されていない.

Example 6.14.9 このプログラムは, 文字列の前から見て空白文字 (’ ’) を最初に見つけた場所(前から何番目か)を出力している. (ただし, 先頭にある場合は0番目と数えている.)もし, 見つからない場合には, −1 を出力する.

#include <stdio.h>int main(int argc, char **argv){char s[] = "abcdef" ;char *p ;

p = s ;while(*p) {if (*p == ’ ’) {

printf("%d\n", p-s) ;return 0 ;

}

79 なお, これに対応する他の型の初期化は, 要素数1の配列を int a[]=1 ;, int *p=1 ; と初期化することに対応する. これは認められていない.

Id: C7.tex,v 1.23 2001-03-21 19:23:29+09 naito Exp

250 数理解析・計算機数学特論

p++ ;}printf("-1\n") ;return 0 ;

}

Example 6.14.10 このプログラムは, 文字列 s を空白文字を区切りとしてトークン分解するものである.得られたトークンはすぐに出力されている.

#include <stdio.h>int main(int argc, char **argv){int i ;char s[] = "abc d ef ", t[10] ;char *ps, *pt ;

for(i=0;i<10;i++) *(t+i) = ’\0’ ;ps = s ; pt = t ;while(*ps) {if (*ps != ’ ’) *pt++ = *ps++ ;else {

*pt = ’\0’ ; printf("%s\n", t) ;pt = t ;for(i=0;i<10;i++) *(t+i) = ’\0’ ;ps++ ;

}}if (*t) printf("%s\n",t) ;return 0 ;

}

6.14.4 演習問題

Exercise 6.14.1 double 型の要素数3の配列を R3 のベクトルと思い, double 型の要素数3の配列2つ

に直交するベクトルを求めるプログラムを書け. ただし, エラー処理も適切に行うこと.

Exercise 6.14.2 Example 6.14.9 のように文字列を与えたとき, その文字列の長さ(ただし, 文字列終端文字を含まない)を出力するプログラムを書け.

Exercise 6.14.3 Example 6.14.9 を書き換えて, 文字列の後ろから見て空白文字 (’ ’) を最初に見つけた場所(前から何番目か)を出力するプログラムを書け. (ただし, 先頭にある場合は0番目とする.)もし,見つからない場合には, −1 を出力する.

Exercise 6.14.4 Example 6.14.10 を書き換えて,

#include <stdio.h>int main(int argc, char **argv){char s[] = "abc d ef ", t[10] ;char *ps, *pt ;

ps = s ; pt = t ;while(*ps) {if (*ps != ’ ’) *pt++ = *ps++ ;else {

*pt = ’\0’ ; printf("%s\n", t) ;pt = t ;ps++ ;

}}printf("%s\n",t) ;return 0 ;

}

Id: C7.tex,v 1.23 2001-03-21 19:23:29+09 naito Exp

数理解析・計算機数学特論 251

とした. 正常に動作しない理由を述べよ.

Exercise 6.14.5 Example 6.14.10 を書き換えて,

#include <stdio.h>int main(int argc, char **argv){int i ;char s[] = "abc d ef ", t[10] ;char *p ;

for(i=0;i<10;i++) *(t+i) = ’\0’ ;p = t ;while(*s) {if (*s != ’ ’) *p++ = *s++ ;else {

*p = ’\0’ ; printf("%s\n", t) ;p = t ;for(i=0;i<10;i++) *(t+i) = ’\0’ ;s++ ;

}}if (*t) printf("%s\n",t) ;return 0 ;

}

とした. これが文法エラーとなる理由は何か.

Exercise 6.14.6 次のプログラムの出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>#define MAX 4int a[] = {0,1,2,3} ;int main(int argc, char **argv){int i, *p ;for(i=0;i<MAX;i++) printf("a[i]=%d\t",a[i]) ; printf("\n") ;for(p=&a[0];p<&a[MAX];p++) printf("*p=%d\t",*p) ; printf("\n") ;for(p=a;p<a+MAX;p++) printf("*p=%d\t",*p) ; printf("\n") ;for(i=0;i<MAX;i++) printf("*(a+i)=%d\t",*(a+i)) ; printf("\n") ;

}

Exercise 6.14.7 次のプログラムの出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>int a[] = {0,1,2,3,4} ;int *p[] = {a, a+1, a+2, a+3, a+4} ;int **pp = p ;int main(int argc, char **argv){printf("%X\t%X\n", a, *a) ;printf("%X\t%X\t%X\n", p, *p, **p) ;printf("%X\t%X\t%X\n", pp, *pp, **pp) ;pp++ ; printf("%X\t%X\t%X\n", pp-p, *pp-a, **pp) ;*pp++ ; printf("%X\t%X\t%X\n", pp-p, *pp-a, **pp) ;*++pp ; printf("%X\t%X\t%X\n", pp-p, *pp-a, **pp) ;++*pp ; printf("%X\t%X\t%X\n", pp-p, *pp-a, **pp) ;pp=p ;**pp++ ; printf("%X\t%X\t%X\n", pp-p, *pp-a, **pp) ;*++*pp ; printf("%X\t%X\t%X\n", pp-p, *pp-a, **pp) ;++**pp ; printf("%X\t%X\t%X\n", pp-p, *pp-a, **pp) ;

}

Id: C7-1.tex,v 1.6 2001-03-21 21:23:27+09 naito Exp

252 数理解析・計算機数学特論

6.15 配列とポインタ(その2・関数とポインタ)

6.15.1 変数の参照渡し

C の関数呼出しでは, 実引数に指定したオブジェクトはその値が関数に渡される. したがって, 実引数に指定したオブジェクトの値を関数の副作用として変更するためには, 実引数にポインタを渡す必要がある.

Example 6.15.1 この関数の例は, 二つの int 型変数の和をとり, 第一変数にその結果を返すものである.

void sum(int *a, int b){*a += b ;return ;

}

この例では, 関数 sum 内では a は int 型変数のポインタであるため, その値を参照するには *a と指定

しなければならない. この関数を呼び出すには, 以下の方法をとる.

int a, b ;sum(&a,b) ;

&a はオブジェクト a のアドレスを与えている. したがって, 関数 sum には a のアドレスが渡される.

呼出し前 呼び出した後 *a += b の実行ab

ab

sum の *asum の b

✲ a, ここが書き換えられるb

sum の *asum の b

6.15.2 配列を引数とする関数

配列の識別子はそれ自身配列の先頭のアドレスを持つため, 配列を引数とする関数は, ポインタを引数としていると考えて良い.

Example 6.15.2 double 型の要素数2の配列を R2 のベクトルとみなして, そのノルムの2乗を求める

関数.

double norm(double a[2]){int i ;double x=0.0 ;for(i=0;i<2;i++) x += a[i]*a[i] ;return x ;

}

この関数を呼び出す場合には,

double a[2] = {1.0, 2.0}, x ;x = norm(a) ;

とする. この時, 関数 norm の実引数として関数に渡される値は, 配列 a の先頭のアドレスである.

この関数を

double norm(double a[]){

Id: C7-1.tex,v 1.6 2001-03-21 21:23:27+09 naito Exp

数理解析・計算機数学特論 253

.... この部分は同じ}

または,double norm(double *a){.... この部分は同じ

}

として定義しても良い.つまり, 配列を引数とする関数の仮引数定義において,

double a[2]double a[]double *a

はどれも同じ意味をもつ. すなわち, 関数の実引数として与えられる値は, double 型のポインタである.したがって,

#include <stdio.h>double norm(double a[3]){int i ;double x ;for(i=0;i<2;i++) x += a[i]*a[i] ;return x ;

}

int main(int argc, char **argv){double a[2] = {1.0,2.0} ;printf("%f\n", norm(a)) ;return 0 ;

}

としても, コンパイラは警告もエラーも出さないので注意すること. 同様に,#include <stdio.h>extern double norm(double *) ;double norm(double a[]){....

}

としても, コンパイラは警告もエラーも出さない. なお, double norm(double a[]) に対応する関数プロ

トタイプ宣言は,double norm(double []) ;

とすればよい.

Example 6.15.3 double 型の要素数2の配列を R2 のベクトルとみなして, それに直交するベクトルを

一つ求める関数.

void normal_vector(double *a, double *b){b[0] = -a[1] ; b[1] = a[0] ;return ;

}

この関数を呼び出す場合には,

double a[2] = {1.0, 2.0}, b[2] ;normal_vector(a,b) ;

とする.

Id: C7-1.tex,v 1.6 2001-03-21 21:23:27+09 naito Exp

254 数理解析・計算機数学特論

6.15.3 文字列操作関数

また, ポインタを利用することにより, 文字列を関数の引数として渡すことができ, その戻り値にポインタを利用することもできる.

Example 6.15.4 C の標準関数 strchr は

char *strchr(const char *s, int c);

と string.h 内で宣言される関数であり, s の中に最初にあらわれる文字 c のポインタを返す. もし, c で指定した文字が見つからないときには NULL80 を返す.この例では, この標準文字列操作関数 strchr を実現してみよう. まず, 次のような関数を書いてみよう.

char *strchr(const char *s,int c){while ((*s!=’\0’)&&(*s!=c)) s++ ;if (*s!=’\0’) return (char *)s ;return NULL ;

}

この関数を呼び出すには, 以下の方法をとる.

char a[]="test test" ;printf("%p\n",a) ;printf("%p\n",strchr(a,’e’)) ;

この例のように, 文字列を初期化するには, strcpy 関数を利用する81 .

Example 6.15.5 Example 6.15.4 で用いた, C の標準関数 strcpy は

char *strcpy(char *dst, const char *src);

と定義され, src で示される文字列を dst で示される文字列にコピーする. また, 戻り値はコピーされたdst を返す.

char *strcpy(char *dst, const char *src){while(*dst++=*src++) ;return dst ;

}

しかし, strcpy では, コピーされる文字列の長さが指定されておらず, dst のために確保された領域を越えてコピーされる可能性があるため, 実際に文字列をコピーするには, 標準関数 strncpy を用いる方が望

ましい.

Example 6.15.6 C の標準関数 strtok は

char *strtok(char *s1, const char *s2);

と定義され, 次のような仕様を持つ.s1 の中から s2 に含まれる文字を区切り文字の集合としてトークン分解を行い, 戻り値にはそのトーク

ン文字列を返す. また, 第一引数に NULL を入れて, strtok を続けて呼んだ場合には, 1回目の呼出しで用いた文字列の次のトークンを返す.strtok の使用例は以下の通りである.

80 NULL とは, 何も指し示さないという特別なポインタである. NULL と 0 を示すポインタとは異なることに注意せよ.81 その他の文字列操作関数については, man -s 3c strcpy を見よ.

Id: C7-1.tex,v 1.6 2001-03-21 21:23:27+09 naito Exp

数理解析・計算機数学特論 255

#include <stdio.h>#include <string.h>int main(int argc, char **argv){char a[100]="test test_test" ;char *p ;

p = strtok(a,"_ ") ; printf("%s\n",p) ;while((p = strtok(NULL,"_ ")) != NULL)printf("%s\n",p) ;

return 0 ;}

このプログラムの実行結果は

testtesttest

となる. ところが, 上のプログラムを書き換えて,

#include <stdio.h>#include <string.h>int main(int argc, char **argv){char a[100]="test test_test" ;char *p ;

p = strtok(a,"_ ") ; printf("p=%s, a=%s\n",p,a) ;while((p = strtok(NULL,"_ ")) != NULL)printf("p=%s, a=%s\n",p,a) ;

return 0 ;}

とすると,

p=test, a=testp=test, a=testp=test, a=test

となってしまう. つまり, strtok 関数に渡した第一引数 a も書換えられてしまう. このからくりを見るために, ここでは, strtok の代りに, 第二引数を int 型変数として, その文字を区切り文字としてトークン分解を行う, strtok に類似の関数を書いてみよう.

char *strtok(char *s, const int c){static char *p ;char *q ;

if (s == NULL) s = p ;else q = s ; /* 先頭を保持 *//* トークンが残っていなければ NULL を返す */while(*s == c) s++ ;if (*s == 0) return NULL ;/* トークンが残っているとき */while((*s != c)&&(*s)) s++ ;*s = ’\0’ ;p = s+1 ; /* 次の呼出しのため */s = q ;return s ;

}

この例では, 2回目以後の第一引数を NULL とした呼出しのために, 直前の呼出しに用いたポインタを静的変数として保持している.

Id: C7-1.tex,v 1.6 2001-03-21 21:23:27+09 naito Exp

256 数理解析・計算機数学特論

ここで学んだことを利用すると, getchar などを利用して, 文字を読み出し, それを文字列として保持した後, その構文解析を行うことで, 標準入力からの数値の入力が可能になる82 .

6.15.4 関数へのポインタ

C では, 変数へのポインタだけではなく, 関数へのポインタも利用できる. 関数識別子は関数が定義されているテキストセグメント内の関数の先頭アドレスを持つオブジェクトと考えれば, 関数のポインタは容易に理解できる.int 型の値を返す関数へのポインタを表す変数は,

int (*p)() ;

と定義する. ここで, int (*p)() という書き方が本質的である. 関数を表す演算子 () と間接演算子 * の

優先順位は, () の方が高く, int *p() とすると int へのポインタを返す関数と認識される.

Example 6.15.7 この例は, sum という関数を, ポインタ p に代入している.

int sum(int a, int b){return a+b ;

}

main(){int a, b ;int (*p)() ;

a = 1 ; b = 2 ;p = sum ;printf("%d\n",p(a,b)) ;

}

関数へのポインタは以下のような場合に便利に利用できる.

Example 6.15.8 次のプログラムはコマンドライン引数から 1+2 などという入力を取り, その計算結果を出力するものである.

#include <stdio.h>#include <stdlib.h>extern int calc(const char *) ;extern int _chrstr(char, char *) ;extern int _add(int,int) ;extern int _sub(int,int) ;extern int _mul(int,int) ;extern int _div(int,int) ;extern int _mod(int,int) ;char op[]="+-*/%" ;int main(int argc, char **argv){if (!(argc-1)) return 0 ;printf("%d\n",calc(*(argv+1))) ;

}int calc(const char *arg){char p[100], *q, *r ;char a[100], b[100], operator ;int i = 0, j, int_a, int_b ;int (*func)() ;

82 詳しくは, getchar, strtod, strtol などのオンラインマニュアルを見よ.

Id: C7-1.tex,v 1.6 2001-03-21 21:23:27+09 naito Exp

数理解析・計算機数学特論 257

while(*(p+i) = *(arg+i)) i += 1 ; q = p ;while(_chrstr(*q,op)) q++ ; j = q-p ; i = 0 ;while((*(a+i) = *(p+i)) && (i < j)) i += 1 ; *(a+i) = 0 ;operator = *q ; q++ ; r = q ;while(_chrstr(*q,op)) q++ ; i = 0 ;while((*(b+i) = *(r+i)) && (i < j)) i += 1 ; *(r+i) = 0 ;int_a = atoi(a) ; int_b = atoi(b) ;switch (operator) {case ’+’: func = _add ; break ;case ’-’: func = _sub ; break ;case ’*’: func = _mul ; break ;case ’/’: func = _div ; break ;case ’%’: func = _mod ; break ;default: func = NULL ;}return func(int_a,int_b) ;

}

int _chrstr(char c, char *p){while(*p && *p != c) p++ ;if (*p) return 0 ;else return 1 ;

}

int _add(int a, int b) { return a+b ; }int _sub(int a, int b) { return a-b ; }int _mul(int a, int b) { return a*b ; }int _div(int a, int b) { return a/b ; }int _mod(int a, int b) { return a%b ; }

また, このプログラム中の文字列解析部分は, strspn 関数などの文字列解析関数で代用する方が容易である. (ここでは, ポインタの解説のため, わざわざ上のように書いてある.)上のプログラム中の calc 関数

をそのように書き換えると, 以下のようになる.

int calc(const char *arg){char p[100], operator ;int int_a, int_b ;int (*func)() ;size_t len ;

int_a = atoi(strncpy(p,arg,len=strspn(arg,digit))) ;operator = *(arg+len) ;int_b = atoi(strncpy(p,arg+len+1,strspn(arg+len+1,digit))) ;switch (operator) {case ’+’:func = _add ; break ;

case ’-’:func = _sub ; break ;

case ’*’:func = _mul ; break ;

case ’/’:func = _div ; break ;

case ’%’:func = _mod ; break ;

default:func = NULL ;

}return func(int_a,int_b) ;

}

Id: C7-1.tex,v 1.6 2001-03-21 21:23:27+09 naito Exp

258 数理解析・計算機数学特論

関数へのポインタが本質的な役割を果たすものとして, 次のような例がある. 例えば,int 型の配列の中で, 適当な順序に対して最も大きなものを求める関数を考えてみよう.

Example 6.15.9 int 型の要素数10個の配列の中で通常の順序で最も大きな数を求める.

#include <stdio.h>int get_max(int *) ;

int main(int argc, char **argv){int a[10]={2,1,3,7,6,5,0,9,4,8} ;int result ;

result = get_max(a) ;printf("%d\n",result) ;return 0 ;

}

int get_max(int *a){int i ;int max ;

max = a[0] ;for(i=0;i<10;i++)max = (max < a[i]) ? a[i] : max ;

return max ;}

これでは, 関数 get max 内に要素数 10 が書かれている.

Example 6.15.10 int 型の要素数10個の配列の中で通常の順序で最も大きな数を求める. (改良版1)

#include <stdio.h>int get_max(int *, unsigned int) ;

int main(int argc, char **argv){int a[10]={2,1,3,7,6,5,0,9,4,8} ;int result ;

result = get_max(a,10) ;printf("%d\n",result) ;return 0 ;

}

int get_max(int *a, unsigned int nel){int i ;int max ;

max = a[0] ;for(i=0;i<nel;i++)max = (max < a[i]) ? a[i] : max ;

return max ;}

次に整数要素の順序として, 通常と逆順の順序をいれたらどうなるだろうか?最も単純なものは,

int get_min(int *a, unsigned int nel){int i ;int min ;

min = a[0] ;for(i=0;i<nel;i++)

Id: C7-1.tex,v 1.6 2001-03-21 21:23:27+09 naito Exp

数理解析・計算機数学特論 259

min = (min > a[i]) ? a[i] : min ;return min ;

}

と定義してしまう方法である. しかし, これでは2つの順序の入れ方に対して, 別の関数を用意しなければならない. そのために, 次のような例を考えてみよう.

Example 6.15.11 この例では, 関数へのポインタを利用して, 順序を与える関数を get max 内から独立

させている.

#include <stdio.h>int get_max(int *, unsigned int, int (*)(int, int)) ;int max_func(int, int) ;

int main(int argc, char **argv){int a[10]={2,1,3,7,6,5,0,9,4,8} ;int result ;

result = get_max(a,10,max_func) ;printf("%d\n",result) ;return 0 ;

}

int get_max(int *a, unsigned int nel, int (*func)(int, int)){int i ;int max ;

max = a[0] ;for(i=0;i<nel;i++) {if (func(a[i],max) > 0) max = a[i] ;

}return max ;

}

int max_func(int a, int b){return a-b ;

}

この中で, get max の第3引数は, 2つの int 型の引数をとり, int 型を返す関数である. get max は第3

引数の関数 func に対して, func(a,b) > 0 が a > b となる順序によって, 最も大きな値を返すことになる. get max の第3引数の関数を取り替えることにより, どのような順序を入れることも自由になる.この手法は, 配列の与えられた順序による並び替えを行う qsort 関数で利用されている, C における極

めて重要な手法の一つである.

6.15.5 ポインタの演算(比較)

2つのポインタは次のいずれかの条件が満たされるとき, それを比較することが出来る.

• それが指し示すものの型が同一. ただし const などの修飾子を除いて考える. この時の結果は, 2つのオブジェクトのメモリ内での相対位置によって決定される.

• 同じ構造体のメンバーを指すとき. この時の結果は, メンバーがあとに書かれたものを指すポインタの方が, 相対位置が高いとする.

• 同じ配列の要素を指すとき. この時の結果は配列の添字と同一の順序となる.

これ以外のポインタの比較を行うと結果は不定になる.

int a[4] ;int *p, *q ;p = &a[0] ; q = &q[2] ;

Id: C7-1.tex,v 1.6 2001-03-21 21:23:27+09 naito Exp

260 数理解析・計算機数学特論

とすると, p < q が成り立つ.

Example 6.15.12 以下のプログラムでは, a は bss セグメント, b は stack セグメント, c は data セグ

メントにあり, 関数 main は text セグメントにある.

#include <stdio.h>int a ;int c = 1 ;int main(int argc, char **argv){int b ;int *pa, *pb, *pc ;int (*pf)() ;pa = &a ; pb = &b ; pc = &c ; pf = main ;printf("pa = %p\n", pa) ;printf("pb = %p\n", pb) ;printf("pc = %p\n", pc) ;printf("pf = %p\n", pf) ;if (pa < pb) printf("pa < pb\n") ;else printf("pa > pb\n") ;if (pb < pc) printf("pb < pc\n") ;else printf("pb > pc\n") ;if (pc < pa) printf("pc < pa\n") ;else printf("pc > pa\n") ;return 0 ;

}

6.15.6 各種の宣言の違い

ここで, このような複雑な宣言をまとめておこう.

char **argv argv は char へのポインタのポインタ

int (*daytab)[13] daytab は int の 13 個の配列へのポインタ

int *daytab[13] daytab は int へのポインタの 13 個の配列

void *comp() comp は void 型のポインタを返す関数

void (*comp)() comp は void 型を返す関数へのポインタ

char (*(*x())[]) x は char 型を返す関数へのポインタの不定個の配列へのポインタを返す関数

char (*(*x[3])())[5] x は char 型の 5 個の配列へのポインタを返す関数へのポインタの 3 個の配列

6.15.7 演習問題

Exercise 6.15.1 double 型の要素数3の配列を R3 のベクトルと思い, double 型の要素数3の配列2つ

に直交するベクトルを求める関数を書け. ただし, エラー処理も適切に行うこと.

Exercise 6.15.2 二つの int 型の変数を入れ換える関数 swap int を書け.

Exercise 6.15.3 二つの同じ型の変数を入れ換える関数 swap を書け.

Exercise 6.15.4 Example 6.15.8 では, 入力される文字列に空白を許していない. 空白が許されるように変更せよ. さらに, 項が3つ以上の場合にも対応せよ. また, 数値を表さない項を入力した場合の対応を書け.

Id: C7-1.tex,v 1.6 2001-03-21 21:23:27+09 naito Exp

数理解析・計算機数学特論 261

Exercise 6.15.5 Example 6.15.4 では, その文字列に含まれる最初の文字のポインタを返したが, これをその文字列に含まれる最後の文字のポインタを返すように書き変えよ.

Exercise 6.15.6 C の標準関数 strncpy の仕様を調べ, これを書け.

Exercise 6.15.7 Exercise 6.15.6 で書いた strtok 関数で, 第一引数の文字列が破壊されないように関数を書き直せ.

Exercise 6.15.8 標準入力から(一つ以上の)空白文字で区切られた整数値を読み, その和を計算するプログラムを書け. ただし, 入力は1行で行われるものとし, その入力文字数は改行文字を含めて1024文字以内で, その中に int 型で格納可能な10進整数が複数個書かれていると仮定する. 10進整数を表さない文字列や, int 型で格納可能でない10進数値がある場合には, エラーと判断せよ. (エラー処理の方法は任意)

6.16 配列とポインタ(その3・多次元配列)

ここまでで次の2つの事実を学んだ.

1. C では配列とポインタは「ほぼ」等価なものと扱える.2. C ではどのような型も配列にすることが出来る.

したがって, 2 の事実により, 配列の配列(多次元配列)などが定義できることがわかる. ここでは, 多次元配列を定義して, 多次元配列において, どの程度 1 の事実が通用するかを注意深く見ていこう.

6.16.1 多次元配列の定義

はじめに多次元配列の定義を行う.

int str[2][5] ;

配列を構成する演算子 [] の結合規則は「左から右」であるので, str[i] は, int 型の5個の要素を持つ配列であり, str は2個の要素を持つ「 int 型の5個の要素を持つ配列」の配列である. 各種の教科書には, 以下のような図が書かれていることが多い.

str[0][0] str[0][1] str[0][2] str[0][3] str[0][4]

str[1][0] str[1][1] str[1][2] str[1][3] str[1][4]

より正確にメモリ上で str が表す2次元配列の様子を見ると, 次の図のようになる.

str[0] str[1]

str[0][0] str[0][1] str[0][2] str[0][3] str[0][4] str[1][0] str[1][1] str[1][2] str[1][3] str[1][4]

多次元配列を初期化する方法は,

char daytab[2][13] = {{0,31,28,31,30,31,30,31,31,30,31,30,31},{0,31,29,31,30,31,30,31,31,30,31,30,31}

} ;

とすれば良い.

Id: C7-2.tex,v 1.2 2001-03-21 19:38:00+09 naito Exp

262 数理解析・計算機数学特論

Example 6.16.1 次の例は, N × N , N = 10 の単位行列を作成している.

int i, j ;int unit_mat[10][10] ;

for(i=0;i<10;i++) {for(j=0;j<10;j++) unit_mat[i][j] = 0 ;unit_mat[i][i] = 1 ;

}

Remark 6.16.1 多次元配列は, 文法的には配列を識別子の型とする配列である. 配列の宣言では, 「配列の限界を指定する定数式がないときには, 配列は不完全な型を持つ。」とあり, 配列の要素の型は完全でなければならない. これは, 多次元配列においては, 最初の次元のみが省略できることを意味している. すなわち, 明示的な初期化により配列を完全にすることが可能なので,

int y[][2] = { {1,2}, {2,3}, {3,4} } ;

により, この配列は int 型の2つの要素を持つ配列の3つの要素を持つ配列として定義される. また, この初期化は

int y[3][2] = { 1,2,2,3,3,4 } ;

と等価である.

Remark 6.16.2 また, 次のような配列の初期化子による定義も可能である.

int y[][2] = { {1,2}, {2}, {3} } ;

この定義では, y[1], y[2]は初期化子には1つの要素しか持たないが, y[0]が2つの要素を持つため, y[1],y[2] は2つの要素を持つ配列として定義され, y は不完全な型を持つ配列となる.しかし, これを

int y[][] = { {1,2}, {2}, {3} } ;

とは定義できない. この理由は, 次の Section 6.16.2 の多重配列をポインタで書換えることと関連している.

Remark 6.16.1, Remark 6.16.2 の詳細については, [2, A8.6, A8.7] を参照.

6.16.2 多重配列とポインタ

1次元配列では, 配列とポインタは, ほぼ同じものを示していた. すなわち,

int a[3] ;

として与えられたオブジェクトに対して, a[0], *a または a[i], *(a+i) は, それぞれ, 同じメモリ領域へのアクセスを表し, a, &a[0] または a+i, &a[i] も, それぞれ, 同じアドレスを指し示していた. ここでは多重配列においては, 配列とポインタは(このような意味で)同じものと見なせるかどうかを考えてみよう.はじめに, 二重配列

int a[2][5] ;

を考えてみる. この時 [2, A8.6.2] によれば, a[0][0], **a, または a[i][j], *(*(a+i)+j), *(a[i]+j) は,それぞれ, 同じ領域へのアクセスを示し, a, &a[0][0], または *(a+i)+j, a[i]+j, &a[i][j] も, それぞれ同じアドレスを示す. このことを

Id: C7-2.tex,v 1.2 2001-03-21 19:38:00+09 naito Exp

数理解析・計算機数学特論 263

a[0] a[1]

a[0][0] a[0][1] a[0][2] a[0][3] a[0][4] a[1][0] a[1][1] a[1][2] a[1][3] a[1][4]

を用いて考えてみよう.これを理解するには, 次の2つのポインタ演算の差を理解する必要がある.

1. a に対して a+1 が何を表すか?

2. a[0] に対して a[0]+1 が何を表すか?

これに対する解答のヒントとして,

sizeof(a)sizeof(a[0])sizeof(a[0][0])

の式の値を見てみるのがよい. これを int 型が4バイトの処理系で調べると, 順に 40, 20, 4 という答えが得られる.上の図を見ると, a は「int 型の5個の要素を持つ配列」という型の配列であるので, a+i は a から「int

型の5個の要素を持つ配列」のバイト数(20バイト)の i 倍だけ先を表す. つまり, *(a+i) は a[i] と

等価である.さらに, a[0] は int 型の要素を持つ配列であるので, a[0]+j は a[0] から int 型のバイト数(4バイ

ト)の j 倍だけ先を表す. つまり, *(a[0]+j) は a[0][j] と等価である.したがって, a[i][j] は *(a[i]+j) と等価であり, *(*(a+i)+j) と等価であることがわかる. 当然,

*(a+i)+j は a[i][j] のアドレスを示すポインタとなる.このことから,

int y[][] = {{1,2}, {2}, {3}} ;int x[][] = {{1,2}, {2,3}, {3,4}} ;

という定義では, 配列 y[i] に対するインクリメント y[i]+1 のインクリメントのバイト数が計算できない

ことになり, このような定義が認められないことがわかる.同様に, 3次元以上の配列も

int a[2][5][10] ;

と定義できる. 2次元配列と同様に, 3次元以上の場合も以下の参照はすべて同じものとなる.

• a[i][j][k], *(a[i][j]+k), *(*(a[i]+j)+k), *(*(*(a+i)+j)+k).• &a[i][j][k], a[i][j]+k, *(a[i]+j)+k, *(*(a+i)+j)+k.

したがって, 適切な初期化子をおくことにより,

int a[][5][10] = {{{0,1,2,3,4,5,6,7,8,9},{1,2,3,4,5,6,7,8,9,0},{2,3,4,5,6,7,8,9,0,1},{3,4,5,6,7,8,9,0,1,2},{4,5,6,7,8,9,0,1,2,3}},{{1,2,3,4,5,6,7,8,9,0},{2,3,4,5,6,7,8,9,0,1},{3,4,5,6,7,8,9,0,1,2},{4,5,6,7,8,9,0,1,2,3},{5,6,7,8,9,0,1,2,3,4}}} ;

という定義が可能である.

Id: C7-2.tex,v 1.2 2001-03-21 19:38:00+09 naito Exp

264 数理解析・計算機数学特論

6.16.3 ポインタの配列と二重ポインタ

二重配列と似た変数の定義には,

int *b[2] ;int **c

が考えられる.

6.16.3.1 ポインタの配列

Section 6.14.3, および複雑な宣言をまとめた表 (p. 260) で述べた通り, int *b[2] は “int へのポインタの2個の要素からなる配列” であるので,

int a0, a1 ;int *b[2] = {&a0, &a1} ;

とすることにより, 2つの int 型のオブジェクトを指し示すことができる. もし,

int a0[5], a1[5] ;int *b[2] = {a0, a1} ;

とすると, b[0] は配列 a0 の先頭アドレスを示し, b[1] は配列 a1 の先頭アドレスを示す. したがって,*(b+i), b[i] がともに同じアドレスを指し示しているので, *(*(b+i)+j), *(b[i]+j) は, 結果として, 同じアドレスを指し示すこととなる. さらに, b[i] が配列の先頭を指し示すポインタであることを考えると,*(b[i]+j) は b[i][j] と書換えることができ, b[i][j] も *(b[i]+j) と同じ領域へのアクセスを示すこ

とになる83 .しかし, int *b[2] = 1,2,3,4,5,2,3,4,5,6 ; とは初期化できない. なぜなら, この変数定義では,

これらの値を格納するメモリ領域が確保できないからである. 定義 *b[2] で確保される記憶領域は, 他の変数を示すアドレス2個分に過ぎないことに注意しよう.

b[0]b[1]

✯a0

❥a1

6.16.3.2 二重ポインタ

C ではどのような型のオブジェクトへのポインタも利用可能であるので, 「ポインタへのポインタ」が利用できる. それは,

int **c ;

と定義する. このようなポインタへのポインタ(二重ポインタ)は,

int a ;int *b ;int **c ;

と定義されている時,

b = &a ; c = &b ;

とすれば, **c によって, a の値を参照可能になる.もちろん, 単純にこんな利用法をするためにポインタへのポインタをつくる必要はなく, 実際に利用する

場面は, 「配列へのポインタ」をポイントするために用いる. すなわち,83 理解しにくい場合には, int 型の変数へのポインタ p を用意し, p = b[i] と考えると良い.

Id: C7-2.tex,v 1.2 2001-03-21 19:38:00+09 naito Exp

数理解析・計算機数学特論 265

int a0[5], a1[5] ;int *b[2] = {a0, a1} ;int **c = b ;

とすれば, c には配列 b の先頭のアドレスが格納される. したがって, c+1 は int 型のポインタの配列(ポ

インタ)の意味で, インクリメントが行われるので, *(c+i) は b[i] と等価なアクセスを実現する. すなわち, この定義では,

• *(*(c+i)+j), *(c[i]+j), c[i][j] は同じ参照を表し,• *(c+i)+j, c[i]+j, &c[i][j] は同じアドレスを表す.

6.16.3.3 配列へのポインタ

ポインタの配列と混乱をおこし易いものに, 「配列へのポインタ」がある.

int (*b)[3] ;

と定義すると, b は先に間接演算子 * と結合するので, 「ポインタ」となり, 「その指し示す先が int [3]

」と読むことができる. よって, int (*b)[3] は 「int 型の3個の要素からなる配列へのポインタ」とな

る. この時,

int a[3] = {1,2,3} ;int (*b)[3] ;b = &a ;

とすることにより, (*b)[i] または (*b)+i は a[i] と等価なアクセスを実現する.前に関数の戻り値の型として配列を返すことは出来ないと書いたが, 配列へのポインタを利用すること

により, 類似のことを行うことは可能である.

Example 6.16.2 int 型の3個の要素からなる配列へのポインタを返す関数.

#include <stdio.h>int (*foo(int n))[3]{static int b[3] ;int i ;for(i=0;i<3;i++) b[i] = n+i ;return &b ;

}int main(int argc, char **argv){int (*a)[3] ;int i ;a = foo(1) ;for(i=0;i<3;i++)printf("%p\n", (*a)[i]) ;

return 0 ;}

この関数 foo では, 実際に値を代入する配列 b は static 宣言されている. これは, 戻り値の値(ポインタ)が指し示す先を, 関数終了後も保持するためである.

6.16.4 配列を仮引数とする関数

関数の引数(仮引数)が単純な型やそれに対するポインタの場合は, 仮引数の記述の方法は容易であるが, 多重配列, 構造体の配列など複雑な場合は, 仮引数の記述の方法は注意が必要である.仮引数においては

Id: C7-2.tex,v 1.2 2001-03-21 19:38:00+09 naito Exp

266 数理解析・計算機数学特論

int *aint a[]

は同じであると書いた. これらは, ともにそれぞれの変数の先頭アドレスが関数実引数となる.また多重配列においても,

int **aint *a[]

それぞれで, 先頭アドレスが関数引数となることは同じである. しかし,

int a[][]

とした場合には, 関数内で a[i][j] としてオブジェクトを指定しようとしても, a[i] としてどこを示しているかが分からない. 具体的には,

int a[2][3] = {{1,2,3},{3,4,5}} ;

で定義された変数を関数に渡す際に, 仮引数を a[][] と書き, 関数内で a[1] としたとする. この時, 我々が期待するのは, a[1] が {3,4,5} という配列であるが, 実際には, a のアドレス(すなわち, a[0] を示すアドレス)に対して, a[1] を示すアドレスとの差が分からないので, a[1] を正しく参照することができない. これを回避するためには, 仮引数として a[][3] と宣言する必要がある. こうすれば, a[0] に対してa[1] が int 型の3つ分のずれがあることが分かる.一方, 仮引数として *a[2] とした関数に対して, 二重配列 a[2][3] を実引数とすると, すなわち,

void foo(int *a[2]){return ;

}

int main(int argc, char **argv){int a[2][4] ;foo(a) ;return 0 ;

}

とすると,

warning: passing arg 1 of ‘foo’ from incompatible pointer type

という警告が出される. これは, 仮引数 int *a[2] と実引数 int a[2][4] は, そのオブジェクトの型が異なることが理由である. なぜなら, 関数内で a[i][j]を参照しようとすると, 先頭アドレス a からポインタ

のバイト数の i 倍先のアドレスが参照され, そこに書かれているアドレスを参照して, そこから int 型の

バイト数の j 倍先のアドレスにある値が参照される. しかし, 実引数として渡されたオブジェクトは, int型のオブジェクトの 2 × 4 個のならびであるので, 実引数側では a[i][j] を参照するには, a から int 型

のオブジェクトのバイト数の 4×i+j 倍先を参照しなければならない.

*a[2] で期待されているデータ構造

a[0] のアドレス, 例えば 0xeeffa[1] のアドレス, 例えば 0xef20

0xeeff 番地

0xef20 番地

a[2][4] のデータ構造

0x0001✲????

アドレスが32ビット(=4バイト), int 型が4バイトの時, int *a[2] と仮引数が宣言された関数内で a[0][0] を参照すると, a[0][0] の値をアドレスと思い, そのアドレスを参照する.

Id: C7-2.tex,v 1.2 2001-03-21 19:38:00+09 naito Exp

数理解析・計算機数学特論 267

これを正しく動作させるためには,

void foo(int a[][4]){return ;

}

int main(int argc, char **argv){int a[2][4] ;foo(a) ;return 0 ;

}

とする必要がある. しかし, 仮引数として **a とした関数に対して, *a[2] と定義したオブジェクトを実引数とすることは可能である.

void foo(int **a){return ;

}

int main(int argc, char **argv){int *a[2] ;foo(a) ;return 0 ;

}

また, この関数 foo は

void foo(int *a[])

として定義しても動作は変らない.よって, 多重配列を関数に渡そうとするとき, 関数仮引数定義では最も左の添字は省略が可能であること

がわかる. したがって,

int bar(int a[][5][4])

と定義した関数に

int a[5][5][4], b[100][5][4] ;

を実引数とすることが可能であるが,

int c[5][4][4], d[5][5][10], e[2][10][15] ;

等を実引数とすることは出来ない.これら多重配列を仮引数とする関数の関数プロトタイプ宣言は

int foo(int [][5][4])

などと, 識別子を省略することができる.以上をまとめると,「関数仮引数での配列はポインタ」であると考えれば良いことがわかる. 多重配列で

は, 逆に「関数仮引数でのポインタを配列にする」ことはできない.

Example 6.16.3 次は二重配列の各種の扱いである.

#include <stdio.h>int print_matrix_1(int [][], unsigned int) ;int print_matrix_2(int *[], unsigned int, unsigned int) ;int print_matrix_3(int **, unsigned int, unsigned int) ;

int main()

Id: C7-2.tex,v 1.2 2001-03-21 19:38:00+09 naito Exp

268 数理解析・計算機数学特論

{int a[3][4] = {{1,2,3,4},{2,3,4,5},{3,4,5,6}} ;int *b[3] = {a[0], a[1], a[2]} ;int **c = b ;

print_matrix_1(a,3) ;print_matrix_2(b,3,4) ;print_matrix_3(c,3,4) ;return 0 ;

}

int print_matrix_1(int a[][4], unsigned int n){unsigned int i, j ;

for(i=0;i<n;i++) {for(j=0;j<4;j++) {

/* printf(" %d",a[i][j]) ; *//* printf(" %d",*(a[i]+j)) ; */printf(" %d",*(*(a+i)+j)) ;

}printf("\n") ;

}printf("\n") ;return 0 ;

}

int print_matrix_2(int *a[], unsigned int n, unsigned int m){unsigned int i, j ;

for(i=0;i<n;i++) {for(j=0;j<m;j++) {

/* printf(" %d",a[i][j]) ; *//* printf(" %d",*(a[i]+j)) ; */printf(" %d",*(*(a+i)+j)) ;

}printf("\n") ;

}printf("\n") ;return 0 ;

}

int print_matrix_3(int **a, unsigned int n, unsigned int m){unsigned int i, j ;

for(i=0;i<n;i++) {for(j=0;j<m;j++) {

/* printf(" %d",a[i][j]) ; *//* printf(" %d",*(a[i]+j)) ; */printf(" %d",*(*(a+i)+j)) ;

}printf("\n") ;

}printf("\n") ;return 0 ;

}

Example 6.16.4 上の例と間違えやすいものの例.

#include <stdio.h>extern int print_p(int (*)[], unsigned int) ;

int main(){int a[3] = {1,2,3} ;

Id: C7-2.tex,v 1.2 2001-03-21 19:38:00+09 naito Exp

数理解析・計算機数学特論 269

int (*b)[3] ;

b = &a ;print_p(b,3) ;return 0 ;

}

int print_p(int (*b)[], unsigned int n){unsigned int i ;

for(i=0;i<n;i++) printf(" %d", (*b)[i]) ;printf("\n") ;return 0 ;

}

6.16.5 main 関数の引数について

一番はじめに, main 関数は引数を持つと述べたが, ここで, その引数が何かを解説しよう. main は, 次の型を持つ.int main(int argc, char **argv)

ここで, argc は, そのプログラムが実行された時の引数の数であり, argv は, その引数を格納する文字列へのポインタである. ここで, argv[0] には, そのプログラムの名前が入る. したがって,% program -a -b a c

として program が実行された時には, argc = 5, argv[1] は -a などとなる. プログラムに空白文字を含む引数を渡したいときには,% program -a -b "a c"

とすれば, a c で一つの引数となる.ここで,

char **argv ;char *name[] = {echo, hello, world.} ;char aname[][10] = {echo, hello, world.} ;

の違いをまとめておこう. それぞれを図式化すると次のようになる.

argv

0

echo\0

hello\0

world.\0

echo\0

hello\0

world.\0

echo\0 hello\0 world\0

0 10 15

Id: C7-2.tex,v 1.2 2001-03-21 19:38:00+09 naito Exp

270 数理解析・計算機数学特論

6.16.6 演習問題

Exercise 6.16.1 日付を表す unsigend int 型の変数 year, month, mday (それぞれ「西暦」, 「月」,「日」を表す)を引数とし, 1970年1月1日からの累積日数を表す関数を, 261 ページで定義した配列daytab を用いて書け. ただし, 1970年1月1日に対しては, 0 を返すこととする.

Exercise 6.16.2 次のプログラムの出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>int main(int argc, char **argv){while(*argv) printf("%s\n", *argv++) ;

}

Exercise 6.16.3 2つの N × N 行列のトレースを計算する関数を書け.

Exercise 6.16.4 2つの N × N 行列の和・積を計算するプログラムを書け.

Exercise 6.16.5 次のプログラムの出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>int a[3][3] = { {1,2,3}, {4,5,6}, {7,8,9} } ;int *pa[3] = { a[0], a[1], a[2] } ;int *p = a[0] ;int main(){int i ;for(i=0;i<3;i++) printf("%X\t%X\t%X\n", a[i][2-i], *a[i], *(*(a+i)+i)) ;for(i=0;i<3;i++) printf("%X\t%X\n", *pa[i], p[i]) ;

}

6.17 動的なメモリ確保とポインタ

C の配列はその定義において, 添字範囲を定数式にしなければならない. したがって, 実行時までどれだけの量の配列が必要になるかわからないときには, あらかじめ配列の大きさをきめて配列を定義することは出来ない. また, 配列の代りにポインタを利用するときには, その値を確保するためのメモリ領域を確保しなければならない. このように, 実行時に必要なだけのメモリ領域を確保する必要は, 大規模なプログラムや, 多重ポインタを利用するときには必ず必要となる. それを動的なメモリ確保と呼ぶ.動的なメモリ確保をするために, malloc 関数(または類似の各種の関数84 )を用いて, ヒープ (heep)

領域85 のメモリを利用するか, alloca 関数を用いてスタック領域のメモリを確保する必要がある.

6.17.1 malloc を用いた動的なメモリ確保

malloc 関数は,

#include <stdlib.h>void *malloc(size_t size);

84 malloc に類似の関数に関しては, malloc のオンライン・マニュアルを参照. calloc のように初期化も同時に行ってくれる関数もある.85 ヒープ領域とは, スタックセグメント内で, スタックと逆方法にメモリを利用する領域である. したがって, 最悪の場合, スタックとヒープが重なって, メモリ不足が起きる可能性がある.

Id: C7-3.tex,v 1.4 2001-03-24 16:47:12+09 naito Exp

数理解析・計算機数学特論 271

と定義される標準ライブラリ関数であり, size で指定されるバイト数のメモリをヒープ領域に確保して,そのメモリの先頭のアドレスをポインタとして返す. ここで, size t 型とは, その処理系でのメモリ全体を表すのに十分な符号なし整数を表す型であり, unsigned int または unsigned long 型と同じである86 .malloc 関数で確保した領域が不必要になったときには, free 関数でその領域を開放しなければならない.

Example 6.17.1 int 型のオブジェクト100個分のメモリをヒープに確保し, 値を代入した後に, メモリを開放する.

#include <stdio.h>int main(int argc, char **argv){int *p ;int i ;

if ((p = (int *)malloc(100*sizeof(int))) == NULL) {printf("Could not allocate memory!\n") ;return -1 ;

}for(i=0;i<100;i++) *(p+i) = i ;free(p) ;return 0 ;

}

malloc 関数は, メモリの確保に失敗すると NULL を返す.

Example 6.17.2 malloc 関数で確保したメモリを開放しないため, メモリが確保できなくなる例.

#include <stdio.h>void foo(void){int *p ;if ((p = (int *)malloc(0x10000000)) == NULL) {printf("Could not allocate memory!\n") ;exit(-1) ;

}printf("%p\n", p) ;return ;

}

int main(int argc, char **argv){int i ;for(i=0;i<0x1000;i++) foo() ;return 0 ;

}

また, malloc 関数で確保した領域が足らなくなった場合には, realloc 関数で領域を追加確保もできる.

#include <stdio.h>int main(int argc, char **argv){int i ;int *p ;

if ((p = (int *)malloc(100*sizeof(int))) == NULL) exit(-1) ;printf("%p\n", p) ;if ((p = (int *)realloc(p, 200*sizeof(int))) == NULL) exit(-2) ;printf("%p\n", p) ;free(p) ;return 0 ;

}

86 正確には, 整数型のいずれかに typedef されている.

Id: C7-3.tex,v 1.4 2001-03-24 16:47:12+09 naito Exp

272 数理解析・計算機数学特論

Remark 6.17.1 なお, alloca 関数はヒープ領域の代りにスタック領域で動的にメモリを確保するために用いる. スタック領域でメモリを確保することと, ヒープ領域で確保することの違いは, ヒープ領域のメモリは, malloc 関数(またはその類似物)を呼び出した関数の実行が終わっても, その領域は確保されたままであるが, alloca 関数でスタック領域にメモリを確保すると, そのメモリが利用できるのは, 呼び出した関数の終了までの間に限られる. したがって, スタック領域の動的メモリの有効範囲が狭くなるが, 一方では, free によってメモリを開放する必要がないので, すなわち, 関数の実行が終了すれば, スタック領域は自動的に開放されるので, free を忘れることによる弊害を防ぐことが出来る.動的に確保したヒープメモリを開放せずに使い続けて, メモリを大量消費している状態をメモリ・リーク

(memory leak) とよび, プログラムが正常に動作していない状態の一つである.

6.17.2 動的なメモリ確保を利用する場面

動的なメモリ確保を必要とする場面は各種考えられるが, ここでは, 取りあえずいくつかの例を紹介しておこう.

6.17.2.1 標準入力からの文字列の読み込み

標準入力から文字列を読むには, fgets 関数を用いるのがよい. 1行の最大の文字数が決まっているときには,

#include <stdio.h>#define MAX_LEN 1024int main(int argc, char **argv){char str[MAX_LEN], *p ;while((!feof(stdin))&&((p = fgets(str, MAX_LEN, stdin)) != NULL))printf("%s", str) ;

return 0 ;}

とすればよい. MAX LEN は, プリプロセッサ命令で 1024 に置き換えられる. feof 関数はファイルの終端

かどうかを判定する関数である. このプログラムを

#include <stdio.h>#define MAX_LEN 1024

int main(int argc, char **argv){char str[MAX_LEN], *p ;while(!feof(stdin)) {fgets(str, MAX_LEN, stdin) ;printf("%s", str) ;

}return 0 ;

}

とすると, 最終行を2度表示することになる. (詳細は fgets のオンライン・マニュアルを参照.)また, このプログラムをわかりやすくしたければ,

#include <stdio.h>#define MAX_LEN 1024int main(int argc, char **argv){char str[MAX_LEN], *p ;while(!feof(stdin)) {if ((p = fgets(str, MAX_LEN, stdin)) != NULL)

printf("%s", str) ;}

Id: C7-3.tex,v 1.4 2001-03-24 16:47:12+09 naito Exp

数理解析・計算機数学特論 273

return 0 ;}

としても良い. ここで, 式

((!feof(stdin))&&((p = fgets(str, MAX_LEN, stdin)) != NULL))

では, && (と ||)は他の演算子と異なり, 優先順位と結合順序にしたがって, 式の評価と副作用の完了が行われる. すなわち, feof(stdin) の返す値が 0 であれば, 後半の fgets 関数の呼出しは一切行われない.この2つの式の順序を交換すると, 標準入力が EOF に達しているにも関わらず fgets 関数による読み出し

が行われる. これは, 実行時エラーを発生したり, 予期しない動作をする可能性がある.この fgets 関数の使い方では, MAX LEN を越えた文字数を持つ行を str に一度に格納することは出来な

い. 例えば,

#include <stdio.h>#define MAX_LEN 10int main(int argc, char **argv){char str[MAX_LEN], *p ;while((!feof(stdin))&&((p = fgets(str, MAX_LEN, stdin)) != NULL))printf("***%s", str) ;

return 0 ;}

として, 実行すると何が起こっているのかがわかる. これでは, str に格納した1行を何かの関数に渡して文字列処理をすることが出来ない. これを解決するために, 動的なメモリ確保を行ってみよう.

#include <stdio.h>#include <string.h>#include <strings.h>#define MAX_LEN 10extern void error_jmp(void) ;

int main(int argc, char **argv){char *str, *temp_str, *p ;size_t max = MAX_LEN ; /* その時点での文字列の最大の長さを格納する */size_t len ; /* その時点での str の文字列の長さ */

if (((str = (char *)malloc(MAX_LEN+1)) == NULL)||((temp_str = (char *)malloc(MAX_LEN+1)) == NULL))

error_jmp() ;bzero((char *)str, MAX_LEN+1) ; /* str の内容をゼロクリアする */len = strlen(str) ; /* 0 になっているはず *//* 標準入力からの読み込み */while((!feof(stdin))

&&((p = fgets(temp_str, MAX_LEN, stdin)) != NULL)) {/* 文字列の長さが足りない!* 領域の再確保 */

if (max < len+strlen(temp_str)+1) {if ((str = (char *)realloc(str, len+strlen(temp_str))) == NULL) error_jmp() ;max = len+strlen(temp_str)+1 ;

}strcat(str,temp_str) ; /* 文字列の連結 */len = strlen(str) ;/* 改行文字が見つかったので, 文字列を表示する */if (str[strlen(str)-1] == ’\n’) {

printf("%s", str) ;bzero(str, len+1) ;

}}return 0 ;

}

void error_jmp(void)

Id: C7-3.tex,v 1.4 2001-03-24 16:47:12+09 naito Exp

274 数理解析・計算機数学特論

{printf("Could not allocate memory!\n") ;exit(-1) ;return ;

}

このプログラムでは, 改行文字を読むまでは, str に文字列を連結している. もし, str に確保した領域が足らなくなった場合には, realloc で領域を再確保している. なお, bzero は領域をゼロクリアする関数,strlen は文字列の長さを返す関数である. strlen は文字列終端文字の先頭からのポインタのオフセットを返すので, str[strlen(str)-1] は str の最後の文字を表すことになる. このプログラムでは, 一旦確保した文字列領域は最後まで利用するので, free の呼出しの必要はない.

6.17.2.2 二重ポインタの指し示す先を確保する

C では, 一般のサイズの多重配列を関数に渡す手段は存在しない. そのかわり, 多重ポインタを渡すことによって多重配列を渡すことに代えることが多い. 例えば, main 関数の char **argv という引数は, 呼出し側のシェルから複数の文字列を得るための手段として用いられている.main 関数の char **argv の場合には, それが指し示すメモリ領域はシェルからプログラムが起動され

る段階で確保されているが, プログラム内で二重ポインタを利用して複数の文字列を扱うためには, それらの文字列を格納する領域を動的に確保しなければならない.ポインタのポインタを利用して, 長さの異なる文字列を扱う例を考えてみよう. ここでは, 標準入力から

入力されたテキストファイルの各行87 を一つの文字列と思い, それらを二重ポインタで指し示す88 .

#include <stdio.h>#include <string.h>#include <strings.h>#define MAX_LEN 1024extern void error_jmp(void) ;

int main(int argc, char **argv){int i = 1 ;char *str, *p ;char **q ;char temp_str[MAX_LEN] ;

/* q のための領域を取りあえず1行を指し示す分だけ確保 */if ((q = (char **)malloc(sizeof(char *)*i)) == NULL) error_jmp() ;/* 1行分の文字列領域を確保 */if ((str = (char *)malloc(MAX_LEN+1)) == NULL) error_jmp() ;while((!feof(stdin))

&&((p = fgets(temp_str, MAX_LEN, stdin)) != NULL)) {strcpy(str, temp_str) ;*(q+i-1) = str ;i += 1 ;/* q のための領域をさらに1行を指し示す分だけ確保 */if ((q = (char **)realloc(q, sizeof(char *)*i)) == NULL) error_jmp() ;/* 次の1行分の文字列領域を確保 */if ((str = (char *)malloc(MAX_LEN+1)) == NULL) error_jmp() ;

}/* ファイルの先頭から11行めを表示 */printf("%s", q[10]) ;return 0 ;

}

void error_jmp(void){

87 簡単のため, 1行の最大文字数は 1024 としておこう.88 いささか人為的な例であることは仕方ない.

Id: C7-3.tex,v 1.4 2001-03-24 16:47:12+09 naito Exp

数理解析・計算機数学特論 275

printf("Could not allocate memory!\n") ;exit(-1) ;return ;

}

q は読み込んだ文字列を格納する領域を指し示すポインタの列で, 1行を読み込むごとに, 次の1行を格納する領域と, それを指し示す領域を確保しながら q を構成している.

演習問題

Exercise 6.17.1 Section 6.17.2.2 の例で, 1行の文字数を制限しなくても良いようにプログラムを書換えよ.

Exercise 6.17.2 Section 6.17.2.2の例で,プログラム終了直前に, mallocで確保したすべての領域を free

で開放するようにプログラムを書換えよ.

6.18 データ構造

6.18.1 構造体

構造体とは, 複数のオブジェクトを一つにまとめて, あたかもそれらが一つの変数であるように見せるためのものである. 例えば, 複素数のように二つの実数の組のようなオブジェクトは構造体で表現するのが望ましい.

6.18.1.1 構造体の定義

構造体を定義する構文は,

{\tt struct} 識別子 (opt) {メンバー宣言}

という形である. ここで, 「メンバー宣言」とは, 構造体に含まれる要素(それをメンバー (member)と呼ぶ)の宣言であり, 構造体メンバーは, 関数型と不完全型遺体であれば, どのような型のものでもよい.すなわち, 構造体メンバーにその構造体自身を含んではならない. しかし, その構造体へのポインタを含むことは出来る. struct の直後に書かれた識別子(オプション)は構造体タグと呼ばれるものである.

Example 6.18.1 次は, double 型の2つの変数の組が構造体になったものである.

struct complex {double real ;double imaginary ;

} x ;

ここで, complex が構造体タグであり, double 型の2つのメンバー real と imaginary を持つ構造体と

して定義されている. さらに, 識別子 x は struct complex という型を持ったオブジェクトとして定義さ

れる.

realimaginary struct complex

Example 6.18.2 上の例 (Example 6.18.1) と同じ構造体を定義するが, 構造体タグを持たないもの:

Id: C8.tex,v 1.11 2001-03-23 14:18:24+09 naito Exp

276 数理解析・計算機数学特論

struct {double real ;double imaginary ;

} x ;

Example 6.18.1 との違いは, 構造体タグを持たないことであるが, この場合には, この後でこの型の構造体を持つ変数を定義する際に,

struct {double real ;double imaginary ;

} y ;

と繰り返し書かなくてはいけなくなる. 一方, Example 6.18.1 のように, 構造体タグを利用すれば, その後同じ型の変数を定義する際などに, struct complex y とだけ書けば良い.

構造体メンバーそれ自身が構造体になってもかまわない.

Example 6.18.3 次は複素平面上の円を表す構造体である. 中心を表す複素数と半径を表す実数(倍精度浮動小数点数)の組からなる構造体である.

struct sphere {struct complex center ;double radius ;

}

radius

realimaginary stuct complex struct sphere

このように定義された構造体のメンバーは, 先に書かれた順にメモリ内に格納される.

6.18.1.2 構造体のメンバー参照

上のように定義した構造体変数のそれぞれのメンバーを参照するには, . という演算子(構造体メンバー演算子)を利用する.

Example 6.18.4 構造体 complex に値を代入する.

struct complex {double real ;double imaginary ;

} ;

int main(int argc, char **argv){struct complex z ;

z.real = 1.0 ;z.imaginary = 1.0 ;

}

すなわち, 構造体変数 struct_name に対して, struct_name.member_name によって, その構造体メンバーmember_name を表すことができる. したがって, 上で定義した構造体 struct sphere 型の変数 s に対し

て, その中心と半径を代入するには,

s.center.real = 0.0 ;s.center.imaginary = 0.0 ;s.radius = 1.0

とすれば良いことが分かる.

Id: C8.tex,v 1.11 2001-03-23 14:18:24+09 naito Exp

数理解析・計算機数学特論 277

6.18.1.3 構造体の初期化と代入

構造体を初期化するには, そのメンバーに値を代入しても良いが, 一方で,

struct sphere s0 = {{0.0, 0.0}, 1.0} ;struct sphere s1 = {0.0, 0.0, 1.0} ;

という初期化も可能である89 . また, 構造体の変数への一括代入も可能である.

struct sphere s0 = {{0.0, 0.0}, 1.0} ;struct sphere s1 ;

s1 = s0 ;

によっても, s0 の内容を s1 に代入することが可能である.

6.18.1.4 構造体と関数

構造体を引数にとる関数, 構造体を戻り値とする関数定義することができる90 .

Example 6.18.5 はじめに, struct complex 型の変数に対して, そのノルムを返す関数 complex_norm

をつくってみよう.

double complex_norm(struct complex x){if (x.real != 0) {return abs(x.real)*sqrt(1 + (x.imaginary/x.real)*(x.imaginary/x.real)) ;

}return abs(x.imaginary) ;

}

ここで, 複素数 x +√−1y のノルムを

√x2 + y2 と計算してしまうと, x2 + y2 を計算する時点でオーバ

フローが発生するかもしれない. そのため, わざわざ√

x2 + y2 = |x|√1 + (y/x)2 と計算していることに注意.

Example 6.18.6 次に, 構造体を戻り値とする関数として, 2つの複素数の和を求める関数をつくる.

struct complex complex_add(struct complex z, struct complex w){z.real += w.real ;z.imaginary += w.imaginary ;return z ;

}

Remark 6.18.1 これらの関数 complex_norm, complex_add をプロトタイプ宣言なしに利用すると, ともに戻り値が int ではないため, コンパイラが正しく関数を判断できない. したがって, それらを利用する前に, プロトタイプ宣言

extern double complex_norm(struct complex) ;extern struct complex complex_add(struct complex, struct complex) ;

を行う必要がある.

89 当たり前だが, 上の初期化の方が何をしているかは分かりやすい.90 Kernighan-Ritchie の初版(いわゆる traditional なC)では, 構造体への一括代入, 構造体を引数とする関数, 構造体を戻り値とする関数などは許されてはいなかった. したがって, traditional C でこのようなことを行うためには, すべて, 構造体のポインタを受け渡しする必要があった. また, traditional C では, 自動構造体(関数内部での自動変数となる構造体)の初期化も許されていなかった. ANSI 規格(Kernighan-Ritchie 第2版)では, これら構造体の扱いが簡単になったことが大きな改訂部分である.

Id: C8.tex,v 1.11 2001-03-23 14:18:24+09 naito Exp

278 数理解析・計算機数学特論

このように構造体を引数とする関数, または, 構造体を戻り値とする関数では, 関数呼び出し, およびそこからの復帰の際に, 構造体のデータすべての値のコピーが行われる. したがって, 巨大な構造体に対するこれらの操作には, 関数呼び出しのオーバーヘッドが大きくなってしまう. それを避けるには, 構造体へのポインタを利用することが望ましい. ポインタを利用すれば, その指し示す先が何であっても, そのデータは(OSに依存した)一定のデータ量に過ぎない.

6.18.1.5 構造体へのポインタと関数

はじめに, 構造体へのポインタをつくってみよう. 上で定義した struct complex 型の構造体へのポイ

ンタと, ポインタを経由したメンバーへの参照は次の例のようになる.

Example 6.18.7 はじめに, 構造体変数と, そのポインタを作成する.

struct complex z, *pz ;pz = &z ;

これにより, 構造体へのポインタ pz は構造体変数 z の先頭アドレスを示すこととなる. この時, pz を経由して, z のメンバーへアクセスするための方法としては,

(*pz).real ;pz->real ;

の2種類が考えられる. ここで, 演算子 -> は構造体ポインタの指し示す構造体のメンバーへの参照を表

す演算子であり, 実は上の2つの式は等価である.

Remark 6.18.2 ここで, (*pz).real のかわりに *pz.real と書いたとすると, これは演算子の優先順位より *(pz.real) を表すが, pz.real はポインタではないので, 誤りとなる.

Remark 6.18.3 もし,

struct sphere s, *ps ;ps = &s ;

と定義されているとき,

s.center.realps->center.real(s.center).real(ps->center).real

は等価である. これは, ., -> は最も優先順位が高く, その結合法則が左から右となっているからである.

このような構造体へのポインタを利用して, 上でつくった complex_add のポインタ版をつくってみよう.

Example 6.18.8 一つの例は, 引数として求めるべき値を入れてしまう方法である.

void complex_add(struct complex *z, struct complex *w, struct complex *x){x->real = z->real + w->real ;x->imaginary = z->imaginary + w->imaginary ;return ;

}

この場合, 呼び出し方法は,

complex_add(&z,&w,&x)

とすれば良い.

Id: C8.tex,v 1.11 2001-03-23 14:18:24+09 naito Exp

数理解析・計算機数学特論 279

Example 6.18.9 もう一つの例として, 結果の入っている静的変数へのポインタを返すこともできる.

struct complex *complex_add(struct complex *z, struct complex *w){static struct complex x ;x.real = z->real + w->real ;x.imaginary = z->imaginary + w->imaginary ;return &x ;

}

この場合, 呼び出し方法は,

struct complex w,z,*px ;px = complex_add(&z,&w) ;

とすれば良い.

6.18.1.6 構造体の配列

構造体はそれ自身を配列にしたり, 構造体のフィールドに既に定義されている構造体を用いることができる. 構造体を配列にするには, 以下のような定義をすれば良い.

struct complex z[10] ;

これは, struct complex 型の構造体の10個の配列を定義している.また, 構造体変数がどれだけのメモリ量を利用しているかを知るには, sizeof 演算子を利用すれば良い.

sizeof(struct complex)

とすると, complex というタイプの構造体変数の占めるメモリ量を知ることができる91 .

Remark 6.18.4 たとえば,

struct {char c ;int n ;

} ;

によって定義された構造体は, int が4バイトの時, 必ずしも5バイトを占めるわけではない. 実際, 多くの場合8バイトとなるだろう. これは, int 型の変数は(多くのアーキテクチャで)ワード境界に整列されるという性質があるからである. sizeof 演算子は正しくそのバイト数を返す.

Example 6.18.10 構造体として,

• 氏名• 学籍番号• 試験の得点をメンバーに持つものを作成し, その構造体を型に持つ要素数3の配列を作成する. さらに, その配列を試験の得点の高い順に並び替える. もし, 試験の得点が同じであれば, 学籍番号を文字列の辞書式順序にしたがって, 順序が小さいものを前にする.

91 この例の場合, sizeof(struct complex) と sizeof z では, 返す値が異なっている. sizeof(struct complex) は complex

構造体のバイト数を返すのに対し, sizeof z は, 変数 z の占めるバイト数を返す. したがって, この場合には sizeof(struct

complex)*10 が sizeof z に等しい.したがって, sizeof z / sizeof(struct complex) とすることにより, 配列の要素数を得ることができる. これは, sizeof z

/ sizeof z[0] としても同じである.

Id: C8.tex,v 1.11 2001-03-23 14:18:24+09 naito Exp

280 数理解析・計算機数学特論

#include <stdio.h>#include <stdlib.h>#include <string.h>

struct personal_data {char name[40] ;char number[10] ;int point ;

} ;extern int compare(struct personal_data *, struct personal_data *) ;

int main(int argc, char **argv){struct personal_data pd[3] ={{"内藤久資", "0010011", 80},{"藤原一宏", "0010012", 100},{"木村芳文", "0010013", 100}

} ;int i ;

qsort((struct personal_data *)pd,sizeof(pd)/sizeof(struct personal_data),sizeof(struct personal_data),(int (*)(const void *, const void *))compare) ;

for(i=0;i<3;i++)printf("%s %s %d\n",

(pd+i)->name,(*(pd+i)).number,pd[i].point) ;

return 0 ;}

int compare(struct personal_data *a, struct personal_data *b){if (a->point > b->point) return -1 ;else if (a->point < b->point) return 1 ;return strcmp(a->number,b->number) ;

}

結果表示のところで, 各メンバーへの参照に3種類の方法を用いている. C の標準関数 qsort は

#include <stdlib.h>

void qsort(void *base, size_t nel, size_t width,int (*compar) (const void *, const void *));

と定義され, base で参照されるポインタを先頭にする配列を, compar 関数値によってソート(並び替え)を行う. この時, 一つの配列要素の大きさは width で表され, 配列要素数は nel で表される.

6.18.1.7 演習問題

Exercise 6.18.1 構造体の構成がどのようなものであっても, その構造体変数2つの内容を入れ替える関数を書け.

Exercise 6.18.2 Example 6.18.1 で作った複素数を表す構造体を利用して, かけ算, 割算を計算する関数を作れ.

Exercise 6.18.3 Example 6.18.2 で作った複素平面内の円を表す構造体を利用して, 与えられた複素数が円の内部にあるかどうかを判定する関数を書け.

Exercise 6.18.4 次のプログラムの出力結果がなぜそうなるのかを考えよ.

Id: C8.tex,v 1.11 2001-03-23 14:18:24+09 naito Exp

数理解析・計算機数学特論 281

#include <stdio.h>int main(){struct S1 { char c[4], *s ; } s1 = {"abc", "def" } ;struct S2 { char *cp ; struct S1 ss1 ; }

s2 = { "ghi", { "jkl", "mno" }},s3 = { "pqr", { "stu", "vwx" }} ;

struct S3 { struct S1 *sp1[2] ; }s4 = { &s2.ss1, &s3.ss1} ;

struct S1 *sp2 = s4.sp1[0] ;printf("s1.c[0] = %c\t*s1.s = %c\n",

s1.c[0], *s1.s) ;printf("s1.c = %s\ts1.s = %s\n",

s1.c, s1.s) ;printf("s2.cp = %s\ts2.ss1.s = %s\n",

s2.cp, s2.ss1.s) ;printf("++s2.cp = %s\t++s2.ss1.s = %s\n",

++s2.cp, ++s2.ss1.s) ;printf("s4.sp1[0]->c = %s\ts4.sp1[0]->s = %s\n",

s4.sp1[0]->c, s4.sp1[0]->s) ;printf("s4.sp1[0]->c[0] = %c\ts4.sp1[1]->s[1] = %c\n",

s4.sp1[0]->c[0], s4.sp1[1]->s[1]) ;printf("*(s4.sp1) = %s\t*(s4.sp1+1) = %s\n",

*(s4.sp1),*(s4.sp1+1)) ;}

6.18.2 構造体を利用したデータ構造

構造体を利用すると, アルゴリズムの実現に役立つリスト (list), ツリー (tree) といったデータ構造を実現することができる.これらのデータ構造は, そのデータ量があらかじめわかっているならば, ポインタを利用せずに実現でき

るが, データ量がアプリオリにはわからない時にはポインタを使わざるをえない. ここでは, これらのデータ構造が, どのようなものかを見ていこう.

6.18.2.1 リスト

リストとは, データが一列につながったものである. 各データは次のデータへのポインタを持ち, 最後のデータが持つ次のデータへのポインタは何も指し示していないという形で実現できる. リストになったデータを操作するには, ポインタを動かせば良い. 具体的には, 次のような形式になっている.

6.18.2.2 ツリー

ツリーとは, 各データが1つ以上の他のデータへのポインタを持ったものである. 各データは他のデータへのポインタを持ち, 最後のデータが持つ他のデータへのポインタは何も指し示していないという形で実現できる. 具体的には, 次のような形式になっている.

Id: C8.tex,v 1.11 2001-03-23 14:18:24+09 naito Exp

282 数理解析・計算機数学特論

6.18.2.3 自己参照構造体

構造体は, それ自身を参照することができる. これを利用して, リストやツリーといったデータ構造を実現することができる.例えば, リストを実現するには, 次のような方法を利用する.

Example 6.18.11 80 文字からなる文字列と, 次のデータへのポインタを持った構造体は以下のように定義できる.

struct data {char str[80] ;struct data *next ;

} data ;

このように定義した構造体を利用して, リストを実現することができる.

Example 6.18.12 Example 6.18.11 で定義した構造体を初期化する. 即ち, 一番始めのデータには何も入れない.

strcpy(data.str,"") ;data.next = NULL ;

次に必要なことは, 一番最後のデータ(最初は一番はじめのデータと同じ)にデータを入力したら, もう一つデータを持ってきて, それを初期化することである.

Example 6.18.13 ここでは, ポインタのつなぎ変えと, 次のデータ領域の取得をしている.

struct data *p ;

if ((p = (struct data *)malloc((unsigned int)(sizeof(struct data)))) != NULL) {strcpy(p->str,"b") ;p->next = NULL ;data.next = p ;

}

ここで, malloc 関数は, 必要なメモリ領域を確保するための関数である. ここで確保したメモリ領域は,必要がなくなったら, free 関数で領域を開放する必要がある.

Id: C8.tex,v 1.11 2001-03-23 14:18:24+09 naito Exp

数理解析・計算機数学特論 283

このようにして作った リスト形式のデータを一番最初のデータから順にアクセスするためには, 最初のデータを指し示すポインタを作成して, next が次のデータを指ししていることを利用して, ループを使ってアクセスすれば良い.

Example 6.18.14 ここでは, リストの途中にデータを挿入するための手順を示している.

struct data *p ;struct data *q ;

if ((q = (struct data *)malloc((unsigned int)(sizeof(struct data))))!= NULL) {q->next = p->next ;p->next = q ;

}

ここで, p は, 挿入したい位置を示しているポインタである.

p

p

p->next

q->next

q

Example 6.18.15 各データが2つのポインタを持ったツリーを実現するには, 次のようなデータ形式を利用すれば良い.

struct tree_data {char c ;struct tree_data *r ;struct tree_data *l ;

} ;

Example 6.18.15 定義した二分木構造を実際に使ってみよう.

Example 6.18.16 ここでは, 以下の図のような二分木を構成する.

c

e

g

f

✂✂ ❈❈

d

✔✔ ❈❈

b

i

l m

❈❈

k n

✂✂❏❏

j

�� ❈❈

h

✱✱ ❡❡

a

root

Id: C8.tex,v 1.11 2001-03-23 14:18:24+09 naito Exp

284 数理解析・計算機数学特論

はじめに二分木を入力するための関数 enter を定義する.

char *enter(struct tree_data **t, char *str){struct tree_data *p ;

printf("%c", *str) ;if (*str == ’\0’) return str ;if (*str != ’.’) {if ((p = (struct tree_data *)malloc(sizeof(struct tree_data))) == NULL) {

printf("Could not allocate memory!\n") ; exit(-1) ;}p->c = *str ; p->l = NULL ; p->r = NULL ;*t = p ;str = enter(&(p->l),++str) ;str = enter(&(p->r),++str) ;

}return str ;

}

関数 enter は

struct tree_data *root ;char str[] = "abc..de..fg...hi..jkl..m..n.." ;

と定義された変数に対して

enter(&root, str) ; printf("\n") ;

として呼び出すと, 二分木の構造にデータを格納し, そのデータを表示する.このようにして構成した二分木を3つの方法で巡回してみよう. はじめは, 左優先に探索し, 通ったノー

ドを順に印字するものである.

void preorder(struct tree_data *t){if (t == NULL) return ;printf("%c", t->c) ;preorder(t->l) ;preorder(t->r) ;return ;

}

次に左優先に探索し, ノードを分岐するときに印字するものである.

void inorder(struct tree_data *t){if (t == NULL) return ;inorder(t->l) ;printf("%c", t->c) ;inorder(t->r) ;return ;

}

最後は, 左優先に探索し, 戻るときにノードを印字するものである.

void postorder(struct tree_data *t){if (t == NULL) return ;postorder(t->l) ;postorder(t->r) ;printf("%c", t->c) ;return ;

}

これら3つの関数を

Id: C8.tex,v 1.11 2001-03-23 14:18:24+09 naito Exp

数理解析・計算機数学特論 285

enter(&root, str) ; printf("\n") ;preorder(root) ; printf("\n") ;postorder(root) ; printf("\n") ;inorder(root) ; printf("\n") ;

として呼び出すと,

abc..de..fg...hi..jkl..m..n..abcdefghijklmncegfdbilmknjhacbedgfaihlkmjn

という結果を得る. これが「二分木の巡回」である.

Example 6.18.17 Kernighan & Ritchie の教科書 [2] の 6.5 章には, 二分木の興味ある応用例が述べられている. そこに述べられているプログラムを掲載しておこう. (ただし, 多少改変してある.)

#include <stdio.h>#include <string.h>#include <ctype.h>#define MAXLINE 1024#define FMT " .,"

struct tnode {char *word ;int count ;struct tnode *left ;struct tnode *right ;

} ;

extern struct tnode *addtree(struct tnode *, char *) ;extern struct tnode *talloc(void) ;extern void error_jmp(void) ;extern void print_tree(struct tnode *) ;

int main(int argc, char **argv){struct tnode *root ;char *p, *q, buf[MAXLINE] ;root = NULL ;while((!feof(stdin))&&((p = fgets(buf, MAXLINE, stdin)) != NULL)) {/* 入力のトークン分解と二分木への挿入 */q = strtok(buf,FMT) ;if (isalpha(q[0])) {

q[0] = tolower(q[0]) ;root = addtree(root,q) ;

}while((q = strtok(NULL,FMT)) != NULL) {

if (isalpha(q[0])) {q[0] = tolower(q[0]) ;root = addtree(root,q) ;

}}

}/* 二分木の出力 */print_tree(root) ;return 0 ;

}

void print_tree(struct tnode *p){if (p == NULL) return ;print_tree(p->left) ;printf("%4d: %s\n", p->count, p->word) ;print_tree(p->right) ;return ;

}

Id: C8.tex,v 1.11 2001-03-23 14:18:24+09 naito Exp

286 数理解析・計算機数学特論

struct tnode *addtree(struct tnode *p, char *s){int str_cond ;if (p == NULL) { /* 新しい単語? */if ((p = talloc()) == NULL) /* 領域確保に失敗 */

error_jmp() ;if ((p->word = strdup(s)) == NULL) /* 領域確保に失敗 */

error_jmp() ;p->left = NULL ; p->right = NULL ;p->count = 1 ;

}else if ((str_cond = strcmp(s,p->word)) == 0)/* 同じ単語があるので, カウンタをインクリメント */p->count += 1 ;

else if (str_cond < 0) /* 小さければ左に */p->left = addtree(p->left,s) ;

else /* 大きければ右に */p->right = addtree(p->right,s) ;

return p ;}

/* p の領域を確保する */struct tnode *talloc(void){struct tnode *p ;if ((p = (struct tnode *)malloc(sizeof(struct tnode))) != NULL)return p ;

return NULL ;}

/* s で与えられた文字列を duplicate する */char *strdup(const char *s){char *p ;if ((p = (char *)malloc(strlen(s)+1)) != NULL) {strcpy(p,s) ;return p ;

}return NULL ;

}

void error_jmp(void){fprintf(stderr, "Could not allocate memory!\n") ;exit(-1) ;return ;

}

このプログラムは標準入力から入力されたテキストファイルを, FMT で与えられた文字列の要素を区切り子としてトークン分解し(すなわち, 単語に分解し), それを二分木構造に展開する. 二分木構造は, すべての葉に対して,

• 左部分木は, 単語の辞書式順序で葉よりも小さいものを,• 右部分木は, 単語の辞書式順序で葉よりも大きいものを

格納する. 与えられた順序に対してこのような構造を持つ二分木を整列二分木 (heap) と呼ぶ. この二分木を inorder で巡回すると, 順序に対して整列された出力を得る.例えば, 以下のようなテキストファイルを入力する.

This is a test for a binary tree, which is called heap.this is a test for a binary tree, which is called heap.Thats are test for binary trees.

この場合の出力結果は

Id: C8.tex,v 1.11 2001-03-23 14:18:24+09 naito Exp

数理解析・計算機数学特論 287

5: a1: are3: binary2: called3: for2: heap4: is3: test1: thats2: this3: tree2: which

となる. これは確かに単語の辞書式順序となっている. (ただし, 各単語の先頭文字が大文字の時は, それを小文字に変更している.)

また, リストの特別な形式として, 双方向リスト, 循環リストという形式もある.

双方向リスト

循環リスト

これらのリストは, 単純に “自分の次” だけを示すのではなく, “自分の前” などを示すポインタを持っている.

6.18.2.3.1 typedef C の文法上は記憶クラス指定子となっている typedef を用いると,struct data {char str[80] ;struct data *r_next ;struct data *l_next ;

} data ;

といった構造体を記述する際に持っと簡単に書くことができる.

typedef [元の型] 新しい型の識別子

という形をとる. ここで定義された識別子は typedef 名と呼ばれる. 文法的には「元の型」はなくても良い.

Example 6.18.18 もっとも簡単なものは,

typedef int Length ;

というものであって, これ以後 Length という型の識別子が定義され, それは int と同じである.

Example 6.18.19 もう少し複雑なものは,

typedef struct tnode *treeptr ;

typedef struct tnode {char str[80] ;treeptr *r_next ;treeptr *l_next ;

} treenode ;

Id: C8.tex,v 1.11 2001-03-23 14:18:24+09 naito Exp

288 数理解析・計算機数学特論

これは, treeptr, treenode という2つの新しい型を定義している.

typedef と section 6.20 で述べる #define の違いは, typedef が型を定義しているのに対し, プリプロセッサ文の #define はコンパイル以前に展開されてしまうところにある. すなわち,

#define peach intunsigned peach i ;

は peach が int にプリプロセッサで置き換えられるので, 問題なくコンパイル出来るが,

typdef peach intunsigned peach i ;

は文法エラーとなる.一旦 typedef によって型を宣言してしまうと, その型はそれ以後で自由に利用可能である. したがって,

typedef struct complex Complex ;

によって Complexを定義すれば,それ以後はわざわざ struct complexと書かなくても良い. また, typedefでは同じ typedef 名をより狭いスコープで再宣言できるが, その場合には「元の型」を明示しなければ, 再宣言したことにはならない. (cf. [2, A8.9].)これまでに出てきた size t などの型は, typedef により, 処理系・OSによって適切な型に typedef さ

れている. 実際に typedef が有効に利用できるのは, このような処理系依存の部分を吸収する場合が多い.

6.18.3 その他の構造を持った型

6.18.3.1 共用体

余り使わないが, 共用体というものがある. 基本的には構造体と同じようなものであり, その定義方法も,struct の代りに, union という予約語を使って宣言を行う.

6.18.3.1.1 共用体の定義

Example 6.18.20 次は int, double の型を持つ2つのメンバーがある共用体の定義である.

union int_double {double x ;int n ;

} u ;

共用体と構造体との違いは, 各メンバーが重なり合うメモリ領域を共有していることである. すなわち, 共用体では, 各メンバーを同時に(意味のある方法では)アクセスすることができない. これ以外のことに関しては, 共用体は構造体と同じ性質を持つ.

double x

int n

struct int_double {double x ;int n ;

}

と定義すると, 2つのメンバーの領域は重ならない.

double x または int n

union int_double {double x ;int n ;

}

と定義すると,2つのメンバーの領域が重なってしまう.

Example 6.18.21 上の int_double 共用体では,

Id: C8.tex,v 1.11 2001-03-23 14:18:24+09 naito Exp

数理解析・計算機数学特論 289

u.n = 1 ;u.x = 1.0 ;

といったように, そのメンバーの識別子にしたがって, どの型でもとりうることができるが, 代入時と異なる型で参照したときの値は保証されない. すなわち, 共用体においては, そのメンバー変数がすべてメモリとして確保されるのではなく, (この例の場合は n と x )が同じメモリ領域に確保される.

6.18.3.1.2 共用体のメモリ内でのアロケート 共用体はメモリ内では, もっとも大きなメモリを必要とするフィールドの分だけ確保される. 例えば, Example 6.18.21 の場合は, int と double のメモリサイズ

の大きい方の分だけのメモリが使われ, その整合も各型に沿った方法で行われる.

6.18.3.1.3 共用体の初期化 共用体の初期化は, その最初のメンバーの型の値のみで行うことができる.すなわち, 上の int_double 共用体では, メンバー n を用いた初期化は行えるが, メンバー x を用いた初

期化は行うことができない. つまり,

union int_double x = {1} ;

とすると, x には正しく int 型の 1 という値が入るが,

union int_double x = {1.0} ;

は x には 1.0 を int に変換した 1 が入ることとなる.

6.18.3.1.4 共用体(その他) 共用体の配列, 共用体を構造体メンバーとすること, 共用体メンバーに配列, 構造体を使うことなどはすべて許される.実際に共用体を利用する場面は少ないが, 構造体メンバーとして, Pascal における「可変レコード」のよ

うに, 構造体のメンバーの一部の内容によって, その後のメンバー構成が変る時に利用される他に, 古くはIntel 社の 80286 CPU のレジスタを表現する構造体

union reg {short x ;struct {char h ;char l ;

} y ;}

のような使い方もある. ここで, 80286 CPU は16ビットの演算レジスタを持ち, それぞれ上位バイト, 下位バイトを独立にレジスタとして利用することができた92 . そこで, 16ビットを表すメンバーとして, xを利用し, その上位, 下位ビットを取り出す際には, y.h または y.l を利用するというアクセス方法が実現

できる93 .

6.18.3.2 ビット・フィールド

ビット・フィールドとは, 構造体のフィールドにそのビット数を指定できることである. ただし, そのビット数の合計は1ワードを越えてはならない.

92 これは Intel 社の8ビット CPU である 8086, または, Zailog 社の8ビット CPU である Z80 のコードの互換性を考慮した設計であった. そのため, 80286 のコードは複雑になる傾向が強かった.93 もちろん, このようなアクセスを行って, 最初にメンバー x に値を代入し, メンバー y.h にアクセスして, その上位ビットを取り出すことができる保証は一般にはない. しかし, CPU と処理系を特定することにより, その処理系では演算レジスタへのアクセスを実現することができた.

Id: C8.tex,v 1.11 2001-03-23 14:18:24+09 naito Exp

290 数理解析・計算機数学特論

6.18.3.2.1 ビット・フィールド定義方法

Example 6.18.22

struct flag {unsigned cf:1 ;unsigned :5 ;unsigned zf:1 ;unsigned sf:1 ;unsigned n:8 ;

} a ;

この ビットフィールドは, 次のように参照できる.

a.cf = 1 ;a.zf = 0 ;a.sf = 1 ;a.n = 10 ;

また,

struct bit {unsigned b0:1 ;unsigned b1:1 ;unsigned b2:1 ;unsigned b3:1 ;unsigned b4:1 ;unsigned b5:1 ;unsigned b6:1 ;unsigned b7:1 ;unsigned ubyte:8 ;

} ;union body {struct bit b ;unsigned x ;

} a ;a = 0xFFFF ;

のように一括代入もできる.

ここで, 無名のフィールドはパディング (padding) に利用され, ビット巾 0 のフィールドは次のワード境界に強制的に整合させることができる. ビット・フィールドには & 演算子は適用できない.

6.18.3.2.2 ビット・フィールドのメモリ内でのアロケート ビット・フィールドが宣言順に上位から並

ぶか下位から並ぶかは処理系依存である. 次の図は, Example 6.18.22 が下位ビットから並ぶ時の図である.

flag

15 7 0

sfzf cf

6.18.4 名前空間

識別子は次の4つの名前空間ごとに識別される. 即ち, 異なる名前空間の間では, 同じスコープであっても同じ識別子を利用できる. しかし名前空間が別だからといって, 同じスコープ内での同一識別子の乱用はつつしむべきである.

• 変数, 関数, 引数, 列挙定数, typedef 名.• タグ名.

Id: C8.tex,v 1.11 2001-03-23 14:18:24+09 naito Exp

数理解析・計算機数学特論 291

• (構造体・共用体)メンバー名.• ラベル名.

Example 6.18.23 この例の person はすべて異なる名前空間に属する.

struct person {char person[80] ;int age ;

} person ;

struct person の person は「タグ名」, char person[80] の person は「メンバー名」, “} person”の person は「変数名」である.

このように, 同じ識別子を異なった名前空間で利用することが可能であるが, 混乱の元となるので, やめた方がよい. ちなみに,

typedef struct foo {int x,y } foo ;struct bar {int x,y } bar ;

は似ているが全く異なる内容である. typedef したものは次のように利用可能である.

struct foo a ; /* 構造体タグ名 foo を利用する */foo b ; /* 構造体の型名 foo を利用する */

しかし, 後者は構造体タグ名 bar と変数名 bar を定義している. この時,

struct bar c ; /* 構造体タグ名 bar を利用する */

は許されるが,

bar d ;

は許されないので注意しよう.

6.18.5 演習問題

Exercise 6.18.5 標準入力から, 空白で区切られた学籍番号, ID, 氏名の組を読み込み, それらを構造体の配列として格納し, 標準出力に以下のフォーマット (書式 (format))で出力するプログラムを書け.

学籍番号: xxxxxID : xxxxx氏名 : xxxxx

この際, 入力するデータの数は 100 以下と仮定して良い.

Exercise 6.18.6 Exercise 6.18.5 の問題をリスト形式で書け. この時は, 入力するデータの数はアプリオリにはわからない.

Example 6.18.24 Example 6.18.17のプログラムを元に,単語の出現頻度順で出力を得るように書換えよ.

Example 6.18.25 Example 6.18.17 のプログラムで, 行末が -(ハイフン)の時には, 次の行の先頭の単語と連結して, 単語の出現頻度順, または単語の辞書式順序で出力を得るように書換えよ.

Id: C9.tex,v 1.8 2001-03-23 12:57:19+09 naito Exp

292 数理解析・計算機数学特論

6.19 ファイルへの入出力

ファイルへの入出力とは, プログラム中でファイルからデータを読み取ったり, データをファイルに書き出したりすることをいう. Cでファイルの入出力を行なうには, 実際にファイルを利用する前に, ファイルをオープンし, 利用が終ったらクローズしなくてはならない. ファイルをオープン (open) するとは, システム(OS)に対して, どのようなファイルをどのように利用するかを知らせることであり, ファイルをクローズ (close) するとは, ファイルの利用が終わったことをシステムに通知することである. これらの操作には標準入出力関数と呼ばれる一連の関数を用いる. Cのプログラム中では入出力用のファイルをファイルストリーム (file stream) とよび, 一旦オープンされたファイルは入力用または出力用のバッファ(buffer)と呼ばれる領域を経由してデータがプログラムに渡されたり, プログラムからデータがOSに流れていく.ファイルのオープンに成功すると, プログラムはOSからファイル記述子 (file descripter) と呼ばれる

負でない整数を得ることができる94 . 一方, ファイルをクローズするとは, 実際にはその読み出し, 書き込みなどのためのファイル記述子を再利用可能な状態にし, 書き込みを行ったときには, そのディスク領域を決定するなどの操作が行われる.実際にファイルのオープンやクローズをを行なうには, fopen, fclose 関数を利用する95 . fopen 関数

の戻り値の型は FILE 構造体へのポインタであり, そこにはファイル記述子の他, ファイルへのアクセスのためのバッファなどのメンバーを持つ. FILE 構造体は, 標準ヘッダファイル stdio.h でその構造が定義さ

れている.

Example 6.19.1 実際, a.data というファイルをオープンして, ファイルの内容を char 型変数として読

み込み, それをクローズするには以下のようにしておこなう.

FILE *fp ;int c ;

if ((fp = fopen("a.data","r")) != NULL) {while (!feof(fp)) {c = fgetc(fp) ;putc(c,stdout) ;

}fclose(fp) ;

}

ここで, fopen の第一引数は, ファイル名を表す文字列であり, 第二引数は, ファイルをオープンするモードを表す文字列である96 . また, feof は, ファイルに対する現在の読みとり(書き込み)位置が, EOF(ファイルの終端を表す特別な仮想的な文字)かどうかを判断する関数である.ここで, fgetc 関数の戻り値の型は char ではなく, int であることに注意. もし戻り値の型が char で

あるならば, EOF を表す値(#define EOF (-1) と定義されていることが多い. )と実際の文字とを区別できなくなるからである.また, このファイル中では FILE型へのポインタ fpのための領域を確保していないが, この領域は fopen

関数内で確保され, fclose 関数内で開放される.

また, fopen は指定したファイルがオープンできない時には NULL を返す. fopen 以後は, ファイルへのアクセスは, このポインタを利用する.また, ファイルに出力をする時には, fprintf 関数を利用することが多い. fprintf は, printf とほぼ

同様な利用法をする. すなわち, prinf 関数で94 指定したファイルが存在しない, アクセス権がないなどの場合には, ファイルのオープンに失敗する. この場合, ファイル記述子に対応する値として, 負の値が戻ってくる仕様になっているものが多い.95 Cには「低レベル入出力」と呼ばれる関数群(UNIX の場合には, 実際にはシステムコール)があり, それらはファイル記述子を利用して, 入出力を制御する. fopen や fclose などの標準ファイル入出力関数は, その内部で低レベル入出力システムコールを利用している.96 この場合は, 読みとり専用にオープンしている. 新規ファイルや, ファイルの内容を新しく書き込むためには w を指定したりする. 詳しくは fopen のオンラインマニュアルを参照.

Id: C9.tex,v 1.8 2001-03-23 12:57:19+09 naito Exp

数理解析・計算機数学特論 293

printf(fmt, ....)

としたものを, ファイルポインタ fp で示されるファイルに出力するには,

fprintf(fp, fmt, ...)

とする. printf 関数は標準出力 stdout への fprintf を行っているに過ぎない.これまでに利用してきた標準入出力も, ファイルとして定義されている. それらは, stdio.h に以下のよ

うに定義されている.

• stdin: 標準入力(ファイル記述子 0).• stdout: 標準出力(ファイル記述子 1).• stderr: 標準エラー出力(ファイル記述子 2).

これらは, fopen などを利用せずに使うことができる.ファイルへの入出力を行なう関数としては, fread, fwrite がある. fprintf がテキストのみを出力す

る関数であるのに対して, fwrite はどのようなデータでも出力することができる. 一般に, ファイルからデータを読みとる時には, fread を使うことが多い. どのような形式で格納されているわからないデータを読むには fread を使い, 次のようにする.

Example 6.19.2 a.data というファイルの内容を文字列としてとるには以下のようにしておこなう.

FILE *fp ;char buf[80] ;

if ((fp = fopen("a.data","r")) != NULL) {while (!feof(fp)) {if (fread(buf,1,80,fp)!=0)

fprintf(stdout,"%s",buf) ;bzero(buf,80) ;

}fclose(fp) ;

}

ここで, fread の第一引数は, データを格納する領域, 第二引数は, 読みとるデータの数, 第三引数は最大どれだけのデータを読みとるかである. この時, buf を再利用するため, 出力した後には buf の中を bzero

関数でクリアしている.

また, 読み込むものが文字列とわかっている時には, 関数 fgets を利用するのが望ましい97 .

Example 6.19.3 実際, a.dataというファイルの内容を文字列としてとるには以下のようにしておこなう.

FILE *fp ;char buf[80] ;

if ((fp = fopen("a.data","r")) != NULL) {while (!feof(fp)) {if (fgets(buf,80,fp) != NULL) {

fprintf(stdout,"%s",buf) ;}fclose(fp) ;

}

fread 関数ではストリーム内にあるデータを, その値が何であっても指定のバイト数だけ読み込みを行うのに対して, fgets 関数は, 指定のバイト数だけの文字列を改行文字まで読み込みを行う. すなわち, 現在のファイルポインタ (file pointer) の位置(読み込みを行っているファイル内での位置)から指定された

97 詳しくは, man 3 gets 参照.

Id: C9.tex,v 1.8 2001-03-23 12:57:19+09 naito Exp

294 数理解析・計算機数学特論

文字数(上の例では80文字)以内に改行文字があれば, そこで読み込みを中断する. なお, fgets に良く似た gets という関数もあるが, こちらは, 読み込みの最大データ量を指定できないため, 読み込み領域を越えて読み込みが行われ, データが破壊される原因となるので使わない方がよい98 . したがって, テキストデータの場合の読み込みには fgets 関数が適しているが, バイナリファイルは fread 関数で読み込む必要

がある. 同様に, fprintf 関数で書き出しを行うとテキストデータしか出力は出来ないが, fwrite 関数では, データそのものが書き出される.

Example 6.19.4 b.out というファイルに配列の内容をテキスト形式で書き出す.

FILE *fp ;int a[10] = {0,1,2,3,4,5,6,7,8,9} ;int i ;

FILE *fp ;int a[10] = {0,1,2,3,4,5,6,7,8,9} ;int i ;

if ((fp = fopen("b.out", "w")) != NULL) {for(i=0;i<10;i++) fprintf(fp, "%d = %d\n", i, a[i]) ;fclose(fp) ;

}}

このプログラムの出力結果は,

0 = 01 = 12 = 23 = 34 = 45 = 56 = 67 = 78 = 89 = 9

となる. この出力ファイルのように, fprintf 関数を用いて出力されたテキストファイルは, fscanf 関数で読み出すことが出来る.

FILE *fp ;int a[10] ;int i ;

if ((fp = fopen("b.out", "r")) != NULL) {for(i=0;i<10;i++) fscanf(fp, "%d = %d", &i, &a[i]) ;fclose(fp) ;for(i=0;i<10;i++) fprintf(stdout, "%d = %d\n", i, a[i]) ;

}

fscanf, scanf 関数を使うのは, fprintf, printf 関数で定型のフォーマットで書き出したファイルを読む場合だけである.

Example 6.19.5 b.out というファイルに配列の内容をバイナリ形式で書き出す.

FILE *fp ;int a[10] = {0,1,2,3,4,5,6,7,8,9} ;

if ((fp = fopen("b.out", "w")) != NULL) {fwrite((int *)a, sizeof(int), sizeof(a)/sizeof(a[0]), fp) ;fclose(fp) ;

}98 UNIX のいくつかのアプリケーションに見られる, “buffer overflow” によるセキュリティホールは, fgets を利用すべきところで, 不用意に gets を利用したことに起因するものがある.

Id: C9.tex,v 1.8 2001-03-23 12:57:19+09 naito Exp

数理解析・計算機数学特論 295

このプログラムの出力結果を od コマンドで, od -x として見てみると,

0000000 0000 0000 0000 0001 0000 0002 0000 00030000020 0000 0004 0000 0005 0000 0006 0000 00070000040 0000 0008 0000 0009

となり, 4バイトの整数値が順に書かれていることがわかる. このように fwrite 関数で書き出したバイナ

リファイルは, fread 関数で読み出す.

FILE *fp ;int a[10] ;int i ;

if ((fp = fopen("b.out", "r")) != NULL) {fread((int *)a, sizeof(int), sizeof(a)/sizeof(a[0]), fp) ;fclose(fp) ;

}for(i=0;i<10;i++) printf("%d\n", a[i]);

上の int 型の数値を fwrite 関数で書き出した結果を, 例えば char 型で読み出すと, すなわち,

FILE *fp ;char a[10] ;int i ;

if ((fp = fopen("b.out", "r")) != NULL) {fread((char *)a, sizeof(char), sizeof(a)/sizeof(a[0]), fp) ;fclose(fp) ;

}for(i=0;i<10;i++) printf("%d\n", a[i]);

として読み出すと, a[0] から順に

0 0 0 0 0 0 0 1 0 0

という値が入力される. これは, int 型が4バイトで, little endiean で書かれているため, a[0] から a[3]

までが int の 0 を読み, a[4] から a[7] までが int の 1 を読んだ結果である. このように, バイナリで出力した場合には, どのような型で, どのような順序(little endiean か big endiean か)で出力したかを管理しなければならない.

Example 6.19.6 プログラムの第一引数に与えられたファイル名を持つファイルから, 第二引数に与えられたファイル名を持つファイルにデータをコピーする.

#include <stdio.h>int main(int argc, char **argv){FILE *in, *out ;char buf[1024] ;size_t len ;

if (argc < 2) {fprintf(stderr,"Usage: %s inputfile outputfile\n", argv[0]) ;exit(-1) ;

}

if ((in = fopen(argv[1], "r")) == NULL) {fprintf(stderr,"Could not open file %s\n", argv[1]) ;exit(-1) ;

}if ((out = fopen(argv[2], "w")) == NULL) {fprintf(stderr,"Could not open file %s\n", argv[2]) ;exit(-1) ;

}

while((!feof(in))

Id: C9.tex,v 1.8 2001-03-23 12:57:19+09 naito Exp

296 数理解析・計算機数学特論

&&(len = fread((char *)buf, sizeof(char), sizeof(buf)/sizeof(buf[0]), in))) {fwrite((char *)buf, sizeof(char), len, out) ;

}return 0 ;

}

このプログラムでは, 第二引数に与えられたファイルは w でオープンしているため, 既存のファイルがあっても, それを上書きする99 .

6.19.1 演習問題

Exercise 6.19.1 標準入力から入力されたファイルの行数を印字するプログラムを書け.

Exercise 6.19.2 標準入力から入力されたファイルの行数, 単語の数, 文字数を印字するプログラムを書け.

Exercise 6.19.3 標準入力から入力されたファイルの中の, 数字, 空白, その他の文字の出現頻度を数えるプログラムを書け. ただし, 空白とは, 空白文字, タブ, 改行の3種である.

Exercise 6.19.4 ファイルもしくは標準入力から, 空白で区切られた学籍番号, ID, 氏名の組を読み込み,それらをリスト形式として格納し, 標準出力もしくはファイルにに以下のフォーマットで出力するプログラムを書け.

学籍番号: xxxxxID : xxxxx氏名 : xxxxx

その際, このプログラムを起動する時に与えた引数が読み込みもしくは書き込みのファイルとなるようにしなさい. 具体的には, 以下の通り. (実行コード名を prog とした).

% prog # このときには, 入出力とも標準入出力.% prog - - # このときには, 入出力とも標準入出力.% prog infile # このときには, 入力はファイル, 出力は標準出力.% prog infile - # このときには, 入力はファイル, 出力は標準出力.% prog infile outfile # このときには, 入出力ともファイル.

Exercise 6.19.5 ファイルもしくは標準入力から入力されたテキストファイルの行数, 文字数を標準出力に出力するプログラムを書け. その際, このプログラムを起動する時に与えた引数が読み込みもしくは書き込みのファイルとなるようにしなさい. 具体的には, 以下の通り. (実行コード名を prog とした).

% prog # このときには, 標準入力.% prog - # このときには, 標準入力.% prog infile # このときには, 入力はファイル.

6.20 プリプロセッサ命令

C 言語の文法の最後として, プリプロセッサ命令を解説する.これまで, #include などを利用してきたが, これら, # からはじまる文は, コンパイラ以前に, プリプロ

セッサといわれるものが解釈をして, 必要な処理を行なった後コンパイラに渡される.プリプロセッサ命令として, 代表的なものは以下の通りである.

• #include 指定したファイルを取り込む.• #define この後に続く一つのトークンをそれ以後のトークンに置き換える. もしくは,(トークンが一つの時)そのトークンが定義されたと宣言する.

99 もし, 上書きをしたくない場合. すなわち, 同じファイル名(パス名)をもつファイルが存在する場合に何かの警告を出したいときには, stat システムコールなどで, そのパス名に対応するファイルの情報を得る必要がある.

Id: C9-1.tex,v 1.5 2001-03-22 22:36:20+09 naito Exp

数理解析・計算機数学特論 297

• #ifdef と #endif の対. #ifdef で指定されたマクロ変数が定義されている場合にのみ, #endif までのプログラム列を有効にする.

例えば,

#define MAX 100

は MAX を 100 に置き換える.

Example 6.20.1 #define 命令の例は次の通りである.

#define MAX 100int a[MAX] ;int main(int arc, char **argv){int i ;for(i=0;i<MAX;i++)a[i] = i ;

}

ここで,

#define MAX 100 ;

とするのは間違いである.

また,

#define UNIX

は UNIX というトークンが定義されていると宣言する. これは, 以下のようにして利用されることがある.

#ifdef UNIX....

#else....

#endif

これは, UNIX が定義されている時と, 定義されていない時にそれぞれどの部分をコンパイルするかを分岐している. これは, 移植性を高めるために用いられることが多い.最も標準的に利用されるマクロ定義は,

#define ARRAY_MAX /* 配列の最大値 */int a[ARRAY_MAX]

for(i=0;i<ARRAY_MAX;i++) a[i] = i ;

などと, 配列の大きさ, 文字列の長さなどを定義するために用いられるものである. しかし, この時に利用するマクロ名として, 安易に MAX 等とするのはやめよう. 何の MAX なのかすぐにわからなくなってしまう

ことがある.また, 複雑なマクロとしては, 次のようなことができる.

#define max(A,B) ((A) > (B) ? (A) : (B))

この時,

x = max(p+q,r+s)

は,

x = ((p+q) > (r+s) ? (p+q) : (r+s))

に置き換えられる. このようなマクロが有効であるのは, その引数がどのような型であっても良いからであるが, 一方,

Id: C9-1.tex,v 1.5 2001-03-22 22:36:20+09 naito Exp

298 数理解析・計算機数学特論

max(i++,j++)

などとすると, 副作用が影響するので, 期待した動作をしないことがある.また, マクロの引数の直前に # がつくと, 対応する引数は " で囲まれ, # とパラメータ識別子は引数に置

き換えられる. 置き換えられた結果, 文字列が並ぶときにはそれらは連結される. したがって,

#define PR(fmt,val) printf(#val " = %" #fmt "\t", (val))

で PR(d,x1) とすると,

printf("x1 = %d\t", (x1))

と展開される.また, マクロ定義中に ## があると, パラメータ置換後に, 両側の空白文字とともに ## が削除され, 隣接

するトークンが連結される. しかし, これら # ## 演算子は, 展開の再スキャンの際に現れても置換されない. したがって,

#define cat(x,y) x ## y

に対して cat(var,123) を行うと var123 が現れるが, cat(cat(1,2),3) は不定となる. これは, 一度めの呼び出しの後の cat( 1 , 2)3 が正しいトークンを含まないからであり, これを避けるには, さらに

#define xcat(x,y) cat(x,y)

とし, xcat(xcat(1,2),3) とすることにより, 123 を得ることができる. これは, xcat が ## を含まない

ことによる.マクロの詳しい内容については, [2, 4.11, A12] を参照.

6.21 落ち穂拾い

ここでは, これまでに述べることが出来なかった重要な注意点などを列挙しておこう.

6.21.1 最適化について

C コンパイラでは, プログラムの最適化が行われることが多い. 一般に最適化とは, そのプログラムの実行時間, または必要なメモリ量, またはその両方を短縮または少量で済むように, 実行コードを作成することである.例えば, 次のプログラムを考えてみよう.

int main(){int i=1,j ;

i = 0 ;j += i ;

}

このプログラムのアセンブル結果(主要部分のみ)は,

mov 1, %o0st %o0, [%fp-20]st %g0, [%fp-20]ld [%fp-24], %o0ld [%fp-20], %o1add %o0, %o1, %o0st %o0, [%fp-24]

Id: C10.tex,v 1.7 2001-03-24 16:39:01+09 naito Exp

数理解析・計算機数学特論 299

となり, 実際に変数に値が代入されていることがわかる. しかし, 現実にはそれらの変数は代入, 演算後何も利用されていないので, 最適化を行ってアセンブルすると,

nop

となり, 実際には何も実行されないようなコードが出力されることがわかる.このように C では処理系が実行速度の最適化やメモリ利用効率の最適化を行う. この最適化の方法によ

り実行結果が異なるようなプログラムを書いてはならない. 最適化の方法により実行結果が異なることは,オブジェクトのメモリ内での配置の様子を仮定したり, 文法上は不定となっている, 演算の結合規則, 評価順序などを仮定してしまうことが原因となることが多い.

6.21.2 コメント

C では /* から */ までのプログラム部分は, コメント(注釈)として扱われ, プリプロセッサによってコンパイラに渡される前に取り除かれる100 . C ではコメントは入れ子に出来ないので,

/* /* これはテスト */ここは取り除く */

となっていると, 最初の /* を見つけたあと, 次に見つかる */ までコメントと扱われ, 2行めはコメントとならないので, 構文エラーとなる. また, ポインタ参照を利用した演算式

int *p, *q ;*p/*q

とすると, /* の部分がコメントの始まりとみなされるので注意すること.一部の本には C ではコメントとして, 行頭に // をおけば良いと書いてあるが, これは C++ の流れを

受けたもので, 正式な ANSI の規格ではないことに注意しよう. すなわち, gcc のように C++ コンパイラとしても利用できる処理系では, これをコメントとみなすことがありうるが, これをコメントとみなさない処理系も多い.プログラムをデバッグする際には変数の値の出力文を書くことが多く, その後それを消去したくない場

合には, その部分をコメントにしてしまうことが多い. 例えば,

for(i=0;i<10;i++) {j += i ;/* これは debug 用 *//* printf("j = %d, i = %d\n", j,i) ; */

}

などとする. しかし, プログラムの完成時にはこれは見苦しくなるので,

for(i=0;i<10;i++) {j += i ;

#ifdef DEBUGprintf("j = %d, i = %d\n", j,i) ;

#endif /* DEBUG */}

としておき, デバッグ時には

#define DEBUG

をつけておく方法がある. これは, すべての DEBUG 部分を一度に制御できるため, デバッグ用の文のとり忘れがなく, 便利である.

[8, Section 8.3] では, この方法は奨励されておらず,

100 プリプロセッサ終了直後のコードの様子は, gcc -E test.c とすることで見ることが出来る.

Id: C10.tex,v 1.7 2001-03-24 16:39:01+09 naito Exp

300 数理解析・計算機数学特論

int DEBUG=1 ;for(i=0;i<10;i++) {j += i ;if (DEBUG)printf("j = %d, i = %d\n", j,i) ;

}

または,enum {DEBUG=1} ;for(i=0;i<10;i++) {j += i ;if (DEBUG)printf("j = %d, i = %d\n", j,i) ;

}

という方法が紹介されている. #ifdef による「条件付きコンパイル」は, 条件を変更することにより, コンパイルに失敗する場合が考えられる. すなわち, コンパイラのチェックを受けないコードが存在する. そのようなことを避けるために, [8] では条件付きコンパイルを推奨していない. しかし, 多くのフリーウェアなどでは, 多様なプラットフォームに対応するコードを記述するため, 条件付きコンパイルが行われている. これは, Makefile からコンパイラに条件を渡すことが可能であるため, 多様なプラットフォーム上でのコンパイルが容易になるというメリットを採用しているためである.

6.21.3 実行時エラー

C で作成したプログラムなどを実行する際に,Segmentation faultBus errorFloating exception

などというエラーが発生することがある.

6.21.3.1 Segmentation fault

Segmentation fault というエラーは, 以下のように割り当てられていないメモリ領域に対するアクセスがあった場合に, 「アクセス違反」として発生する.

/* Segmentation fault を起こす* 意味:非割り当てメモリへのアクセス.*/

#include <stdlib.h>#include <stdio.h>int main(int argc, char **argv){struct data {char x ;struct data *next ;

} *data_p, *p ;

data_p = (struct data *)malloc(sizeof(struct data)) ;data_p->x = ’1’ ;p = data_p ;printf("%c\n",p->x);p = p->next ;printf("%c\n",p->x);/* ここで割り当てられていないメモリにアクセスしている */return 0 ;

}

Id: C10.tex,v 1.7 2001-03-24 16:39:01+09 naito Exp

数理解析・計算機数学特論 301

6.21.3.2 Bus error

Bus error というエラーは, 以下のように, ワード境界に合わせられていないアドレスから, ワード単位で読み取りを行おうとした場合などに発生する.

/* Bus error を起こす* 意味:適切に境界を合わせていないアドレスからデータを読み取ろうとした.* 原因:* ハーフワード, ワード, ダブルワードの境界に合わせられていないアドレスから,* それぞれ2バイト, 4バイト, 8バイトを読み取った.*/

#include <stdlib.h>int main(int argc, char **argv){char *s = "hello world" ;int *i = (int *)&s[1] ;int j ;

j = *i ;return 0 ;

}

6.21.3.3 Floating exception

Floating exception エラーは, 0 での除算を行おうとすると発生する.

/* 0 で除算を行う. */#include <stdio.h>int main(int argc, char **argv){printf("%d\n",1/0) ;return 0 ;

}

これの代りに

printf("%f\n",1.0/0.0) ;

とすると, Inf という答えが返ってくる.

6.21.3.4 実行時エラーのトラップ

UNIX では上のような実行時エラーはカーネルによって検出され, カーネルからプロセスに対してシグナル (signal) を送ることによって, プロセスはエラーの発生を知ることが出来る. C では, 標準ライブラリ関数 signal を利用することによって, 受け取ったシグナルの種類ごとにそのハンドラ (handler) を記述することが可能になっている. 上のそれぞれの実行時エラーに対して, プロセスが受け取るシグナルは,

• segmentation fault に対しては SIGSEGV,• bus error に対しては SIGBUS,• floating exception に対しては SIGFPE

と定められている101 . したがって,

/* 0 で除算を行う.実行時エラーをトラップする */

#include <stdio.h>#include <signal.h>101 これらのシンボルは signal.h 内で定義される整数定数である.

Id: C10.tex,v 1.7 2001-03-24 16:39:01+09 naito Exp

302 数理解析・計算機数学特論

extern int signal_handler(void) ;int main(int argc, char **argv){signal(SIGFPE, (void (*)(int))signal_handler) ;printf("%d\n",1/0) ;return 0 ;

}

int signal_handler(void){fprintf(stderr,"Floating exception が発生したので, 実行を停止します\n") ;exit(-1) ;return ;

}

として, ハンドラを記述すれば, 実行時エラーに対して, 適切な処理を行うことも可能である.

6.21.4 ライブラリ呼出しとシステムコール

これまでに各種の標準関数を利用してきたが, それらのほとんどはライブラリ関数の呼出しという手順で行われていた. これに良く似た概念でシステムコールと呼ばれるものが UNIX 上では存在する102 . 例えば, ファイルをオープンする関数として fopen があるが, この関数内では実際にはシステムコール open

が用いられている. また, C のプログラム内からファイルを削除するためには, unlink システムコールが用いられる.このように, C のプログラム内から呼び出すことが出来る関数として, ライブラリコールとシステムコー

ルの2種類があることがわかる. ここでは, この2つの違いを簡単にまとめておこう.

• ライブラリコールは ANSI 規格で定められ, すべての処理系でその呼出し方法は同一であるが, システムコールは, OS によって異なる呼出し方法が異なる.

• ライブラリコールは, ライブラリ内にあるサブルーチンの呼出しであるが, システムコールは, サービスを受けるためのカーネル呼出しである.

• ライブラリコールは, プロセスのアドレス空間で実行されるが, システムコールは, カーネルの空間で実行される.

• 時間測定では, ライブラリコールは,「ユーザ」時間になるが, システムコールは,「システム」時間になる.

• ライブラリコールは呼出しに時間が掛らないが, システムコールは, 呼出しのオーバヘッドが大きい.

このように, 一見似ているが, ライブラリコールとシステムコールはその役割が異なり, 処理系に依存するシステムコールの部分を, ライブラリ関数によって吸収するという意味がある.

6.21.5 ANSI で定められた翻訳の最低基準

最後に, ANSI で定められた, 処理系に求められている最低基準を列挙しておこう. これらに挙げる数値は翻訳限界と呼ばれ, 「各限界の出現をそれぞれ少なくとも一つ含むプログラムのうち少なくとも一つを翻訳および実行できなければならない」と定められている.

• 複合文, 繰り返し制御構造および選択制御構造に対する入れ子のレベル数 (15)• 条件付き取り込みにおける入れ子のレベル数 (15)• 一つの宣言中の一つの算術型, 構造体型, 共用体型または不完全型を修飾するポインタ, 配列および関数宣言子(の任意の組み合わせ)の個数 (12)

• 一つの完全宣言子における括弧に囲まれた宣言子の入れ子のレベル数 (21)

102 MS-DOS では, これに相当するのは BIOS コールと呼ばれるものがある.

Id: C10.tex,v 1.7 2001-03-24 16:39:01+09 naito Exp

数理解析・計算機数学特論 303

• 一つの完全式における括弧に囲まれた式の入れ子のレベル数 (32)• 内部識別子またはマクロ名において意味のある先頭の文字数 (31)• 外部識別子において意味のある先頭の文字数 (6)• 一つの翻訳単位における外部識別子数 (511)• 一つのブロックにおけるブロック有効範囲を持つ識別子数 (127)• 一つの翻訳単位中で同時に定義されうるマクロ識別子数 (1024)• 一つの関数定義における仮引数の個数 (31)• 一つの関数呼出しにおける実引数の個数 (31)• 一つのマクロ定義における仮引数の個数 (31)• 一つのマクロ呼出しにおける仮引数の個数 (31)• 一つの論理ソース行における文字数 (509)• (連結後の)単純文字列リテラルまたはワイド文字列リテラル中における文字数 (509)• (ホスト環境の場合)一つのオブジェクトのバイト数 (32767)• #include で取り込まれるファイルの入れ子のレベル数 (8)• 一つの switch 文(入れ子になった switch 文を除く)中における case 名札の個数 (257)• 一つの構造体または共用体のメンバ数 (127)• 一つの列挙体における列挙定数の個数 (127)• 一つのメンバ宣言並びにおける構造体または共用体定義の入れ子のレベル数 (15)

これらの翻訳限界を越えたプログラムは, 必ずしも他の処理系で翻訳できるとは限らないことに注意しよう.また, 標準ヘッダファイル limits.h には, 各算術型で格納できる限界の数が書かれている. ここでは,

そのマクロ名と, ANSI 規格に定められた最低限の数値を書いておく.

• ビットフィールドでない最小のオブジェクト(バイト)におけるビット数CHAR_BIT 8

• signed char のオブジェクトにおける最小値SCHAR_MIN -127

• signed char のオブジェクトにおける最大値SCHAR_MAX +127

• unsigned char のオブジェクトにおける最大値UCHAR_MIN 255

• char のオブジェクトにおける最小値と最大値CHAR_MIN CHAR_MAX

char のオブジェクトの値を符号付き整数として扱う場合, CHAR MIN の値は, SCHAR MIN と同じであ

り, CHAR MAX の値は, SCHAR MAX と同じでなければならない.その他の場合, CHAR MIN の値は 0 でなければならず, CHAR MAX の値は UCHAR MAX と同じでなければ

ならない.• サポートするロケールに体する多バイト文字の最大バイト数MB_LEN_MAX 1

• short int のオブジェクトにおける最小値SHRT_MIN -32767

• short int のオブジェクトにおける最大値SHRT_MAX +32767

• unsigned short int のオブジェクトにおける最大値USHRT_MAX 65535

• int のオブジェクトにおける最小値INT_MIN -32767

• int のオブジェクトにおける最大値

Id: C10.tex,v 1.7 2001-03-24 16:39:01+09 naito Exp

304 数理解析・計算機数学特論

INT_MAX +32767

• unsigned int のオブジェクトにおける最大値UINT_MAX 65535

• long int のオブジェクトにおける最小値LONG_MIN -2147483647

• long int のオブジェクトにおける最大値LONG_MAX +2147483647

• unsigned long int のオブジェクトにおける最大値LONG_MAX 4294967295

この他にも ANSI 規格には float.h 内で定める, 浮動小数点型の特性も定められている.

References

[1] B. W. Kernighan and D. M. Ritchie. プログラミング言語C. 共立出版, 1981.

[2] B. W. Kernighan and D. M. Ritchie. プログラミング言語C(第2版). 共立出版, 1989.

[3] 日本規格協会. JISハンドブック(情報処理-プログラム言語編). 日本規格協会.

[4] ANSI. ANSI C Rationale. Silicon Press, 1990.

[5] ANSI. ANSI C Rationale. ftp://ftp.uu.net/doc/standards/ansi/X3.159-1989, 1989.

[6] P. van der Liden. エキスパートCプログラミング. アスキー出版局, 1996.

[7] N. Wirth. アルゴリズム+データ構造=プログラム. 日本コンピュータ協会, 1979.

[8] B. W. Kernighan and P. J. Plauger. プログラミング作法. アスキー出版局, 2000.

[9] B. W. Kernighan and D. M. Ritchie. The C Programing Language (2nd Ed.). Addison-Wesley,1988.

[10] B. W. Kernighan and P. J. Plauger. プログラム書法(第2版). 共立出版, 1982.

[11] B. W. Kernighan and P. J. Plauger. ソフトウェア作法. 共立出版, 1982.

[12] B. W. Kernighan and P. J. Plauger. Software Tools in Pascal. Addison-Wesley, 1981.

[13] A. R. Feuer and N. Gehani. Ada, C, Pascal. 工学社, 1981.

[14] S. Oualline. C実践プログラミング(第3版). オライリー・ジャパン, 1998.

[15] N. Wirth. アルゴリズムとデータ構造. 近代科学社, 1990.

[16] A. R. Feuer. The C Puzzle Book (Revised Edition). Addison-Wesley, 1999.

[17] S. McConnel. Code Complete. アスキー出版局, 1994.

[18] E. Post. Real programmers don’t use Pascal.http://www.mit.edu/people/rjbarbel/Humor/Computers/real.programmers, 1982.

Id: C.tex,v 1.4 2001-03-22 22:29:22+09 naito Exp

数理解析・計算機数学特論 305

第7章 Make

7.1 make とは

C などの言語では, ソース・コードが複数のファイルに分割され, それらをリンカによってリンクすることで実行コードを作成することができる. その際, どれか一つのソース・ファイルを修正した場合に, すべてのソース・ファイルをコンパイルし, リンクしなおすことは非効率的である. そこで, 各ファイルの依存関係を適切に記述することによって, 目的のコードを生成するために必要な最小限の作業を行わせるためのツールが make である.ここでは C で記述されたプログラムのコンパイルを行う例を述べているが, make は Makefile で指定

されたファイルの依存関係から, 必要な操作を行うためのソフトウェアであるので, 必ずしも C 言語のプログラムファイルだけを対象にしているわけではない. なお, make, Makefile に関しては, [1] が詳しいので, 必要であれば参照すること.

7.2 make の利用法

make を利用するには, 実行コードを生成するための手順と依存関係を記述した Makefile を作成する.

7.2.1 分割コンパイル時のファイルの依存関係

ここでは,

• file1.c

• file2.c

という2つのソース・コードから実行コード ex_code を生成するための手順を考えてみよう. そのためには次の3つのステップが必要である.

1. file1.c から file1.o というオブジェクト・コードを生成する.2. file2.c から file2.o というオブジェクト・コードを生成する.3. file1.o と file2.o をリンクし, ex_code を生成する.

もちろん, 1 と 2 のステップは逆に行っても良い.すなわち, これら file1.c, file2.c, file1.o, file2.o, ex_code の間の依存関係は,

• ex_code は file1.o と file2.o に依存している.• file1.o は file1.c に依存している.• file2.o は file2.c に依存している.

と読み取ることができる. また, 1, 2, 3 の各ステップを実現するためのコマンドは,

1. gcc -c file1.c

2. gcc -c file2.c

Id: make.tex,v 1.7 2001-03-12 19:18:34+09 naito Exp

306 数理解析・計算機数学特論

3. gcc -o ex_code file1.o file2.o

である. (ここで, 余分なライブラリ, コンパイル・オプション等は省略した.)

7.2.2 Makefile の記述

上で調べた依存関係を適切に記述するための Makefile の書式は次の通りである1 .

macro=value

target: dependencycommand

ここで, target とは, make が実行する対象となるコマンドの名称であり, dependency とは, その target

を生成するために依存しているターゲットまたはファイルを表す. target を生成するために必要なコマンドを記述した部分が command であり, command は(ある例外を除き) Tab����で始まらなければならない. また, macro とは, target, dependency, command 内で利用可能な, 置換対象となる文字列を定義するものである.

Example 7.2.1 上で調べた依存関係を記述する Makefile は以下の通りである.

all: ex_code

ex_code: file1.o file2.ogcc -o ex_code file1.o file2.o

file1.o: file1.cgcc file1.o -c file1.c

file2.o: file2.cgcc file2.o -c file2.c

このような Makefile を作成し, make というコマンドを実行すると, そのときの依存関係に応じたコマンドが実行される. すなわち, ex_code よりも file1.c が新しい場合, file1.o を作成し, file1.o とfile2.o から ex_code を作成するコマンドが実行される.ex_codeが file1.c, file2.cよりも新しい場合には,コマンドは何も実行されない. すなわち, ex_code

は file1.o と file2.o に依存し, file1.o は file1.c に, file2.o は file2.c に依存するという関係

が記述できたことになる.

make は引数に target を指定するが, 何も指定しない場合, Makefile の最も始めにある target を実行

する.ところが, XXXX.c から XXXX.o を生成するための手順は, 必ず

gcc -c XXXX.c

という書式である. したがって, 上の Makefile をもっと単純に書き直すことができる.

Example 7.2.2  

1 極めて単純化してあり, ここで説明するために必要のない部分は省いてある.

Id: make.tex,v 1.7 2001-03-12 19:18:34+09 naito Exp

数理解析・計算機数学特論 307

SRCS = file1.c file2.cOBJS = ${SRCS:.c=.o}all: ex_code

ex_code: ${OBJS}gcc -o $@ ${OBJS}

file1.o: file1.cgcc $@ -c file1.c

file2.o: file2.cgcc $@ -c file2.c

ここで, ${SRCS} はマクロによって, file1.c file2.c という文字列に置換され, ${OBJS} を定義するマクロは, ${SRCS} の .c を .o に置換することで定義されている. したがって, ex_code の dependency

は file1.o file2.o という文字列に置き換えられている. また, $@ というマクロは, そのときの target

を表す特殊なマクロである.

ところが, これでも各 C のソース・ファイルからオブジェクト・コードを生成する手順を書くのが面倒である. そこで, make コマンドには「サフィックス・ルール」, すなわち, .c という拡張子を持つファイルから.o という拡張子を持つファイルを作成するための, 暗黙の依存関係と暗黙のコマンドが定義されている.

Example 7.2.3  

SRCS = file1.c file2.cOBJS = ${SRCS:.c=.o}all: ex_code

ex_code: ${OBJS}gcc -o $@ ${OBJS}

ここでは, サフィックス・ルール

.c.o:$(COMPILE.c) $(OUTPUT_OPTION) $<

を暗黙のうちに利用している.

7.2.3 マクロ

Makefile 中で特別な意味を持つマクロには $< や $@ の他に, 以下のようなものがある.

$? 現在処理中のターゲットよりも後で変更されたコンポーネントのリスト.サフィクスルール中では利用できない.

$@ 現在処理中のターゲット名.$$@ 現在処理中のターゲット名. 依存関係行の中で利用する.$< 現在処理中のターゲットよりも後で変更されたコンポーネント.

サフィックスルールと .DEFAULT エントリ内でのみ利用可能.

Example 7.2.4 Example 7.2.2 は以下のように書き直すこともできる.

Id: make.tex,v 1.7 2001-03-12 19:18:34+09 naito Exp

308 数理解析・計算機数学特論

SRCS = file1.c file2.cOBJS = ${SRCS:.c=.o}CC = gccall: ex_code

ex_code: ${OBJS}${CC} -o $@ ${OBJS}

file1.o: file1.c${CC} $@ -c $?

file2.o: file2.c${CC} $@ -c $?

この例では, 暗黙のルールは利用していない.

また, -D マクロを用いて, C のプログラム内にある #define マクロの値を定義することもできる. さらに, シェルの環境変数で与えられた値は Makefile 中にマクロとして利用することが出来る.

7.2.4 オプション

なお, make はコマンドにエラーが発生すると, そこで実行を中止するが, -k オプションを与えると, エラーが発生しても実行を継続させることができる. また, -n オプションを与えると, 実際にはコマンドを実行せず, その時点での依存関係を判断して, どのようなコマンドが実行されるかだけを表示させることができる.

7.2.5 Makefile 中に記述するシェルコマンド

Makefile 中には各種のコマンドが記述できるが, 良く使われるものとしては,

clean:/bin/rm -f *.o *~

というターゲットである. これは, *.o, *~ を消去する. このようなコマンドの前に @ を書くと, そのコマンドは make 実行中に標準出力へは出力されない.

clean:@/bin/rm -f *.o *~

また, シェルのコマンド列を以下のように Makefile 中に記述することも出来る.

clean:for j in ${OBJS} ; do \

/bin/rm -f $${j} ; \done

これは, 以下のシェルのコマンド列を記述したもので, \ を用いて, make から起動されたサブ・シェルへ1行で渡していることに注意.

for j in file1.o file2.o ; do/bin/rm -f $j

done

7.2.6 Makefile の最終的な例

ここでは, これまでの Example や解説をまとめて, 上の依存関係を表す Makefile を書いてみよう.

Id: make.tex,v 1.7 2001-03-12 19:18:34+09 naito Exp

数理解析・計算機数学特論 309

SRCS = file1.c file2.cOBJS = ${SRCS:.c=.o}CC = gccCFLAGS = -DDEFINE_STR=XXX -O

all: ex_code

ex_code: ${OBJS}${CC} -o $@ ${OBJS}

clean:@/bin/rm -f *.o *~

ここでは, C コンパイラに対して

-DDEFINE_STR=XXX -O

というオプションを渡している.

References

[1] A. Oram and S. Talbott. make (改訂版). オライリー・ジャパン, 1997.

Id: Make.tex,v 1.1 2001-03-20 15:36:20+09 naito Exp