• 検索結果がありません。

note.dvi

N/A
N/A
Protected

Academic year: 2021

シェア "note.dvi"

Copied!
193
0
0

読み込み中.... (全文を見る)

全文

(1)

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 の方が望ましいと考えられている.

(2)

多くのプログラムソースを書いた時に, それぞれのファイルがどのような内容のものかがわからなくなるこ とが良くある. また, プログラムソースを見やすくするために, 「字下げ」をきちんとすること. 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 という名前で作成することを指示している. もし, -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 -O hello.c -o hello

ここで, 新しく付け加えたオプションの意味は次の通りである. -Wall 文法エラーではない「警告」を軽微なレベルまで出力する. -ansi -pedantic ANSI規格の C としてコンパイルする. -O オブジェクトコードの最適化を行う.

2これは mule の “c-mode” の特徴である.

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

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

(3)

これらのオプションの詳細については, Section 6.21 を参照せよ. 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 という名前の特別な関数がはじめに実行され, そこに記述されている順序にしたがって実行される.

6.5.2 C で利用できる文字

C では, 英文字, 数字, 空白文字(スペース, タブなど), 記号文字, 改行文字などが利用できる. 記号文 字には特別な意味があることが多いので, 注意すること. また C では, 大文字と小文字は区別される. C の コンパイラにおいて, 日本語が利用できるといっても, 変数名などに日本語が利用できるわけではないので 注意すること. 日本語が利用できるのは, 文字列に日本語が利用できるという程度の意味であり, 今回利用 する処理系では, このコードは EUC でなければならない. また, 以下のものは全て空白と見なして無視される. • 空白文字, 改行文字, タブ, 改ページ記号, コメント. 行末に \ を書くと, 行の連結を表し, 1 行として扱われる. 6.5.2.1 行 C 言語では「行」という概念は存在しない. 改行文字は空白文字と見なされるが, 改行文字の直前に \ が ある場合には, 行の連結を表し, 処理系によって \ と改行文字の連続は一つの空白文字に置き換えられる.

6.5.3 コメント

コメント (comment) とは, プログラミングの補助となるようソースプログラム中に書かれた注釈部分 のこと. コンパイラは単にこれを無視するので, プログラムには影響を与えない. コメントは, /* ではじ まり, */ で終り, 入れ子にはできない. 即ち, コメントの中にコメントを入れることはできない. また, 文字定数, 文字列の中にはコメントを書くことはできない.

(4)

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 型 • l または L: long double 型 文字定数とは, ’ で括られた文字の列である. 例えば a という文字を表すには ’a’ と書く. 以下の特別な文字を表す以外には, 文字定数は一文字でなくてはならない. • \n 改行 • \r 復帰 • \f 改ページ • \t 水平タブ

(5)

• \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つの名前は区別されるとは限らない. また, C 言語には名前空間, スコープという概念があり, 同じ名前を与えても, 違うものを示しているこ とがあるので注意すること. これについては後ほど解説する. 5「リテラル」 (“literal”) とは, 「文字通りの」という意味である. 6正確には, • 内部識別子またはマクロ名においては意味のある先頭の文字数は 31 文字であり, 大文字と小文字が区別される. • 外部識別子においては意味のある先頭の文字数を 6 文字に制限して良く, 大文字と小文字の区別を無視しても良い. というのが ANSI の規格 [3, X3010 6.1.2, p. 1856] である. しかし, 最近の処理系で外部識別子が6文字に制限されたり, 大文字と 小文字の区別を無視するようなものは見当たらない.

(6)

6.5.4.4 キーワード

以下の単語はキーワード (keyword) と呼ばれ, 特別な意味を持ち, 識別子としては利用できない. char double int long enum float short

unsigned signed void typedef auto register extern static volatile union struct const sizeof if else switch case default break return for while 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) と呼ぶ.

(7)

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 */

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.7

最も簡単なプログラム

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

(8)

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

/* Program 1 *

* Hello World. を出力する. *

* */

