Comments
Description
Transcript
第6章 C 言語入門
数理解析・計算機数学特論 225 第 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 の方が望ましいと考えられている. C1.tex,v 1.17 2003-04-20 13:58:01+09 naito Exp 数理解析・計算機数学特論 226 多くのプログラムソースを書いた時に, それぞれのファイルがどのような内容のものかがわからなくなるこ とが良くある. また, プログラムソースを見やすくするために, 「字下げ」をきちんとすること. Emacs で XXX.c とい Tab を押すことにより行なう. これだけで Emacs うファイルを編集する際の「字下げ」の方法は各行頭で が状況に応じて適当に処理してくれる2 . 6.4 C のプログラムの書き方・実行の方法 講義で利用する処理系は gcc とよばれるコンパイラである. 実行コードを作成するには以下の手順で行 なう. 1. エディタ (Emacs など) を利用して, プログラムソースを書く. 2. コンパイラを起動して, 実行コードを作成する. 3. 作成した実行コードを実行する. 例えば, Emacs を利用して, hello.c というプログラムソースを作成した時, これをコンパイルするには, 以下のようにする. % gcc hello.c -o hello 最後の -o hello という部分は, 実行コードを hello という名前で作成することを指示している. もし, -o hello という部分を省くと, コンパイラは a.out という名前で実行コード3 を作成する. ここで作成した hello を実行するには % ./hello とする. ここで, ./ をわざわざ指定していることに注意せよ4 . 6.4.1 C のプログラムのコンパイル方法の詳細 gcc を利用して ANSI 規格の C 言語のプログラムをコンパイルする時には, 単に % gcc hello.c -o hello とするだけではなく, gcc のオプションをより詳しく設定すべきである. 具体的には, % gcc -Wall -ansi -pedantic-errors -O hello.c -o hello ここで, 新しく付け加えたオプションの意味は次の通りである. -Wall 文法エラーではない「警告」を軽微なレベルまで出力する. -ansi ANSI 規格の C としてコンパイルする. -pedantic-errors ANSI 規格の C 以外のコードをエラーとして扱う. 2 これは emacs の “c-mode” の特徴である. UNIX 上の処理系では, コンパイラが出力する実行コードのデフォールトの名前は a.out になる. これは, Assembra Output の略. 最近の LINUX, FreeBSD では elf という名前になるものがある. これら実行形式の名前の違いは, 実行形式の違い でもある. 4 UNIX ではカレントディレクトリはデフォールトではコマンドサーチパスには入っていないし, 入れない方が望ましい. 3 多くの C1.tex,v 1.17 2003-04-20 13:58:01+09 naito Exp 数理解析・計算機数学特論 227 -O オブジェクトコードの最適化を行う. これらのオプションの詳細については, Section 6.21 を参照せよ. Exercise 6.4.1 test.c というプログラムを % gcc test.c -o test として, 作成して, % test とすると, どのようなことが起こるか. それは何故か? より高度なプログラムを書く場合には, 複数のファイルからなるプログラムを作成することがある. その ような場合には, 一度にコンパイルすることはできないので, それぞれのファイルをコンパイルして, リン クと呼ばれる操作で実行コードを作成する. 6.5 6.5.1 C 言語の基本的な注意 C の基本的な構造 C のプログラムは, コメント (注釈) (comment), プリプロセッサ命令 (preprosessing), 文 (statement), 関数 (function) の集まりで構成されている. それぞれの文は, ; で終るか, { } で囲まれた文の集 まりである複文 (compound statement) で構成されている. 関数自身もまた文である. 実際には, main という名前の特別な関数がはじめに実行され, そこに記述されている順序にしたがって実行される. 6.5.2 C で利用できる文字 C では, 英文字, 数字, 空白文字(スペース, タブなど), 記号文字, 改行文字などが利用できる. 記号文 字には特別な意味があることが多いので, 注意すること. また C では, 大文字と小文字は区別される. C の コンパイラにおいて, 日本語が利用できるといっても, 変数名などに日本語が利用できるわけではないので 注意すること. 日本語が利用できるのは, 文字列に日本語が利用できるという程度の意味であり, 今回利用 する処理系では, このコードは EUC でなければならない. また, 以下のものは全て空白と見なして無視される. • 空白文字, 改行文字, タブ, 改ページ記号, コメント. 行末に \ を書くと, 行の連結を表し, 1 行として扱われる. 6.5.2.1 行 C 言語では「行」という概念は存在しない. 改行文字は空白文字と見なされるが, 改行文字の直前に \ が ある場合には, 行の連結を表し, 処理系によって \ と改行文字の連続は一つの空白文字に置き換えられる. C1.tex,v 1.17 2003-04-20 13:58:01+09 naito Exp 数理解析・計算機数学特論 228 6.5.3 コメント コメント (comment) とは, プログラミングの補助となるようソースプログラム中に書かれた注釈部分 のこと. コンパイラは単にこれを無視するので, プログラムには影響を与えない. コメントは, /* ではじ まり, */ で終り, 入れ子にはできない. すなわち, コメントの中にコメントを入れることはできない. また, 文字定数, 文字列の中にはコメントを書くことはできない.5 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) を表す. 浮動小数点定数は, 以下の形をしている. 整数部 . 小数部 e 指数 接尾子 “e 指数” 部分は省略することができる. また, 指数の記号 e は E を用いても良い. 接尾子は以下のいず れか. • f または F: float 型 • 接尾子なし: double 型 5 コメント文の説明として, 「// から始まり, その行末まではコメント文」であると記述してあるものがある. これは ANSI の規 格では誤りである. C++ の ANSI 規格ではこれをコメント文としていて, gcc は C++ のコンパイラでもあるので, -ansi オプショ ンをはずしてコンパイルすると, そのまま通ってしまう. C1.tex,v 1.17 2003-04-20 13:58:01+09 naito Exp 数理解析・計算機数学特論 229 • l または L: long double 型 文字定数とは, ’ で括られた文字の列である. 例えば a という文字を表すには ’a’ と書く. 以下の特別な文字を表す以外には, 文字定数は一文字でなくてはならない. \n \f 改行 \r \t 復帰 \v \a 垂直タブ \b \? 後退 \’ \\ ’ \ \" " \ooo 8進数で ooo. 3桁以下 \xhh 16進数で hh. 2桁以下 改ページ ベル 水平タブ ? このような特別な文字のことをエスケープ文字 (escape charactor) と呼ぶ. 6.5.4.2 文字列リテラル 文字列定数とも呼ばれ, " で囲まれた文字の列である. 文字列リテラル6 (string literal) が記憶域に格納 される時には, 末尾に \0 が付けられる. また, 隣接する2つ以上の文字列リテラルは連結される. 例えば "abc" "ABC" は "abcABC" となる. また, "abc" "ABC" は "abcABC" に連結される. 6.5.4.3 識別子 識別子 (identifier) とは, 変数, 関数などに付けられる名前のことである. ここで与えられた名前にした がって, コンパイラはそれぞれを区別する. 識別子として使える文字は, 英文字, 数字, _ であって, 数字を先頭にすることはできない. また, C 言語 の規約によって, 31 文字までは区別され, 大文字と小文字は区別される7 . 即ち, 32 文字目以後が異なるよ うな2つの名前は区別されるとは限らない. また, C 言語には名前空間, スコープという概念があり, 同じ名前を与えても, 違うものを示しているこ とがあるので注意すること. これについては後ほど解説する. 6.5.4.4 キーワード 以下の単語はキーワード (keyword) と呼ばれ, 特別な意味を持ち, 識別子としては利用できない. 6 「リテラル」 7 正確には, (“literal”) とは, 「文字通りの」という意味である. • 内部識別子またはマクロ名においては意味のある先頭の文字数は 31 文字であり, 大文字と小文字が区別される. • 外部識別子においては意味のある先頭の文字数を 6 文字に制限して良く, 大文字と小文字の区別を無視しても良い. というのが ANSI の規格 [3, X3010 6.1.2, p. 1856] である. しかし, 最近の処理系で外部識別子が6文字に制限されたり, 大文字と 小文字の区別を無視するようなものは見当たらない. C1.tex,v 1.17 2003-04-20 13:58:01+09 naito Exp 数理解析・計算機数学特論 230 char double int long enum float short unsigned static signed volatile void union typedef struct auto const register sizeof extern if else while switch do case continue default goto break return for 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 の値 */ */ */ C1.tex,v 1.17 2003-04-20 13:58:01+09 naito Exp 数理解析・計算機数学特論 231 printf("Hello World.\n") ; /* 値はこの関数の戻り値 a = b = c ; a = b, c = d ; /* 値は代入された a の値 /* 値は最後に代入された c の値 a < b ; /* 値は真ならば 1, 偽ならば 0 */ 6.6 6.6.1 */ */ */ 用語 処理系とその動作 C において, 処理系 (implementation) とは, 特定の環境中の特定のオプションの下で, 特定の実行環境 用のプログラムに翻訳を行うソフトウェア群を指し, 処理系依存 (implementation-defined behavior) とは, その処理系ごとにどのように振舞いかが規定されているものである. 一方, 不定(または未定義) (undefined behavior) とは, 同じ処理系であっても, その振舞いが規定されていないものを指す. 特に, 最適化(オプティマイザ (optimizer))の指定によって振舞いが変わることが多い. 未規定 (unspecified behavior) とは, 規格がその動作を一切指定しないものを指す. この他に, 処理系依存の項目の一部として, 文化圏固有動作 (locale-specific behavior) がある. これは, 処理系そのものに依存するのではなく, 処 理系動作またはプログラム動作中に与えられた文化圏情報 (locale) ごとに, 処理系が明示的に動作を規定 するものである. Example 6.6.1 未規定の動作の例としては, 関数の実引数の評価順序. 不定(未定義)の動作の例として は, 整数演算のオーバフローに対する動作. 処理系定義の例としては, 符号付き整数を右シフトした場合の 最上位ビットの伝播. 文化圏固有動作の例としては, islower 関数が26個の英小文字以外の文字に関し て, 真を返すかどうかがある. ANSI の規格にしたがった処理系とは, ANSI の規格で規定されているすべての動作を受理しなくてはなら ない. 6.6.2 その他 ANSI 規格で定められているその他の用語として, バイト, 文字, オブジェクトがある. バイト (byte) と は, 実行環境中の基本文字集合の任意の要素を保持するために十分な大きさを持つデータ記憶領域の基本単 位と定められ, 1バイトのビット数は処理系依存, バイトは連続するビット列からなる. 文字 (character) とは, 1バイトに収まるビット表現. オブジェクト (object) とは, その内容によって, 値を表現できる実行 環境中の記憶領域. ビットフィールド以外のオブジェクトは, 連続する一つ以上のバイトの列からなる. ま た, オブジェクトを参照する場合, オブジェクトは特定の型を持っていると解釈して良い. 6.6.3 コンパイラとインタープリタの違い 処理系に関する用語を解説したついでに, コンパイラとインタープリタという用語と, その違いを解説し ておこう. 言語処理系は, 対応する言語の文法にしたがって記述されたソースコード (source code) を受取り, ソー スコードに記述された内容にしたがって, 次のいずれかの動作を実行する. C1.tex,v 1.17 2003-04-20 13:58:01+09 naito Exp 数理解析・計算機数学特論 232 1. ソースコードを読み取り, 指定された環境8 に対するオブジェクトコード (object code) または, 実 行可能コード (executable code) を出力する. すなわち, ソースコードの内容を解釈し, その結果となるオブジェクトコードまたは実行可能コード を出力する. 2. 読み取ったソースコードに記述された命令列を, 一つの命令を読み取ると, すぐにその命令を実行す る. すなわち, 読み取ったソースコードの内容を逐次実行する. このように, 処理系の動作には2種類があり, 前者の動作を行う処理系をコンパイラ (compiler), 後者の動 作を行う処理系をインタープリタ (interpreter) と呼ぶ. Example 6.6.2 多くのC言語処理系の場合には, C言語のプログラムソースコード(これは, 単なる「テ キストファイル」であり, コンピュータにとっては, 単なる文字のならび以上の意味を持たないデータで ある.)を受取り, そこに記述されたCの文法通りのオブジェクトコードまたは実行可能コードを出力する. ユーザにとっては, 処理系が出力した実行可能コードを実行することにより, C言語で記述されたプログラ ムを動作させることができる. したがって, gcc はコンパイラである. 標準的なC言語処理系はコンパイラであり, 一つのソースコードを2度にわたって解釈系に通す.9 Example 6.6.3 標準的な perl はインタープリタであり, 入力された perl スクリプトを逐次実行する. し たがって, 入力ソースコードに文法エラーがあれば, 文法エラーの直前までは実行が行われる. このように, 処理系には2種類があり, 安易に考えるとインタープリタの方がうれしいように思えるのだが, コンパイラでは複数回の解釈系の実行を通じて, 「変数名」などの「シンボル名」の相互参照や, 実行可能 コードの最適化 (optimization) を効率よく行うことが可能となる.10 6.7 最も簡単なプログラム はじめにいくつかの最も簡単と思われるプログラムを書いてみよう. 6.7.1 Hello World プログラムを実行すると画面に何かを表示するものである. Example 6.7.1 もっとも簡単なプログラムの例 8 わざわざ「指定された環境」と書いたのには理由がある. 一般には, コンパイラは処理系を動作させた環境でのオブジェクトコー ドまたは実行可能コードを出力するが, クロスコンパイラ (cross compiler) と呼ばれる, 他の環境でのオブジェクトコードまたは 実行可能コードを出力する処理系も存在する. 9 不思議なことに, インタープリタとして動作するC言語処理系も存在する. 10 最適化とは, オブジェクトコード中の無駄や意味のない部分などを, より高速化が可能なようにする操作である. C2.tex,v 1.9 2003-04-20 14:02:04+09 naito Exp 数理解析・計算機数学特論 /* Program 1 * Hello World. を出力する. 233 * * * #include <stdio.h> */ int main(int argc, char **argv) { printf("Hello World.\n") ; return 0 ; } 以下では, このプログラムの内容を説明する. (ただし, コメント, 空白行は行数として数えない.) 1行目 #include <stdio.h> # ではじまる行はプリプロセッサと呼ばれるものによって処理される. C コンパイラは, 実際には以下の手順によって実行される. 1. プリプロセッサによる前処理. 2. コンパイラによるオブジェクト・コードの作成. 3. リンカによるオブジェクト・コードとライブラリの結合. C 言語のプログラム中に, # ではじまる行があらわれると, プリプロセッサはその文法にしたがって, コー ドを書き換える. 実際, #include という指示は, これに続くトークンで指示されたファイルを, その位置 に挿入する命令である. C 言語では, 原則として全ての関数は, 定義されたり, 利用される前に宣言されなくてはならない. そこ で, 標準的な関数(このプログラムでは printf)を使うためには, その宣言が書かれているファイル(こ こでは stdio.h)を挿入することによって, その関数の宣言をする. このような(標準関数の)宣言が書か れているファイルのことをヘッダ・ファイルと呼ぶ. また, ヘッダ・ファイルの挿入には #include <XXXX.h> #include "XXXX.h" の2つの書き方がある. コンパイラの実装によって決まる標準的な場所11 にあるファイルを挿入するには 前者の方法を使い, カレント・ディレクトリにあるファイルを挿入するには後者の方法を使う. どのような関数が, どのヘッダ・ファイルで宣言されているかはオンライン・マニュアルを見ればわかる. 2行目 int main(int argc, char **argv) これは main という関数の定義である. この関数の本体は3行目の { と6行目の } に囲まれた部分である. この部分は次の3つの部分に分解される. 11 これは, コンパイル時のオプションで変更可能 C2.tex,v 1.9 2003-04-20 14:02:04+09 naito Exp 数理解析・計算機数学特論 234 • int • main • (int argc, char **argv) はじめの int は, この関数の戻り値が int 型であることを示す. 関数の戻り値が書かれていない時には, コンパイラは int であると解釈する. 次の main は関数の識別子である. ここで使われている (int argc, char **argv) に関しては後に議論するので, 取りあえずここでは「お 約束」としておこう. ここには, (存在すれば)その関数の引数が書かれる. 引数をとらない場合にも () または (void) と書かなくてはならない. プログラムが開始されるときには, その時点で呼び出される関数の名前は main でなければならない. す なわち, main という名前を持つ関数が, プログラム開始時点で最初に呼び出され実行される. 4行目 printf("Hello World.\n") ; ここで利用された printf という関数は, その引数として, 文字列リテラルをとり, その文字列リテラルを 標準出力に出力する.12 ここで, 最後の ; によって, この一行が文になっていることを注意せよ. 本来, この関数には戻り値が存在するが, ここではその戻り値は利用していない. 5行目 return 0 ; return という文は次の形でなくてはならない. return 式 ; 式の部分には, どのような式を書いても良い. ここでは, 単に 0 という式を書いている. この文は, main 関数の戻り値を与えている. main 関数が終了した時点でプログラムの終了処理が行わ れ, main 関数の戻り値はプログラムを実行したシェルに返される13 . Exercise 6.7.2 Example 6.7.1 を真似て, 次のような出力を得るプログラムを書け. 各自の学籍番号 (改行) 各自の名前 (改行) 何でも好きなこと (改行) Exercise 6.7.3 printf という関数の戻り値は, 出力した文字数である. main の戻り値として, 出力した 文字数を返すように Example 6.7.1 を変更せよ. ただし, 変数を用いてはならない. シェルに戻された戻り 値は, csh の場合は % echo $status を実行することで得ることができる。 12 ここの解説は本当は正しくない。この関数はもっと多くの引数をとり、最初の引数も文字列リテラルである必要はない. 13 main 関数が明示的な戻り値を持たない場合には, シェル(ホスト環境)に返される値は不定となる. C2.tex,v 1.9 2003-04-20 14:02:04+09 naito Exp 数理解析・計算機数学特論 6.7.2 235 値を表示する 上の例 (Hello World) で, printf 関数は「画面に出力する」手続きを行っているものであることがわかっ た. 次の例では, 「値」を出力することを考えてみよう. Example 6.7.4 定数の値を出力する. /* Next example */ /* ex02.c */ int main(int argc, char **argv) { printf("Hello World\n") ; printf("ここには値1が出力される: %d\n", 1) ; printf("ここには値 1.0 が出力される: %f\n", 1.0) ; printf("ここには文字 x が出力される: %c\n", ’x’) ; printf("ここには文字 x のコード値が出力される: %d\n", ’x’) ; printf("ここには文字 x のコード値が16進で出力される: %x\n", ’x’) ; printf("ここには文字 y のコード値が16進で出力される: %x\n", ’y’) ; printf("ここには文字列 abc が出力される: %s\n", "abc") ; printf("ここには値 1 と 1.0 が出力される: %d, %f\n", 1, 1.0) ; printf("ここには値 3 が出力される: %d\n", 1+2) ; printf("ここには文字 x と y のコード値の和が16進で出力される: %x\n", ’x’+’y’) ; return 0 ; } このように, printf 関数は, 定数式の値を出力することができる. この時, 以下のことに注意をしよう. • 定数の値が「整数」の場合には, “%d” とした場所に値が(10進で)出力される. • 定数の値が「整数」の場合には, “%x” とした場所に値が(16進で)出力される. • 定数の値が「実数」の場合には, “%f” とした場所に値が出力される. • 定数の値が「文字」の場合には, “%c” とした場所に値(文字)が出力される. • 定数の値が「文字列」の場合には, “%s” とした場所に値(文字列)が出力される. • 定数の値と “%d” などの(上のような)対応が取れていない場合には, 何が出力されるかはわからない. • 複数の “%d” や “%f” などを使った場合には, それ以後の「値」の対応する順番で出力が行われる. • 定数の値が「算術式」である場合には, その算術式の結果の値が出力される. なお, 値が「実数」の場合に “%f” の出力結果の小数点以下の表示桁数は処理系に依存している. 6.8 変数とは 変数 (variable) とは, 識別子によって区別された初期化, 代入などが許される記憶領域のことである. C 言語の変数には, 多くの型があり, それぞれの型によって, どれだけの記憶領域が確保されるかが異なる. ま た, C 言語の変数には記憶クラス, スコープ, 寿命, リンケージなどの概念があるが, それらについては関数, 分割コンパイルの後で述べる. ここでは, 変数の宣言, 型などについて考える. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 236 6.8.1 変数の宣言 C 言語では全ての変数は使う前に宣言しておかなくてはならない. 宣言は変数の性質を告げるためのも ので, int step ; int lower, upper ; float x ; のように, 型の名前と変数(の識別子)の名前のリストからなる. これらの宣言を色々な場所に書くことで, それらの変数の意味が変わるが, ここでは, 次のように, どのブ ロックにも含まれず, すべての手続きの前に書くことにする. (下の例を参照.) #include <stdio.h> int step ; int lower, upper ; float x ; int main(int argc, char **argv) { ..... } 6.8.2 変数の初期値 C では変数は, 定義されただけでは値は定まらない(と考えた方が良い)14 . そのため, (必要なら)そ の変数を使う前に初期化を明示的に行なう必要がある. 変数の初期化の方法には2通りある. ..... int a=0, b ; int main(int argc, char **argv) { b = 0 ; ..... } このように, 宣言と同時に初期化することもできる. a の初期化は実行時にただ一度だけ行なわれるが, b の場合は, この文を実行されるたびに b に 0 が代入される. C においては, 変数の宣言と定義は異なり, 宣言だけではメモリ領域が確保されない. これに関しては, extern 宣言を参照. 14 [2, 2.4] によれば, 次のように書かれている: 外部変数, 静的変数はゼロに初期化される. 明示的な初期化式がない自動変数は不 定(ゴミ)の値を持つ. (External and static variables are initialized to zero by default. Automatic variables for which there is no explicit initializer have undefined (i.e., garbage) values.) C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 237 Example 6.8.1 変数の初期化及び, 値の代入を行ってみる. #include <stdio.h> int a=0, b ; int main(int argc, char **argv) { printf("a = %d, b = %d\n", a, b) ; b = 1 ; printf("a = %d, b = %d\n", a, b) ; return 0 ; } この時, 最初の printf 関数で出力される, 変数 b に格納された値は「不定」であることに注意. 6.8.3 変数の型 C で定義されている変数の型は以下の通りである.15 変数の型 型の名前 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 は符号つき であると解釈される. しかし, char に関しては, どちらになるかは処理系依存である. 例えば, 今回使用す る gcc の場合は char は signed char である. 6.8.3.1 const 修飾子 変数の型に const という修飾子をつけると, 初期化はできるが, プログラム中で変更のできない定数と して扱うことができる. 例えば, 次のように宣言する. const int a=0 ; float const b=1.0 ; const 宣言をした変数を変更した時の振る舞いは不定である16 . 15 void, enum, long double は ANSI の規格ではじめて定義された. Kernighan-Ritchie の初版 [1] では定義されていない. 「const 修飾型を持つオブジェクトを, 非 const 修飾型の左辺値を使って変更しようとした場合, その動作は未 定義とする.」というのが ANSI の規定. 16 より正しくは, C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 238 Remark 6.8.2 この remark は非常に高度で面倒な内容を含んでいるので, 興味のない人は無視してもよ い. また, ここでのプログラム断片は表示を少なくするため, あまりきれいな形にはなっていない. 実は const 宣言をした変数の扱いが非常に厄介で, 以下のようなプログラム断片を調べてみよう. int const int cn = n ; n n ; cn ; = 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]) • 両オペランドが適合する型の修飾版または非修飾版へのポインタであり, かつ左オペランドで指される型が右オ ペランドで指される型の修飾子をすべて持つ. 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]) そこには, 関数呼び出しの制約として, 「各実引数は, 対応する仮引数の型の非修飾版を持つオブジェクトに その値を代入できる型を持たなければならない」と書かれている. つまり, 引数を渡すと代入が行われ, 実引数と仮引 数は代入が許される関係になければならないということである. したがって, C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 239 int foo (const char *p) {} int main(int argc, char **argv) { char *q ; foo(q) ; } という例であれば, p = q という代入が行われることに相当し, 上で述べた単純代入の規約を満たす. しかし, const char ** の例では, 仮引数 p は「const char * へのポインタ」であり, 実引数 argv は「char * へのポインタ」で あるため, 単純代入の規約を満たさない. なお, char * を仮引数とする多くの標準ライブラリ関数(例えば, strcpy など)は, 仮引数として const char * を宣言している. これは, 関数内で明示的に仮引数の指し示す値を変更しないための措置である. 変数と記憶領域 6.8.3.2 変数は宣言と同時に対応する記憶領域が確保される. しかしながら, それぞれの型に対して, どれだけの 記憶領域が確保されるかは処理系依存である. それぞれの型がどれだけの記憶領域をとるかを調べるには, sizeof 演算子を使う. sizeof 演算子の利 用法は以下の通りである. sizeof (型) ; sizeof オブジェクト ; ここで, その結果として得られる値は, 符号なし整数で表現され17 , その意味は, char 型の何倍の記憶領 域が確保されるかを表す. 即ち, sizeof(char) の結果は処理系によらず 1 である. それでは, char 型がどれだけの記憶領域を使うかを知るには, どのようにすれば良いのだろうか. それ には, C 言語の標準的なヘッダ・ファイルを見れば良い. 実際, limits.h に定義されている CHAR BIT と いうマクロ18 の値が char 型のビット数である. Sun Sparc Station の C コンパイラの場合, #define CHAR_BIT 0x8 となっているので, char 型は8ビット(1バイト)であることがわかる.19 また, int 型の長さは, その計 算機の自然な長さであると定義されている. それぞれの変数が記憶領域に確保される時, 宣言した順序で記憶領域内に確保されるという保証はない. また, 多くの処理系では, int, long はワード境界にアロケートされる. Example 6.8.3 int が2バイト, long が4バイトの処理系で, char c ; int n ; long l ; char d ; と変数を定義した場合, 下の図のいずれのメモリ配置をとるかは処理系や最適化に依存する. これ以外の取 り方をする可能性もある. 17 正確には size t 型で表現される. size t 型がどの型に対応するかは処理系依存である. 18 マクロの意味は後日解説する. 19 ANSI の規格書によれば, char 型の占めるビット幅を1バイトと定義している. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 240 16 bits c n l 16 bits (padding) c n l 16 bits c n n(続き) l d d d (padding) (a) (b) (c) この中で (c) のメモリ・アロケーションはアライメント (alignment, 境界調整) に適合していない環境が 多いため, ほとんどこのようなアロケーションは行われない. Example 6.8.4 変数に値を代入する操作とは, 変数に対して与えられたメモリに数値を書き込むことに他 ならない. 例えば, (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.5 例えば, ASCII コード体系の処理系で, char c ; c = ’a’ ; C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 241 とすると, c には16進整数値 0x61 が入り, 整数値 0x61 として計算や比較が行われる. したがって, char c, d ; c = ’a’ ; d = ’A’ ; の時, c + d は16進数値 0x41 + 0x61 = 0xA2 となる. また, char c ; c = 0x61 ; とすると, c には ’a’ が代入されたこととなる. それでは, 整数型がどれだけの記憶領域を使うかを考えてみよう. C では, short, int, long などの記憶 領域については, 全て処理系に依存していると定義されているが, 次の関係だけは C の定義にある. char ≤ short ≤ int ≤ long ここで, char ≤ short という意味は, short 型の記憶領域は char 型のそれよりも短くないという意味で ある. また, short, int 型は最低16ビット, long 型は最低32ビットが保証されている. 実際, それぞれの整数型の長さ(sizeof の返す値)を見てみると, 今回利用する処理系では, 次のように なる. 32 ビットコンパイラの例 64 ビットコンパイラの例 short 2 2 int long 4 4 4 8 これで, 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 の返す値)を見てみると, 今回利用する処理系では, 次の ようになる. 32 ビットコンパイラの例 64 ビットコンパイラの例 float double 4 8 4 8 long double 16 16 これで, char 型が1バイトであることを使うと, float は4バイトであることがわかる. (10を底とする)浮動小数点数とは, 以下のような型で表現された数のことである. (0 でない1桁の数).<仮数部> × 10^<指数部> ここで, 任意の 0 でない実数は, このような表示が可能であることに注意せよ. (もちろん, その表示は有 限小数と仮定すれば一意的である.) C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 242 浮動小数点数の内部表現 6.8.3.4.1 浮動小数点数の変数の内部での表現は, Section 2.3.2 で述べた表現 がとられていることが多い. ANSI 規格では整数型の内部表現の具体的な方法については何も規定してい ない. オーバーフロー, アンダーフローをした数の演算, 比較等の結果がどうなるかは規格では定められていな い. しかし, それぞれの処理系によって規定されている. 6.8.3.5 列挙定数 列挙定数 (enumeration constant) とは, 次のようなものである. enum day {sun, mon, tue, wed, thu, fri, sat} ; この時, eum 型の変数は int として扱われる. 即ち, 上の例では, sun <-> 0 mon <-> 1 などというように対応づけが行なわれて処理される. 6.8.4 変換 C のプログラムで演算を行なう時には, 数多くの型の変換が行なわれてから演算が実行される. 6.8.4.1 整数への格上げ 汎整数型(char, short, int, long, このような型を integral type と呼ぶ.)に対して, 演算を行なう時 には, 整数への格上げ (Integral Promotion) (または汎整数拡張)と呼ばれる操作が行なわれることが ある. それは, 以下のように定義されている20 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 が埋められる. 20 [1] の定義によれば, 「符号なし型はより広い符号なし型に変換される」とされているので注意すること. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 243 したがって, 0x7F を越える char 型の変数を扱う時には, 必ず unsigned char とし, より広い整数への 変換がある時には, 符号なしで受けなくてはいけない21 . 例えば, char が signed char の時, char a = 0x80 とすると, (int)a は 0xFFFFFF80 となるが, char が unsigned の時には, 0x80 のままである. 6.8.4.3 符号拡張と整数への格上げの演算への影響 符号拡張と整数への格上げは, char 型の変数同士の演算の場合に大きな影響をおよぼす. CPU の演算レ ジスタ長よりも短いメモリサイズを持つ変数に対する演算を行う場合, 何らかの形で演算レジスタ長に合 う値(ビットパターン)に変換を行ってから演算を行う必要がある. 符号拡張・整数への格上げは, 演算レ ジスタ長に値を合わせる変換と理解して良い. Example 6.8.6 標準演算レジスタ長が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 整数と浮動小数点数 浮動小数点数を汎整数に変換する時には, 小数部は無視される. また, 結果として得られる整数が目的の 型で表現できない時の振舞いは不定である. 逆に, 整数を浮動小数点数に変換する時には, その結果が表現可能な範囲にある時でも, 正確に表現がで きない時には, 一番近い大きな数か小さな数のどちらかに変換される. 21 C の定義によれば, 「標準文字セットのすべての文字は正の値を持つ」となっている. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 244 6.8.4.6 浮動小数点数 浮動小数点数がより精度の高い浮動小数点数に変換される時には, その値は不定である. 逆に精度の高いものが低いものに変換される時には, その結果が表現可能な範囲にある時でも, 正確に表 現ができない時には, 一番近い大きな数か小さな数のどちらかに変換される. 6.8.4.7 算術変換 算術変換 (arithmetic conversion) とは, 算術演算が行なわれている時に, 被演算数の型を揃え, その 結果も同じ型にするという操作である. ほとんどの演算において, この変換が行なわれる. その手順は, 以下の通りである. (条件に一致した最 初の変換が行なわれる). 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 として計算する. 要するに, 「算術演算で異なる型の値を指定すると, 型変換が行われる. 変換は情報が欠落しない限り, 実 数, 高精度, 符号付きの方向で行われる」ということである22 . Example 6.8.7 例えば, 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 ; 22 この文章は [6, p. 53] から引用. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 245 とする. (後述の Section 6.8.6 参照.) Example 6.8.8 int が2バイトである時, unsigned int a = 256 ; (a * a * 1L) == (a * (a * 1L)) ; /* この式は正しくない. */ ということが起こる. Example 6.8.9 次の例は, 符号拡張, 算術変換などの例である. int a,b,c ; char n ; double x ; a = 1 ; b = 2 ; x = a/b ; /* x の値は 0 である. */ x = 1/b ; x = 1.0/b ; /* x の値は 0 である. */ /* 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 でなければならず, そ れが結果の型となる. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 246 と書かれている. ANSI C では「値を保存」する方向に変換が行われるが, K&R C では「unsigned を保 存」する方向に変換が行われる. Example 6.8.10 次のコード23 は, ANSI C と K&R C では異なった結果を出力する24 . if (-1 < (unsigned char)1) printf("-1 is less than (unsigned char)1: ANSI\n") ; else printf("-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 に変換されたビットパターンとして比較される25 . unsigned char が unsigned int の場合には, K&R でも ANSI でもともに unsigned int としての比較が行われ るため, -1 < (unsigned int)1 は「偽」の値を返す. 6.8.5 演算 6.8.5.1 2項算術演算 2項算術演算とは, 通常の足し算, 引き算, かけ算, 割算, Modulo である. 2項算術演算の項の評価順序 は不定であるので注意すること. 加法演算子 6.8.5.1.1 加法演算子には +, - がある. これらは, 左から右に作用する. 被演算数が算術 26 的 (即ち, 整数や浮動小数点数)であれば, 算術変換が適用される. 乗法演算子 6.8.5.1.2 乗法演算子には *, /, % がある. これらは, 左から右に作用する. * (かけ算), / (割算)27 は, 被演算数が算術的でなくてはならない. % (余りを出す)は, 被演算数は汎整数でなくてはな らない. これらの演算には, 算術変換が適用される. 即ち, 整数同士の割算の結果は, 再び汎整数となり, そ の商が求められる. /, % の第2被演算数が 0 で無い場合には, (a/b)*b+a%b が a に等しいということが常に保証され, 両方 の被演算数が非負の場合には, あまりが非負で, 除数よりも小さい. そうでないときには, あまりの絶対値 が除数の絶対値よりも小さいことが保証される. すなわち, どちらか片方の被演算数が負の時には, 除算(/ または %)を行ってはいけない. この場合除 算を行うと, 結果は処理系依存となる. 6.8.5.2 単項算術演算子 ここでは, インクリメントのみを扱う. ここで述べる2種類の演算子は, 汎整数かポインタに対してのみ 適用できる. 23 [6] からの引用 K&R の規約では, unsigned char は存在しない. unsigned, short, long は int につく限定詞と定義されている. しか し, 古い ANSI 規格ではない処理系の多くで unsigned char が利用できる. 25 しかし, 正しくは K&R C には unsigned char は規定されていない. 26 加法演算子は, 後述するポインタにも作用する. 27 もちろん, /, % の第2被演算数は 0 であってはならない. /, % の第2被演算数が 0 の場合には結果は不定となる. 一般には「 0 除算による例外割り込み」が発生する. 24 実は, C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 6.8.5.2.1 前置インクリメント演算子 247 式の前に ++ もしくは -- がついている式もまた, 式になる. これ は, その値が使われる前に 1 だけ増やされる(++ の場合). 6.8.5.2.2 後置インクリメント演算子 式の後に ++ もしくは -- がついている式もまた, 式になる. これ は, その値が使われた後に 1 だけ増やされる(++ の場合). Example 6.8.11 インクリメントの例は以下のものである. int a ; a = 1 ; a++ ; /* この値を表示させると, 1 を表示したあとに, 1 増分される. */ a = 1 ; ++a ; /* この値を表示させると, 1 増分した後に, 1 を表示する. */ a = 1 ; --a++ ; /* これはエラーである. */ 次のような計算を考える. int x, y ; x = 1 ; y = (x++) - (x++) ; この結果は, 次の3通りが考えられる. 1. y = 0 これは, 先に - を実行し, それからインクリメントをした. 2. y = -1 これは, 1 - 2 を実行した. 3. y = 1 これは, 2 - 1 を実行した. このように2項演算と各項の評価をどの順序で行なうかは, 不定であるので注意すること28 . Example 6.8.12 次のような式を考えよう. x+++y ; この式は, x+++y と x+++y と2通りに解釈できるが, C の構文解析では, 最大一致法をとるという規 約があり, そのため, 構文に合致する最大のトークンである x++ を採用し, x+++y と解釈される. x+++++y は x+++++y という解釈が可能であるが, C の構文解析パーサには x+++++y と解釈することが求めら れている. 28 このような評価式は ANSI 規約 [3, X3010 6.3, p.1873] の「式」の規定で, 「直前の副作用完了点から次の副作用完了点までの 間に, 式の評価によってオブジェクトに格納された値を変更する回数は高々1回でなければならない. さらに, 変更前の値は, 格納さ れる値を決定するためだけにアクセスしなければならない. 」とある. したがって, y = (x++) - (x++) が不定であるだけでなく, i = ++i + 1 も不定であることに注意しよう. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 248 6.8.5.3 代入演算子 次のような代入を考える. a += 1 ; a -= 1 ; a *= 1 ; a /= 1 ; これらは, それぞれ, a の値を 1 加えた(減らした, 掛けた, 割った)値を再び, a に代入する演算である. Example 6.8.13 代入の例は以下のものである. 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 である. 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 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 0 0 ↓| 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 1 C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 249 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 0 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 0 ↓ >>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 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 負の数のシフトで, どちらが起きるかは処理系依存 Example 6.8.14 <<, >> の演算の例は, 以下の通りである. 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 1 1 1 1 1 1 1 0 符号拡張と格上げ 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 ↓ >>1 (算術シフト) 1111111111111111 0xFFFF ↓ >>1 (論理シフト) 0111111111111111 0x8FFF 格上げ 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 ↓ >>1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0x0077 C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 250 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.15 論理 AND, OR 演算子の例は以下の通りである. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 251 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.16 論理 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 では, 評価が全て行なわれる前に式の値が決定することがあり, その時点で式の評価が終了 するので, 注意しなくてはならない. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 252 6.8.5.7 3項演算子 3項演算子 ? : は条件演算子とも呼ばれる演算子である. 式1 ? 式2 : 式3 まず, すべての副作用を含めて, 式1が評価され, それが 0 でなければ結果は式2の値となり, そうでな ければ式3の値となる. この時評価されるのは式2または式3のいずれかである. 式2, 式3の被演算数が 算術型であれば, 通常の算術変換が行われて, それらは共通の型となり, それが結果の型となる. 3項演算子は if-else 文で書くと長くなる場合に, それを短く表現する手法として使われるが, すべて の条件分岐を表現できるわけではないことに注意. Example 6.8.17 2つの変数の小さくない方の値を代入する. max = (a > b) ? a : b ; Example 6.8.18 例えば, 次の 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 演算子の優先順位と結合規則 これら演算子の優先順位, 結合規則は, 次の通りである. 上ほど優先順位が高い. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 253 演算子 結合規則 ( ) [ ] -> . 左から右 ! ~ ++ -- + - * & (type) sizeof 右から左 * / % 左から右 + - 左から右 << >> 左から右 < <= > >= 左から右 == != 左から右 & 左から右 ^ 左から右 | 左から右 && 左から右 || 左から右 ?: 右から左 = += -= *= /+ %= &= ^= |= <<= >>= 右から左 , 左から右 ただし, 単項の + - * は二項のそれよりも上である. 最も注意しなければならないのは, &, | と == の優先順位である. Example 6.8.19 int 型の変数 n が偶数か奇数かを判定するために, if (n & 1 == 0) としたとしよう. プログラマは, if ((n & 1) == 0) の意味で書いているかもしれないが, 実際には if (n & (1 == 0)) と解釈される. Example 6.8.20 括弧は不要な場合であっても, 括弧を書くことにより, 意味が明快になる場合がある. こ の例は, 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.21 - の結合規則は「左から右」であるので, a - b - c ; C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 254 は, ((a-b)-c) という意味となる. 通常の数式でも a− b − c = ((a− b)− c) であって, a− b − c = (a− (b − c)) ではない. Example 6.8.22 = の結合規則は「右から左」であるので, a = b = c ; は, b = c の代入が行われ, その結果の値(この場合は代入された b の値)が a に代入される. すなわち, a = (b = c) という意味となる. ただし, (a = b) = c は (a=b) が左辺値とならないので, 文法エラーと なる. Example 6.8.23 == の結合規則は「左から右」であるので, a == b == c ; は, 比較 a == b が行われ, その結果の値と c との比較が行われる. すなわち, (a == b) == c という意味 となる. この場合は, (a == b) == c は文法上正しい構文である. Example 6.8.24 式 a < b < c の意味は数学的な不等式ではなく, もし, b が a よりも大きければ, 1 と c を比較し, そうでなければ, 0 と c を比較していることに注意. Example 6.8.25 数学的には 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 と等しくなる保 証はない. 6.8.6 型変換 異なる型の被演算数が式に現れると, いくつかの規則にしたがって, 明示的もしくは暗黙に共通の型への 変換が行なわれる. 前のセクションで述べた変換がその代表的なものであるが, ここでは, それ以外に明示的に型変換 (cast) を行なう方法を述べる. それは, 次のような方法である. (型名) 式 この方法によって, 式はその前に書いた型名で示された型に変換される. その際の規則は, Section 6.8.4 で示した通りである. Example 6.8.26 次のような例がある. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 255 char a ; int b ; a = 0x01 ; b = (int) a ; この場合は, char がより広い型の int に変換されている. しかし, この型変換は整数への格上げそのも のである. このように, 整数への格上げがあっても, 明示的に型変換をすることが望ましい. 次のような例もある. Example 6.8.27 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 その利用法は, 次の通りである. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 256 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 変換に関しては, 最大の有効桁数. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 257 – 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 を用いる. 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 を参照.) C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 258 char a = ’a’ ; int long b = -1 ; c = 10L ; unsigned int d = 2U ; char s[3] = "ab" ; double x = 1.0e-4 ; double long double y = 1.0e-5 ; z = 1.0L ; printf("a = %c\n", a) ; a=a printf("a = %x\n", a) ; printf("b = %d, c = %ld\n",b, c) ; a=61 b=-1,c=10 printf("b = %d, c = %+ld\n",b, c) ; printf("b = % .3d, c = % .3ld\n",b, c) ; b=-1,c=+10 b=-001,c=010 printf("b = %0.3d, c = %0.3ld\n",b, c) ; printf("c = %3ld\n",c) ; b=-001,c=010 c=10 printf("d = %u\n", d) ; printf("d = %0.5u\n", d) ; d=2 d=00002 printf("d = %X\n", d) ; printf("d = %.3x\n", d) ; d=2 d=002 printf("d = %#.3x\n", d) ; d=0x002 printf("s = %p\n", (void *)s) ; printf("x = %f\n", x) ; s=effff9c8 x=0.000100 printf("y = %e\n", y) ; printf("x = %G\n", x) ; y=1.000000e-05 x=0.0001 printf("y = %g\n", y) ; printf("z = %LE\n", z) ; y=1e-05 z=1.000000E+00 6.9.2 プログラムの演習 Exercise 6.9.2 色々な型の演算の値を出力するプログラムを書け. 例えば, 次のようなものである. #include <stdio.h> int a,b ; int main(int argc, char **argv) { printf("%d + %d = %d\n", a, b, a + b); return 0 ; } Exercise 6.9.3 次のプログラムの出力結果がなぜそのようになるかを考えよ. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 259 #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.4 次のプログラムの出力結果がなぜそのようになるかを考えよ. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 260 #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.5 次のプログラムの出力結果がなぜそのようになるかを考えよ. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 261 #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.6 次のプログラムの出力結果がなぜそのようになるかを考えよ. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 262 #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 ; } ヒント:算術変換が各所に含まれている. Exercise 6.9.7 short, int, unsigned int, long のそれぞれの型の変数が何バイトの記憶領域をとるか を表示するプログラムを書け. C3.tex,v 1.27 2003-04-20 14:04:31+09 naito Exp 数理解析・計算機数学特論 263 Exercise 6.9.8 int, long のそれぞれの型で表現される最大の数に 1 を加えたらどうなるかを考察せよ. Exercise 6.9.9 正の浮動小数点数の小数点以下を四捨五入した値を求めるプログラムを書け. Exercise 6.9.10 AND, OR, NOT から XOR を作れ. Exercise 6.9.11 Example 6.8.8 はどうしてかを考察せよ. Exercise 6.9.12 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が評価される. ここで, 各式の副作用は, 評価の直後に完了する. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 264 Example 6.10.1 この例は, 0 から 9 までの整数を順に印字するものである. #include <stdio.h> #include <stdio.h> int i ; int i ; int main(int argc, char **argv) { int main(int argc, char **argv) { for(i=0;i<10;i++) { printf("%d\n",i) ; for(i=0;i<10;i++) printf("%d\n",i) ; } return 0 ; 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) { j = 0 ; for(i=0;i<10;i++) j += i+1 ; return 0 ; } このプログラムでは, 変数 i はループ制御に使われ, 変数 j は, そこまでの繰り返しの和の値を保存する変 数になっている. 繰り返し文を実行する前に j の値を 0 で初期化していることに注意しよう. このプログラムの for 文の終了後に, 変数 j には 1 から 10 までの和の値が代入されている. Exercise 6.10.4 for 文を利用して, 2 から 10 までの偶数の和を計算して印字するプログラムを書け. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 Exercise 6.10.5 for 文を利用して, 265 10 n=1 n2 を計算して印字するプログラムを書け. Remark 6.10.6 1 から 10 までの和を計算するプログラムでは, for 文を使ったものでも, いろいろな書 き方が可能である. #include <stdio.h> int i, j ; int main(int argc, char **argv) { j = 0 ; for(i=1;i<=10;i++) j += i ; return 0 ; } 確かに問題なく動作するのだが, C では境界判定条件(この例では i <= 10 )では, i <= 10 と書くより も i < 10 と書く方が(すなわち, 直前の例の方が)標準的である. 可能な限り標準的な C の書き方を身 につける方が良い. #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式のコンマ演算子の利用法に注意. しかし, このようなコンマ演算子の利用法はわかりやすいものではない. すなわち, このプログラムはわか りやすいものとはいえない. #include <stdio.h> int i, j ; int main(int argc, char **argv) { j = 0 ; for(i=10;i>0;i--) j += i ; return 0 ; } 普通に考えれば i の値をインクリメントするに決まっている. 間違いなく動くのだが, このような無理な 書き方はやめた方が良い. バグの元となる. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 266 #include <stdio.h> int i, j ; int main(int argc, char **argv) { j = 0 ; for(i=11;--i>0;j+=i) ; return 0 ; } もうここまでいくと, 何をやっているのかわからない. 絶対ダメ! 6.10.2.2 while 文 while 文は以下のような構文を持つ. while(式) 文 ここで, 式は省くことはできない. ここで, 式は算術型もしくはポインタでなくてはならない. このような while 文は, 次のように制御される. 1. 式が最初に評価される. 2. 式が 0 でない限り, 繰り返しの文が実行される. ここで, 式の副作用は, 繰り返しのはじまる前に完了する. Example 6.10.7 この例は, 0 から 9 までの整数を順に印字するものである. #include <stdio.h> int i=0; #include <stdio.h> int i=0; int main(int argc, char **argv) int main(int argc, char **argv) { { while (i < 10) { while (i < 10) printf("%d\n",i++) ; } return 0 ; printf("%d\n",i++) ; return 0 ; } } この例では, はじめに i = 0 が実行され, i < 10 である間は, { } の部分が実行される. 各繰り返し中で は, i++ が実行されているので, 繰り返し毎に i は 1 だけ増加する. i が 9 になると, 繰り返しは実行され るが, 繰り返しの文中で, i++ が実行され, i は 10 となる. したがって, i < 10 が 1 となり, 繰り返しの文 は実行されずに, while は終る. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 267 Example 6.10.8 この例は, 無限ループ29 を実現している. #include <stdio.h> int i ; int main(int argc, char **argv) { while(1) ; return 0 ; } Example 6.10.9 この例は, 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.10 while 文を利用して, 2 から 10 までの偶数の和を計算して印字するプログラムを書け. Remark 6.10.11 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 ; i += 1 ; } 29 無限ループはウインドウシステムでのアプリケーションの作成に利用される. 前に述べた for を使った無限ループか, こちらの 例かどちらかの利用にとどめておいた方が良い. 無限ループの実現方法はいくらでも考え付くが, これら2つのどちらかであれば, C を少しでも知っているプログラマなら, 見ただけで無限ループと理解可能である. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 268 確かに問題なく動作するのだが, C では境界判定条件(この例では i <= 10 )では, i <= 10 と書くよ りも i < 10 と書く方が(すなわち, 直前の例の方が)標準的30 である. 可能な限り標準的な C の書き方 を身につける方が良い. j = 0 ; i = 10 ; while(i > 0) { j += i ; i -= 1 ; } 普通に考えれば i の値をインクリメントするに決まっている. 間違いなく動くのだが, このような無理 な書き方はやめた方が良い. バグの元となる31 . 結局, 1 から 10 までの和をとるプログラムは for 文を用いた方が自然であることがわかる. じゃあ, while 文はどんな時に使うかって?それは, 境界条件があらかじめ決まっていないときには for 文よりもきれい になります. Example 6.10.12 この例は, 標準入力から 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文字づつ書き出している32 . このプログラムを getchar.c とし, これを実行形式にコンパイルしたコマンドを a.out としたとき, ./a.out < getchar.c を実行してみよ. Remark 6.10.13 上の例で while(c = getchar() != EOF) とすると, 正しく動作しない. 代入演算子と等値演算子の優先順位は, 等値演算子の方が上である. 6.10.2.3 do 文 do 文は以下のような構文を持つ. 30 でも, この辺は難しいところかもしれない. 前の for 文を使ってデクリメントする例よりはよほどマシ. 32 char c ではなく, int c としている理由は, Section 6.19 を参照. 31 でも, C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 269 do 文 while(式) ; ここで, 式は省くことはできない. ここで, 式は算術型もしくはポインタでなくてはならない. このような do 文は, 次のように制御される. 1. はじめに繰り返しの文が実行される. 2. それが終るたびに式が評価される. 3. 式が 0 でない限り, 繰り返しの文が実行される. ここで, 式の副作用は, 各繰り返しの後に完了する. Example 6.10.14 この例は, 0 から 9 までの整数を順に印字するものである. #include <stdio.h> #include <stdio.h> int i=0; int i=0; int main(int argc, char **argv) { int main(int argc, char **argv) { do { printf("%d\n",i++) ; do } while (i < 10) ; while (i < 10) ; } printf("%d\n",i++) ; } この例では, はじめに i = 0 が実行される. 繰り返しの部分は, 1回目は無条件に実行され, その後に i<10 が評価される. 各繰り返し中では, i++ が実行されているので, 繰り返し毎に i は 1 だけ増加する. i が 9 になると, 繰り返しは実行されるが, 繰り返しの文中で, i++ が実行され, 繰り返しが終ると, i は 10 とな る. したがって, i < 10 が 1 となり, 繰り返しの文は実行されずに, do は終る. do は while とは異なり, 少なくとも1回は繰り返しの文が実行される. Example 6.10.15 この例は, 1 から 10 までの整数の和を計算するプログラムである. #include <stdio.h> int i=0,j=0; int main(int argc, char **argv) { do j += ++i ; while(i < 10) ; } ここでは和をとる直前に前置インクリメント演算子を利用していることに注意. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 270 Exercise 6.10.16 do 文を利用して, 2 から 10 までの偶数の和を計算して印字するプログラムを書け. やはり 1 から 10 までの和を計算するプログラムを, do 文を使っていろいろな書き方が可能である. しか し, これまでに見てきたように, わざわざプログラムを難しく書く必要はない. do 文は C では余り多用される構造ではない. なぜなら, ループ終了条件が一番最後にあり, ひとめでは ループの構造がわかりにくいからである. 一般には, 「必ず一度はループに入る」ということを明示的に示 したいときに用いるのだが, ループ全体が短く, ひとめでループの構造がわかるときだけに使うのが良い. do 文は, while 文と break 文の組合わせで同等なものを実現することが出来る. 6.10.2.4 繰り返し文と浮動小数点数 繰り返し文 for, while, do-while 文を用いて, 浮動小数点数の演算を行ってみよう. 例えば, 0.1 の10 回の和を求めるというプログラムを考えてみよう. Example 6.10.17 for 文を用いて, 0.1 の和を計算する. #include <stdio.h> int main(int argc, char **argv) #include <stdio.h> int main(int argc, char **argv) { { double x ; double x ; for(x=0.0;x<0.5;x+=0.1) printf(‘‘%f\n’’, x) ; for(x=0.0;x<1.0;x+=0.1) printf(‘‘%f\n’’, x) ; return 0 ; return 0 ; } } この2つのプログラムは, それぞれ, 0.1 の4回, 9回の和を計算するという意図を持つ. それぞれのプログ ラムは正しく実行できるだろうか? Solaris 2.6 上の gcc 2.95.1 を用いて実行すると, それぞれ 0.000000 0.100000 0.000000 0.100000 0.200000 0.300000 0.200000 0.300000 0.400000 0.400000 0.500000 0.600000 0.700000 0.800000 0.900000 1.000000 という出力を得る. 5回の和は期待通りに動作しているが, 10回の和は余分な動作が含まれている. Example 6.10.18 while 文を用いて 0.1 の和を計算する. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 271 #include <stdio.h> #include <stdio.h> int main(int argc, char **argv) { int main(int argc, char **argv) { double x=0.0 ; while(x != 0.5) { double x=0.0 ; while(x != 1.0) { x += 0.1 ; printf(‘‘%f\n’’, x) ; x += 0.1 ; printf(‘‘%f\n’’, x) ; } } return 0 ; return 0 ; } } 同様に Solaris 2.6 上の gcc 2.95.1 を用いて計算すると, それぞれ 0.000000 0.000000 0.100000 0.200000 0.100000 0.200000 0.300000 0.400000 0.300000 0.400000 0.500000 0.500000 0.600000 0.700000 0.800000 0.900000 1.000000 1.100000 . . . となり, 右のプログラムは終了しない. これら2つのプログラムは, 浮動小数点数の誤差に起因し, 正しく条件判断が行われていないことが原因と なる. これらを正しく動作させるにはいくつかの方法がある. Example 6.10.19 繰り返し回数を整数型変数を用いて制御する. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 272 #include <stdio.h> #include <stdio.h> int main(int argc, char **argv) { int main(int argc, char **argv) { int i double x=0.0; int i=0 ; double x=0.0 ; for(i=0;i<10;i++) { x += 0.1 ; while(i != 10) { x += 0.1 ; i += 1 ; printf("%f\n", x) ; printf("%f\n", x) ; } return 0 ; } return 0 ; } } Example 6.10.20 浮動小数点数の誤差を見込んで制御する. #include <stdio.h> #include <math.h> #define EP 1.0E-12 #include <stdio.h> #include <math.h> #define EP 1.0E-12 int main(int argc, char **argv) { int main(int argc, char **argv) double x=0.0 ; { double x ; while(fabs(x)< 1.0+EP) { printf("%f\n", x) ; for(x=0.0;fabs(x)<= 1.0+EP;x+=0.1) x+=0.1 ; printf("%f\n", x) ; return 0 ; } return 0 ; } } このように, 繰り返し文で浮動小数点数の計算を行うには, 浮動小数点演算の誤差を考慮したプログラムを 書かなければならない. 6.10.3 ラベル文 はじめにラベル文の構造を見ておこう. ラベル文は以下のいずれかの構造を持つ. 識別子 : 文 cast 定数式 : 文 default : 文 case, default ラベルは switch 文で用いられる. case ラベルの定数式は整数式でなければならない. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 6.10.4 273 ジャンプ文 ジャンプ文とは, 無条件または条件付きで, プログラム制御を他の部分に移すために用いられる. ジャン プ先には, ラベル文で定義されたラベルを指定できる場合がある. 6.10.4.1 goto 文 goto 文は以下のような構文を持つ. goto ラベル このラベルは, goto 文がある関数内にあるラベルでなくてはならない. goto 文はプログラムの流れを混乱させることが多いため, C 言語ではほとんど用いられない. もし goto 文を使うようなことがあれば, プログラム全体をもう一度見直して書き直した方が良い. 6.10.5 return 文 return 文は以下のような構文を持つ. return 式 関数実行中に return 文に出会うと, プログラム制御は直ちに関数の実行をやめ, 関数を呼び出した部分 に戻る. この時, 関数が戻り値を持つと定義されているときには, (式)で指定した値を関数の戻り値とす る. void 型の戻り値を持つ関数には, 式を持つ return 文は使えない. また, void 型以外の戻り値を持つ 関数で, 式を指定しない return 文によって関数を終了したときの戻り値は不定となる. Example 6.10.21 以下の例はプログラム呼び出しの引数が無い場合に, プログラムをすぐに終了するも のである. 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.22 C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 274 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 文は以下の構文を持ち, 繰り返し文の中にだけおくことが出来る. continue ; continue 文に出会うと, それを含んだ最小のループを直ちに実行させる. この時, while 文, do 文の場 合は, 評価に移り, for 文の場合には, 第三式の評価に移る. Example 6.10.23 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.24 continue 文と break 文を用いると, 1 から 10 までの偶数の和を計算するものを以下 のように書くことが出来る. int i = 0, j = 0 ; while(++i) { if (i&1) continue ; j += i ; if (i == 10) break ; } C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 275 Example 6.10.25 次の例は, 0 から 9 までの整数を順に印字する. int n=0 ; for(;;) { printf("%d\n", n) ; if (++n == 10) break ; } Remark 6.10.26 しかし, この3つの Example のようなコードを書くのはやめよう. あくまで, break 文 と continue 文の例として書いたに過ぎない. 6.10.8 条件文 条件文とは, 条件によって実行を分岐させるための制御文である. 6.10.8.1 if 文 最も簡単な条件文は if 文である. if 文は以下のような構文を持つ. if (式) 文 ここで, 式は省くことはできない. ここで, 式は算術型もしくはポインタでなくてはならない. このような if 文は, 次のように制御される. • 式が 0 でなければ, 文が実行される. 6.10.8.2 if–else 文 if–else 文は以下のような構文を持つ. if (式) 文1 else 文2 ここで, 式は省くことはできない. ここで, 式は算術型もしくはポインタでなくてはならない. このような if–else 文は, 次のように制御される. • 式が 0 でなければ, 文1が実行される. • 式が 0 であれば, 文2が実行される. if–else は繰り返して使うことができ, 次のような形になる. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 276 if (式1) 文1 else if (式2) 文2 else if (式3) 文3 else 文4 この形では, 式1から順番に式が評価される. 式1が 0 であれば, 式2を評価するという順番で, 条件式 が評価されていく. Example 6.10.27 次の例は n が 0 か正か負かを判定している. int n; if (n == 0) printf("n is equal 0\n") ; else if (n < 0) printf("n is negative\n") ; else printf("n is positive\n") ; Remark 6.10.28 上の 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 が等しいときの分岐を表すことになる. 条件式を簡潔に書くことは, プログラムを見易くするために重要なことであるが, 条件式を余りに簡潔に しすぎると思わぬバグを産む可能性がある. if–else 文には曖昧な構文がある. Example 6.10.29 次の例は else がどちらの if に結び付いているかわかりにくくなった例である. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 277 if (n > 0) if (a > b) z = a ; else z = b ; ここで, 文法的には else は2番めの if に結び付いている. このような表現を避けるため, if–else で制御される文は {} をつけて複文にすることが望ましい. if (n > 0) { if (n > 0) { if (a > b) z = a ; } if (a > b) z = a ; else z = b ; 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.30 この例では, n の値によって, 結果を印字しようとしているが, 期待通りには動かない. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 278 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 の時, n は 1 だよ n は 0 だよ n は -1 だよ n は 1, 0, -1 のどれでもない という結果をうち出す. このように switch 文の中で, 一つの case ラベルを実行し, 次の case ラベルに制御が移ってしまうこと を “Fall Throught” と呼ぶが, Fall Throught を使う仕様はバグの原因となる. このようなことを避けるためには, break 文を使う. Example 6.10.31 上の Example を改良した. int n ; switch (n) { default: printf("n は 1, 0, -1 のどれでもない\n") ; case 1: break ; printf("n は 1 だよ\n") ; case 0: break ; printf("n は 0 だよ\n") ; break ; case -1: printf("n は -1 だよ\n") ; break ; } この改良により, この switch 文ではどれか一つのラベルに対する文しか実行しなくなる. 出来れば default ラベルを持つ文は一番最後につけよう. その方がわかりやすい. Example 6.10.32 switch 文の Fall Throught を効果的に使う例. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 279 int c ; switch (c) { case ’y’: case ’Y’: printf("OK\n") ; break ; default: printf("NG\n") ; break ; } この例では, 変数 c はキーボードからの1文字の入力を想定している. そのとき, 入力が y または Y の時 に OK を出力し, それ以外の時には NG と出力する例である. ここで, y を入力した際に Fall Throught を 利用して Y の入力の処理に移行するようにしてある. 本来ならば, y, Y, n, N 以外の入力の場合には, 再度入力を促すように書いた方が望ましい. そのようなプログラムを switch 文を使って書くのであれば, #include <stdio.h> int c, flag ; int main(int argc, char **argv) { flag = 0 ; do { printf("Input Y or N: ") ; c = getchar() ; getchar() ; switch(c) { case ’y’: case ’Y’: printf("OK\n") ; break ; case ’n’: case ’N’: printf("NG\n") ; break ; default: flag = 1 ; } } while(flag) ; return 0 ; } という書き方が思いつく. 変数 flag はループ脱出のためのフラグとして用いられている. また, 入力は「一文字入力 した後に を入力する」ため, 実際には2文字の入力が行われている. その2文字目(理想的には のコード値が入 力される)を破棄するために getchar 関数を2度呼び出している. 6.10.8.4 if 文と浮動小数点演算 if 文の場合にも, 繰り返し文と同様に, 浮動小数点演算の誤差に注意しなければならない. Example 6.10.33 1.0 から 0.1 を 0.0 になるまで繰り返し減算する. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 280 #include <stdio.h> int main(int argc, char **argv) { double x=1.0 ; while(1) { x -= 0.1 ; printf("x = %f\n", x) ; if (x == 0.0) break ; } return 0 ; } Solaris 2.6 上の gcc 2.95.1 では正常に動作しない. これは, 次のように書換えてみると現象を良く理解 できる. #include <stdio.h> int main(int argc, char **argv) { double x=1.0 ; while(1) { x -= 0.1 ; printf("x = %.16f\n", x) ; if (x == 0.0) break ; } return 0 ; } この場合, 0.0 になっていると思われる時の前後の出力は, x = 0.1000000000000001 x = 0.0000000000000001 x = -0.0999999999999999 となり, 正しく 0.0 にはなっていない. この例を正しく動作するように直すには, C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 281 #include <stdio.h> #include <math.h> #define EP 1.0E-12 int main(int argc, char **argv) { double x=1.0 ; while(1) { x -= 0.1 ; printf("x = %.16f\n", x) ; if (fabs(x) < EP) break ; } return 0 ; } とすればよい. 6.10.9 演習問題 ここの演習問題は, ここまでに学んだ繰り返し文や条件文を使って書くことができる. はじめに演習問題をやるために必要な関数について注意しておく. 6.10.9.1 必要な関数と簡単な例 ここで述べた関数については, 詳しくはオンライン・マニュアルを参照すること.33 6.10.9.1.1 getchar 標準入力から1文字を入力するための関数は, getchar である. int getchar() 利用法は, 以下の通り. Example 6.10.34 標準入力から1文字を読んで, それを出力する. プログラムを終了するには, Control + D を入力する. int c ; while ((c = getchar()) != EOF) { printf("%c",c) ; } ここで, getchar の戻り値として, int 型の変数で受けていることに注意. char 型ではない. また, EOF とは, ファイルの終りを表す特別な文字である. 33 オンライン・マニュアルで, 期待のものが出てこない時には, man -s 3s getchar などと -s 3s を入れてみると良い. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 282 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.35 乱数を2つ発生させる. long n,m ; srandom((unsigned int)time(NULL)) ; n = random() ; m = random() ; srandom の部分は, 乱数を初期化する部分で, 現在の時刻から決まる数を使って初期化をしている. C言語中で乱数を発生させる場合には, 関数 random を呼び出すことが標準的な方法である. この場合, srandom 関数を用いて乱数を「初期化」しない限り, 毎回同じ系列の乱数が発生してしまう. すなわち, srandom 関数を用いないと, random() によって得られる値は, 毎回同じ値の列となる. また, C言語中では rand という乱数発生関数も存在するが, rand よりも random の方がより望ましい 乱数を発生する.34 34 計算機上での乱数は, 真の意味の乱数ではなく, 極めて長い周期をもつ周期的な数列である. そのため, 乱数発生方法とその利用 法に適切な工夫が必要となる. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 6.10.9.2 283 演習問題 Exercise 6.10.36 摂氏と華氏の温度対応は, ◦ C = (5/9)(◦ F − 32) である. 華氏の温度(整数)を入力し て, 摂氏の温度を印字するプログラムを書け. その際, 摂氏の温度として, 小数点以下切捨て, 小数点以下四 捨五入, 小数点以下第2桁めを四捨五入の3種類を書け. Exercise 6.10.37 標準入力から入力された1文字が英字でなければ, そのまま印字し, 大文字の英字なら 小文字に, 小文字の英字なら大文字を印字するプログラムを書け. ただし, 改行だけが入力された場合には, 終了するようにしなさい. また, 入力された文字は ASCII コードで表現されていることに注意せよ. Exercise 6.10.38 unsigned long 型の数値を入力して, それを2進数で表示するプログラムを書け. Exercise 6.10.39 0 から 28 − 1 までの乱数を十分沢山発生させ, その値が 27 以下の確率を表示するプロ グラムを書け. Exercise 6.10.40 次のコードの最後の比較は, どうなるかを考察せよ. 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.41 次のプログラムの出力結果がなぜそのようになるかを考えよ. C4.tex,v 1.13 2003-04-20 14:56:43+09 naito Exp 数理解析・計算機数学特論 284 #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 言語では多くの標準的な関数が用意されている. 一方, 我々が作るプログラ ムにおいても, 関数を利用することによって, プログラムの構造をわかりやすくできる利点がある. C 言語 の標準的な関数にはどのようなものがあるかは, [2, B] に書かれている. 6.11.1 関数の定義 6.11.1.1 関数の定義と例 C 言語では関数は0個もしくは1個以上の引数(ひきすう) (parameter) を持ち, 0個もしくは1個の 戻り値(もどりち) (return value) を持つ. 引数, 戻り値は, 数学における関数の独立変数, 関数の値に 相当すると考えられるが, C 言語の関数は, 引数が関数を呼び出した後に変化することも多い.35 関数は, 以下の形をしている. <戻り値の型> <関数の識別子> ( <関数の引数> ) 関数の本体となる複文 35 このようなことを副作用と呼ぶ. C5.tex,v 1.19 2001-07-19 17:06:07+09 naito Exp