#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つの書き方がある. コンパイラの実装によって決まる標準的な場所7にあるファイルを挿入するには前 者の方法を使い, カレント・ディレクトリにあるファイルを挿入するには後者の方法を使う. どのような関数が, どのヘッダ・ファイルで宣言されているかはオンライン・マニュアルを見ればわかる.

2行目

int main(int argc, char **argv)

(9)

これは main という関数の定義である. この関数の本体は3行目の { と6行目の } に囲まれた部分である. この部分は次の3つの部分に分解される.

• int • main

• (int argc, char **argv)

はじめの int は, この関数の戻り値が int 型であることを示す. 関数の戻り値が書かれていない時には, コンパイラは int であると解釈する.

次の main は関数の識別子である.

ここで使われている (int argc, char **argv) に関しては後に議論するので, 取りあえずここでは「お 約束」としておこう. ここには, (存在すれば)その関数の引数が書かれる. 引数をとらない場合にも () または (void) と書かなくてはならない. プログラムが開始されるときには, その時点で呼び出される関数の名前は main でなければならない. す なわち, main という名前を持つ関数が, プログラム開始時点で最初に呼び出され実行される.

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 の場合は

8ここの解説は本当は正しくない。この関数はもっと多くの引数をとり、最初の引数も文字列リテラルである必要はない.

(10)

% echo $status を実行することで得ることができる。

6.8

変数とは

変数 (variable) とは, 識別子によって区別された初期化, 代入などが許される記憶領域のことである. C 言語の変数には, 多くの型があり, それぞれの型によって, どれだけの記憶領域が確保されるかが異なる. ま た, C 言語の変数には記憶クラス, スコープ, 寿命, リンケージなどの概念があるが, それらについては関数, 分割コンパイルの後で述べる. ここでは, 変数の宣言, 型などについて考える.

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 では変数は, 定義されただけでは値は定まらない(と考えた方が良い)10. そのため, (必要なら)そ の変数を使う前に初期化を明示的に行なう必要がある. 変数の初期化の方法には2通りある. 10[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.)

(11)

...

int a=0, b ;

int main(int argc, char **argv) { b = 0 ; ... } このように, 宣言と同時に初期化することもできる. a の初期化は実行時にただ一度だけ行なわれるが, bの場合は, この文を実行されるたびに b に 0 が代入される. C においては, 変数の宣言と定義は異なり, 宣言だけではメモリ領域が確保されない. これに関しては, extern宣言を参照. 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 で定義されている変数の型は以下の通りである.11 変数の型 型の名前 char 文字型

short int 短い整数型 short と書いても良い

int 整数型

long int 長い整数型 long と書いても良い float (単精度)浮動小数点型

double 倍精度浮動小数点型 long double 長い倍精度浮動小数点型

void 何もない型

enum 列挙型

(12)

また, 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宣言をした変数を変更した時の振る舞いは不定である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とする. すなわち,

12より正しくは, 「const 修飾型を持つオブジェクトを, 非 const 修飾型の左辺値を使って変更しようとした場合, その動作は未

(13)

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])そこには,関数呼び出しの制約として,「各実引数は,対応する仮引数の型の非修飾版を持つオブジェクトに

その値を代入できる型を持たなければならない」と書かれている. つまり,引数を渡すと代入が行われ,実引数と仮引

数は代入が許される関係になければならないということである. したがって,

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 オブジェクト ; ここで, その結果として得られる値は, 符号なし整数で表現され13, その意味は, char 型の何倍の記憶領 域が確保されるかを表す. 即ち, sizeof(char) の結果は処理系によらず 1 である. それでは, char 型がどれだけの記憶領域を使うかを知るには, どのようにすれば良いのだろうか. それ には, C 言語の標準的なヘッダ・ファイルを見れば良い. 実際, limits.h に定義されている CHAR BIT と いうマクロ14の値が char 型のビット数である. Sun Sparc Station の C コンパイラの場合,

#define CHAR_BIT 0x8

13正確には size t 型で表現される. size t 型がどの型に対応するかは処理系依存である.

(14)

となっているので, char 型は8ビット(1バイト)であることがわかる.15 また, int 型の長さは, その計 算機の自然な長さであると定義されている.

それぞれの変数が記憶領域に確保される時, 宣言した順序で記憶領域内に確保されるという保証はない. また, 多くの処理系では, int, long はワード境界にアロケートされる.

Example 6.8.2 int が2バイト, long が4バイトの処理系で,

char c ; int n ; long l ; char d ; と変数を定義した場合, 下の図のいずれのメモリ配置をとるかは処理系や最適化に依存する. これ以外の取 り方をする可能性もある. 16 bits c (padding) n l d 16 bits c d n l 16 bits c n n(続き) l d (a) (b) (c) この中で (c) のメモリ・アロケーションはアライメント (alignment, 境界調整) に適合していない環境が 多いため, ほとんどこのようなアロケーションは行われない. Example 6.8.3 変数に値を代入する操作とは, 変数に対して与えられたメモリに数値を書き込むことに他 ならない. 例えば, (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 になって いる.

(15)

6.8.3.3 文字型と整数型 文字型 (character type) とはその名の通り, (1)文字を変数として扱う型である. 例えば, char c ; c = ’a’ ; とすると, 変数 c には a という文字が代入される. 文字型では, その中身を文字の持つコードの値とし て扱う. したがって, 文字型変数の実体は “整数” と思って良い. (しかしながら, char 型を文字として扱 う時には, 常に正の数値として扱う.)その意味で, char は整数型 (integer type) (short, long なども 含む)の一部として考えると都合が良いことが多い. Example 6.8.4 例えば, ASCII コード体系の処理系で, char c ; c = ’a’ ; とすると, 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 の返す値)を見てみると, 今回利用する処理系では, 次のように なる. short 2 int 4 long 4 これで, char 型が1バイトであることを使うと, int は4バイトであることがわかる. 6.8.3.3.1 整数の内部表現 整数型の変数の内部での表現は, Section 2.3.2 で述べた表現がとられている ことが多い. ANSI 規格では整数型の内部表現の具体的な方法については何も規定していない.

(16)

6.8.3.4 浮動小数点型

浮動小数点型 (floating type) についても, C では以下のことしか定義されていない. float≤ double ≤ long double

実際, それぞれの浮動小数点型の長さ(sizeof の返す値)を見てみると, 今回利用する処理系では, 次の ようになる. float 4 double 8 long double 8 これで, char 型が1バイトであることを使うと, float は4バイトであることがわかる. (10を底とする)浮動小数点数とは, 以下のような型で表現された数のことである. (0でない1桁の数).<仮数部> × 10^<指数部> ここで, 任意の 0 でない実数は, このような表示が可能であることに注意せよ. (もちろん, その表示は有 限小数と仮定すれば一意的である.) 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) (または汎整数拡張)と呼ばれる操作が行なわれることが ある.

(17)

それは, 以下のように定義されている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 の演算レ ジスタ長よりも短いメモリサイズを持つ変数に対する演算を行う場合, 何らかの形で演算レジスタ長に合 う値(ビットパターン)に変換を行ってから演算を行う必要がある. 符号拡張・整数への格上げは, 演算レ ジスタ長に値を合わせる変換と理解して良い. Example 6.8.5 標準演算レジスタ長が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") ; 16[1] の定義によれば, 「符号なし型はより広い符号なし型に変換される」とされているので注意すること. 17C の定義によれば, 「標準文字セットのすべての文字は正の値を持つ」となっている.

(18)

としなければならない. すなわち, 文字型変数を「符号なし」と明示的に指定し, 符号拡張の影響を排除 しなければならない. 6.8.4.4 整数への変換 任意の整数が符号つき型に変換される時, その数が新しい型で表現可能ならば, その値は不変になるが, そうでない時の結果は処理系依存である. 6.8.4.5 整数と浮動小数点数 浮動小数点数を汎整数に変換する時には, 小数部は無視される. また, 結果として得られる整数が目的の 型で表現できない時の振舞いは不定である. 逆に, 整数を浮動小数点数に変換する時には, その結果が表現可能な範囲にある時でも, 正確に表現がで きない時には, 一番近い大きな数か小さな数のどちらかに変換される. 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 として計算する.

(19)

要するに, 「算術演算で異なる型の値を指定すると, 型変換が行われる. 変換は情報が欠落しない限り, 実 数, 高精度, 符号付きの方向で行われる」ということである18.

Example 6.8.6 例えば, 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.7 int が2バイトである時, unsigned int a = 256 ; (a * a * 1L) == (a * (a * 1L)) ; /* この式は正しくない. */ ということが起こる. Example 6.8.8 次の例は, 符号拡張, 算術変換などの例である. 18この文章は [6, p. 53] から引用.

(20)

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.9 次のコード19は, ANSI C と K&R C では異なった結果を出力する20.

19[6] からの引用

20実は, K&R の規約では, unsigned char は存在しない. unsigned, short, long は int につく限定詞と定義されている. しか し, 古い ANSI 規格ではない処理系の多くで unsigned char が利用できる.

(21)

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 に変換されたビットパターンとして比較される21. 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 加法演算子 加法演算子には +, - がある. これらは, 左から右に作用する. 被演算数が算術 的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 だけ増やされる(++ の場合).

21しかし, 正しくは K&R C には unsigned char は規定されていない.

22加法演算子は, 後述するポインタにも作用する.

23もちろん, /, % の第2被演算数は 0 であってはならない. /, % の第2被演算数が 0 の場合には結果は不定となる. 一般には「 0

(22)

Example 6.8.10 インクリメントの例は以下のものである. 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項演算と各項の評価をどの順序で行なうかは, 不定であるので注意すること24. Example 6.8.11 次のような式を考えよう. 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 ; 24このような評価式は ANSI 規約 [3, X3010 6.3, p.1873] の「式」の規定で, 「直前の副作用完了点から次の副作用完了点までの 間に, 式の評価によってオブジェクトに格納された値を変更する回数は高々1回でなければならない. さらに, 変更前の値は, 格納さ れる値を決定するためだけにアクセスしなければならない. 」とある. したがって, y = (x++) - (x++) が不定であるだけでなく, i = ++i + 1も不定であることに注意しよう.

(23)

これらは, それぞれ, a の値を 1 加えた(減らした, 掛けた, 割った)値を再び, a に代入する演算である. Example 6.8.12 代入の例は以下のものである. 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 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 で割ることに等しい. そうでない時には, 結果は処理系依存である.

(24)

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.13 <<, >> の演算の例は, 以下の通りである. 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 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 比較演算子, 等値演算子は被演算数が算術型の時には, 通常の算術変換を行う. 比較演算子は被演算数が 算術型の時, 算術変換をした後, その型に適合した数値としての比較を行う.

(25)

等値演算子は, == で表される. 式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.14 論理 AND, OR 演算子の例は以下の通りである.

(26)

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.15 論理 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 では, 評価が全て行なわれる前に式の値が決定することがあり, その時点で式の評価が終了 するので, 注意しなくてはならない.

(27)

6.8.5.7 3項演算子 3項演算子 ? : は条件演算子とも呼ばれる演算子である. 式1 ? 式2 : 式3 まず, すべての副作用を含めて, 式1が評価され, それが 0 でなければ結果は式2の値となり, そうでな ければ式3の値となる. この時評価されるのは式2または式3のいずれかである. 式2, 式3の被演算数が 算術型であれば, 通常の算術変換が行われて, それらは共通の型となり, それが結果の型となる. 3項演算子は if-else 文で書くと長くなる場合に, それを短く表現する手法として使われるが, すべて の条件分岐を表現できるわけではないことに注意. Example 6.8.16 2つの変数の小さくない方の値を代入する. max = (a > b) ? a : b ; Example 6.8.17 例えば, 次の 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 演算子の優先順位と結合規則 これら演算子の優先順位, 結合規則は, 次の通りである. 上ほど優先順位が高い.

(28)

演算子 結合規則

( ) [ ] -> . 左から右

! ~ ++ -- + - * & (type) sizeof 右から左

* / % 左から右 + - 左から右 << >> 左から右 < <= > >= 左から右 == != 左から右 & 左から右 ^ 左から右 | 左から右 && 左から右 || 左から右 ?: 右から左 = += -= *= /+ %= &= ^= |= <<= >>= 右から左 , 左から右 ただし, 単項の + - * は二項のそれよりも上である. 最も注意しなければならないのは, &, | と == の優先順位である. Example 6.8.18 int 型の変数 n が偶数か奇数かを判定するために, if (n & 1 == 0) としたとしよう. プログラマは, if ((n & 1) == 0) の意味で書いているかもしれないが, 実際には if (n & (1 == 0)) と解釈される. Example 6.8.19 括弧は不要な場合であっても, 括弧を書くことにより, 意味が明快になる場合がある. こ の例は, 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.20 = の結合規則は「右から左」であるので,

(29)

は, b = c の代入が行われ, その結果の値(この場合は代入された b の値)が a に代入される. すなわち, a = (b = c)という意味となる. ただし, (a = b) = c は (a=b) が左辺値とならないので, 文法エラーと なる. Example 6.8.21 == の結合規則は「左から右」であるので, a == b == c ; は, 比較 a == b が行われ, その結果の値と c との比較が行われる. すなわち, (a == b) == c という意味 となる. この場合は, (a == b) == c は文法上正しい構文である. Example 6.8.22 式 a < b < c の意味は数学的な不等式ではなく, もし, b が a よりも大きければ, 1 と cを比較し, そうでなければ, 0 と c を比較していることに注意. Example 6.8.23 数学的には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.24 次のような例がある. char a ; int b ; a = 0x01 ; b = (int) a ; この場合は, char がより広い型の int に変換されている. しかし, この型変換は整数への格上げそのも のである. このように, 整数への格上げがあっても, 明示的に型変換をすることが望ましい.

(30)

次のような例もある. Example 6.8.25 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 その利用法は, 次の通りである. 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 などと, 変数を対応させ, 表示させることができる.

(31)

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 を用いる.

f double 型の実引数を [-] dddd.dddd の10進表記にする. 精度が省略されたときには 6 である と解釈する. また, 最終桁は適切な桁数への丸めを行う.

(32)

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=a a=61 b=-1,c=10 b=-1,c=+10 b=-001,c=010 b=-001,c=010 c=10 d=2 d=00002 d=2 d=002 d=0x002 s=effff9c8 x=0.000100 y=1.000000e-05 x=0.0001 y=1e-05 z=1.000000E+00

(33)

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 ; } 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 次のプログラムの出力結果がなぜそのようになるかを考えよ.

参照

関連したドキュメント

問についてだが︑この間いに直接に答える前に確認しなけれ

そのほか,2つのそれをもつ州が1つあった。そして,6都市がそれぞれ造

注:一般品についての機種型名は、その部品が最初に使用された機種型名を示します。

えて リア 会を設 したのです そして、 リア で 会を開 して、そこに 者を 込 ような仕 けをしました そして 会を必 開 して、オブザーバーにも必 の けをし ます

で実施されるプロジェクトを除き、スコープ対象外とすることを発表した。また、同様に WWF が主導し運営される Gold

大阪府では、これまで大切にしてきた、子ども一人ひとりが違いを認め合いそれぞれの力

「就労に向けたステップアップ」と設定し、それぞれ目標値を設定した。ここで

一︑意見の自由は︑公務員に保障される︒ ントを受けたことまたはそれを拒絶したこと