6 C 言語入門 - Welcome - Grad. Sch. of Math., Nagoya Univ.•°理解析・計算機数学特論...

193
数理解析・計算機数学特論 217 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.13 2002-03-20 11:15:18+09 naito Exp

Transcript of 6 C 言語入門 - Welcome - Grad. Sch. of Math., Nagoya Univ.•°理解析・計算機数学特論...

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

第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.13 2002-03-20 11:15:18+09 naito Exp

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

多くのプログラムソースを書いた時に, それぞれのファイルがどのような内容のものかがわからなくなることが良くある.また, プログラムソースを見やすくするために, 「字下げ」をきちんとすること. Mule で XXX.c という

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

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

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

1. エディタ (Mule など)を利用して, プログラムソースを書く.

2. コンパイラを起動して, 実行コードを作成する.

3. 作成した実行コードを実行する.

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

% gcc hello.c -o hello

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

% ./hello

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

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 という名前になるものがある. これら実行形式の名前の違いは, 実行形式の違いでもある.

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

C1.tex,v 1.13 2002-03-20 11:15:18+09 naito Exp

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

これらのオプションの詳細については, 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) とは, プログラミングの補助となるようソースプログラム中に書かれた注釈部分のこと. コンパイラは単にこれを無視するので, プログラムには影響を与えない. コメントは, /* ではじ

まり, */ で終り, 入れ子にはできない. 即ち, コメントの中にコメントを入れることはできない.また, 文字定数, 文字列の中にはコメントを書くことはできない.

C1.tex,v 1.13 2002-03-20 11:15:18+09 naito Exp

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

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 水平タブ

C1.tex,v 1.13 2002-03-20 11:15:18+09 naito Exp

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

• \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文字に制限されたり, 大文字と小文字の区別を無視するようなものは見当たらない.

C1.tex,v 1.13 2002-03-20 11:15:18+09 naito Exp

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

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) と呼ぶ.

C1.tex,v 1.13 2002-03-20 11:15:18+09 naito Exp

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

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))の指定によって振舞いが変わることが多い. 未規定 (unspecifiedbehavior) とは, 規格がその動作を一切指定しないものを指す. この他に, 処理系依存の項目の一部として,文化圏固有動作 (locale-specific behavior) がある. これは, 処理系そのものに依存するのではなく, 処理系動作またはプログラム動作中に与えられた文化圏情報 (locale) ごとに, 処理系が明示的に動作を規定するものである.

Example 6.6.1 未規定の動作の例としては, 関数の実引数の評価順序. 不定(未定義)の動作の例としては, 整数演算のオーバフローに対する動作. 処理系定義の例としては, 符号付き整数を右シフトした場合の最上位ビットの伝播. 文化圏固有動作の例としては, islower 関数が26個の英小文字以外の文字に関して, 真を返すかどうかがある.

ANSI の規格にしたがった処理系とは, ANSI の規格で規定されているすべての動作を受理しなくてはならない.

6.6.2 その他

ANSI 規格で定められているその他の用語として, バイト, 文字, オブジェクトがある. バイト (byte) とは, 実行環境中の基本文字集合の任意の要素を保持するために十分な大きさを持つデータ記憶領域の基本単位と定められ, 1バイトのビット数は処理系依存, バイトは連続するビット列からなる. 文字 (character)とは, 1バイトに収まるビット表現. オブジェクト (object) とは, その内容によって, 値を表現できる実行環境中の記憶領域. ビットフィールド以外のオブジェクトは, 連続する一つ以上のバイトの列からなる. また, オブジェクトを参照する場合, オブジェクトは特定の型を持っていると解釈して良い.

6.7 最も簡単なプログラム

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

C2.tex,v 1.7 2002-03-04 14:40:22+09 naito Exp

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

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)7これは, コンパイル時のオプションで変更可能

C2.tex,v 1.7 2002-03-04 14:40:22+09 naito Exp

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

これは 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ここの解説は本当は正しくない。この関数はもっと多くの引数をとり、最初の引数も文字列リテラルである必要はない.9main 関数が明示的な戻り値を持たない場合には, シェル(ホスト環境)に返される値は不定となる.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

% 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 thereis no explicit initializer have undefined (i.e., garbage) values.)

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

.....

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 列挙型

11void, enum, long double は ANSI の規格ではじめて定義された. Kernighan-Ritchie の初版 [1] では定義されていない.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

また, 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 修飾型の左辺値を使って変更しようとした場合, その動作は未定義とする.」というのが ANSI の規定.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

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 という代入が行われることに相当し, 上で述べた単純代入の規約を満たす. しかし, constchar ** の例では, 仮引数 p は「const char * へのポインタ」であり, 実引数 argv は「char * へのポインタ」であるため, 単純代入の規約を満たさない.なお, char * を仮引数とする多くの標準ライブラリ関数(例えば, strcpy など)は, 仮引数として const char *

を宣言している. これは, 関数内で明示的に仮引数の指し示す値を変更しないための措置である.

6.8.3.2 変数と記憶領域

変数は宣言と同時に対応する記憶領域が確保される. しかしながら, それぞれの型に対して, どれだけの記憶領域が確保されるかは処理系依存である.それぞれの型がどれだけの記憶領域をとるかを調べるには, sizeof 演算子を使う. sizeof 演算子の利

用法は以下の通りである.

sizeof (型) ;

sizeof オブジェクト ;

ここで, その結果として得られる値は, 符号なし整数で表現され13, その意味は, char 型の何倍の記憶領域が確保されるかを表す. 即ち, sizeof(char) の結果は処理系によらず 1 である.それでは, char 型がどれだけの記憶領域を使うかを知るには, どのようにすれば良いのだろうか. それ

には, C 言語の標準的なヘッダ・ファイルを見れば良い. 実際, limits.h に定義されている CHAR BIT と

いうマクロ14の値が char 型のビット数である. Sun Sparc Station の C コンパイラの場合,

#define CHAR_BIT 0x8

13正確には size t 型で表現される. size t 型がどの型に対応するかは処理系依存である.14マクロの意味は後日解説する.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

となっているので, 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)nl

d

16 bits

c dnl

16 bits

c nn(続き)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 になっている.

15ANSI の規格書によれば, char 型の占めるビット幅を1バイトと定義している.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

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 2int 4long 4

これで, char 型が1バイトであることを使うと, int は4バイトであることがわかる.

6.8.3.3.1 整数の内部表現 整数型の変数の内部での表現は, Section 2.3.2 で述べた表現がとられていることが多い. ANSI 規格では整数型の内部表現の具体的な方法については何も規定していない.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

6.8.3.4 浮動小数点型

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

float ≤ double ≤ long double

実際, それぞれの浮動小数点型の長さ(sizeof の返す値)を見てみると, 今回利用する処理系では, 次のようになる.

float 4double 8long double 8

これで, char 型が1バイトであることを使うと, float は4バイトであることがわかる.(10を底とする)浮動小数点数とは, 以下のような型で表現された数のことである.

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

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

6.8.3.4.1 浮動小数点数の内部表現 浮動小数点数の変数の内部での表現は, Section 2.3.2 で述べた表現がとられていることが多い. ANSI 規格では整数型の内部表現の具体的な方法については何も規定していない.オーバーフロー, アンダーフローをした数の演算, 比較等の結果がどうなるかは規格では定められていな

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

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) (または汎整数拡張)と呼ばれる操作が行なわれることがある.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

それは, 以下のように定義されている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 の定義によれば, 「標準文字セットのすべての文字は正の値を持つ」となっている.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

としなければならない. すなわち, 文字型変数を「符号なし」と明示的に指定し, 符号拡張の影響を排除しなければならない.

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 として計算する.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

要するに, 「算術演算で異なる型の値を指定すると, 型変換が行われる. 変換は情報が欠落しない限り, 実数, 高精度, 符号付きの方向で行われる」ということである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] から引用.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

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 が利用できる.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

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除算による例外割り込み」が発生する.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

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 も不定であることに注意しよう.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

これらは, それぞれ, 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 で割ることに等しい. そうでない時には, 結果は処理系依存である.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

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

比較演算子, 等値演算子は被演算数が算術型の時には, 通常の算術変換を行う. 比較演算子は被演算数が算術型の時, 算術変換をした後, その型に適合した数値としての比較を行う.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

等値演算子は, == で表される.

式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 演算子の例は以下の通りである.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

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

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

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 演算子の優先順位と結合規則

これら演算子の優先順位, 結合規則は, 次の通りである. 上ほど優先順位が高い.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

演算子 結合規則

( ) [ ] -> . 左から右

! ~ ++ -- + - * & (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 = の結合規則は「右から左」であるので,

a = b = c ;

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

は, 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 に変換されている. しかし, この型変換は整数への格上げそのものである. このように, 整数への格上げがあっても, 明示的に型変換をすることが望ましい.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

次のような例もある.

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 などと, 変数を対応させ, 表示させることができる.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

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 であると解釈する. また, 最終桁は適切な桁数への丸めを行う.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

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

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

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

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

#include <stdio.h>

int x, y, z ;

int main(int argc, char **argv)

{

x = y = z = 1 ;

++x || ++ y && ++z ;

printf("x=%d,y=%d,z=%d\n",x,y,z) ;

x = y = z = 1 ;

++x && ++ y || ++z ;

printf("x=%d,y=%d,z=%d\n",x,y,z) ;

x = y = z = 1 ;

++x && ++ y && ++z ;

printf("x=%d,y=%d,z=%d\n",x,y,z) ;

x = y = z = -1 ;

++x && ++ y || ++z ;

printf("x=%d,y=%d,z=%d\n",x,y,z) ;

x = y = z = -1 ;

++x || ++ y && ++z ;

printf("x=%d,y=%d,z=%d\n",x,y,z) ;

x = y = z = -1 ;

++x && ++ y && ++z ;

printf("x=%d,y=%d,z=%d\n",x,y,z) ;

return 0 ;

}

ヒント: &&, ||, ++ の意味と評価順序を考えよ. これは, 変数の値によっては, 評価が行われない部分があり, このようなコードを書いてはいけない典型的な例である.

Exercise 6.9.4 次のプログラムの出力結果がなぜそのようになるかを考えよ.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

#include <stdio.h>

int x, y, z ;

int main(int argc, char **argv)

{

x = 3 ; y = 2 ; z = 1 ;

printf("%d\n", x | y & z) ;

printf("%d\n", x | y & ~z) ;

printf("%d\n", x ^ y & ~z) ;

printf("%d\n", x & y && z) ;

x = 1 ; y = -1 ;

printf("%d\n", !x | x) ;

printf("%d\n", ~x | x) ;

printf("%d\n", x ^ x) ;

x <<= 3 ; printf("x = %d\n", x) ;

y <<= 3 ; printf("y = %d\n", y) ;

y >>= 3 ; printf("y = %d\n", y) ;

return 0 ;

}

この中には, 処理系依存になっているものがある. どれが処理系依存かを考えよ.

Exercise 6.9.5 次のプログラムの出力結果がなぜそのようになるかを考えよ.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

#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.6 short, int, unsigned int, long のそれぞれの型の変数が何バイトの記憶領域をとるかを表示するプログラムを書け.

C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp

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

Exercise 6.9.7 int, long のそれぞれの型で表現される最大の数に 1 を加えたらどうなるかを考察せよ.

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

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

Exercise 6.9.10 Example 6.8.7 はどうしてかを考察せよ.

Exercise 6.9.11 int が16ビット, long が32ビットの時, -1L < 1U, -1L > 1UL となる. 何故か?

6.10 文

6.10.1 制御文

制御文とは, プログラムの流れを制御する構造で, 以下のものがある.

• 繰り返し文

• 条件文

• ラベルつき文

• ジャンプ文

ここでは繰り返し文と条件文のみを扱う.ジャンプ文の中でも goto 文は BASIC などの言語では多用されるが, C 言語においては, 殆んど必要な

い(はず).繰り返し文には, for 文, while 文, do–while 文がある. 条件文には, if 文, if–else 文, if–else if

文, switch 文がある. ジャンプ文の中で, C で使われるものは break 文, continue 文, return 文がある.

6.10.2 繰り返し文

繰り返し文とは, ある条件の元に文(複文)を繰り返すための制御文である.

6.10.2.1 for 文

繰り返し文の代表例としては, for 文がある. for 文の構文は次の通りである.

for(式1;式2;式3) 文

ここで, 式1から式3はどれを省いても良い. 式2は, 算術型もしくはポインタでなくてはならない.このような for 文は, 次のように制御される.

1. 式1が最初に評価される.

2. 式2は各繰り返しの前に評価される. もし, 式2の結果が 0 となると, for は終りとなる.

3. 繰り返しの部分が終る毎に, 式3が評価される.

ここで, 各式の副作用は, 評価の直後に完了する.

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

Example 6.10.1 この例は, 0 から 9 までの整数を順に印字するものである.

#include <stdio.h>

int i ;

int main(int argc, char **argv)

{

for(i=0;i<10;i++) {

printf("%d\n",i) ;

}

return 0 ;

}

#include <stdio.h>

int i ;

int main(int argc, char **argv)

{

for(i=0;i<10;i++)

printf("%d\n",i) ;

return 0 ;

}

この例では, はじめに i = 0 が実行され, i < 10 である間は, { } の部分が実行される. 各繰り返しが終ると, i++ が実行される. i が 9 になると, 繰り返しは実行されるが, それが終了した後, i++ が実行され,i は 10 となる. したがって, i < 10 が 1 となり, 繰り返しの文は実行されずに, for は終る.

Example 6.10.2 この例は, 式1から式3が省かれたもので, 無限ループを実現している.

#include <stdio.h>

int main(int argc, char **argv)

{

for(;;) ;

return 0 ;

}

式2が省かれた場合は, 常にその値が non-zero と認識される.

Example 6.10.3 この例は, 1 から 10 までの和を計算している.

#include <stdio.h>

int i, j ;

int main(int argc, char **argv)

{

for(i=0,j=0;i<10;i++)

j += i+1 ;

return 0 ;

}

ここで, コンマ演算子が2個所に利用されている. 特に, for 文の第1式のコンマ演算子の利用法に注意.

Exercise 6.10.1 for 文を利用して, 2 から 10 までの偶数の和を計算して印字するプログラムを書け.

Remark 6.10.1 1 から 10 までの和を計算するプログラムでは, for 文を使ったものでも, いろいろな書き方が可能である.

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

j = 0 ;

for(i=0;i<10;i++)

j += i+1 ;

上の例の書き方よりも, この方が望ましい. なぜなら, j=0 と初期化を行う部分が独立し, プログラムの意図が明確になっている.

j = 0 ;

for(i=1;i<=10;i++)

j += i ;

確かに問題なく動作するのだが, C では境界判定条件(この例では i <= 10 )では, i <= 10 と書くよ

りも i < 10 と書く方が(すなわち, 直前の例の方が)標準的である. 可能な限り標準的な C の書き方を身につける方が良い.

j = 0 ;

for(i=10;i>0;i--)

j += i ;

普通に考えれば i の値をインクリメントするに決まっている. 間違いなく動くのだが, このような無理な書き方はやめた方が良い. バグの元となる.

j = 0 ;

for(i=11;--i>0;j+=i) ;

もうここまでいくと, 何をやっているのかわからない. 絶対ダメ!

6.10.2.2 while 文

while 文は以下のような構文を持つ.

while(式)

ここで, 式は省くことはできない. ここで, 式は算術型もしくはポインタでなくてはならない.このような while 文は, 次のように制御される.

1. 式が最初に評価される.

2. 式が 0 でない限り, 繰り返しの文が実行される.

ここで, 式の副作用は, 繰り返しのはじまる前に完了する.

Example 6.10.4 この例は, 0 から 9 までの整数を順に印字するものである.

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

#include <stdio.h>

int i=0;

int main(int argc, char **argv)

{

while (i < 10) {

printf("%d\n",i++) ;

}

return 0 ;

}

#include <stdio.h>

int i=0;

int main(int argc, char **argv)

{

while (i < 10)

printf("%d\n",i++) ;

return 0 ;

}

この例では, はじめに i = 0 が実行され, i < 10 である間は, { } の部分が実行される. 各繰り返し中では, i++ が実行されているので, 繰り返し毎に i は 1 だけ増加する. i が 9 になると, 繰り返しは実行されるが, 繰り返しの文中で, i++ が実行され, i は 10 となる. したがって, i < 10 が 1 となり, 繰り返しの文は実行されずに, while は終る.

Example 6.10.5 この例は, 無限ループ25を実現している.

#include <stdio.h>

int i ;

int main(int argc, char **argv)

{

while(1) ;

return 0 ;

}

Example 6.10.6 この例は, 1 から 10 までの整数の和を計算するプログラムである.

#include <stdio.h>

int i=0,j=0;

int main(int argc, char **argv)

{

while (i < 10)

j += ++i ;

return 0 ;

}

ここでは和をとる直前に前置インクリメント演算子を利用していることに注意.

Exercise 6.10.2 while 文を利用して, 2 から 10 までの偶数の和を計算して印字するプログラムを書け.25無限ループはウインドウシステムでのアプリケーションの作成に利用される. 前に述べた for を使った無限ループか, こちらの例かどちらかの利用にとどめておいた方が良い. 無限ループの実現方法はいくらでも考え付くが, これら2つのどちらかであれば, Cを少しでも知っているプログラマなら, 見ただけで無限ループと理解可能である.

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

Remark 6.10.2 1 から 10 までの和を計算するプログラムでは, while 文を使ったものでも, いろいろな書き方が可能である.

i = j = 0 ;

while(i < 10) {

i += 1 ;

j += i ;

}

上の例の書き方よりも, この方が望ましいかもしれない. なぜなら, j に i の値をインクリメントする際

に, i がインクリメントされているというプログラムの意図が明確になっている.

j = 0 ; i = 1 ;

while(i <= 10) {

j += i ;

i += 1 ;

}

確かに問題なく動作するのだが, C では境界判定条件(この例では i <= 10 )では, i <= 10 と書くよ

りも i < 10 と書く方が(すなわち, 直前の例の方が)標準的26である. 可能な限り標準的な C の書き方を身につける方が良い.

j = 0 ; i = 10 ;

while(i > 0) {

j += i ;

i -= 1 ;

}

普通に考えれば i の値をインクリメントするに決まっている. 間違いなく動くのだが, このような無理な書き方はやめた方が良い. バグの元となる27.

結局, 1 から 10 までの和をとるプログラムは for 文を用いた方が自然であることがわかる. じゃあ, while文はどんな時に使うかって?それは, 境界条件があらかじめ決まっていないときには for 文よりもきれい

になります.

Example 6.10.7 この例は, 標準入力から EOF(ファイル終端)が検出されるまで, 1文字づつをよみ, それを標準出力に書き出す.

26でも, この辺は難しいところかもしれない.27でも, 前の for 文を使ってデクリメントする例よりはよほどマシ.

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

#include <stdio.h>

int c ;

int main(int argc, char **argv)

{

while((c = getchar())!= EOF)

printf("%c",c) ;

return 0 ;

}

このプログラムでは, c = getchar() により, 標準入力から1文字を読み, 読んだ文字が EOF でない間

は, その文字を標準出力に1文字づつ書き出している28.このプログラムを getchar.c とし, これを実行形式にコンパイルしたコマンドを a.out としたとき,

./a.out < getchar.c

を実行してみよ.

Remark 6.10.3 上の例で

while(c = getchar() != EOF)

とすると, 正しく動作しない. 代入演算子と等値演算子の優先順位は, 等値演算子の方が上である.

6.10.2.3 do 文

do 文は以下のような構文を持つ.

do

while(式) ;

ここで, 式は省くことはできない. ここで, 式は算術型もしくはポインタでなくてはならない.このような do 文は, 次のように制御される.

1. はじめに繰り返しの文が実行される.

2. それが終るたびに式が評価される.

3. 式が 0 でない限り, 繰り返しの文が実行される.

ここで, 式の副作用は, 各繰り返しの後に完了する.

Example 6.10.8 この例は, 0 から 9 までの整数を順に印字するものである.

28char c ではなく, int c としている理由は, Section 6.19 を参照.

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

#include <stdio.h>

int i=0;

int main(int argc, char **argv)

{

do {

printf("%d\n",i++) ;

} while (i < 10) ;

}

#include <stdio.h>

int i=0;

int main(int argc, char **argv)

{

do

printf("%d\n",i++) ;

while (i < 10) ;

}

この例では, はじめに i = 0 が実行される. 繰り返しの部分は, 1回目は無条件に実行され, その後に i<10

が評価される. 各繰り返し中では, i++ が実行されているので, 繰り返し毎に i は 1 だけ増加する. i が 9になると, 繰り返しは実行されるが, 繰り返しの文中で, i++ が実行され, 繰り返しが終ると, i は 10 となる. したがって, i < 10 が 1 となり, 繰り返しの文は実行されずに, do は終る.

do は while とは異なり, 少なくとも1回は繰り返しの文が実行される.

Example 6.10.9 この例は, 1 から 10 までの整数の和を計算するプログラムである.

#include <stdio.h>

int i=0,j=0;

int main(int argc, char **argv)

{

do

j += ++i ;

while(i < 10) ;

}

ここでは和をとる直前に前置インクリメント演算子を利用していることに注意.

Exercise 6.10.3 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.10 for 文を用いて, 0.1 の和を計算する.

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

#include <stdio.h>

int main(int argc, char **argv)

{

double x ;

for(x=0.0;x<0.5;x+=0.1)

printf(‘‘%f\n’’, x) ;

return 0 ;

}

#include <stdio.h>

int main(int argc, char **argv)

{

double x ;

for(x=0.0;x<1.0;x+=0.1)

printf(‘‘%f\n’’, x) ;

return 0 ;

}

この2つのプログラムは, それぞれ, 0.1 の4回, 9回の和を計算するという意図を持つ. それぞれのプログラムは正しく実行できるだろうか?

Solaris 2.6 上の gcc 2.95.1 を用いて実行すると, それぞれ

0.000000

0.100000

0.200000

0.300000

0.400000

0.000000

0.100000

0.200000

0.300000

0.400000

0.500000

0.600000

0.700000

0.800000

0.900000

1.000000

という出力を得る. 5回の和は期待通りに動作しているが, 10回の和は余分な動作が含まれている.

Example 6.10.11 while 文を用いて 0.1 の和を計算する.

#include <stdio.h>

int main(int argc, char **argv)

{

double x=0.0 ;

while(x != 0.5) {

x += 0.1 ;

printf(‘‘%f\n’’, x) ;

}

return 0 ;

}

#include <stdio.h>

int main(int argc, char **argv)

{

double x=0.0 ;

while(x != 1.0) {

x += 0.1 ;

printf(‘‘%f\n’’, x) ;

}

return 0 ;

}

同様に Solaris 2.6 上の gcc 2.95.1 を用いて計算すると, それぞれ

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

0.000000

0.100000

0.200000

0.300000

0.400000

0.500000

0.000000

0.100000

0.200000

0.300000

0.400000

0.500000

0.600000

0.700000

0.800000

0.900000

1.000000

1.100000

.

.

.

となり, 右のプログラムは終了しない.

これら2つのプログラムは, 浮動小数点数の誤差に起因し, 正しく条件判断が行われていないことが原因となる.これらを正しく動作させるにはいくつかの方法がある.

Example 6.10.12 繰り返し回数を整数型変数を用いて制御する.

#include <stdio.h>

int main(int argc, char **argv)

{

int i

double x=0.0;

for(i=0;i<10;i++) {

x += 0.1 ;

printf("%f\n", x) ;

}

return 0 ;

}

#include <stdio.h>

int main(int argc, char **argv)

{

int i=0 ;

double x=0.0 ;

while(i != 10) {

x += 0.1 ; i += 1 ;

printf("%f\n", x) ;

}

return 0 ;

}

Example 6.10.13 浮動小数点数の誤差を見込んで制御する.

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

#include <stdio.h>

#include <math.h>

#define EP 1.0E-12

int main(int argc, char **argv)

{

double x ;

for(x=0.0;fabs(x)<= 1.0+EP;x+=0.1)

printf("%f\n", x) ;

return 0 ;

}

#include <stdio.h>

#include <math.h>

#define EP 1.0E-12

int main(int argc, char **argv)

{

double x=0.0 ;

while(fabs(x)< 1.0+EP) {

printf("%f\n", x) ;

x+=0.1 ;

}

return 0 ;

}

このように, 繰り返し文で浮動小数点数の計算を行うには, 浮動小数点演算の誤差を考慮したプログラムを書かなければならない.

6.10.3 ラベル文

はじめにラベル文の構造を見ておこう. ラベル文は以下のいずれかの構造を持つ.

識別子 : 文

cast 定数式 : 文

default : 文

case, default ラベルは switch 文で用いられる. case ラベルの定数式は整数式でなければならない.

6.10.4 ジャンプ文

ジャンプ文とは, 無条件または条件付きで, プログラム制御を他の部分に移すために用いられる. ジャンプ先には, ラベル文で定義されたラベルを指定できる場合がある.

6.10.4.1 goto 文

goto 文は以下のような構文を持つ.

goto ラベル

このラベルは, goto 文がある関数内にあるラベルでなくてはならない.goto 文はプログラムの流れを混乱させることが多いため, C 言語ではほとんど用いられない. もし goto

文を使うようなことがあれば, プログラム全体をもう一度見直して書き直した方が良い.

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

6.10.5 return 文

return 文は以下のような構文を持つ.

return 式

関数実行中に return 文に出会うと, プログラム制御は直ちに関数の実行をやめ, 関数を呼び出した部分に戻る. この時, 関数が戻り値を持つと定義されているときには, (式)で指定した値を関数の戻り値とする. void 型の戻り値を持つ関数には, 式を持つ return 文は使えない. また, void 型以外の戻り値を持つ関数で, 式を指定しない return 文によって関数を終了したときの戻り値は不定となる.

Example 6.10.14 以下の例はプログラム呼び出しの引数が無い場合に, プログラムをすぐに終了するものである.

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

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の抜け出し先はここ */

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

6.10.7 continue 文

continue 文は以下の構文を持ち, 繰り返し文の中にだけおくことが出来る.

continue ;

continue 文に出会うと, それを含んだ最小のループを直ちに実行させる. この時, while 文, do 文の場合は, 評価に移り, for 文の場合には, 第三式の評価に移る.

Example 6.10.16 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.17 continue 文と break 文を用いると, 1 から 10 までの偶数の和を計算するものを以下のように書くことが出来る.

int i = 0, j = 0 ;

while(++i) {

if (i&1) continue ;

j += i ;

if (i == 10) break ;

}

Example 6.10.18 次の例は, 0 から 9 までの整数を順に印字する.

int n=0 ;

for(;;) {

printf("%d\n", n) ;

if (++n == 10) break ;

}

Remark 6.10.4 しかし, この3つの Example のようなコードを書くのはやめよう. あくまで, break 文と continue 文の例として書いたに過ぎない.

6.10.8 条件文

条件文とは, 条件によって実行を分岐させるための制御文である.

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

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 は繰り返して使うことができ, 次のような形になる.

if (式1)

文1

else if (式2)

文2

else if (式3)

文3

else

文4

この形では, 式1から順番に式が評価される. 式1が 0 であれば, 式2を評価するという順番で, 条件式が評価されていく.

Example 6.10.19 次の例は n が 0 か正か負かを判定している.

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

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.5 上の Exampleの最初の if (n == 0)は if (!n)と書くこともできるが,後の if--else

との整合性を考えると, ここは n == 0 と書いた方が良い.C の標準関数の中で, int 型の変数が 英小文字であるかどうかを判定するための関数として, islower

関数がある. c が英小文字であるときには islower(c) は 1 となり, そうでないときには 0 となる. このような関数を利用するときには,

if (islower(c))

と書けば(そのままこの文章を英語で読めば)非常にわかりやすい.しかし, C の標準関数の中で, 2つの文字型の配列(文字列)が同一であるかどうかを判定する関数とし

て, strcmp がある. char *s, *t に対して, strcmp(s,t) はその辞書式順序に従う差を与える. すなわち,s, t の指し示す文字列が同一内容であるときに 0 を返すため,

if (strcmp(s,t) == 0)

によって, s, t が等しいときの分岐を表すことになる.条件式を簡潔に書くことは, プログラムを見易くするために重要なことであるが, 条件式を余りに簡潔に

しすぎると思わぬバグを産む可能性がある.

if–else 文には曖昧な構文がある.

Example 6.10.20 次の例は else がどちらの if に結び付いているかわかりにくくなった例である.

if (n > 0)

if (a > b)

z = a ;

else

z = b ;

ここで, 文法的には else は2番めの if に結び付いている.このような表現を避けるため, if–else で制御される文は {} をつけて複文にすることが望ましい.

if (n > 0) {

if (a > b) z = a ;

}

else z = b ;

if (n > 0) {

if (a > b) z = a ;

else z = b ;

}

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

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.21 この例では, n の値によって, 結果を印字しようとしているが, 期待通りには動かない.

int n ;

switch (n) {

case 1: printf("n は 1 だよ\n") ;

case 0: printf("n は 0 だよ\n") ;

case -1: printf("n は -1 だよ\n") ;

default: printf("n は 1, 0, -1 のどれでもない\n") ;

}

これは, n = 1 の時,

n は 1 だよ

n は 0 だよ

n は -1 だよ

n は 1, 0, -1 のどれでもない

という結果をうち出す.

このように switch 文の中で, 一つの case ラベルを実行し, 次の case ラベルに制御が移ってしまうこと

を “Fall Throught” と呼ぶが, Fall Throught を使う仕様はバグの原因となる.このようなことを避けるためには, break 文を使う.

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

Example 6.10.22 上の Example を改良した.

int n ;

switch (n) {

default: printf("n は 1, 0, -1 のどれでもない\n") ;

break ;

case 1: printf("n は 1 だよ\n") ;

break ;

case 0: printf("n は 0 だよ\n") ;

break ;

case -1: printf("n は -1 だよ\n") ;

break ;

}

この改良により, この switch 文ではどれか一つのラベルに対する文しか実行しなくなる. 出来ればdefault ラベルを持つ文は一番最後につけよう. その方がわかりやすい.

6.10.8.4 if 文と浮動小数点演算

if 文の場合にも, 繰り返し文と同様に, 浮動小数点演算の誤差に注意しなければならない.

Example 6.10.23 1.0 から 0.1 を 0.0 になるまで繰り返し減算する.

#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 では正常に動作しない. これは, 次のように書換えてみると現象を良く理解できる.

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

#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 にはなっていない.

この例を正しく動作するように直すには,

#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 演習問題

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

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

6.10.9.1 必要な関数と簡単な例

ここで述べた関数については, 詳しくはオンライン・マニュアルを参照すること.29

6.10.9.1.1 getchar 標準入力から1文字を入力するための関数は, getchar である.

int getchar()

利用法は, 以下の通り.

Example 6.10.24 標準入力から1文字を読んで,それを出力する. プログラムを終了するには, Control��������+ D�

を入力する.

int c ;

while ((c = getchar()) != EOF) {

printf("%c",c) ;

}

ここで, getchar の戻り値として, int 型の変数で受けていることに注意. char 型ではない. また, EOFとは, ファイルの終りを表す特別な文字である.

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 ;

}29オンライン・マニュアルで, 期待のものが出てこない時には, man -s 3s getchar などと -s 3s を入れてみると良い.

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

とするしかないであろう. 文字列を使えば, もう少しエレガントに操作することが出来る.

6.10.9.1.3 乱数の発生 random() という関数は 0 から 231 − 1 の乱数を発生させる. 通常, 次のようにして使う.

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

long n,m ;

srandom((unsigned int)time(NULL)) ;

n = random() ;

m = random() ;

srandom の部分は, 乱数を初期化する部分で, 現在の時刻から決まる数を使って初期化をしている.

6.10.9.2 演習問題

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

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

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

Exercise 6.10.7 0 から 28 − 1 までの乱数を十分沢山発生させ, その値が 27 以下の確率を表示するプロ

グラムを書け.

Exercise 6.10.8 次のコードの最後の比較は, どうなるかを考察せよ.

double x ;

x = 0.1 ;

if (0.5 == 5*x) {

print ("0.5 と 5 * 0.1 は等しい\n") ;

}

else {

print ("0.5 と 5 * 0.1 は等しくない\n") ;

}

Exercise 6.10.9 画面に

次のうちの誰が好きですか?

1. 本上まなみ

2. 桜井淳子

C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp

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

3. 戸田菜穂

4. 中山美穂

5. 今井美樹

6. 上原多香子

7. 若村麻由美

8. 葉月里緒菜

9. 松雪泰子

0. どれでもない

と表示して, 0 ~ 9 迄を入力した時には, 何か好きなことを表示して, それ以外の時には, もう一度質問を表示するプログラムを書け. (当たり前なのだが, 別に葉月里緒菜などにする必要はない.)

Exercise 6.10.10 次のプログラムの出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>

int x ;

int main(int argc, char **argv)

{

x = 0 ;

if (x == 0) { printf("if part\n") ; }

else { printf("else part\n") ; }

if (x) { printf("if part\n") ; }

else { printf("else part\n") ; }

if (!x) { printf("if part\n") ; }

else { printf("else part\n") ; }

x = 1 ;

if (x&1) { printf("if part\n") ; }

else { printf("else part\n") ; }

if (x&1==0) { printf("if part\n") ; }

else { printf("else part\n") ; }

return 0 ;

}

6.11 関数とは

関数とは, ある一定の処理をさせるための部分的なプログラムのことである.これまでに利用してきた, printf, getchar, random などは, 全てあらかじめ組み込まれた関数の例であ

る. このような例のように, C 言語では多くの標準的な関数が用意されている. 一方, 我々が作るプログラムにおいても, 関数を利用することによって, プログラムの構造をわかりやすくできる利点がある. C 言語の標準的な関数にはどのようなものがあるかは, [2, B] に書かれている.

C5.tex,v 1.19 2001-07-19 17:06:07+09 naito Exp

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

6.11.1 関数の定義

6.11.1.1 関数の定義と例

C 言語では関数は0個もしくは1個以上の引数(ひきすう) (parameter) を持ち, 0個もしくは1個の戻り値(もどりち) (return value) を持つ. 引数, 戻り値は, 数学における関数の独立変数, 関数の値に相当すると考えられるが, C 言語の関数は, 引数が関数を呼び出した後に変化することも多い.30

関数は, 以下の形をしている.

<戻り値の型> <関数の識別子> ( <関数の引数> )

関数の本体となる複文

関数の引数として書かれた変数(これを仮引数 (parameter) と呼ぶ)の識別子は, その関数内のみで有効な識別子である.(Section 6.11.1.4 参照.)また, 関数を呼び出したときの引数を実引数 (argument) と呼ぶ.

Example 6.11.1 仮引数として int 型の引数を2つ持ち, それらの和を int 型で返す関数は以下のよう

に書かれる.

int sum(int n, int m)

{

return n + m ;

}

このように, 関数の戻り値を指定するには return 文を用いる.

return 文に出会うと関数の実行は終了し, その後の部分は実行されない.

Example 6.11.2 次は, 2つの int 型の引数の小さくない方の値を返す関数.

int max(int n, int m)

{

if (n < m) return m ;

return n ;

}

int max(int n, int m)

{

return (n < m) ? m : n ;

}

関数の戻り値の型を省いた時には int であると解釈される. (cf. [3, X 3010 6.7.1, p. 1922], [2, A10])したがって, 戻り値を持たない関数の場合, 戻り値の型は void であると明示的に宣言しなくてはならない.関数の戻り値の型として配列をとることは出来ない.また, 引数を持たない関数も作ることができる.

Example 6.11.3 この例は, 実際には余り意味がない.

30このようなことを副作用と呼ぶ.

C5.tex,v 1.19 2001-07-19 17:06:07+09 naito Exp

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

void only_print(void)

{

printf("Hello\n") ;

return ;

}

文法上は only_print() としても良いが, 明示的に only_print(void) とした方が良い.

一方, printf などは, その場合により引数の数が異なる関数の例である. このような関数を可変引数を持つ関数と呼ぶ.31

Example 6.11.4 このプログラムでは, 関数の引数の識別子(仮引数)と, 関数を呼び出している部分で使われている変数(実引数)の識別子が同じものになっているが, それぞれの実体が違うことに注意. (→Section 6.12).

#include <stdio.h>

int a,b,c ;

int sum(int b, int c)

{

return b + c ;

}

int main(int argc, char **argv)

{

a = 0 ; b = 1 ;

c = sum(a,b) ;

return c ;

}

main もまた関数になっていることに注意. また, 関数はプログラムファイル中の(他の関数内でなければ)どこに書いても良い. さらに, Section 6.13 で述べるように, C では一つのプログラムを複数のファイルに分割することができ, 関数全体が一つのファイル内にあれば, 呼び出される関数が他のファイルにあっても良い.

6.11.1.2 関数の引数について

関数の引数は, どのような型を持っても良い. 関数の引数として用いられた識別子を持つ変数は, その関数内でのみ有効であることは, 前に述べた. ここでは, 関数に渡される引数がどのような型変換を受けるかを解説する.

C 言語においては数の累乗は演算子では定義されていないので, 関数を呼び出すことになる. 累乗を計算する C 言語の標準的な関数としては pow がある. この関数は, 以下のように定義されている.32

31可変引数の関数は後ほど述べる.32man 3 pow 参照.

C5.tex,v 1.19 2001-07-19 17:06:07+09 naito Exp

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

double pow(double x, double y)

これは, x の y 乗の値を返す関数である. ここで, 問題となるのは, 全ての変数が double で宣言してある

ことである. この関数を次のように呼び出したらどうなるだろうか?

int n=2,m=3 ;

pow(n,m) ;

pow(3,5) ;

第一の呼び出しは, int 型の変数を用いて呼び出しているし, 第二の呼び出しは, 整数の定数を用いて呼び出している.このように, 関数の定義の異なる型の変数や定数を用いて関数を呼び出す時には, 暗黙の型変換(integer

promotion など)が行なわれる. 実際, int 型の変数は double に変換される. (もちろん, 整数から浮動小数点数への変換の規則が用いられる.)

Example 6.11.2 で書いた, 2つの int 型の変数の小さくない方の値を返す関数を以下のように使ってみ

よう.

double x = 2.5, y = 0.3 ;

int max(int n, int m)

{

return (n < m) ? m : n ;

}

int main(int argc, char **argv)

{

printf("%f\n", max(x,y)) ;

return 0 ;

}

これでは全く正しい結果が得られない. 実際には, 2つの数の大きさを比較して小さくない方の値を表示させるには,

#define max(A,B) (A < B) ? B : A

printf("%f\n", max(1.0, 2.0)) ;

とした方が良い.

Remark 6.11.1 関数の実引数の評価順序は不定であるので注意すること.

int v ;

func(v++,v++) ;

とすると, 実引数としてどのような値が渡されるかはわからない.

Remark 6.11.2 演算においては, 演算の優先順位と結合規則は, 式の構造のみを決定するだけで, その評価順序は不定であることに注意しよう. たとえば,

C5.tex,v 1.19 2001-07-19 17:06:07+09 naito Exp

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

f() + g() * h()

としても, g と h の呼出しが先に行われる保証はない. Example 6.8.23 の例, a/b*b および a*b/b ではど

のような順序で式 a, b の評価が行われても, その優先順位と結合規則にしたがって計算が行われていると理解すれば十分であったが, 関数呼出しのように副作用がある場合には, その副作用が完全に終了33するの

は式全体の評価が終了した時点であることに注意しなければならない.

6.11.1.3 関数のプロトタイプ宣言とヘッダファイル

Section 6.11.1.1 では, 関数の戻り値の型を省略した時には int であると述べた. また, 関数を書く場所はどこでも良いと書いた. このことで, 次のような2つの問題が起きる.例えば, double 型の引数と戻り値を持つような関数 test fn を作り, 以下のようにしたとする.

int main(int argc, char **argv)

{

int n ;

n = test_fn(n) ;

}

double test_fn(double a)

{

.

.

.

}

これは, 正常にコンパイルできるだろうか?まず, 次のような問題があることに気が付く.

• main 関数内にある識別子 test fn が, その時点で何を示すかわからない.

C コンパイラは識別子 test fn が実引数を伴っていることにより, 関数であることを認識する. 関数シンボル(識別子)の解決は, 最終的にはリンカ(Section 6.13 参照)で行うので, この問題は本質的な問題にはならない.もう一つの問題は, 次のようなところにある. ANSI 規格 [3, X 3010 6.7.1, p. 1922] および K&R [2,

A10] によると,「関数定義の宣言子において省略されている型は int とみなす」とされている. すなわち,戻り値の型や仮引数の型が省略されると, コンパイラはそれを int とみなして処理を進める. したがって,コンパイラは以下のような処理を行う.

• コンパイラが main 内で test fn を呼び出す時には, test fn の戻り値を int と解釈している. 同時に test fn の仮引数の型が int であると仮定して, 処理を進める.

• 次に, test fn の部分をコンパイルする時には, 戻り値や仮引数の型が double となっているので, 矛盾がありエラーとなる.

この様な, 呼び出しと定義の部分の矛盾を避けるために, 関数のプロトタイプ (prototype) 宣言を行なう必要がある. プロトタイプ宣言とは, 関数の持つ引数の型と戻り値の型のみを書いた文を, その関数が利用される前に書いておくことである.

33式の評価において, 副作用が完全に終了する場所を, 副作用完了点と呼ぶ. この場合, 副作用完了点は式全体の評価が終わった点である.

C5.tex,v 1.19 2001-07-19 17:06:07+09 naito Exp

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

上の例の場合には

extern double test_fn(double) ;

という一行を, main の前に書けば良い. 複数の引数を持つような関数のプロトタイプ宣言は

extern double test_fn(double, double) ;

などと書く. すなわち, 全体としては

extern double test_fn(double) ;

int main(int argc, char **argv)

{

int n ;

n = test_fn(n) ;

}

double test_fn(double a)

{

.

.

.

}

となる. ここで使われた extern は記憶クラス指定子 (storage class specifier) の一つであり, 詳しくはsection 6.12 で議論する.

Remark 6.11.3 上の例で用いた関数プロトタイプ宣言は

int main(int argc, char **argv)

{

extern double test_fn(double) ;

int n ;

n = test_fn(n) ;

}

double test_fn(double a)

{

.

.

.

}

と書くことも可能である. この場合, 関数 test fn のプロトタイプが有効なのは, main 関数の内部に限られる.

C5.tex,v 1.19 2001-07-19 17:06:07+09 naito Exp

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

C 言語のプログラムを書く際に, #include <stdio.h> などということを書いた. ここで使われたヘッダファイル stdio.h には, いくつかの関数(printf など)のプロトタイプ宣言などが書かれている. そのような意味で, 標準的な関数を利用する際には, それぞれのプロトタイプ宣言を含むヘッダファイルを#include でインクルードしなくてはならない.

6.11.1.4 関数内の局所変数

関数内で局所的にしか利用できない変数を作ることができる. このような変数を定義するには, 関数のブロック内に変数の定義をすることで, 局所変数の定義となる.局所変数は static 宣言をしない限り34関数が呼び出されるごとに変数領域が確保され, しかるべき初期

化を受ける35.また, 局所変数の識別子は, その関数内でのみ有効である. 局所変数の識別子と, 関数引数の識別子は重

なってはいけない. 即ち, 関数内で局所的に有効な変数は, 関数引数と局所変数である.

Example 6.11.5 この例では, sum, main の両方の関数内で局所変数を定義している.

#include <stdio.h>

extern int sum(int, int) ;

int main(int argc, char **argv)

{

int a,b,c ;

a = 0 ; b = 1 ;

c = sum(a,b) ;

return c ;

}

int sum(int b, int c)

{

int a ;

a = b + c ;

return a ;

}

ここで, main の変数 a,b,c と, sum の変数 a,b,c は異なるものであることに注意.

また, どの関数にも含まれない部分で定義された変数(このようなものを大域変数と呼ぶ.)は, そのファイル中の定義(宣言)以後はどこでも有効であるが, 大域変数と局所変数もしくは関数引数の識別子が重なった時には, その識別子は関数内では局所変数のものと見倣される36.

Example 6.11.6 この例では, sum という関数内で局所変数を定義し, 一方大域変数も利用している.

34static に関しては後述する. static も記憶クラス指定子の一つである.35明示的に初期化をしない限り, 初期値は不定である.36このようなことを, 変数のスコープと呼び, Section 6.12.2.3 で詳しく述べる.

C5.tex,v 1.19 2001-07-19 17:06:07+09 naito Exp

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

#include <stdio.h>

int a,b,c ;

extern int sum(int, int) ;

int main(int argc, char **argv)

{

a = 0 ; b = 1 ;

c = sum(a,b) ;

return c ;

}

int sum(int b, int c)

{

int a ;

a = b + c ;

return a ;

}

ここで, sum の中では, 変数 a,b,c は, 大域変数ではなく, 局所変数, 関数引数で定義されたものが見えていることに注意せよ.

局所変数に関しては, Section 6.12 で詳しく議論する.

Exercise 6.11.1 次のプログラムの出力結果がなぜそうなるかを考えよ.

#include <stdio.h>

int a = 1, b = 2, c = 3 ;

extern int foo(void) ;

int main(int argc, char **argv)

{

int a = 0, b = 1 ;

printf("a = %d, b = %d, c = %d\n", a,b,c) ;

foo() ;

foo() ;

printf("a = %d, b = %d, c = %d\n", a,b,c) ;

return 0 ;

}

int foo(void)

{

int a = 2, b = 0 ;

C5.tex,v 1.19 2001-07-19 17:06:07+09 naito Exp

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

printf("a = %d, b = %d, c = %d\n", a,b,c) ;

a += 1 ;

printf("a = %d, b = %d, c = %d\n", a,b,c) ;

return ;

}

6.11.1.5 main 関数の引数

main 関数の引数は ANSI には厳密には来ていされていないが, 通常は2つの引数をとり,

int main(int argc, char **argv)

となる37. これらの引数については Section 6.14.3 で述べる.

6.11.1.6 ライブラリのリンク

標準関数を利用した場合, その関数の実体はどこにあるのかを考えてみる.printf などは, 何も問題なくコンパイルでき, 実行できるが, pow などの数学関数を使い, 通常のように

コンパイルすると,

ld: Undefined symbol

_pow

collect2: ld returned 2 exit status

という error が出る. これは, 関数 pow の実体が探せないということで, その実体のありかを指定しなくてはならない. このように関数の実体が集まって, あらかじめ用意されているものをライブラリと呼び,libXXXX.a などというファイルになっている. そこで, libXXXX.a を使うには, 次のようにしてコンパイルする.

% gcc a.c -lXXXX -o a

-l の後に書いたのは, libXXXX.a の XXXX の部分である.pow などの数学関数は, libm.a にあるので,

% gcc a.c -lm -o a

とすれば, 正常にコンパイルできる. ライブラリのリンクに関しては Section 6.13 で詳しく議論する.

6.11.1.7 関数呼び出しの実際

関数が呼び出された時に, プログラムはどのように動作しているかを考えてみよう.プログラム中で関数が呼び出された時には, 次のような手続きが行なわれて, 関数が呼び出される.

• 関数の引数として与えられた変数の値が実際の引数として扱われる.

• その引数の値をスタックに積み, 関数のコード部分へ制御を移す. ただし, 複数の引数がある時に, どの順序でスタックに積むかは処理系によってことなる.

• その際に, 関数内のローカルな変数38はスタック内に確保される.37システムによっては, より多くの引数(たとえば, 環境変数など)を取ることもある.38Section 6.12 で述べる.

C5.tex,v 1.19 2001-07-19 17:06:07+09 naito Exp

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

ここで重要なことは, 関数に渡されるものは変数の値であって, 変数そのものではないことである. このような関数の呼び出しを call by value (値渡し)と呼ぶ.

Pascal の var を利用した変数の渡し方 (call by address(参照呼び出し)) と同様なことを C 言語で実現するには, ポインタが必要となる.

6.11.2 再帰的関数呼びだし

関数はそれ自身をその内部で呼び出すことができる. これを再帰的関数呼び出し (recursive function call)と呼ぶ. 再帰的関数呼び出しを利用すると, 帰納的に定義されたものを計算することが容易になる. 数学的には, 再帰で計算できるものは必ず再帰を利用しなくても計算できることが証明されているが, 再帰で計算するとプログラムが簡潔になるという利点がある. 一方, 関数, 手続き等を呼び出す際には, 多くの処理系において呼び出しの手順として時間がかかることが多い. したがって, 再帰には時間がかかることが多い.

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

int recursive_function(unsigned int n)

{

if (n == 0) return 0 ;

return recursive_function(n-1) + 2 ;

}

再帰的関数呼び出しの欠点は, 関数呼び出し手続きに時間が掛ること, メモリ領域としてスタック領域を大量に消費する可能性があることなどの欠点を持つ. 数学的に再帰的な定義があるからと言って, 安易に再帰呼び出しとして実現するのは必ずしも望ましくない. 単純な繰り返しを利用して書けるものをわざわざ再帰で書くのは避けるべきである. (cf. [7, Section 3.2])

6.11.3 可変引数を持つ関数

printf に代表される, 可変個の引数を持つ関数の定義方法と性質について述べておこう. ここでは, 後に解説する「ポインタ」や「文字列」を利用している.関数引数が “...” で終る時には, その関数はパラメータより多い引数をつけて呼んで良い. この余分な

変数を参照するには, ヘッダ stdarg.h で定義される va_arg マクロを使う必要がある39. また, 可変個の引数を持つ関数は, 少なくとも一つの名前つきパラメータを持たなくてはならない. さらに, 名前なし引数をそのまま他の関数に渡すことはできない.

Example 6.11.8 ここでは具体的に “%s” のみを許す printf に似た関数を書いてみよう.

39これらのマクロに関しては, [2, B7] 参照.

C5.tex,v 1.19 2001-07-19 17:06:07+09 naito Exp

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

#include <stdarg.h>

int test_va(char *fmt,...)

{

va_list ap ;

char *p,*sval ;

va_start(ap,fmt) ;

for(p=fmt;*p;p++) {

if ( *p != ’%’ ) {

putchar(*p) ;

continue ;

}

switch (*++p) {

case ’s’:

for(sval = va_arg(ap,char *);*sval;sval++) putchar(*sval) ;

}

}

va_end(ap) ;

}

この例では, test_va("test %s",str) として, 文字列を標準出力に出力する関数を実現している.関数内では, 最初に va_start マクロを利用して, 先頭の名前つき引数と va_list とを結び付けている.

引数 fmt が % でない文字の場合には, そのまま出力を行ない, continue 文により, ループを実行させる.一方, 読んだ文字が % であり, その後の文字が s の場合には, 対応する文字列(引数)を va_arg によって

探し, その文字列を表示する. 最後には, va_end により, 可変引数リストをクリーン・アップしている.この例では, 可変引数リストは char * であることが仮定されているので, それ以外の引数を代入すると

エラーとなる.

上の例では, 最初の引数である文字列によって, 可変引数リストがいくつからなるかを知ることができるが,一般に可変引数を持つ関数を作成する場合には, このようにすることができないことがある. そのような場合には, 可変引数リストを NULL で終了 (NULL terminate) することが必要になるかも知れない.

6.11.4 関数のエラー処理

ここでは, 関数のエラー処理の方法を C の標準関数である atoi 関数を例にとって考察してみよう.atoi 関数とは, 一つの文字列(char 型へのポインタ)を引数にとり, その数が10進数を表す文字列,

すなわち, 先頭以外には空白を含まない, 0 から 9 までの文字と, 先頭の符号文字だけで構成された文字列の時には, その文字列に対応する int 型10進整数値を返し, それ以外の時には int 型の 0 を返す関数である. この場合, 実引数の文字列が10進整数を表さない文字列の時と, 10進整数の 0 を表す文字列の時

に, 結果だけを見ているだけでは区別がつかない. これを区別するための方法が, 関数のエラー処理である.関数のエラー処理を行うために, C の標準ヘッダの中に errno.h というファイルがあり, 標準関数には

strerror がある (strerror のプロトライプを含むヘッダは string.h). errno.h では大域的な変数 int

errno が定義され, 関数に何らかのエラーが発生した時には, errno に 0 以外の数値をセットすることで,呼出し側の関数にエラー発生を伝えることが出来る.

C5.tex,v 1.19 2001-07-19 17:06:07+09 naito Exp

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

Example 6.11.9 実引数に10進数を表さない文字列リテラルを指定して, atoi 関数を呼出し, エラーを検出する例.

#include <stdio.h>

#include <stdlib.h>

#include <errno.h>

int main(int argc, char **argv)

{

int n ;

n = atoi("x") ;

printf("errno = %d\n", errno) ;

if (errno) printf("Error: %s\n", strerror(errno)) ;

else printf("Integer = %d\n",n) ;

return 0 ;

}

実は, atoi 関数の C の規約における定義では, この場合に, errno に 0 以外の値をセットする義務はないので, このプログラムをそのまま実行すると, else 部の出力が得られる. しかし, 可能であれば, この場合に errno に 0 以外の数値をセットするような関数のコードを書くことが望ましい.

errno の数値の意味は, UNIX の場合 /usr/include/sys/errno.h にシステムに依存して値が定義され,strerror 関数はその値に対応するエラーメッセージを表す文字列(文字型へのポインタ)を返す関数で

ある. 望ましいエラー処理の例としては, Example 6.11.9 のコードの実行結果として, Solaris 2.6 の場合には, errno として, EINVAL (値は 22) を返し, 結果として Invalied argiment という出力をするのが良い.

6.11.5 演習問題

Exercise 6.11.2 次のような関数を書け.

int mul(int n, int m)

mul は n と m の積を返す. ただし, mul 関数の中で n*m を計算してはならない.

Exercise 6.11.3 次のような関数を書け.

int div(int n, int m)

div は n を m で割った商を返す. ただし, div 関数の中で n/m を計算してはならない. また, m が非正の場合には, errno に EINVAL に対応する値を返す.

Exercise 6.11.4 次のような関数を書け.

int mod(int n, int m)

mod は n を m で割った余りを返す. ただし, div 関数の中で n%m を計算してはならない. また, m が非正の場合には, errno に EINVAL に対応する値を返す.

Exercise 6.11.5 次のような関数を書け.

C5.tex,v 1.19 2001-07-19 17:06:07+09 naito Exp

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

int pow_int(int n, int m)

pow int は n の m 乗を返す. ただし, 負の数の累乗の時には, errno に EINVAL に対応する値を返す.

Exercise 6.11.6 次のような関数を書け.

double pow_d(double n, int m)

pow d は n の m 乗を返す. ただし, 負の数の累乗の時には, errno に EINVAL に対応する値を返す.

Exercise 6.11.7 次のような関数を書け.

int is_upper(int c)

c が数字であれば 0 以外, そうでなければ 0 を返す. ただし, ASCII コード体系であると仮定して良い.

Exercise 6.11.8 摂氏の温度(整数)に対して, 華氏の温度を求める関数を書け.

Exercise 6.11.9 unsigned int 型の変数 x のビット位置 p から n ビットを反転し, 他のビットはそのままにしたものを返す関数 invert bit(x,p,n) を書け. ただし, 最下位ビットをビット位置 0 とする.

Exercise 6.11.10 整数 x の値を右に n ビット回転する関数 rot right(x,n) を書け.

Exercise 6.11.11 2つの正の整数の最大公約数を求める関数を書け.

Exercise 6.11.12 再帰的関数呼び出しを用いて, 2つの正の整数の最大公約数を求める関数を書け.

Exercise 6.11.13 再帰的関数呼び出しを用いて, n! を求めるプログラムを書け. また, 再帰を使わない方法を考えよ.

Exercise 6.11.14 再帰的関数呼び出しを用いて, n×n 行列の行列式を求めるプログラムを書け. (注意:この関数を作るためには, 配列を必要とする.)

Exercise 6.11.15 フィボナッチ数列, すなわち, F0 = 0, F1 = 1, Fn+2 = Fn+1 + Fn を満たす数列 {Fn}を再帰的関数呼び出しを用いて求める関数を安易に書くと,

int fib(unsigned int n)

{

if (n == 0) return 0 ;

if (n == 1) return 1 ;

return fib(n-1) + fib(n-2) ;

}

となる. この関数では計算効率が悪いことが容易にわかるが, その理由を説明し, 効率よく計算できるように, 単純な繰り返しを利用して関数を書き直せ.

Exercise 6.11.16 次のような printf 関数の類似の関数を作れ. (ただし, 関数内部で printf を利用し

ても良い.)通常の printf の記述子の他に, b として, 引数の2進数による表示を行なう. ただし, フィールド幅の指定子はサポートしなくても良い.

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

6.12 識別子

C の識別子や関数に対する重要な概念として, スコープ, 寿命, リンケージがある. ここでは, それらに関する解説を行い, C で書かれたプログラムが実行されるときに, 変数がメモリ上にどのように配置されるか, 関数呼び出しの手続きとは何かを考えていこう.

6.12.1 識別子とは

これまでにも「識別子」という言葉を何度も利用してきたが, ここで, もう一度正しく識別子 (identifier)を定義しなおそう.

6.12.1.1 識別子の分類

識別子は, オブジェクト(つまり変数), 関数または, 次のいずれかを表す(後で定義するものも含まれる).

• 構造体, 共用体, 列挙体のタグまたはメンバー,

• 型定義名,

• ラベル名,

• マクロ名,

• マクロ仮引数.

6.12.1.2 名前空間

識別子は次の4つの分類ごとに別の名前空間 (name space) に属する.

• ラベル名.

• 構造体, 共用体, 列挙体のタグ名.

• 構造体, 共用体のメンバー名.

• それ以外のすべて. (これを通常の識別子と呼ぶ)

すなわち, 同じスコープを持つ識別子でも, 属する名前空間が異なるものは区別される40. 具体的な例はSection 6.18.4 参照.

6.12.2 基本概念

変数や関数の定義に関わる基本的な概念として, 「宣言」, 「定義」, 「翻訳単位」, 「スコープ」, 「寿命」, 「リンケージ」を説明していくが, はじめにそれらの用語の意味をきちんと定義しておこう.

40確かに処理系が識別子がどの名前空間に属するかを区別するのは易しい. しまし, 違う名前空間に属する, 同じ文字列からなる識別子をむやみやたらに多用すると, プログラマにとっては混乱の元になり, 自分自身の書いたコードでさえ, 何が書いてあるかわからなくなるので, そのようなことはやってはいけない.

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

6.12.2.1 定義と宣言

これまでは, オブジェクトや関数の「定義」・「宣言」という言葉を曖昧なまま利用してきた. ここで, それらの言葉を正しく理解しよう. 宣言 (declaration) とは, 識別子の組の解釈および属性を指定することである. 識別子によって名前付けられたオブジェクトまたは関数のために記憶域の確保を引き起こす宣言を定義 (definition) という41.つまり, オブジェクトや関数の「定義」とは「宣言」の中に含まれている. オブジェクトの「宣言」を行

う場合には, オブジェクトは型 (type)とともに宣言されなくてはならない. また, 必要であれば記憶クラス指定子や型修飾子を伴って宣言される. 関数の「宣言」を行う場合には, 関数は戻り値の型, 仮引数の型とともに宣言されなくてはならない42. また, 必要であれば記憶クラス指定子を伴って宣言される.このように「定義」と「宣言」を定義すると, どれが識別子の「定義」で, どれが「宣言」かわからなく

なるのだが, 一つのオブジェクトや関数に対して「定義」はただ一度だけである. 関数の「定義」は関数本体とともに宣言された時に行われる. それ以外のものはすべて「定義」を伴わない「宣言」である. オブジェクトの「定義」は通常 extern 指定子を伴わない「宣言」で行われる43. extern 宣言の詳細については後に解説する.なお, 変数の宣言と同時に初期化を行うことが出来るが, 初期化を行うと, その宣言は定義とみなされる.

Example 6.12.1

extern int sum(int, int) ; <==== これは宣言(定義にはならない)

extern int x ; <==== これは宣言(定義になるかどうかは,

他のプログラムファイルに依存する)

int main(int argc, char **argv) <==== これは定義

{

int a ; <==== これは定義

....

}

int sum(int a, int b) <==== これは定義

{

int c ; <==== これは定義

}

Example 6.12.2

extern int x = 1 ;

と書くと, 「extern 宣言と初期化を同時にしているけどいい?」なんて警告が出される. 初期化をする場合には, extern 宣言は書かない方が良い. (というよりも, extern 宣言の趣旨とは矛盾する.)

41ANSI 規格書によれば, ファイルスコープのオブジェクトを, 初期化子を使わず, 記憶クラス指定子なしか static で宣言する場合を, 仮宣言 (tentative definition) と呼んでいる. これは, オブジェクトコードのリンク時に最終的にリンケージが決定されることを意味している. (cf. [3, X 3010 6.7.2, p. 1924])

42正しくは, traditional な形式の宣言も許されている. すなわち, 仮引数の型を伴わない宣言も許されるが, バグを引き起こす原因となる.# 何でこんなのを許す仕様を残しておいたのだろう?43つまり, これまでオブジェクトを宣言してきたものは, すべてオブジェクトの定義となっている. オブジェクトの宣言にすべて

extern をつけてもエラーとはならない. リンク時にそれらの宣言のうちのいずれか一つを「定義」とみなす. これが「リンケージ」というやつ.# 要するに, extern 宣言とはきちんと使わなければ, 「メチャメチャ」になるものの典型的なものである.

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

Example 6.12.3 2つのファイルからなるプログラムで以下のようなことをすると, リンク時にエラーとなる. (分割コンパイルに関しては, Section 6.13 を参照.)

/* file1.c */

int x = 1 ;

/* file2.c */

extern int x = 1 ;

これは x という識別子が2ヶ所で定義されていることがエラーの原因となる.

6.12.2.2 翻訳単位

C において翻訳単位とは, プログラムのファイル1個づつを指す44. C ではプログララムを複数のファイルに分割し, ファイルごとにコンパイルを行い, リンカでそれぞれのオブジェクトコードを結び付けることが出来る.

6.12.2.3 スコープ

オブジェクトや関数のスコープ (scope, 日本語では有効範囲という) とは, そのオブジェクトや関数の識別子をプログラム中のどこから見えるか(可視 (visible) かどうか)を示す概念である. スコープには次の4種類がある.

1. ファイル・スコープ

2. 関数・スコープ

3. ブロック・スコープ,

4. 関数プロトタイプ・スコープ.

スコープは, 変数を宣言する場所から決定される. 変数を宣言できる場所は以下のいずれかである.

1. どの関数にも含まれない部分. 正しい言い方では「どのブロックにも, 仮引数ならびよりも外側にあらわれる時」. この場合は「ファイル・スコープ」となる.

スコープは翻訳単位の終了, すなわちファイルの終了によって終了する.

2. 「関数・スコープ」となる場合は2通りあり,

• 関数定義の仮引数.

• 関数の先頭部分.

この場合, スコープは関数ブロックの終了によって終了する.

3. ブロックに入った直後45. これは「ブロック・スコープ」となり, 対応する } の出現で終了する46.

4. 関数プロトタイプ宣言の仮引数. この場合, 「関数プロトタイプ・スコープ」となり, スコープはその宣言内のみとなる.

44より正確には, プリプロセッシングを終了した翻訳フェーズにおけるファイルが翻訳単位となる.45これは余り利用しないが, 有効に利用できる場合がある. 変数を関数内で極めて局所化したい場合に利用することがある. これは, デバック (debug) を行う場合などで利用することがある. 恒久的なコードでこの手法を用いると, 思わぬ変数の隠蔽が起きる可能性があるので, デバック時などの一時的なコードにのみ用いる方がよいだろう.

46正確には, 上の「関数・スコープ」は「ブロック・スコープ」の一部であり, ANSI 規格に定める「関数・スコープ」とは, goto文にあらわれるラベル名だけが適用対象であり, このラベル名は関数内のどこからでも参照可能である. なお, ラベル名の識別子は構文の出現とともに暗黙に宣言される.このノートでは「関数・スコープ」と「ブロック・スコープ」を便宜上区別しよう.

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

Example 6.12.4 これらの実際の例は以下の通りである.

#include <stdio.h>

int i ; /* ファイルスコープ */

extern int sum(int a, int b) ; /* 関数プロトタイプスコープ */

int main(int argc,char **argv) /* 関数スコープ */

{

int j ; /* 関数スコープ */

{

int k ; /* ブロックスコープ */

k = 0 ;

}

return 0 ;

}

int l ; /* ファイルスコープ */

識別子は(ラベル名を除いて)すべて宣言以後でないと可視でないことに注意する. したがって, Example6.12.4 の例の識別子 l のスコープを, ファイル全体にしたい場合には, 以下の例のいずれかに書き直す必要がある.

Example 6.12.5 Example 6.12.4 の識別子 l のスコープをファイル全体にする.

#include <stdio.h>

int i, l ;

int main(int argc,char **argv)

{

int j ;

j = 0 ;

{

int k ;

k = 0 ;

}

return 0 ;

}

#include <stdio.h>

int i ;

extern int l ;

int main(int argc,char **argv)

{

int j ;

j = 0 ;

{

int k ;

k = 0 ;

}

return 0 ;

}

int l ;

右の例では, l の定義の位置を変えずに, extern 宣言を用いてスコープを拡大した. しかし, このような例は「奇妙な書き方」であり, わざわざこのようなことをする必要はない47.

Example 6.12.6 文法上許されない変数宣言の例.47要するに, Example 6.12.4 の int l の宣言は, 文法上可能というだけのことである.

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

int main(int argc,char **argv)

{

int j ;

j = 0 ;

int k ; /* この宣言は文法上許されない */

k = 0 ;

return 0 ;

}

この例における int k の宣言は文の後に書かれているため, 文法エラーとなる. 複文内で宣言は文のならびの前に書かなければならない.

6.12.2.3.1 識別子の隠蔽 次のような例では, 識別子の可視性はどうなるのであろうか?

int i ;

int main(int argc, char **argv)

{

int i ;

{

int i ;

}

}

このようにプログラム中に同じ名前空間に属する識別子が複数あり, プログラム中のある点において, それらのうちのいくつかが可視であるとき, その点において見えている識別子は, スコープの最も小さいものとなる. したがって他の識別子は見えなくなる. これを識別子の隠蔽と呼ぶ.

int i ; <= この識別子の示すオブジェクトを i0 としよう

/* この点では識別子 i は, オブジェクト i0 を参照する */

int main(int argc, char **argv)

{

int i ; <= この識別子の示すオブジェクトを i1 としよう

/* この点では識別子 i は, オブジェクト i1 を参照する */

{

int i ; <= この識別子の示すオブジェクトを i2 としよう

/* この点では識別子 i は, オブジェクト i2 を参照する */

}

/* この点では識別子 i は, オブジェクト i1 を参照する */

}

/* この点では識別子 i は, オブジェクト i0 を参照する */

6.12.2.3.2 関数名のスコープ 関数名は通常はファイル・スコープを持つが, 次のような例では関数宣言がブロックスコープとなる.

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

int main(int argc,char **argv)

{

int a ;

extern int foo(int) ;

foo(a) ;

return 0 ;

}

double foo(int a)

{

...

}

この場合, 関数 foo の関数プロトタイプ宣言は, main 関数内の関数スコープとなる.

6.12.2.4 寿命

オブジェクトの寿命とは, 正しくは記憶域期間 (storage duration) とは, プログラム実行状態において,そのオブジェクトの記憶域が存在する期間を指す. C における記憶域期間は静的 (static) と自動 (auto)の2種類がある. 寿命という概念は記憶域と関わる概念であるので, 識別子に対する概念ではなく, オブジェクトに対する概念である.オブジェクトが静的であるとは, プログラム実行の開始から終了までの期間, そのオブジェクトの記憶域

が記憶領域内に存在することをいう. オブジェクトが自動であるとは, プログラム実行中のある期間にのみ,そのオブジェクトの記憶域が記憶領域内に存在することをいう.オブジェクトが静的かどうかは, その宣言方法に依存する. ファイルスコープを持つと宣言されたオブ

ジェクトは, 必ず静的である. 一方, ブロックスコープと関数スコープを持つと宣言されたオブジェクトは,デフォールトでは自動であり, その記憶域は, その関数の実行開始から実行終了までで, 記憶領域内に存在し, その関数の実行期間以外は記憶領域内には存在しない. ブロックスコープと関数スコープを持つオブジェクトを静的にするには, 記憶クラス指定子 static をつけて宣言する.なお, 関数プロトタイプ宣言内で宣言された仮引数宣言は, 定義ではないため, 寿命とは無関係である.

Example 6.12.7 オブジェクトの寿命を見てみよう.

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

int j ;

static int n ;

int add(int x)

{

int i ;

static int l ;

.....

{

int k ;

.....

sub(k) ;

.....

}

}

int sub(int y)

{

int m ;

static int s ;

.....

}

この例で j, l, n, s は静的であり, x, i, k, y, m は自動である. さらに, x, i, k の記憶域存在期間は, 関数add が実行されている間であり, y, m の記憶域存在期間は, 関数 sub が実行されている間である.この例では, add から sub が呼び出されているので, add から呼び出された sub が実行されている間は,

add 内の自動変数の記憶域は存在している.

6.12.2.4.1 オブジェクトの初期化 オブジェクトの初期化の手続きは, 寿命と関連している. オブジェクトが明示的に初期化宣言48されていない場合, 静的オブジェクトはプログラム実行開始時に記憶領域がビットパターン 0 で初期化される. 自動オブジェクトは記憶領域確保時に初期化は行われない.static 宣言されて, 明示的に初期化宣言がされているオブジェクトは, プログラム実行開始時にただ1

度だけ, その値により初期化が行われる. (cf. Example 6.12.10)

6.12.2.5 リンケージ

リンケージ (linkage, 結合) とは, 異なる有効範囲または同じ有効範囲を持って2回以上宣言された識別子を, 同じオブジェクトまたは関数を参照できるようにする操作(概念)である49. リンケージは

• 外部リンケージ (external linkage),

• 内部リンケージ (internal linkage),

• 無結合 (no linkage)

48初期化宣言とは, オブジェクトの定義とともに初期化を行うこと.49C の識別子の概念の中で, もっともわかりにくいのがリンケージであり, その定義はほとんどメチャクチャと思える.

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

の3種類に分類される. 外部リンケージを持つ同じ名前の識別子がプログラム内に複数回現れた場合には,それらは同じオブジェクトまたは関数を表し, 内部リンケージを持つ同じ名前の識別子が一つの翻訳単位(プログラムファイル)中に複数回現れた場合には, それらは同じオブジェクトまたは関数を表す. 無結合を持つ同じ名前の識別子は, それぞれが一意に決まる実体を持つ.すなわち, 同じ名前の識別子が異なったプログラムファイル中に現れ, それらが内部リンケージを持てば,

それらは別々の実体を表し, 同じ名前の識別子が同じファイル中にあっても, それらが無結合であれば, それらは別々の実体を表す.識別子の宣言で記憶クラス指定子 extern または static を指定することにより, リンケージを変えるこ

とが出来る. そのルールは以下の通りである.

1. オブジェクトまたは関数のファイルスコープの識別子の宣言が static を含む場合, 内部リンケージを持つ.

2. オブジェクトまたは関数のファイルスコープの識別子の宣言が extern を含む場合, ファイルスコープで宣言された可視であるその識別子の宣言と同じリンケージを持つ. ファイルスコープで宣言された可視であるその識別子の宣言が無い場合には, 外部リンケージを持つ.

3. 関数の識別子が記憶クラス指定子を持たない場合には, extern を宣言したかのようにリンケージを決定する.

4. オブジェクトの識別子がファイルスコープを持ち, 記憶クラス指定子を持たない場合には外部リンケージを持つ.

5. オブジェクトまたは関数以外を宣言する識別子, 関数仮引数を宣言する識別子, extern を持たないブロックスコープ(または関数スコープ)のオブジェクトを宣言する識別子は, 無結合となる.

さて, こんなことを書かれて一発でわかるわけがないので, いくつかの例を見ていこう.

6.12.2.5.1 プログラムが単一のファイルからなる場合 まず, 外部リンケージは, プログラムが複数の翻訳単位(プログラムファイル)からなる場合にのみ関係する. プログラムが単一のプログラムファイルからなる場合には, 外部リンケージと内部リンケージは, この場合には同一の意味になる.この場合に上の規約を要約すると,

• 関数の場合には, extern をつけても static をつけても, 意味は変らない. すなわち, 単一ファイル内に同じ識別子をもつ関数があれば, 同じものとみなされる.

• オブジェクトの場合には,

– オブジェクトが extern を持つとき, そのオブジェクトと同じ識別子をもつ, ファイルスコープのオブジェクトがあれば, それと同じものとみなされる.

– それ以外, すなわち static か, 何も記憶クラス指定子を持たないときには, 無結合となる. つまり, 同じ識別子を持つ他のオブジェクトとは別のものとなる.

Example 6.12.8 単一のプログラムファイルからプログラムが構成されていると仮定する. 関数 foo と

変数 l のリンケージに注意.

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

#include <stdio.h>

int i ;

int l = 2 ;

static int k ;

int k ;

extern void foo(void) ;

int main(int argc,char **argv)

{

int i ;

extern int l ;

printf("l = %d\n", l) ;

foo() ;

printf("l = %d\n", l) ;

return 0 ;

}

void foo(void)

{

l = 0 ;

return ;

}

関数 foo はファイル中で2度宣言されているが, 4行めの extern があってもなくても, ともに外部リンケージとなり, 同じ実体を示す. 仮に, foo の関数プロトタイプ宣言(4行め)が main 関数ブロック内に

あっても, 結果は変らない. foo の関数プロトタイプ宣言(4行め)が main 関数ブロック内にあると, プロトライプ宣言の可視性の問題により, 他の関数内からの foo の呼び出しには問題を生じる(次の Exampleを参照).変数 l はファイル中で2度宣言されているが, main 関数内の宣言において extern 宣言されているため,

3行めの宣言(定義)と結合し, 結果として l はファイルスコープを持つ. つまり, このプログラムはコンパイル可能であり, 実行すると,

l = 2

l = 0

という出力を得る.その他のオブジェクト, すなわち, ファイルスコープの i と関数スコープの i は無結合であり, それぞれ

は異なる実体を持つ.オブジェクト k の static を含む宣言は内部リンケージを持つ仮定義であり, 次の宣言 int k と記憶

クラスが矛盾し, 動作は不定となることに注意. これを extern int k とすれば, 正しい宣言となり, k はstatic 記憶クラスに属することになる.

Example 6.12.9 関数プロトタイプ宣言の可視性に問題を生じた例.

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

#include <stdio.h>

extern void bar(void) ;

int main(int argc,char **argv)

{

extern void foo(void) ;

foo() ;

return 0 ;

}

int bar(void)

{

foo() ;

}

void foo(void)

{

return ;

}

この例では, foo の関数プロトタイプ宣言は, bar からは可視ではないため, foo の呼び出しに警告が生じる.

これら2つの例は, あくまでリンケージの例で無理やり作ったものであり, 通常は, 関数プロトタイプ宣言はファイルスコープで行い, ブロックスコープ(関数スコープ)のオブジェクトを extern 宣言したりはし

ない.

6.12.2.5.2 プログラムが複数のファイルからなる場合 元々, リンケージとはプログラムが複数のファイルからなる場合に, それぞれのファイルで宣言された識別子を結び付けるために考えられた概念である.ここでは, 関数とオブジェクトの識別子に関して別々に考えよう50

6.12.2.5.2.1 関数のリンケージ まず, 関数の識別子は決して無結合にはならないことに注意しよう. そして, 関数の識別子は extern をつけてもつけなくても, 基本的には外部リンケージを持つという事実に注意する.次のような2つのファイルからなるプログラムを考えよう. この部分では左のファイルを file1.c, 右の

ファイルを file2.c とする.

int main(int argc, char **argv)

{

foo() ;

return 0 ;

}

void foo(void)

{

return ;

}

50それ以外の識別子は無結合なので, 考慮する必要はない.

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

この場合, file1.cで呼び出している関数 foo の実体は, file2.cに書かれている関数 foo なのだろうか?

答えは YES である. なぜなら, file2.c の関数 foo の宣言は外部リンケージを持ち, 外部リンケージとはファイルを跨がって識別子を結び付ける操作である. もちろん, file1.c には foo の関数プロトタイプ

宣言がないので, まずいことがあるのは事実である. また, file1.c における識別子 foo は関数であるこ

とがわかっているので, その宣言がファイル内に存在しなくてもかまわない.それでは, 次の例は?

extern void foo(void) ;

int main(int argc, char **argv)

{

foo() ;

return 0 ;

}

void foo(void)

{

return ;

}

今度は, file1.c に foo の関数プロトライプ宣言を入れた. この場合にも, この関数プロトタイプ宣言は外部リンケージを持ち, file2.c の関数 foo の宣言も外部リンケージを持ち, 外部リンケージとはファイルを跨がって識別子を結び付ける操作であるので, この2つの識別子の宣言は同じ実体を表すことになる. ここで, file1.c の関数プロトタイプ宣言では extern がなくても良い. もちろん, 通常は関数プロトタイプ宣言を書くのが望ましく, その場合には, 「関数プロトタイプ宣言」であることを明示する意味でも, externをつけた方が良い.ところが, 次の例はどうなるだろうか?

int main(int argc, char **argv)

{

foo() ;

return 0 ;

}

static void foo(void)

{

return ;

}

この例では, file2.c で関数 foo を static 宣言している. したがって, file2.c の関数 foo は内部リン

ケージとなり, file1.c の関数 foo の呼び出しは, file2.c の関数 foo を呼び出すわけではない51.すると2つの疑問が生じる.

1. static で定義した関数の関数プロトタイプ宣言はどうするの?

2. 関数の static 宣言って一体何に使うの?

まず,「static で定義した関数の関数プロトタイプ宣言はどうするの?」という疑問に対する答えは,「関数プロトタイプ宣言にも extern なしで static をつける」というのが答えです. static をつけても内部リンケージは残るので, これで問題は解決.「関数の static 宣言って一体何に使うの?」ってのに対する答えは何通りも考えられる. 「プログラムを複数のプログラマで開発する際に, 自分の担当するファイル中だけで利用したい関数は内部リンケージにしておく」というのが, 最もよくある通常の答え.

51file1.c で呼び出される関数 foo がどのようになるかは, この2つのプログラムファイルのオブジェクトコードをリンクするまではわからない. このことについては, Section 6.13 で詳細に議論する.

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

6.12.2.5.2.2 オブジェクトのリンケージ オブジェクトに関しては, ファイルスコープを持つものだけを考えると, リンケージに関しては, 関数とほとんど同一となる. この場合にはオブジェクトは無結合にはならない. そして, ファイルスコープの識別子は extern をつけてもつけなくても, 外部リンケージを持つという事実に注意する. すると, 関数とリンケージの扱いが同一になる.次のような2つのファイルからなるプログラムを考えよう. この部分では左のファイルを file1.c, 右の

ファイルを file2.c とする.

int i ;

int main(int argc, char **argv)

{

i = 0 ;

foo() ;

return 0 ;

}

int i ;

void foo(void)

{

i = 1 ;

return ;

}

この場合, file1.c で利用している変数 i と, file2.c で利用してる変数 i は同一の実体を持つ. なぜなら, file1.c, file2.c のそれぞれのオブジェクト i の宣言は外部リンケージを持ち, 外部リンケージとはファイルを跨がって識別子を結び付ける操作であるため, その実体は同じものとなる.もちろん, どれか一つの宣言を除いて, 他の宣言には extern をつけるのが望ましい. すなわち,

int i ;

int main(int argc, char **argv)

{

i = 0 ;

foo() ;

return 0 ;

}

extern int i ;

void foo(void)

{

i = 1 ;

return ;

}

とするのが良い. したがって, 次のような初期化は明らかな文法エラーとなる.

int i = 0 ;

int main(int argc, char **argv)

{

i = 0 ;

foo() ;

return 0 ;

}

extern int i = 1 ;

void foo(void)

{

i = 1 ;

return ;

}

つまり, 同一の実体を表すオブジェクトを2ヶ所で初期化宣言することは出来ない.オブジェクトの場合も, やはり static 宣言は, 他のプログラムファイルからそのオブジェクトを隠蔽す

るために用いられる. すなわち,

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

int i = 0 ;

int main(int argc, char **argv)

{

i = 0 ;

foo() ;

return 0 ;

}

static int i = 1 ;

void foo(void)

{

i = 1 ;

return ;

}

とすることで, file2.c の i は内部リンケージとなり, file1.c の i とは異なった実体を持つ. この場合でも file1.c の i は外部リンケージを持っている.

Remark 6.12.1 次の2つのプログラムで変数 i の宣言に注意.

int i ;

int i ;

int main(int argc,char **argv)

{

int i ;

return 0 ;

}

int i ;

int main(int argc,char **argv)

{

int i ;

int i ;

return 0 ;

}

左のプログラムは文法エラーではない. なぜならファイルスコープの2つの宣言 int i はともに外部リン

ケージを持ち, この2つの識別子は同じ実体を表している.しかし, 右のプログラムは文法エラーとなる. ブロックスコープ(関数スコープ)の2つの宣言 int i

はともに無結合であるため, 同じ識別子で同じスコープを持つ2つのオブジェクトが存在する.

6.12.2.6 内部静的変数の利用法

内部リンケージまたは無結合である静的変数(オブジェクト)は, 簡単に内部静的変数と呼ばれることがある.ファイルスコープを持つ内部静的変数の利用方法は, 上に述べた通り, 他のファイルからのオブジェクト

の隠蔽であったが, ブロックスコープ(関数スコープ)を持つ内部静的変数は, ブロック外への変数の隠蔽という効果の他に重要な役割を果たす.すなわち, 関数内で定義した変数に static をつけて宣言すると, その変数に対する寿命は大域的となる.

しかし, スコープは static をつけない時と同じである. しまも, static 変数の初期化は, それがはじめて利用される時にただ一度だけ行われる. この静的な宣言は, 関数のカウンタ, フラグなどに用いる.

Example 6.12.10 この例で, 変数 i は i+=1 以外に値を変える操作がないとする.

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

int function()

{

static int i = 0 ;

i += 1 ;

.....

}

この時, i はこの関数が呼び出された回数を保持している.

6.12.3 初期化

これまでにみてきたように, オブジェクトはその定義時に初期化子により初期化を行うことが出来る52.ここでは, どのようなオブジェクトに対して, 初期化子による初期化が可能かを考えてみよう. まず, [3,

X 3010, 6.5.7, p. 1910] を参照しよう. そこには, 「静的記憶域期間を持つオブジェクトの初期化子, または集成体型もしくは共用体型を持つオブジェクトの初期化子並びにおいて, 全ての式は定数式でなければならない」とある. 逆にいえば, 自動記憶域期間をもつ任意のオブジェクトを初期化することができる. (cf.[3, X 3010, 6.5.7, p. 1910, Footnote 74].) すなわち, 静的記憶期間を持ち, 初期化子を持つオブジェクトは, プログラムの実行開始時に data セグメントに配置され, 定数式によって与えられた初期化式により決まる値が代入される. したがって, 次のような例は文法違反となる.

Example 6.12.11 静的記憶期間を持つオブジェクトに対して, 定数式ではない初期化子を与えている例.

int a = 1, b = a ;

int main(int argc, char **argv)

{

....

}

これは, b = a の右辺の初期化子が定数式になっていない.

一方, 自動記憶域期間を持つオブジェクトは, 任意の式で初期化が可能である.

Example 6.12.12 自動記憶域期間を持つオブジェクトを, 関数仮引数の値で初期化した例.

int foo(int a)

{

int b = a ;

...

}

もっと邪悪な例として, 次のようなものが考えられる.

Example 6.12.13 次の2つの初期化の違いは重要である.

52集成体型または共用体型を持つオブジェクトの場合には, 初期化子並びによって初期化を行うことが出来る.

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

int foo(void)

{

int a = 0, b = a ;

...

}

この例は正しく動作する. すなわち, オブジェクト a が定義された後, b を定義し, それを a

の値で初期化している.

int foo(void)

{

int b = a, a = 0 ;

...

}

この例は文法違反となる. すなわち, オブジェクト b の定義中に与えられた初期化式 a は, この時点では未定義となっている.

複数のオブジェクトを同時に宣言している場合, すなわち, 識別子並び53において, 複数の識別子がある場合には, 左から右に対して, 一つづつ宣言が行われていると解釈すべきである54

さらに, 次のような例もありうる.

Example 6.12.14 この例は, 文法上は正しいのだが, 「期待した通り」には動作してくれない.

int a ;

foo(void)

{

int a = a ;

...

}

このような例を書く場合には, 関数 foo 内のオブジェクト a の初期化式 a は, ファイルスコープのオブジェクト a の値を代入したいと考えているのだろう. しかし, オブジェクトのスコープを考えてみれば, 初期化式 a は関数(ブロック)スコープの a を参照することになる.一方, 次の例を考えてみよう.

int a = 1 ;

int foo(void)

{

int b = a, a = 0 ;

...

}

この例では, int b = a の段階で, 関数スコープの a は定義されていないため, int b = a の初期化式 a

はファイルスコープの a の値を参照することとなる.

Example 6.12.15 ブロックスコープを持つ識別子であっても, 静的記憶期間をもつものがあった.

int foo(int n)

{

static int a = n ;

...

}53ここで用いられている “,” は, 「コンマ演算子」ではなく, 「識別子並び」中の「区切り子」であることに注意.54[3, X 3010, 6.5.4 宣言子, p. 1904] の「意味規則」によると, 「各宣言子は一つの識別子を宣言する. 式の中にその宣言子と同じ形式のオペランドが現れた場合, そのオペランドは, 宣言子指定子列が指示する有効範囲, 記憶域期間及び型を持つ関数またはオブジェクトを指し示す.」とあり, 複数の識別子を一つの宣言に並べたとしても, それは複数の宣言子が与えられたと解釈すべきであり,Cの文法規則により, 左から右に解釈される.

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

という初期化は許されない.

なお, [3, X 3010, 6.5.7, p. 1910] では, 「識別子の宣言がブロック有効範囲を持ち, かつ識別子が内部結合または外部結合を持つ場合, その宣言にその識別子に対する初期化子があってはならない」とある. これは, ブロック内で extern を伴って宣言した識別子には, 初期化子をつけてはならないことを意味している.

6.12.4 演習問題

Exercise 6.12.1 次のプログラムの出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>

int i=0 ;

int main()

{

auto int i=1 ;

printf("i=%d\n",i) ;

{

int i=2 ;

printf("i=%d\n",i) ;

{

i += 1 ;

printf("i=%d\n",i) ;

}

printf("i=%d\n",i) ;

}

printf("i=%d\n",i) ;

}

Exercise 6.12.2 次のプログラムの出力結果がなぜそのようになるかを考えよ.

C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp

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

#include <stdio.h>

int i=0 ;

int main()

{

int i=1 ;

func_1(i) ;

printf("1: i=%d\n",i) ;

func_1(i) ;

printf("1: i=%d\n",i) ;

}

int func_1(int n)

{

int i=0 ;

i += 1 ; n += 1;

printf("2: i=%d\n",i) ;

}

また, 関数 sub_function 内で定義された変数 i を static int i = 0 と定義するとどうなるかを考察

せよ.

6.13 コンパイルとリンク

C 言語で書かれたプログラムを実行形式に翻訳する手順は, 次のステップに分解される.

1. プログラムファイル中に書かれたマクロ定義などの処理を行うプリプロセッサ (preprosessor55.

2. プログラムファイルをオブジェクトコード (object code) と呼ばれる, 機械が認識可能な命令の列に置き換えるコンパイル (compile). コンパイルを行う一連の処理系をコンパイラ (compiler) と呼ぶ.

このステップでは, プログラムテキストを解析して, 中間言語に翻訳し, 中間言語からアセンブラコード(命令のニーモニックで書かれた言語)に翻訳する. さらに, アセンブラコードをオブジェクトコードに変換するアセンブラの3ステップからなることが多い.

3. 複数のオブジェクトコードと標準関数などのオブジェクトコードの集まりである, ライブラリとを結合して, 実行形式を出力するリンク (link). リンクを行うプログラムをリンカ (linker) と呼ぶ.

55プリプロセッサの終了時のプログラムコードを出力するには, gcc -E file.c とすれば良い. ここでは, マクロ定義等が展開された後の, コンパイラにかかる直前のプログラムを得ることが出来る.

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

link

file2.ofile1.o

compilecompile

exec code

library

file1.c file2.c

単一のプログラムファイルから実行形式を作成するための手順

% gcc file.c -o target

というコマンドは, これらの一連の操作を一度に行わせる命令である. 以下では, 複数のプログラムファイルからなるプログラムを, オブジェクトコードの作成, リンクの手順に分けて, そのためのコマンドと, それらの役割を見ていこう.

6.13.1 オブジェクトコード

6.13.1.1 オブジェクトコードの作成

file1.c というプログラムファイルからオブジェクトコードを作成するには,

% gcc -c file1.c

というコマンドを利用する. これによってオブジェクトコード file1.o が生成される.オブジェクトコードの作成は, アセンブラコードの作成とアセンブラコードの翻訳という2段階にわか

れる. プログラムファイルからアセンブラコードを出力させるためには,

% gcc -S file1.c

とすれば, file1.s というアセンブラコードを作成させることもできる. もちろん, 実行形式の作成のために必要なステップは, オブジェクトコードの作成だけである.

6.13.1.2 オブジェクトコードの中身

ここでは,オブジェクトコードには何が書かれるかを調べるために,以下の2つのファイル(左を file1.c,右を file2.c とする)を利用しよう.

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

#include <stdio.h>

int k = 0 ;

extern int i ;

extern int add(int) ;

int main(int argc,char **argv)

{

int j ;

static int l = 0 ;

i = j = 0 ;

add(j) ;

printf("%d\n", i) ;

printf("%d\n", j) ;

return 0 ;

}

extern int k ;

extern int i ;

static int l = 0 ;

int add(int j)

{

i += 1 ; l += 1 ; k += 1 ;

return j + 1 ;

}

static void foo(void)

{

k = 0 ; l = 0 ;

return ;

}

このプログラムコードをコマンド gcc -S file1.c, gcc -S file2.c を用いて得たアセンブラコードは

以下のようになる56.

############## file1.c のアセンブラコード

.file "file1.c"

gcc2_compiled.:

.global k

.section ".data"

.align 4

.type k,#object

.size k,4

k:

.uaword 0

.align 4

.type l.3,#object

.size l.3,4

l.3:

.uaword 0

.section ".rodata"

.align 8

.LLC0:

.asciz "%d\n"

.section ".text"

.align 4

.global main

.type main,#function

56これは, SunOS 5.6 上の gcc version 2.95.1 を用いて作成したアセンブラコードで, アセンブラコード, オブジェクトコードの出力は, 処理系(環境, OS, コンパイラ等)に依存する.

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

.proc 04

main:

!#PROLOGUE# 0

save %sp, -120, %sp

!#PROLOGUE# 1

st %i0, [%fp+68]

st %i1, [%fp+72]

sethi %hi(i), %o1

or %o1, %lo(i), %o0

st %g0, [%fp-20]

st %g0, [%o0]

ld [%fp-20], %o0

call add, 0

nop

sethi %hi(i), %o0

or %o0, %lo(i), %o1

sethi %hi(.LLC0), %o2

or %o2, %lo(.LLC0), %o0

ld [%o1], %o1

call printf, 0

nop

sethi %hi(.LLC0), %o1

or %o1, %lo(.LLC0), %o0

ld [%fp-20], %o1

call printf, 0

nop

mov 0, %i0

b .LL2

nop

.LL2:

ret

restore

.LLfe1:

.size main,.LLfe1-main

.ident "GCC: (GNU) 2.95.1 19990816 (release)"

############## file2.c のアセンブラコード

.file "file2.c"

gcc2_compiled.:

.section ".data"

.align 4

.type l,#object

.size l,4

l:

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

.uaword 0

.section ".text"

.align 4

.global add

.type add,#function

.proc 04

add:

!#PROLOGUE# 0

save %sp, -112, %sp

!#PROLOGUE# 1

st %i0, [%fp+68]

sethi %hi(i), %o1

or %o1, %lo(i), %o0

sethi %hi(i), %o2

or %o2, %lo(i), %o1

ld [%o1], %o2

add %o2, 1, %o1

st %o1, [%o0]

sethi %hi(l), %o1

or %o1, %lo(l), %o0

sethi %hi(l), %o2

or %o2, %lo(l), %o1

ld [%o1], %o2

add %o2, 1, %o1

st %o1, [%o0]

sethi %hi(k), %o1

or %o1, %lo(k), %o0

sethi %hi(k), %o2

or %o2, %lo(k), %o1

ld [%o1], %o2

add %o2, 1, %o1

st %o1, [%o0]

ld [%fp+68], %o1

add %o1, 1, %o0

mov %o0, %i0

b .LL2

nop

.LL2:

ret

restore

.LLfe1:

.size add,.LLfe1-add

.align 4

.type foo,#function

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

.proc 020

foo:

!#PROLOGUE# 0

save %sp, -112, %sp

!#PROLOGUE# 1

sethi %hi(k), %o1

or %o1, %lo(k), %o0

st %g0, [%o0]

sethi %hi(l), %o1

or %o1, %lo(l), %o0

st %g0, [%o0]

b .LL3

nop

.LL3:

ret

restore

.LLfe2:

.size foo,.LLfe2-foo

.ident "GCC: (GNU) 2.95.1 19990816 (release)"

ここで, プログラムファイル file1.c と file2.c の中で利用している関数, オブジェクトと, アセンブラコード中の記述の対応を見ていこう. ここで, gcc -c file1.c, gcc -c file2.c によって出力したオ

ブジェクトコードは, 通常の人間が理解できる形式ではないし, アセンブラコードも良くわからないので,UNIX のコマンド nm によってオブジェクトコードのシンボルテーブル (symbol table) を出力させ, このテーブルと, アセンブラコード, プログラムファイルを比較してみよう.

file1.o:

[Index] Value Size Type Bind Other Shname Name

[9] | 0| 0|SECT |LOCL |0 |.comment |

[2] | 0| 0|SECT |LOCL |0 |.text |

[3] | 0| 0|SECT |LOCL |0 |.data |

[4] | 0| 0|SECT |LOCL |0 |.bss |

[7] | 0| 0|SECT |LOCL |0 |.rodata |

[8] | 0| 0|NOTY |LOCL |0 |ABS |*ABS*

[13] | 0| 0|NOTY |GLOB |0 |UNDEF |add

[1] | 0| 0|FILE |LOCL |0 |ABS |file1.c

[5] | 0| 0|NOTY |LOCL |0 |.text |gcc2_compiled.

[12] | 0| 0|NOTY |GLOB |0 |UNDEF |i

[10] | 0| 4|OBJT |GLOB |0 |.data |k

[6] | 4| 4|OBJT |LOCL |0 |.data |l.3

[11] | 0| 108|FUNC |GLOB |0 |.text |main

[14] | 0| 0|NOTY |GLOB |0 |UNDEF |printf

file2.o:

[Index] Value Size Type Bind Other Shname Name

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

[2] | 0| 0|SECT |LOCL |0 |.text |

[3] | 0| 0|SECT |LOCL |0 |.data |

[4] | 0| 0|SECT |LOCL |0 |.bss |

[9] | 0| 0|SECT |LOCL |0 |.comment |

[8] | 0| 0|NOTY |LOCL |0 |ABS |*ABS*

[10] | 0| 120|FUNC |GLOB |0 |.text |add

[1] | 0| 0|FILE |LOCL |0 |ABS |file2.c

[7] | 120| 44|FUNC |LOCL |0 |.text |foo

[5] | 0| 0|NOTY |LOCL |0 |.text |gcc2_compiled.

[11] | 0| 0|NOTY |GLOB |0 |UNDEF |i

[12] | 0| 0|NOTY |GLOB |0 |UNDEF |k

[6] | 0| 4|OBJT |LOCL |0 |.data |l

コマンド nm の出力の最右行にある “Name” は, シンボル名と呼ばれ, “Bind” で示されるスコープを持つ.また, “Shndx” で UNDEF とされたシンボルは, リンク時にその配置が決定されることを示す.

関数 file1.c の中では, 関数 printf, add が利用されているが, これらの関数は, アセンブラコード中で,call printf, call add という形で, サブルーチン呼び出しとして書かれていることに注意しよう.

また, file1.c で定義されている関数 main と, file2.c で定義されている関数 add は, 外部リンケージを持つので, それぞれのオブジェクトコード中で, Type が FUNC, Bind が GLOB と定義されて

いる57. しかし, file1.o では, 関数 add と, ライブラリ関数 printf は UNDEF とされ, それらの場所はリンク時に解決が行われるものとして処理されている.

file2.c の関数 foo は static 宣言され, 内部リンケージとなっているので, file2.o 中で, Typeが FUNC, Bind が LOCL と定義されている.

オブジェクト file1.c, file2.c で利用されているオブジェクトは, 次の5つに分類できる.

内部自動変数 file1.c の main 関数内の j が該当する.

アセンブラコード内では, 明示的なラベル(行頭から書かれていて, : がついている識別子)には表れず, %hi(i), %lo(i) 等として参照されている. したがって, シンボルテーブルにもこのオブジェクトは表れない.

内部静的変数 file1.c 中の main 関数内の l が該当する.

シンボルテーブルでは, LOCL な OBJT (オブジェクト)とされ, その “Size” が4バイトであると明示されている58.

外部リンケージを持ち初期化宣言されている変数 file1.c の k が該当する.

シンボルテーブルでは, GLOB な OBJT とされ, 4バイトであることがわかる.

外部リンケージを持ち初期化宣言されていない変数 file1.c と file2.c の i が該当する. これら2つの識別子は外部リンケージで同じオブジェクトを指していることに注意.

シンボルテーブルでは, GLOB な NOTY (No Type) とされ, その配置は UNDEF となっている59.

内部リンケージを持つ大域変数 file2.c の l が該当する.

57アセンブラコード中で, text セクション内で global と定義されていることに対応する.58アセンブラコード内(file1.s 内)で, data セクション内に定義され, 0 で初期化されていることに相当している.59アセンブラコード中でも, 内部自動変数と同じ扱いを受けている.

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

シンボルテーブルでは, LOCL な OBJT とされ, 4バイトであることがわかる60.

なお, シンボルテーブル中の関数に対する “Size” の値は, その関数の実行コードサイズをあらわし, “Value”はオブジェクトコード中における先頭からのバイト数を表している61.このように, オブジェクトコードは, 未解決なシンボル名を含む, 環境に依存した命令の列やオブジェク

トの配置情報を含んだデータである. これらの未解決シンボルとオブジェクトの配置は, 複数のオブジェクトコードの結合を行うリンカによって解決される.

6.13.2 リンク

プログラムファイルからコンパイラを利用して生成したオブジェクトコードを結合して, シンボルを解決して, オブジェクトを配置することによって, 実行可能コードを作成することが出来る. この操作をリンク(link) とよぶ.だが, ちょっと待った!Section 6.13.1.2 での例を見ればわかるように, file1.o と file2.o を結合した

だけでは, シンボル printf が解決できない. 関数 printf は C の標準ライブラリ関数であるため, この関数の実体を含むライブラリ (library) もついでに結合しておかなければ, すべてのシンボルを解決し, その実体を明らかにすることが出来ない. つまり, リンクとは必要であれば, ライブラリも結合するという操作を含むことになる.

6.13.2.1 実行形式の作成

Section 6.13.1.2 での例で作成した2つのオブジェクトコード file1.o と file2.o, さらに, C の標準ライブラリをリンクするには,

gcc file1.o file2.o -o target

とする. ここで, -o target に書かれた target が, リンカが出力する実行形式のファイル名となる.でも, この命令では「標準ライブラリ」を指定していないが?通常のリンカは「標準ライブラリ」を必

ず結合するようになっているため, 明示的に標準ライブラリを指定しなくても良い. 標準関数ライブラリはUNIX の場合, 通常 /usr/lib/libc.a というファイルである.しかし, C の標準関数の中には, 「数学関数」と呼ばれる関数群があり62, これらの実体は標準関数には

入っていない. 数学関数ライブラリは UNIX の場合には通常は /usr/lib/libm.a であり, 数学関数ライブラリ63を必要とする場合には,

gcc file1.o file2.o -o target -lm

60オブジェクトコード(アセンブラコード)中での扱いは, 内部静的変数とほとんど同じ扱いであることに注意. したがって, C プログラムのレベルでは static の意味が多少異なるが, 生成するオブジェクトコードレベルになると, 内部リンケージを持つファイルスコープの変数と, 内部静的変数は全く同じ扱いになることに注意しよう.# だから, 同じ static という記憶クラス指定子を持つ.61正確には, そのオブジェクトコード中の “text” セグメントの先頭からの「オフセット」と呼ばれる値である.62例えば, 三角関数の値を求める sin や, 対数関数 log がある.63なぜ数学関数ライブラリのリンクを明示的に指定しなければいけないのだろうか?数学関数ライブラリは, ユーザの目的によっては, その精度や速度に問題がある可能性が否定できない. 標準的な数学関数ライブラリの場合には, 精度と速度が適切になるようなコードから生成されていることが多く, より高い精度や, より高速な実行を求める場合には, 必要に応じて, 異なった数学関数ライブラリを用いることが考えられる. そのため, 数学関数ライブラリが標準関数ライブラリから独立していると考えられる.しかし, Darwin (MacOS X) の Public Beta Version では, 数学関数は標準関数ライブラリに組み込まれていた. Darwin の元

となった NeXTSTEP でどのような構成になっていたかは, 私は良く知らないが, まあ, Darwin では数値計算はするなということなのだろう.

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

のように, -lm というオプションをリンカに渡す必要がある64. 最後に, なぜ -lm オプションを最後につけ

るかという理由を考えてみよう. 例えば, file1.o は関数 sin の呼び出しを含むとき,

gcc -lm file1.o file2.o -o target

としても, リンカは正しく動作する. しかし,

Undefined first referenced

symbol in file

sin file1.o

ld: fatal: Symbol referencing errors.

というエラーを出力するだろう. これは, 先に libm.a のシンボルが評価され, file1.o にある未解決シンボル sin の解決が出来なくなっていることを表している. したがって, ライブラリの指定は一番最後にしなくてはならない. また, 同様にライブラリの指定の順序によっては, シンボルの解決が出来ない場合がありうるので注意が必要である.

6.13.2.2 動的リンクライブラリ

C で作成したプログラムをコンパイル(リンク)すると, 必ず標準ライブラリがリンクされてしまう. 標準ライブラリの大きさは,

• Sun Microsystems の Solaris 2.6 (SunOS 5.6 Generic 105181-05 sparc SUNW) で, 約 1.6 MB,

• Sun Microsystems の SunOS 4.1.4 (SunOS 4.1.4-JL 1) で, 約 670 KB,

• FreeBSD 4.2-Release (4.2-RELEASE FreeBSD) で, 約 1.1 MB

と非常に巨大なファイルである. すべてのプログラムの実行コードにこの大きさのライブラリがリンクされると, 巨大なディスクスペースが必要となる.そのため, 最近の UNIX システムや Windows, MacOS 等では, 動的リンクライブラリ (Dynamic

Linking Library) を用いて, 標準ライブラリなどをプログラム実行時にリンクするという方法をとっている.動的リンクライブラリを用いるもう一つの長所として, もし, 標準ライブラリなどにバグがあった場合,

プログラムを再リンクすることなく, 動的リンクライブラリだけを入れ替えることにより, バグを解消可能となる. しかしながら, 動的リンクライブラリを用いると, プログラムの実行時でのライブラリのリンクの時間だけ実行時間が大きくなるという欠点があり, 現状では, 標準的なライブラリに関しては動的リンクライブラリを, いくつかの特殊な(そのプログラムだけで利用するものなど)ライブラリでは, 静的リンク(static link) (リンク時にライブラリをリンクしてしまう方法)を用いるという使い分けをしている.

6.13.2.2.1 ライブラリの作成方法 オブジェクトコードを静的リンクライブラリとしてまとめる(アー

カイブ (archive) するという)時には, コマンド ar を用いて,

ar -q libx.a file2.o file3.o

とすれば, file2.o, file3.o を libx.a にアーカイブでき, リンク時に -lx オプションで静的にリンクで

きる65. また, オブジェクトコードを動的リンクライブラリにアーカイブするときには,64-l の後に空白なしに指定した文字を XXXX とすると, リンカは指定されたディレクトリから libXXXX.a という名前のライブラリを探し, それをリンクする. 指定されたディレクトリとは, 通常は /usr/lib であり, それ以外のディレクトリもライブラリの検索対象としたい場合には, -L/usr/local/lib のように -L オプションで明示的にディレクトリを指定する必要がある.

65静的リンクライブラリの拡張子 .a は archive の略であるのは明らかだろう.

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

gcc -o libx.so -G file2.o file1.o

とすればよい6667.

6.13.2.3 インターポジショニング

インターポジショニング (interpositioning) とは, ライブラリ中で定義されている関数を自前の関数で置き換えてしまうことを指す. C では, 標準関数の識別子名は予約 (reserve) されているが, その識別子を使ってはいけないという意味ではない68. 例えば, C の標準関数 islower を考えてみよう. 次のようなプログラムを書いたら何が起こるかということである69.

#include <stdio.h>

extern int islower(int) ;

int main(int argc, char **argv)

{

int c=’a’ ;

if (islower(c)) printf("%c is lower character\n",c) ;

else printf("%c is not lower character\n",c) ;

return 0 ;

}

int islower(int c)

{

if ((c >= ’A’)&&(c <= ’Z’)) return 1 ;

return 0 ;

}

当然, “a is not lower character” という出力を得る. これだけであれば, 正しく動作するのだが, もし,他の標準関数で islower 関数を利用している関数70を利用したらどうなるのだろうか?この場合には, 標準ライブラリの islower ではなく, このプログラムファイル中にある islower が利用される. ということは, 悲惨な結末を向かえることになるのは明らかである.このように, 標準関数内で定義されているシンボル名を関数名に利用してはいけない.

66動的リンクライブラリの拡張子 .so は shared object の略である. また, あるプログラムがどのような動的リンクライブラリを用いているかは, ldd コマンドで知ることが出来る. ldd /usr/bin/cp としてみるとよい.

67しかし, Solaris 2.xの動的リンクライブラリには少々面倒なところがあり, 実行時の動的リンクライブラリの検索パスを指定するために, プログラムのリンク時に -R オプションにより明示的に動的リンクライブラリを指定するか, シェルの環境変数 LD LIBRARY PATH

で動的リンクライブラリのあるディレクトリを指定する必要がある. SunOS 4.x などでは, 動的リンクライブラリのリンクキャッシュld.so があり, そこに動的リンクライブラリのハッシュテーブルを構成できた. 個人的にはこちらの方が好みなのだが, ld.so をつぶしてしまうと悲惨なことが起きるという欠点がある.# 実際, 私は ld.so を間違って消してしまった経験がある.68[6] にも書かれている通り, これは「警告」対象とはならない. せめて警告くらいはしてくれる仕様にして欲しいのは誰でも思うことなのだが...

69islower は ctype.h で宣言されている.70FreeBSD 4.2 RELEASE のライブラリ群のソースコード (/usr/src/lib 以下) を見てみると, libc/net/inet network.c などで利用されている.

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

6.13.3 メモリ配置

ここでは, C で書かれたプログラムが実行される場合に, 各種のオブジェクトがどのようにメモリ上に配置されていくかを調べてみよう. これによって,関数呼び出しの場合の引数の評価,実行時のエラー (Section6.22.3 参照) の意味が明確になってくる.

6.13.3.1 オブジェクトのメモリ配置

UNIX では実行形式のファイルが呼び出されると, mmap システムコールにより, 実行形式のファイルが主記憶上に配置され, そのエントリポイントに処理が移ることによって, 実行形式が実行される. ここでは,実行形式が主記憶上に配置されたときのメモリ配置を見てみよう.簡単な例として, 次のプログラムコードを考えてみよう.

int i, j = 0 ;

static int k, l = 1 ;

int main(int argc, char **argv)

{

int n, m = 1 ;

static int s, t = 0 ;

n = l + j ;

return 0 ;

}

このコードをコンパイルして, 実行形式を作成し, そのシンボルテーブル(一部省略)を見てみると71,

a.out:

[Index] Value Size Type Bind Other Shname Name

[61] | 67200| 116|FUNC |GLOB |0 |.text |_start

[73] | 133784| 4|OBJT |WEAK |0 |.bss |environ

[62] | 133780| 4|OBJT |GLOB |0 |.bss |i

[63] | 133688| 4|OBJT |GLOB |0 |.data |j

[50] | 133776| 0|OBJT |LOCL |0 |.bss |k

[47] | 133692| 4|OBJT |LOCL |0 |.data |l

[79] | 67600| 72|FUNC |GLOB |0 |.text |main

[33] | 133728| 24|OBJT |LOCL |0 |.bss |object.11

[48] | 133760| 0|OBJT |LOCL |0 |.bss |s.3

[49] | 133696| 4|OBJT |LOCL |0 |.data |t.4

となる. 前にも述べた通り, 変数 i, j, k, l, s, t がシンボルテーブル上に表れ, 変数 n, m は内部自動変数であるので, シンボルテーブルには表れない.ここで, UNIX のメモリ管理のセグメント (segment) という概念が必要となる. UNIX におけるセグメ

ント72とは, 実行形式に割り当てられた主記憶の区切りのことであり, UNIX では

71これは, nm -s a.out により生成した.72MS-DOS におけるセグメントとは異なるので注意すること. MS-DOS におけるセグメントとは, 80286 CPU のアドレス管理方法に依存したもので, 16 ビットアドレス管理で管理可能なメモリの区切りを指す. ちなみに 80286 CPU は 20 ビットアドレス線を持ち, 上位から 16 ビットと下位から 16 ビットのオフセットとセグメントによるメモリ管理を行っていた.

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

• text セグメント.

実行可能形式のコマンド列を格納する部分.

• data セグメント.

実行可能形式の大域的なオブジェクトのうち, 明示的な初期化が与えられたオブジェクトが格納される部分.

• bss セグメント.

実行可能形式の大域的なオブジェクトのうち, 明示的な初期化が与えられていないオブジェクトが格納される部分.

• stack セグメント.

実行中に自動変数や関数呼び出し, 動的なメモリ割り当てで利用する部分.

の4つに分けられる. これらのセグメントは, 次の図のように割り当てられるのが普通である.

text セグメント

data セグメント

bss セグメント

stack セグメント

スタック

ヒープ

命令の列

jlt

iks

n (main 呼出し時に)m (main 呼出し時に)

text セグメント, data セグメントは, 主記憶への配置時に, 実行ファイル中にある値で埋め尽くされる.bss セグメントは実行開始

時に 0 でクリアされる.stack セグメントは, 関数呼び出しに伴い, 上位メモリから順に利用される.(スタックの利用)また, 動的メモリ割り当てのうち

alloca関数では,スタックの利用が可能である.実行中の動的メモリ割り当

て (malloc 関数の呼出しなど) では, stack セグメントが下位メモリから順に

利用される. (ヒープ領域の利用)

size コマンドで, a.out の各セグメントの大きさを調べると,

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

% size a.out

2288 + 360 + 68 = 2716

となり, text セグメントが 2288 バイト, data セグメントが 360 バイト, bss セグメントが 68 バイトで

あることがわかる.仮にプログラム内部でこれらのセグメントを越えてメモリのアクセスを行う73と, “segmentation fault”

という実行時エラーを発生して, プログラムの実行が停止する.stack セグメントは, 次に述べる関数呼び出し手順の中で利用され, stack セグメントにどれだけの大き

さが割り当てられるかは, 実行形式を呼び出したシェルの環境に依存する. stack セグメントの大きさは

limit コマンドで表示される値

cputime unlimited

filesize unlimited

datasize 2097148 kbytes

stacksize 8192 kbytes

coredumpsize 0 kbytes

vmemoryuse unlimited

descriptors 64

で知ることが出来る.

6.13.4 関数呼び出しの手順

Section 6.11.1.7 では, 関数呼出しを行った場合のプログラムの動作の様子を考察し, 関数への実引数は「値渡し」が行われることを述べた. ここでは, それがメモリ内で何をしていることになるのかを考察してみよう.

6.13.4.1 関数実引数の渡し方

ここでは, 次のようなプログラムを例としよう.

extern int add(int, int) ;

int main(int argc, char **argv)

{

int n, m ;

n = 1 ; m = 2 ;

n = add(n,m) ;

return 0 ;

}

int add(int a, int b)

{

73これは, 「ポインタ」を用いると容易に実現できる.

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

return a + b ;

}

このプログラム中で, 関数 add を呼び出す直前, 呼び出した後, add の終了時のメモリ内の様子は, 以下の通りとなる.【注意】n, m は内部自動変数なので, メモリはすべてスタックセグメントが用いられる.

呼出し前 呼び出した後 関数終了直前main の n, 値 1main の m, 値 2

main の n, 値 1main の m, 値 2

add の戻り値を格納する部分add の a, 値 1add の a, 値 2

値のコピー

値のコピー

main の n, 値 1main の m, 値 2

値 3add の a, 値 1add の a, 値 2

main に戻ってきたとき 関数呼出し終了main の n, 値 1main の m, 値 2戻り値 3

main の n, 値 3main の m, 値 2

この図のように, 関数呼出しを行うと, 関数の実引数はスタック内に新しい記憶領域が確保され, そこへ実引数の値がコピーされる. したがって,

extern int add(int) ;

int main(int argc, char **argv)

{

int n ;

n = 1 ; add(n) ;

return 0 ;

}

int add(int a) ;

{

return ++a ;

}

というプログラムでは,

呼出し前 呼び出した後 関数終了直前main の n, 値 1 main の n, 値 1

add の戻り値を格納する部分add の a, 値 1

main の n, 値 1値 2

add の a, 値 2

main に戻ってきたとき 関数呼出し終了main の n, 値 1戻り値 2

main の n, 値 1

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

となり, n の値が変化しない理由は明らかとなる. しかし, ファイルスコープのオブジェクトは静的であり,データセグメントまたは bss セグメントに格納されるため, それらを関数内で変更すると, その変更は永続的となる.

extern int add(int) ;

int k = 0 ;

int main(int argc, char **argv)

{

int n = 1 ;

n = add(n) ;

return 0 ;

}

int add(int a) ;

{

int b = 1 ;

b += 1 ; k += 1 ;

return ++a ;

}

というプログラムの場合には,

呼出し前 呼び出した後 return 文実行直前main の n, 値 1

k, 値 0

main の n, 値 1add の戻り値を格納する部分

add の a, 値 1add の b, 値 1

k, 値 0

main の n, 値 1add の戻り値を格納する部分

add の a, 値 1add の b, 値 2

k, 値 1

関数終了直前 main に戻ってきたとき 関数呼出し終了main の n, 値 1戻り値 2

add の a, 値 2add の b, 値 2

k, 値 1

main の n, 値 1戻り値 2

k, 値 1

main の n, 値 2

k, 値 1

となり, k の値は変更されている. (k はデータセグメントに格納されている.)

Remark 6.13.1 関数呼出しの時には, ここで説明したものよりも多くのデータがスタックに積まれる. 関数呼出しの時には, その時点でのレジスタ情報, 関数終了時にプログラム制御が戻るべきテキストセグメント内のアドレス(プログラム・カウンタ)など, 多くの情報がスタックに積まれ, その後に戻り値領域, 関数実引数領域が確保される.また, 関数実引数がスタック上に積まれる順序は処理系依存である. 実際には, オペレーティングシステ

ムとライブラリ, 処理系などで整合性のある渡し方が行われる74.74多くの処理系では後ろに書かれた実引数が先にスタックに積まれることが多い. また, Pascal, Fortran などの言語では, スタックに積まれる順序が指定されていて, それらで書かれたライブラリを使う場合には, 処理系依存のオプションを利用することにより,スタックに実引数を積む順序を指定できることが多い.

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

6.13.4.2 再帰的関数呼出しの様子

次に再帰的関数呼出しを行う時の様子を見てみよう. Example 6.11.7 で用いた, 帰納的に定義された数列 an+1 = an + 2, a0 = 0 の an を求める関数を利用しよう.

extern int func(unsigned int) ;

int main(int argc, char **argv)

{

recursive_function(2) ;

return 0 ;

}

int func(unsigned int n)

{

if (n == 0) return 0 ;

return func(n-1) + 2 ;

}

この関数の呼出しは以下のように行われることがわかる.

呼出し前 func(2) 呼び出し後 func(1) 呼び出し後(func(2) の戻り値)

(func(2) の実引数) 2(func(2) の戻り値)

(func(2) の実引数) 2(func(1) の戻り値)

(func(1) の実引数) 1

func(0) 呼出し後 func(0) 終了 func(1) 終了(func(2) の戻り値)

(func(2) の実引数) 2(func(1) の戻り値)

(func(1) の実引数) 1(func(0) の戻り値)

(func(0) の実引数) 0

(func(2) の戻り値)(func(2) の実引数) 2(func(1) の戻り値)

(func(1) の実引数) 1(func(0) の戻り値) 0

(func(2) の戻り値)(func(2) の実引数) 2(func(1) の戻り値) 2

func(2) 終了 関数呼出し 終了(func(2) の戻り値) 4

各 func() 終了時には,

return func(n-1) + 2

が行われ, 直前の戻り値に 2 を加えたものがその関数の戻り値となる. このことから, 再帰的な関数呼出しがスタックを順に利用していることがわかる.

6.13.4.2.1 再帰的関数呼出しでスタックをあふれさせる さて, 再帰的関数呼出しを実行して, スタック領域が使い尽くされていくことを実感するために, 以下のような「荒っぽい」ことをしてみよう.上で利用した関数 func を大量に呼び出して, スタックセグメントが使い尽くされると何が起こるだろう

か?まず, 以下のプログラムを実行してみよう.

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

#include <stdio.h>

extern int func(unsigned int) ;

int main(int argc, char **argv)

{

func(10) ;

return 0 ;

}

int func(unsigned int n)

{

char c ;

printf("n = %3d, c = %p\n",n,&c) ;

if (n == 0) return 0 ;

return func(n-1) + 2 ;

}

ここで, c = %p の出力は, &c, すなわち, c のアドレスを出力する. したがって, この値はその時点でのおおよそのスタックの先頭のアドレスを示していることになる. 実行結果は,

n = 10, c = effff9a7

n = 9, c = effff92f

n = 8, c = effff8b7

n = 7, c = effff83f

n = 6, c = effff7c7

n = 5, c = effff74f

n = 4, c = effff6d7

n = 3, c = effff65f

n = 2, c = effff5e7

n = 1, c = effff56f

n = 0, c = effff4f7

となり, 1回の呼出しで120バイトのスタックを利用していることがわかる. そこで, スタックセグメントを小さくするために,

limit stacksize 1

とする. これにより, スタックセグメントは1Kバイトに制限される. そのうえ, func(100) を呼び出してみる. この結果はシステムに依存するが, 多くの場合, 途中で Segmentation fault というエラーを出し

て実行が停止する. これは, スタックセグメントが足らなくなって, 実行が停止する例となっている.

6.13.5 演習問題

Exercise 6.13.1 次の2つのファイルからなるプログラムをコンパイル・リンクし, その出力結果がなぜそのようになるかを考えよ.

C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp

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

#include <stdio.h>

extern int sub_function(void) ;

int i=0 ;

int j=0 ;

int main()

{

sub_function() ;

printf("i=%d, j=%d\n",i,j) ;

return 0 ;

}

extern int i ;

static int j ;

int sub_function()

{

i += 1 ; j += 1 ;

return 0 ;

}

Exercise 6.13.2 次の2つのファイルからなるプログラムをコンパイル・リンクし, その出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>

extern int sub_function(void) ;

int i=0 ;

int j=0 ;

int main()

{

sub_function() ;

printf("i=%d, j=%d\n",i,j) ;

return 0 ;

}

extern int i ;

extern int j ;

int sub_function()

{

i += 1 ; j += 1 ;

return 0 ;

}

6.14 配列とポインタ(その1)

6.14.1 配列

配列 (array) とは, 特定の型の変数をひとまとまりにして, 利用できる構造である.

6.14.1.1 配列の定義と宣言

配列を利用するには, 配列としての宣言をしなくてはならない. 例えば, int 型の 10 個の配列 digit は

次のように宣言される.

int digit[10] ;

この時, 識別子の名前は digit であり, 「演算子」 [ ] は配列宣言演算子と呼ばれ, 配列の要素数を表す.配列の定義において, 配列の要素数を表すものは 0 より大きい値を持つ整数定数式でなければならない. したがって, 配列の要素の数は, unsigned long で表せる範囲内であれば良いことがわかる. また, 任意の型,及びその派生型に対して配列を定義することができる.

C7.tex,v 1.27 2001-07-19 17:13:15+09 naito Exp

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

上のようにして定義された配列の要素は digit[0], ..., digit[9] のように [ ] の中(添字)に整数

式を代入することで要素を参照することが出来る. 注意すべきことは, 添字は必ず 0 から始まり, 定義された添字の範囲を越えて配列の参照を行った場合の動作は不定となる75.

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

配列の各要素は, メモリ内では連続した部分にアロケートされる.配列の宣言では,

extern int digit[] ;

という宣言が可能であり, これは, プログラム中の他のどこかで定義される配列を表し, この配列の定義においてはじめて配列のサイズ(要素数)が決定される. このように, 配列の要素数が決まっていなかったり,(後の配列の初期化で述べるように)配列要素の成分のすべてが決定できないような配列を, 不完全な配列と呼ぶ.

Example 6.14.1 int 型の要素数が 10 個の配列に値を代入し, その後, それらの値を表示させる.

#include <stdio.h>

int main(int argc, char **argv)

{

int i ;

int digit[10] ;

for(i=0;i<10;i++) digit[i] = i ;

for(i=0;i<10;i++) printf("digit[%d] = %d\n", i, digit[i]) ;

return 0 ;

}

この時, 上の図でいえば,

0 1 2 3 4 5 6 7 8 9

と値が代入されたこととなり, i が 8 の時, digit[i] で digit の8番目の要素を参照できる.

6.14.1.2 配列の初期化

配列を宣言と同時に初期化することができ, この時に配列が定義される. 配列を初期化子で初期化するには,

int digit[10] = {0,1,2,3,4,5,6,7,8,9} ;

とする. これで, digit[i] = i と初期化できたことになる. また,

int digit[] = {0,1,2,3,4} ;

と初期化すると, 自動的に5個の要素を持った配列として digit が定義される. したがって, この場合にdigit[8] を参照してはならない. 一方,

75他のオブジェクトを参照してしまうかも知れないし, 実行時エラーをおこすかもしれない.

C7.tex,v 1.27 2001-07-19 17:13:15+09 naito Exp

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

int digit[10] = {0,1,2,3,4} ;

とすると, digit は10個の要素を持った配列として定義されるが, 6番目以後の要素は初期化されない.したがって, この場合に digit[8] を参照することが出来る.

Remark 6.14.1 配列の要素数を越えて配列の要素に参照を行った場合には, どのようなことが起るかは不定である. たとえば,

int vec[2] ;

vec[2] = 1 ;

とした場合には, 実行時エラーとなることもあれば, 他のオブジェクトの指し示す領域にアクセスする可能性もある. したがって, 配列の要素にアクセスする場合に, それが正しいアクセスかどうかを管理するのはプログラマの責任である.

Remark 6.14.2 配列の宣言では, 明示的な初期化を行っている場合を除いて, 配列の要素数として, 非負の整数型の値を持つ式を用いなければならない. しかし,

int i ;

int vec[i] ;

という宣言は文法的にエラーとなるわけではなく, 実際に配列に対応するメモリを確保できない可能性がある.

6.14.2 ポインタ

ポインタ (pointer) とは, 他の変数のアドレスを持つ変数である. ポインタとして定義したオブジェクトの中身は, 記憶領域上のアドレスに他ならないので, ポインタはどのような型のオブジェクトのアドレスも格納できるように思えるのだが, C では, どのような型のオブジェクトのアドレスを持つポインタかを明示的に指定して宣言しなければならない.例えば, char 型の変数のアドレスを持つポインタ p を作るには, 以下のように行なう.

char *p ;

この定義により, 変数 p は char 型のオブジェクトを指し示すことが出来る. すなわち, p には char 型のオ

ブジェクトのアドレスを代入することができる. 実際に char 型の変数 c のアドレスを p に代入するには,

char c ;

char *p ;

p = &c ;�

p c

とする. 変数に & をつけると, そのアドレスを示す. & はアドレス演算子と呼ばれ, 被演算数は, ビット・フィールド, register と宣言されたものを参照する左辺値, 関数型であってはならない. また, 単項の * は

間接演算子と呼ばれ. 単項の * つけた変数が指し示すアドレスを返す. 単項 * をポインタに適用すると, そのポインタの指すオブジェクトがアクセスできる.

Example 6.14.2 極めて人為的だが, この例はポインタの利用法を的確に示している.

C7.tex,v 1.27 2001-07-19 17:13:15+09 naito Exp

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

int x = 1, y = 2 ;

int *ip ;

ip = &x ; /* ip は x を指す. すなわち, ip の中には x のアドレスが入っている. */

y = *ip ; /* y は 1 となる. *ip は ip の指し示す先の値を表す.

この時点では, ip は x を指し示している. */

*ip = 0 ; /* x は 0 となる. *ip は左辺値となりうる.

ip は x を指し示しているので, x の値を変えている. */

この例のように, ポインタを介して, 変数の値を受け渡すことができる. ここで, *ip の値は ip = &x に

よって x を指し示し, *ip = 1 で x の値が 1 となったことに注意.

Example 6.14.3 この例では, ip の内容 (アドレス)が iq にコピーされる76.

int *ip, *iq ;

iq = ip ;

この例と

int *ip, *iq ;

*iq = *ip ;

とは全く意味が異なる. こちらの例では, ip が指し示している変数の値が iq が指し示している変数の値

に代入される.したがって,

int *ip, *iq ;

int p=1, q=2 ;

ip = &p ; iq = &q ;

iq = ip ;

とすると, iq は p を指し示めし, q は値 2 を持つが,

int *ip, *iq ;

int p=1, q=2 ;

ip = &p ; iq = &q ;

*iq = *ip ;

とすると, iq は q を指し示し, q は値 1 を持つことになる.

6.14.2.1 ポインタの演算

まず, 次のようなことはできるだろうか.

int *ip ;

ip += 1 ;

76もともと iq が指し示していたアドレスの中身にはなんら変化はないことに注意.

C7.tex,v 1.27 2001-07-19 17:13:15+09 naito Exp

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

ip それ自身は, アドレスを指している. これを行うと, ip の値(指し示すアドレス)が1増えるのではなく, ip の指し示しているアドレス自身が int 型の分だけ増加する. もし, int が4バイトを占めていれば,ip は4バイト分増加する77.また, 次のようにすると, x の値を 1 だけ増やすことができる.

int *ip ;

int x ;

ip = &x ;

*ip += 1 ;

これは, ip が x を指し示していることを考えれば, 当たり前である.

6.14.2.2 配列とポインタ

「C では配列とポインタは強い関係を持ち, ほぼ同様に扱っても良い.」と, C のどのような教科書を見ても書いてある. この言葉は半分は正しく, 半分は間違っている. まず, この言葉の意味を明確にし, 配列とポインタを同様に扱ってもよい文脈を明らかにしよう.例えば,

int a[10] ;

は, int 型の要素数 10 の配列を定義しているが, 識別子 a が何を表しているかを考えてみよう. 配列の識別子は, その配列の先頭のアドレスを表している. すなわち, 次の2つのコードは同じものである78.

int *pa ;

pa = &a[0] ;

int *pa ;

pa = a ;

左のプログラムでは, &a[0] はオブジェクト a[0] のアドレスを表し79, 右のプログラムでは, a が配列の先頭要素 a[0] のアドレスを表している. したがって, いずれのプログラムでも, pa は配列 a の先頭を指

し示すこととなる.この時, ポインタの演算により, pa+i は a[0] から int 型変数 i 個分先を指し示すことになり, すなわ

ち, pa+i は a[i] を指し示していることとなる. したがって, *(pa+i) は a[i] を参照することとなる.

a[0] a[4] a[5]a[1] a[2] a[3]

pa

pa=a pa+1

もっと極端なことを書けば,

int a[3] = {1,2,3} ;

printf("%d, %d\n", a[2], 2[a]) ;77ポインタをインクリメントしたとき, どれだけのバイト数移動するかは, そのポインタが指し示す型に依存する.78どのような場合にでも同じオブジェクトコードを生成する.79演算子 & と [] の優先順位に注意.

C7.tex,v 1.27 2001-07-19 17:13:15+09 naito Exp

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

はともに正しい構文であり, 配列 X[Y] という構文はは常に *(X+Y) と変換される.ここまででは, 配列とポインタは完全に等価であり, どちらで記述しても, 相互に書き換えが可能なよう

に思える. しかし, 注意すべきことは, ポインタはアドレスを格納する変数であるため, int *pa などとい

う(仮)定義において, 確保される記憶領域は, オブジェクトのアドレスを格納するために十分な程度の領域に過ぎない80. しかし, 配列として int a[10] と定義すると, 記憶領域上に連続した int 型10個分の

領域が確保され81, その領域は定義が実行された時点で確定したアドレスである. したがって, pa は左辺値であるが, a は左辺値にはなり得ない. すなわち, pa = a, pa++ は意味のある演算であるが, a = pa, a++は正しくない. しかし,

int *pa, a[10] ;

pa = a ;

printf("%d\n",pa[2]) ;

などは意味のある文である. すなわち, pa[2] は *(pa+2) に変換され, pa = a により, pa は a[0] を指し

示すため, *(pa+2) は a[2] に他ならない. ただし, 元々ポインタと等価になっている配列の先頭アドレスをポインタに代入して参照することは, 配列の要素数を越えてアクセスを行ってしまう元となり, バグになる危険性を秘めている.

Remark 6.14.3 このように配列とポインタはある意味では似ているのだが, その識別子の持つ意味が異なる. したがって, 次のような2つのファイルによるプログラムはエラーにはならないが正常には実行されない.

int a[3] ={1,2,3} ;

extern void foo(void) ;

int main(int argc, char **argv)

{

foo() ;

return ;

}

#include <stdio.h>

extern int *a ;

void foo(void)

{

printf("%d\n", *(a+1)) ;

return ;

}

Remark 6.14.4 配列以外を指すポインタに対して加減を行なっても, 意味のある結果が得られるとは限らない. 例えば

int *p ;

int a, b ;

p = &a;

p++;

としたとき, p が b を指していることは期待できない.

Example 6.14.4 int 型の要素数10個の配列 a の各要素に値 0 を代入する4つの方法. ともに

int a[10], *p, i ;

と定義されていると仮定する.

方法180Solaris 2.6 では4バイト(32ビット)である.81Solaris 2.6 の gcc 2.95.1 では40バイトである.

C7.tex,v 1.27 2001-07-19 17:13:15+09 naito Exp

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

for(i=0;i<10;i++) a[i] = 0 ;

方法2

p = a ;

for(i=0;i<10;i++) p[i] = 0 ;

方法3

p = a ;

for(i=0;i<10;i++) *(p+i) = 0 ;

方法4

p = a ;

for(i=0;i<10;i++) *p++ = 0 ;

さて, これらの4つの方法のうちどれが一番お好みだろうか?これら4つの例はプログラムの書き方は異なるが, 生成するコードは全く同一と考えて良い.

Example 6.14.5 double 型の要素数3の配列のコピーを行う例.

int main(int argc, char **argv)

{

int i ;

double a[3] = {1.0, 2.0, 1.0}, b[3] ;

for(i=0;i<3;i++) b[i] = a[i] ;

return 0 ;

}

このコピーを

b = a ;

で行うことはできない.

Example 6.14.6 double 型の要素数2の配列を R2 上のベクトルと思い, それに直交するベクトルを一

つ求める.

int main(int argc, char **argv)

{

int i ;

double a[2] = {1.0, 2.0}, b[2] ;

b[0] = -a[1] ; b[1] = a[0] ;

return 0 ;

}

C7.tex,v 1.27 2001-07-19 17:13:15+09 naito Exp

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

6.14.2.3 汎用データ・ポインタ

ここまでは, char, int などのデータの型が決まったものに対するポインタを考えてきた. しかしながら,どのような型に対しても利用できる汎用データ・ポインタを利用することで, さらに広範囲に利用できる関数などを作ることができる.汎用データ・ポインタは void 型へのポインタとして定義される.

Example 6.14.7 この例の関数は, どのような型の配列であっても, 配列の次の要素をかえす関数である.

void * next_member(void *a, int size, int len, int member)

{

if (member >= len) return NULL ;

return (a+(member+1)*size) ;

}

ここで, ポインタをインクリメントする際に, size を掛けていることに注意. ここで, a は対象となる配列の先頭を指し示すポインタであり, size は配列の要素の型のバイト数, len は配列の要素数, member は対象となる配列の要素の添字の番号である. この関数を利用すると,

int a[3] = {0,1,2} ;

double x[3] = {0.0, 1.0, 2.0} ;

char c[3] = {’a’, ’b’, ’c’} ;

int *pa ;

double *px ;

char *pc ;

pa = (int *)next_member(a,sizeof(int), sizeof(a)/sizeof(int), 1) ;

printf("%d\n", *pa) ;

px = (double *)next_member(x,sizeof(double), sizeof(x)/sizeof(double), 1) ;

printf("%f\n", *px) ;

pc = (char *)next_member(c,sizeof(char), sizeof(c)/sizeof(char), 1) ;

printf("%c\n", *pc) ;

として, 実際に次の要素を出力することができる. ここで, sizeof 演算子を用いて,

sizeof(a)/sizeof(int)

によって配列の要素数を得ていることに注意. これと等価な

sizeof(a)/sizeof(a[0])

でもよい. 後者の方が a の指し示す型を変えたときの可搬性が高く安全である.

void 型へのポインタをインクリメントすると, その変数が指し示すアドレスは 1 だけ増加する. この部分だけを見ると, char 型へのポインタと同じであるが, どのような型の変数をも指し示すことができるようになっているのが汎用データポインタである.

C7.tex,v 1.27 2001-07-19 17:13:15+09 naito Exp

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

6.14.3 文字列

文字列リテラルは, 文字の配列として認識される. そこで, 文字列を変数として表すには char 型の配列

を用いることとなる. 文字列リテラルを文字の配列として変数域に格納する時には, 文字列の終端がわかるように, 配列の最後に \0 (数値 0 )が挿入される. したがって, 文字列リテラルを表現するために必要な文字配列の長さは, 文字数+1である.char 型の配列に限り, 以下のような初期化の方法が認められている82.

char amessage[] = "This is a test." ;

char *pmessage = "This is a test." ;

しかし, この2つの変数には大きな違いがある. pmessage はポインタであるので, それが指し示すアドレスを変更できるが, amessage はそれ自身配列であるので, その中身は変更できるが, そのアドレスは変更できない. pmessage の定義の場合, “This is a test.” という文字列は, 静的メモリ領域のどこかに確保され, pmessage はそのメモリの先頭のアドレスを値に持つ.

Example 6.14.8 文字列が配列またはポインタであることを利用すると, 文字列のコピーは配列またはポインタを利用して行なうことができる.

char *t, *s ;

while(*s++ = *t++) ;

これは, 文字列 t を s にコピーしている.なお, よくやるミスなのだが, この例を用いた完全なプログラムは,

int main(int argc, char **argv) {

char *t="abcdefghi", s[10], *p ;

p = s ;

while(*s++ = *t++) ;

return 0 ;

}

であり,

int main(int argc, char **argv) {

char *t="abcdefghi", *s ;

while(*s++ = *t++) ;

return 0 ;

}

ではない. 下の例ではコピー先の文字列に対応する十分な記憶領域が確保されていない.

Example 6.14.9 このプログラムは, 文字列の前から見て空白文字 (’ ’) を最初に見つけた場所(前から何番目か)を出力している. (ただし, 先頭にある場合は0番目と数えている.)もし, 見つからない場合には, −1 を出力する.

82なお, これに対応する他の型の初期化は, 要素数1の配列を int a[]=1 ;, int *p=1 ; と初期化することに対応する. これは認められていない.

C7.tex,v 1.27 2001-07-19 17:13:15+09 naito Exp

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

#include <stdio.h>

int main(int argc, char **argv)

{

char s[] = "abcdef" ;

char *p ;

p = s ;

while(*p) {

if (*p == ’ ’) {

printf("%d\n", p-s) ;

return 0 ;

}

p++ ;

}

printf("-1\n") ;

return 0 ;

}

Example 6.14.10 このプログラムは, 文字列 s を空白文字を区切りとしてトークン分解するものである.得られたトークンはすぐに出力されている.

#include <stdio.h>

int main(int argc, char **argv)

{

int i ;

char s[] = "abc d ef ", t[10] ;

char *ps, *pt ;

for(i=0;i<10;i++) *(t+i) = ’\0’ ;

ps = s ; pt = t ;

while(*ps) {

if (*ps != ’ ’) *pt++ = *ps++ ;

else {

*pt = ’\0’ ; printf("%s\n", t) ;

pt = t ;

for(i=0;i<10;i++) *(t+i) = ’\0’ ;

ps++ ;

}

}

if (*t) printf("%s\n",t) ;

return 0 ;

}

C7.tex,v 1.27 2001-07-19 17:13:15+09 naito Exp

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

6.14.4 演習問題

Exercise 6.14.1 double 型の要素数3の配列を R3 のベクトルと思い, double 型の要素数3の配列2つ

に直交するベクトルを求めるプログラムを書け. ただし, エラー処理も適切に行うこと.

Exercise 6.14.2 Example 6.14.9 のように文字列を与えたとき, その文字列の長さ(ただし, 文字列終端文字を含まない)を出力するプログラムを書け.

Exercise 6.14.3 Example 6.14.9 を書き換えて, 文字列の後ろから見て空白文字 (’ ’) を最初に見つけた場所(前から何番目か)を出力するプログラムを書け. (ただし, 先頭にある場合は0番目とする.)もし,見つからない場合には, −1 を出力する.

Exercise 6.14.4 Example 6.14.10 を書き換えて,

#include <stdio.h>

int main(int argc, char **argv)

{

char s[] = "abc d ef ", t[10] ;

char *ps, *pt ;

ps = s ; pt = t ;

while(*ps) {

if (*ps != ’ ’) *pt++ = *ps++ ;

else {

*pt = ’\0’ ; printf("%s\n", t) ;

pt = t ;

ps++ ;

}

}

printf("%s\n",t) ;

return 0 ;

}

とした. 正常に動作しない理由を述べよ.

Exercise 6.14.5 Example 6.14.10 を書き換えて,

#include <stdio.h>

int main(int argc, char **argv)

{

int i ;

char s[] = "abc d ef ", t[10] ;

char *p ;

for(i=0;i<10;i++) *(t+i) = ’\0’ ;

p = t ;

while(*s) {

C7.tex,v 1.27 2001-07-19 17:13:15+09 naito Exp

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

if (*s != ’ ’) *p++ = *s++ ;

else {

*p = ’\0’ ; printf("%s\n", t) ;

p = t ;

for(i=0;i<10;i++) *(t+i) = ’\0’ ;

s++ ;

}

}

if (*t) printf("%s\n",t) ;

return 0 ;

}

とした. これが文法エラーとなる理由は何か.

Exercise 6.14.6 次のプログラムの出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>

#define MAX 4

int a[] = {0,1,2,3} ;

int main(int argc, char **argv)

{

int i, *p ;

for(i=0;i<MAX;i++) printf("a[i]=%d\t",a[i]) ; printf("\n") ;

for(p=&a[0];p<&a[MAX];p++) printf("*p=%d\t",*p) ; printf("\n") ;

for(p=a;p<a+MAX;p++) printf("*p=%d\t",*p) ; printf("\n") ;

for(i=0;i<MAX;i++) printf("*(a+i)=%d\t",*(a+i)) ; printf("\n") ;

}

Exercise 6.14.7 次のプログラムの出力結果がなぜそのようになるかを考えよ.

C7.tex,v 1.27 2001-07-19 17:13:15+09 naito Exp

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

#include <stdio.h>

int a[] = {0,1,2,3,4} ;

int *p[] = {a, a+1, a+2, a+3, a+4} ;

int **pp = p ;

int main(int argc, char **argv)

{

printf("%X\t%X\n", a, *a) ;

printf("%X\t%X\t%X\n", p, *p, **p) ;

printf("%X\t%X\t%X\n", pp, *pp, **pp) ;

pp++ ; printf("%X\t%X\t%X\n", pp-p, *pp-a, **pp) ;

*pp++ ; printf("%X\t%X\t%X\n", pp-p, *pp-a, **pp) ;

*++pp ; printf("%X\t%X\t%X\n", pp-p, *pp-a, **pp) ;

++*pp ; printf("%X\t%X\t%X\n", pp-p, *pp-a, **pp) ;

pp=p ;

**pp++ ; printf("%X\t%X\t%X\n", pp-p, *pp-a, **pp) ;

*++*pp ; printf("%X\t%X\t%X\n", pp-p, *pp-a, **pp) ;

++**pp ; printf("%X\t%X\t%X\n", pp-p, *pp-a, **pp) ;

}

6.15 配列とポインタ(その2・関数とポインタ)

6.15.1 変数の参照渡し

C の関数呼出しでは, 実引数に指定したオブジェクトはその値が関数に渡される. したがって, 実引数に指定したオブジェクトの値を関数の副作用として変更するためには, 実引数にポインタを渡す必要がある.

Example 6.15.1 この関数の例は, 二つの int 型変数の和をとり, 第一変数にその結果を返すものである.

void sum(int *a, int b)

{

*a += b ;

return ;

}

この例では, 関数 sum 内では a は int 型変数のポインタであるため, その値を参照するには *a と指定

しなければならない. この関数を呼び出すには, 以下の方法をとる.

int a, b ;

sum(&a,b) ;

&a はオブジェクト a のアドレスを与えている. したがって, 関数 sum には a のアドレスが渡される.

C7-1.tex,v 1.7 2001-07-19 12:43:11+09 naito Exp

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

呼出し前 呼び出した後 *a += b の実行ab

ab

sum の *asum の b

� a, ここが書き換えられるb

sum の *asum の b

6.15.2 配列を引数とする関数

配列の識別子はそれ自身配列の先頭のアドレスを持つため, 配列を引数とする関数は, ポインタを引数としていると考えて良い.

Example 6.15.2 double 型の要素数2の配列を R2 のベクトルとみなして, そのノルムの2乗を求める

関数.

double norm(double a[2])

{

int i ;

double x=0.0 ;

for(i=0;i<2;i++) x += a[i]*a[i] ;

return x ;

}

この関数を呼び出す場合には,

double a[2] = {1.0, 2.0}, x ;

x = norm(a) ;

とする. この時, 関数 norm の実引数として関数に渡される値は, 配列 a の先頭のアドレスである.

この関数を

double norm(double a[])

{

.... この部分は同じ

}

または,

double norm(double *a)

{

.... この部分は同じ

}

として定義しても良い.つまり, 配列を引数とする関数の仮引数定義において,

double a[2]

double a[]

double *a

C7-1.tex,v 1.7 2001-07-19 12:43:11+09 naito Exp

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

はどれも同じ意味をもつ. すなわち, 関数の実引数として与えられる値は, double 型のポインタである.したがって,

#include <stdio.h>

double norm(double a[3])

{

int i ;

double x ;

for(i=0;i<2;i++) x += a[i]*a[i] ;

return x ;

}

int main(int argc, char **argv)

{

double a[2] = {1.0,2.0} ;

printf("%f\n", norm(a)) ;

return 0 ;

}

としても, コンパイラは警告もエラーも出さないので注意すること. 同様に,

#include <stdio.h>

extern double norm(double *) ;

double norm(double a[])

{

....

}

としても, コンパイラは警告もエラーも出さない. なお, double norm(double a[]) に対応する関数プロ

トタイプ宣言は,

double norm(double []) ;

とすればよい.

Example 6.15.3 double 型の要素数2の配列を R2 のベクトルとみなして, それに直交するベクトルを

一つ求める関数.

void normal_vector(double *a, double *b)

{

b[0] = -a[1] ; b[1] = a[0] ;

return ;

}

この関数を呼び出す場合には,

double a[2] = {1.0, 2.0}, b[2] ;

normal_vector(a,b) ;

とする.

C7-1.tex,v 1.7 2001-07-19 12:43:11+09 naito Exp

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

6.15.3 文字列操作関数

また, ポインタを利用することにより, 文字列を関数の引数として渡すことができ, その戻り値にポインタを利用することもできる.

Example 6.15.4 C の標準関数 strchr は

char *strchr(const char *s, int c);

と string.h 内で宣言される関数であり, s の中に最初にあらわれる文字 c のポインタを返す. もし, c で指定した文字が見つからないときには NULL83を返す.この例では, この標準文字列操作関数 strchr を実現してみよう. まず, 次のような関数を書いてみよう.

char *strchr(const char *s,int c)

{

while ((*s!=’\0’)&&(*s!=c)) s++ ;

if (*s!=’\0’) return (char *)s ;

return NULL ;

}

この関数を呼び出すには, 以下の方法をとる.

char a[]="test test" ;

printf("%p\n",a) ;

printf("%p\n",strchr(a,’e’)) ;

この例のように, 文字列を初期化するには, strcpy 関数を利用する84.

Example 6.15.5 Example 6.15.4 で用いた, C の標準関数 strcpy は

char *strcpy(char *dst, const char *src);

と定義され, src で示される文字列を dst で示される文字列にコピーする. また, 戻り値はコピーされたdst を返す.

char *strcpy(char *dst, const char *src)

{

while(*dst++=*src++) ;

return dst ;

}

しかし, strcpy では, コピーされる文字列の長さが指定されておらず, dst のために確保された領域を越えてコピーされる可能性があるため, 実際に文字列をコピーするには, 標準関数 strncpy を用いる方が望

ましい.

Example 6.15.6 C の標準関数 strtok は

char *strtok(char *s1, const char *s2);

83NULL とは, 何も指し示さないという特別なポインタである. NULL と 0 を示すポインタとは異なることに注意せよ.84その他の文字列操作関数については, man -s 3c strcpy を見よ.

C7-1.tex,v 1.7 2001-07-19 12:43:11+09 naito Exp

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

と定義され, 次のような仕様を持つ.s1 の中から s2 に含まれる文字を区切り文字の集合としてトークン分解を行い, 戻り値にはそのトーク

ン文字列を返す. また, 第一引数に NULL を入れて, strtok を続けて呼んだ場合には, 1回目の呼出しで用いた文字列の次のトークンを返す.strtok の使用例は以下の通りである.

#include <stdio.h>

#include <string.h>

int main(int argc, char **argv)

{

char a[100]="test test_test" ;

char *p ;

p = strtok(a,"_ ") ; printf("%s\n",p) ;

while((p = strtok(NULL,"_ ")) != NULL)

printf("%s\n",p) ;

return 0 ;

}

このプログラムの実行結果は

test

test

test

となる. ところが, 上のプログラムを書き換えて,

#include <stdio.h>

#include <string.h>

int main(int argc, char **argv)

{

char a[100]="test test_test" ;

char *p ;

p = strtok(a,"_ ") ; printf("p=%s, a=%s\n",p,a) ;

while((p = strtok(NULL,"_ ")) != NULL)

printf("p=%s, a=%s\n",p,a) ;

return 0 ;

}

とすると,

p=test, a=test

p=test, a=test

p=test, a=test

となってしまう. つまり, strtok 関数に渡した第一引数 a も書換えられてしまう. このからくりを見るために, ここでは, strtok の代りに, 第二引数を int 型変数として, その文字を区切り文字としてトークン分解を行う, strtok に類似の関数を書いてみよう.

C7-1.tex,v 1.7 2001-07-19 12:43:11+09 naito Exp

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

char *strtok(char *s, const int c)

{

static char *p ;

char *q ;

if (s == NULL) s = p ;

else q = s ; /* 先頭を保持 */

/* トークンが残っていなければ NULL を返す */

while(*s == c) s++ ;

if (*s == 0) return NULL ;

/* トークンが残っているとき */

while((*s != c)&&(*s)) s++ ;

*s = ’\0’ ;

p = s+1 ; /* 次の呼出しのため */

s = q ;

return s ;

}

この例では, 2回目以後の第一引数を NULL とした呼出しのために, 直前の呼出しに用いたポインタを静的変数として保持している.

ここで学んだことを利用すると, getchar などを利用して, 文字を読み出し, それを文字列として保持した後, その構文解析を行うことで, 標準入力からの数値の入力が可能になる85.

6.15.4 関数へのポインタ

C では, 変数へのポインタだけではなく, 関数へのポインタも利用できる. 関数識別子は関数が定義されているテキストセグメント内の関数の先頭アドレスを持つオブジェクトと考えれば, 関数のポインタは容易に理解できる.int 型の値を返す関数へのポインタを表す変数は,

int (*p)() ;

と定義する. ここで, int (*p)() という書き方が本質的である. 関数を表す演算子 () と間接演算子 * の

優先順位は, () の方が高く, int *p() とすると int へのポインタを返す関数と認識される.

Example 6.15.7 この例は, sum という関数を, ポインタ p に代入している.

85詳しくは, getchar, strtod, strtol などのオンラインマニュアルを見よ.

C7-1.tex,v 1.7 2001-07-19 12:43:11+09 naito Exp

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

int sum(int a, int b)

{

return a+b ;

}

main()

{

int a, b ;

int (*p)() ;

a = 1 ; b = 2 ;

p = sum ;

printf("%d\n",p(a,b)) ;

}

関数へのポインタは以下のような場合に便利に利用できる.

Example 6.15.8 次のプログラムはコマンドライン引数から 1+2 などという入力を取り, その計算結果を出力するものである.

#include <stdio.h>

#include <stdlib.h>

extern int calc(const char *) ;

extern int _chrstr(char, char *) ;

extern int _add(int,int) ;

extern int _sub(int,int) ;

extern int _mul(int,int) ;

extern int _div(int,int) ;

extern int _mod(int,int) ;

char op[]="+-*/%" ;

int main(int argc, char **argv)

{

if (!(argc-1)) return 0 ;

printf("%d\n",calc(*(argv+1))) ;

}

int calc(const char *arg)

{

char p[100], *q, *r ;

char a[100], b[100], operator ;

int i = 0, j, int_a, int_b ;

int (*func)() ;

while(*(p+i) = *(arg+i)) i += 1 ; q = p ;

while(_chrstr(*q,op)) q++ ; j = q-p ; i = 0 ;

C7-1.tex,v 1.7 2001-07-19 12:43:11+09 naito Exp

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

while((*(a+i) = *(p+i)) && (i < j)) i += 1 ; *(a+i) = 0 ;

operator = *q ; q++ ; r = q ;

while(_chrstr(*q,op)) q++ ; i = 0 ;

while((*(b+i) = *(r+i)) && (i < j)) i += 1 ; *(r+i) = 0 ;

int_a = atoi(a) ; int_b = atoi(b) ;

switch (operator) {

case ’+’: func = _add ; break ;

case ’-’: func = _sub ; break ;

case ’*’: func = _mul ; break ;

case ’/’: func = _div ; break ;

case ’%’: func = _mod ; break ;

default: func = NULL ;

}

return func(int_a,int_b) ;

}

int _chrstr(char c, char *p)

{

while(*p && *p != c) p++ ;

if (*p) return 0 ;

else return 1 ;

}

int _add(int a, int b) { return a+b ; }

int _sub(int a, int b) { return a-b ; }

int _mul(int a, int b) { return a*b ; }

int _div(int a, int b) { return a/b ; }

int _mod(int a, int b) { return a%b ; }

また, このプログラム中の文字列解析部分は, strspn 関数などの文字列解析関数で代用する方が容易である. (ここでは, ポインタの解説のため, わざわざ上のように書いてある.)上のプログラム中の calc 関数

をそのように書き換えると, 以下のようになる.

C7-1.tex,v 1.7 2001-07-19 12:43:11+09 naito Exp

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

int calc(const char *arg)

{

char p[100], operator ;

int int_a, int_b ;

int (*func)() ;

size_t len ;

int_a = atoi(strncpy(p,arg,len=strspn(arg,digit))) ;

operator = *(arg+len) ;

int_b = atoi(strncpy(p,arg+len+1,strspn(arg+len+1,digit))) ;

switch (operator) {

case ’+’:

func = _add ; break ;

case ’-’:

func = _sub ; break ;

case ’*’:

func = _mul ; break ;

case ’/’:

func = _div ; break ;

case ’%’:

func = _mod ; break ;

default:

func = NULL ;

}

return func(int_a,int_b) ;

}

関数へのポインタが本質的な役割を果たすものとして, 次のような例がある. 例えば,int 型の配列の中で, 適当な順序に対して最も大きなものを求める関数を考えてみよう.

Example 6.15.9 int 型の要素数10個の配列の中で通常の順序で最も大きな数を求める.

#include <stdio.h>

int get_max(int *) ;

int main(int argc, char **argv)

{

int a[10]={2,1,3,7,6,5,0,9,4,8} ;

int result ;

result = get_max(a) ;

printf("%d\n",result) ;

return 0 ;

}

C7-1.tex,v 1.7 2001-07-19 12:43:11+09 naito Exp

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

int get_max(int *a)

{

int i ;

int max ;

max = a[0] ;

for(i=0;i<10;i++)

max = (max < a[i]) ? a[i] : max ;

return max ;

}

これでは, 関数 get max 内に要素数 10 が書かれている.

Example 6.15.10 int 型の要素数10個の配列の中で通常の順序で最も大きな数を求める. (改良版1)

#include <stdio.h>

int get_max(int *, unsigned int) ;

int main(int argc, char **argv)

{

int a[10]={2,1,3,7,6,5,0,9,4,8} ;

int result ;

result = get_max(a,10) ;

printf("%d\n",result) ;

return 0 ;

}

int get_max(int *a, unsigned int nel)

{

int i ;

int max ;

max = a[0] ;

for(i=0;i<nel;i++)

max = (max < a[i]) ? a[i] : max ;

return max ;

}

次に整数要素の順序として, 通常と逆順の順序をいれたらどうなるだろうか?最も単純なものは,

int get_min(int *a, unsigned int nel)

{

int i ;

int min ;

C7-1.tex,v 1.7 2001-07-19 12:43:11+09 naito Exp

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

min = a[0] ;

for(i=0;i<nel;i++)

min = (min > a[i]) ? a[i] : min ;

return min ;

}

と定義してしまう方法である. しかし, これでは2つの順序の入れ方に対して, 別の関数を用意しなければならない. そのために, 次のような例を考えてみよう.

Example 6.15.11 この例では, 関数へのポインタを利用して, 順序を与える関数を get max 内から独立

させている.

#include <stdio.h>

int get_max(int *, unsigned int, int (*)(int, int)) ;

int max_func(int, int) ;

int main(int argc, char **argv)

{

int a[10]={2,1,3,7,6,5,0,9,4,8} ;

int result ;

result = get_max(a,10,max_func) ;

printf("%d\n",result) ;

return 0 ;

}

int get_max(int *a, unsigned int nel, int (*func)(int, int))

{

int i ;

int max ;

max = a[0] ;

for(i=0;i<nel;i++) {

if (func(a[i],max) > 0) max = a[i] ;

}

return max ;

}

int max_func(int a, int b)

{

return a-b ;

}

この中で, get max の第3引数は, 2つの int 型の引数をとり, int 型を返す関数である. get max は第3

引数の関数 func に対して, func(a,b) > 0 が a > b となる順序によって, 最も大きな値を返すことにな

C7-1.tex,v 1.7 2001-07-19 12:43:11+09 naito Exp

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

る. get max の第3引数の関数を取り替えることにより, どのような順序を入れることも自由になる.この手法は, 配列の与えられた順序による並び替えを行う qsort 関数で利用されている, C における極

めて重要な手法の一つである.

6.15.5 ポインタの演算(比較)

2つのポインタは次のいずれかの条件が満たされるとき, それを比較することが出来る.

• それが指し示すものの型が同一. ただし const などの修飾子を除いて考える. この時の結果は, 2つのオブジェクトのメモリ内での相対位置によって決定される.

• 同じ構造体のメンバーを指すとき. この時の結果は, メンバーがあとに書かれたものを指すポインタの方が, 相対位置が高いとする.

• 同じ配列の要素を指すとき. この時の結果は配列の添字と同一の順序となる.

これ以外のポインタの比較を行うと結果は不定になる.

int a[4] ;

int *p, *q ;

p = &a[0] ; q = &q[2] ;

とすると, p < q が成り立つ.

Example 6.15.12 以下のプログラムでは, a は bss セグメント, b は stack セグメント, c は data セグ

メントにあり, 関数 main は text セグメントにある.

#include <stdio.h>

int a ;

int c = 1 ;

int main(int argc, char **argv)

{

int b ;

int *pa, *pb, *pc ;

int (*pf)() ;

pa = &a ; pb = &b ; pc = &c ; pf = main ;

printf("pa = %p\n", pa) ;

printf("pb = %p\n", pb) ;

printf("pc = %p\n", pc) ;

printf("pf = %p\n", pf) ;

if (pa < pb) printf("pa < pb\n") ;

else printf("pa > pb\n") ;

if (pb < pc) printf("pb < pc\n") ;

else printf("pb > pc\n") ;

if (pc < pa) printf("pc < pa\n") ;

else printf("pc > pa\n") ;

return 0 ;

}

C7-1.tex,v 1.7 2001-07-19 12:43:11+09 naito Exp

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

6.15.6 各種の宣言の違い

ここで, このような複雑な宣言をまとめておこう.

char **argv argv は char へのポインタのポインタ

int (*daytab)[13] daytab は int の 13 個の配列へのポインタ

int *daytab[13] daytab は int へのポインタの 13 個の配列

void *comp() comp は void 型のポインタを返す関数

void (*comp)() comp は void 型を返す関数へのポインタ

char (*(*x())[]) x は char 型を返す関数へのポインタの不定個の配列へのポインタを返す関数

char (*(*x[3])())[5] x は char 型の 5 個の配列へのポインタを返す関数へのポインタの 3 個の配列

6.15.7 演習問題

Exercise 6.15.1 double 型の要素数3の配列を R3 のベクトルと思い, double 型の要素数3の配列2つ

に直交するベクトルを求める関数を書け. ただし, エラー処理も適切に行うこと.

Exercise 6.15.2 二つの int 型の変数を入れ換える関数 swap int を書け.

Exercise 6.15.3 二つの同じ型の変数を入れ換える関数 swap を書け.

Exercise 6.15.4 Example 6.15.8 では, 入力される文字列に空白を許していない. 空白が許されるように変更せよ. さらに, 項が3つ以上の場合にも対応せよ. また, 数値を表さない項を入力した場合の対応を書け.

Exercise 6.15.5 Example 6.15.4 では, その文字列に含まれる最初の文字のポインタを返したが, これをその文字列に含まれる最後の文字のポインタを返すように書き変えよ.

Exercise 6.15.6 C の標準関数 strncpy の仕様を調べ, これを書け.

Exercise 6.15.7 Exercise 6.15.6 で書いた strtok 関数で, 第一引数の文字列が破壊されないように関数を書き直せ.

Exercise 6.15.8 標準入力から(一つ以上の)空白文字で区切られた整数値を読み, その和を計算するプログラムを書け. ただし, 入力は1行で行われるものとし, その入力文字数は改行文字を含めて1024文字以内で, その中に int 型で格納可能な10進整数が複数個書かれていると仮定する. 10進整数を表さない文字列や, int 型で格納可能でない10進数値がある場合には, エラーと判断せよ. (エラー処理の方法は任意)

6.16 配列とポインタ(その3・多次元配列)

ここまでで次の2つの事実を学んだ.

1. C では配列とポインタは「ほぼ」等価なものと扱える.

2. C ではどのような型も配列にすることが出来る.

したがって, 2 の事実により, 配列の配列(多次元配列)などが定義できることがわかる. ここでは, 多次元配列を定義して, 多次元配列において, どの程度 1 の事実が通用するかを注意深く見ていこう.

C7-2.tex,v 1.5 2001-09-25 20:21:00+09 naito Exp

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

6.16.1 多次元配列の定義

はじめに多次元配列の定義を行う.

int str[2][5] ;

配列を構成する演算子 [] の結合規則は「左から右」であるので, str[i] は, int 型の5個の要素を持つ配列であり, str は2個の要素を持つ「 int 型の5個の要素を持つ配列」の配列である. 各種の教科書には, 以下のような図が書かれていることが多い.

str[0][0] str[0][1] str[0][2] str[0][3] str[0][4]

str[1][0] str[1][1] str[1][2] str[1][3] str[1][4]

より正確にメモリ上で str が表す2次元配列の様子を見ると, 次の図のようになる.

str[0] str[1]

str[0][0] str[0][1] str[0][2] str[0][3] str[0][4] str[1][0] str[1][1] str[1][2] str[1][3] str[1][4]

多次元配列を初期化する方法は,

char daytab[2][13] = {

{0,31,28,31,30,31,30,31,31,30,31,30,31},

{0,31,29,31,30,31,30,31,31,30,31,30,31}

} ;

とすれば良い.

Example 6.16.1 次の例は, N × N , N = 10 の単位行列を作成している.

int i, j ;

int unit_mat[10][10] ;

for(i=0;i<10;i++) {

for(j=0;j<10;j++) unit_mat[i][j] = 0 ;

unit_mat[i][i] = 1 ;

}

Remark 6.16.1 多次元配列は, 文法的には配列を識別子の型とする配列である. 配列の宣言では, 「配列の限界を指定する定数式がないときには, 配列は不完全な型を持つ。」とあり, 配列の要素の型は完全でなければならない. これは, 多次元配列においては, 最初の次元のみが省略できることを意味している. すなわち, 明示的な初期化により配列を完全にすることが可能なので,

int y[][2] = { {1,2}, {2,3}, {3,4} } ;

により, この配列は int 型の2つの要素を持つ配列の3つの要素を持つ配列として定義される. また, この初期化は

int y[3][2] = { 1,2,2,3,3,4 } ;

と等価である.

C7-2.tex,v 1.5 2001-09-25 20:21:00+09 naito Exp

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

Remark 6.16.2 また, 次のような配列の初期化子による定義も可能である.

int y[][2] = { {1,2}, {2}, {3} } ;

この定義では, y[1], y[2]は初期化子には1つの要素しか持たないが, y[0]が2つの要素を持つため, y[1],y[2] は2つの要素を持つ配列として定義され, y は不完全な型を持つ配列となる.しかし, これを

int y[][] = { {1,2}, {2}, {3} } ;

とは定義できない. この理由は, 次の Section 6.16.2 の多重配列をポインタで書換えることと関連している.

Remark 6.16.1, Remark 6.16.2 の詳細については, [2, A8.6, A8.7] を参照.

6.16.2 多重配列とポインタ

1次元配列では, 配列とポインタは, ほぼ同じものを示していた. すなわち,

int a[3] ;

として与えられたオブジェクトに対して, a[0], *a または a[i], *(a+i) は, それぞれ, 同じメモリ領域へのアクセスを表し, a, &a[0] または a+i, &a[i] も, それぞれ, 同じアドレスを指し示していた. ここでは多重配列においては, 配列とポインタは(このような意味で)同じものと見なせるかどうかを考えてみよう.はじめに, 二重配列

int a[2][5] ;

を考えてみる. この時 [2, A8.6.2] によれば, a[0][0], **a, または a[i][j], *(*(a+i)+j), *(a[i]+j) は,それぞれ, 同じ領域へのアクセスを示し, a, &a[0][0], または *(a+i)+j, a[i]+j, &a[i][j] も, それぞれ同じアドレスを示す. このことを

a[0] a[1]

a[0][0] a[0][1] a[0][2] a[0][3] a[0][4] a[1][0] a[1][1] a[1][2] a[1][3] a[1][4]

を用いて考えてみよう.これを理解するには, 次の2つのポインタ演算の差を理解する必要がある.

1. a に対して a+1 が何を表すか?

2. a[0] に対して a[0]+1 が何を表すか?

これに対する解答のヒントとして,

sizeof(a)

sizeof(a[0])

sizeof(a[0][0])

の式の値を見てみるのがよい. これを int 型が4バイトの処理系で調べると, 順に 40, 20, 4 という答えが得られる.

C7-2.tex,v 1.5 2001-09-25 20:21:00+09 naito Exp

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

上の図を見ると, a は「int 型の5個の要素を持つ配列」という型の配列であるので, a+i は a から「int

型の5個の要素を持つ配列」のバイト数(20バイト)の i 倍だけ先を表す. つまり, *(a+i) は a[i] と

等価である.さらに, a[0] は int 型の要素を持つ配列であるので, a[0]+j は a[0] から int 型のバイト数(4バイ

ト)の j 倍だけ先を表す. つまり, *(a[0]+j) は a[0][j] と等価である.したがって, a[i][j] は *(a[i]+j) と等価であり, *(*(a+i)+j) と等価であることがわかる. 当然,

*(a+i)+j は a[i][j] のアドレスを示すポインタとなる.このことから,

int y[][] = {{1,2}, {2}, {3}} ;

int x[][] = {{1,2}, {2,3}, {3,4}} ;

という定義では, 配列 y[i] に対するインクリメント y[i]+1 のインクリメントのバイト数が計算できない

ことになり, このような定義が認められないことがわかる.同様に, 3次元以上の配列も

int a[2][5][10] ;

と定義できる. 2次元配列と同様に, 3次元以上の場合も以下の参照はすべて同じものとなる.

• a[i][j][k], *(a[i][j]+k), *(*(a[i]+j)+k), *(*(*(a+i)+j)+k).

• &a[i][j][k], a[i][j]+k, *(a[i]+j)+k, *(*(a+i)+j)+k.

したがって, 適切な初期化子をおくことにより,

int a[][5][10] = {{

{0,1,2,3,4,5,6,7,8,9},

{1,2,3,4,5,6,7,8,9,0},

{2,3,4,5,6,7,8,9,0,1},

{3,4,5,6,7,8,9,0,1,2},

{4,5,6,7,8,9,0,1,2,3}},

{

{1,2,3,4,5,6,7,8,9,0},

{2,3,4,5,6,7,8,9,0,1},

{3,4,5,6,7,8,9,0,1,2},

{4,5,6,7,8,9,0,1,2,3},

{5,6,7,8,9,0,1,2,3,4}}} ;

という定義が可能である.

6.16.3 ポインタの配列と二重ポインタ

二重配列と似た変数の定義には,

int *b[2] ;

int **c

が考えられる.

C7-2.tex,v 1.5 2001-09-25 20:21:00+09 naito Exp

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

6.16.3.1 ポインタの配列

Section 6.14.3, および複雑な宣言をまとめた表 (p. 342) で述べた通り, int *b[2] は “int へのポインタの2個の要素からなる配列” であるので,

int a0, a1 ;

int *b[2] = {&a0, &a1} ;

とすることにより, 2つの int 型のオブジェクトを指し示すことができる. もし,

int a0[5], a1[5] ;

int *b[2] = {a0, a1} ;

とすると, b[0] は配列 a0 の先頭アドレスを示し, b[1] は配列 a1 の先頭アドレスを示す. したがって,*(b+i), b[i] がともに同じアドレスを指し示しているので, *(*(b+i)+j), *(b[i]+j) は, 結果として, 同じアドレスを指し示すこととなる. さらに, b[i] が配列の先頭を指し示すポインタであることを考えると,*(b[i]+j) は b[i][j] と書換えることができ, b[i][j] も *(b[i]+j) と同じ領域へのアクセスを示すこ

とになる86.しかし, int *b[2] = {{1,2,3,4,5},{2,3,4,5,6}} ; とは初期化できない. なぜなら, この変数定義

では, これらの値を格納するメモリ領域が確保できないからである. 定義 *b[2] で確保される記憶領域は,他の変数を示すアドレス2個分に過ぎないことに注意しよう.

b[0]b[1]

�a0

�a1

6.16.3.2 二重ポインタ

C ではどのような型のオブジェクトへのポインタも利用可能であるので, 「ポインタへのポインタ」が利用できる. それは,

int **c ;

と定義する. このようなポインタへのポインタ(二重ポインタ)は,

int a ;

int *b ;

int **c ;

と定義されている時,

b = &a ; c = &b ;

とすれば, **c によって, a の値を参照可能になる.もちろん, 単純にこんな利用法をするためにポインタへのポインタをつくる必要はなく, 実際に利用する

場面は, 「配列へのポインタ」をポイントするために用いる. すなわち,

int a0[5], a1[5] ;

int *b[2] = {a0, a1} ;

int **c = b ;86理解しにくい場合には, int 型の変数へのポインタ p を用意し, p = b[i] と考えると良い.

C7-2.tex,v 1.5 2001-09-25 20:21:00+09 naito Exp

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

とすれば, c には配列 b の先頭のアドレスが格納される. したがって, c+1 は int 型のポインタの配列(ポ

インタ)の意味で, インクリメントが行われるので, *(c+i) は b[i] と等価なアクセスを実現する. すなわち, この定義では,

• *(*(c+i)+j), *(c[i]+j), c[i][j] は同じ参照を表し,

• *(c+i)+j, c[i]+j, &c[i][j] は同じアドレスを表す.

6.16.3.3 配列へのポインタ

ポインタの配列と混乱をおこし易いものに, 「配列へのポインタ」がある.

int (*b)[3] ;

と定義すると, b は先に間接演算子 * と結合するので, 「ポインタ」となり, 「その指し示す先が int [3]

」と読むことができる. よって, int (*b)[3] は 「int 型の3個の要素からなる配列へのポインタ」とな

る. この時,

int a[3] = {1,2,3} ;

int (*b)[3] ;

b = &a ;

とすることにより, (*b)[i] または (*b)+i は a[i] と等価なアクセスを実現する.前に関数の戻り値の型として配列を返すことは出来ないと書いたが, 配列へのポインタを利用すること

により, 類似のことを行うことは可能である.

Example 6.16.2 int 型の3個の要素からなる配列へのポインタを返す関数.

#include <stdio.h>

int (*foo(int n))[3]

{

static int b[3] ;

int i ;

for(i=0;i<3;i++) b[i] = n+i ;

return &b ;

}

int main(int argc, char **argv)

{

int (*a)[3] ;

int i ;

a = foo(1) ;

for(i=0;i<3;i++)

printf("%p\n", (*a)[i]) ;

return 0 ;

}

この関数 foo では, 実際に値を代入する配列 b は static 宣言されている. これは, 戻り値の値(ポインタ)が指し示す先を, 関数終了後も保持するためである.

C7-2.tex,v 1.5 2001-09-25 20:21:00+09 naito Exp

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

6.16.4 配列を仮引数とする関数

関数の引数(仮引数)が単純な型やそれに対するポインタの場合は, 仮引数の記述の方法は容易であるが, 多重配列, 構造体の配列など複雑な場合は, 仮引数の記述の方法は注意が必要である.仮引数においては

int *a

int a[]

は同じであると書いた. これらは, ともにそれぞれの変数の先頭アドレスが関数実引数となる.また多重配列においても,

int **a

int *a[]

それぞれで, 先頭アドレスが関数引数となることは同じである. しかし,

int a[][]

とした場合には, 関数内で a[i][j] としてオブジェクトを指定しようとしても, a[i] としてどこを示しているかが分からない. 具体的には,

int a[2][3] = {{1,2,3},{3,4,5}} ;

で定義された変数を関数に渡す際に, 仮引数を a[][] と書き, 関数内で a[1] としたとする. この時, 我々が期待するのは, a[1] が {3,4,5} という配列であるが, 実際には, a のアドレス(すなわち, a[0] を示すアドレス)に対して, a[1] を示すアドレスとの差が分からないので, a[1] を正しく参照することができない. これを回避するためには, 仮引数として a[][3] と宣言する必要がある. こうすれば, a[0] に対してa[1] が int 型の3つ分のずれがあることが分かる.一方, 仮引数として *a[2] とした関数に対して, 二重配列 a[2][3] を実引数とすると, すなわち,

void foo(int *a[2])

{

return ;

}

int main(int argc, char **argv)

{

int a[2][4] ;

foo(a) ;

return 0 ;

}

とすると,

warning: passing arg 1 of ‘foo’ from incompatible pointer type

という警告が出される. これは, 仮引数 int *a[2] と実引数 int a[2][4] は, そのオブジェクトの型が異なることが理由である. なぜなら, 関数内で a[i][j]を参照しようとすると, 先頭アドレス a からポインタ

のバイト数の i 倍先のアドレスが参照され, そこに書かれているアドレスを参照して, そこから int 型の

C7-2.tex,v 1.5 2001-09-25 20:21:00+09 naito Exp

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

バイト数の j 倍先のアドレスにある値が参照される. しかし, 実引数として渡されたオブジェクトは, int型のオブジェクトの 2 × 4 個のならびであるので, 実引数側では a[i][j] を参照するには, a から int 型

のオブジェクトのバイト数の 4×i+j 倍先を参照しなければならない.

*a[2] で期待されているデータ構造

a[0] のアドレス, 例えば 0xeeffa[1] のアドレス, 例えば 0xef20

0xeeff 番地

0xef20 番地

a[2][4] のデータ構造

0x0001�????

アドレスが32ビット(=4バイト), int 型が4バイトの時, int *a[2] と仮引数が宣言された関数内で a[0][0] を参照すると, a[0][0] の値をアドレスと思い, そのアドレスを参照する.

これを正しく動作させるためには,

void foo(int a[][4])

{

return ;

}

int main(int argc, char **argv)

{

int a[2][4] ;

foo(a) ;

return 0 ;

}

とする必要がある. しかし, 仮引数として **a とした関数に対して, *a[2] と定義したオブジェクトを実引数とすることは可能である.

void foo(int **a)

{

return ;

}

int main(int argc, char **argv)

{

int *a[2] ;

foo(a) ;

return 0 ;

}

また, この関数 foo は

void foo(int *a[])

C7-2.tex,v 1.5 2001-09-25 20:21:00+09 naito Exp

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

として定義しても動作は変らない.よって, 多重配列を関数に渡そうとするとき, 関数仮引数定義では最も左の添字は省略が可能であること

がわかる. したがって,

int bar(int a[][5][4])

と定義した関数に

int a[5][5][4], b[100][5][4] ;

を実引数とすることが可能であるが,

int c[5][4][4], d[5][5][10], e[2][10][15] ;

等を実引数とすることは出来ない.これら多重配列を仮引数とする関数の関数プロトタイプ宣言は

int foo(int [][5][4])

などと, 識別子を省略することができる.以上をまとめると,「関数仮引数での配列はポインタ」であると考えれば良いことがわかる. 多重配列で

は, 逆に「関数仮引数でのポインタを配列にする」ことはできない.

Example 6.16.3 次は二重配列の各種の扱いである.

#include <stdio.h>

int print_matrix_1(int [][], unsigned int) ;

int print_matrix_2(int *[], unsigned int, unsigned int) ;

int print_matrix_3(int **, unsigned int, unsigned int) ;

int main()

{

int a[3][4] = {{1,2,3,4},{2,3,4,5},{3,4,5,6}} ;

int *b[3] = {a[0], a[1], a[2]} ;

int **c = b ;

print_matrix_1(a,3) ;

print_matrix_2(b,3,4) ;

print_matrix_3(c,3,4) ;

return 0 ;

}

int print_matrix_1(int a[][4], unsigned int n)

{

unsigned int i, j ;

for(i=0;i<n;i++) {

for(j=0;j<4;j++) {

/* printf(" %d",a[i][j]) ; */

C7-2.tex,v 1.5 2001-09-25 20:21:00+09 naito Exp

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

/* printf(" %d",*(a[i]+j)) ; */

printf(" %d",*(*(a+i)+j)) ;

}

printf("\n") ;

}

printf("\n") ;

return 0 ;

}

int print_matrix_2(int *a[], unsigned int n, unsigned int m)

{

unsigned int i, j ;

for(i=0;i<n;i++) {

for(j=0;j<m;j++) {

/* printf(" %d",a[i][j]) ; */

/* printf(" %d",*(a[i]+j)) ; */

printf(" %d",*(*(a+i)+j)) ;

}

printf("\n") ;

}

printf("\n") ;

return 0 ;

}

int print_matrix_3(int **a, unsigned int n, unsigned int m)

{

unsigned int i, j ;

for(i=0;i<n;i++) {

for(j=0;j<m;j++) {

/* printf(" %d",a[i][j]) ; */

/* printf(" %d",*(a[i]+j)) ; */

printf(" %d",*(*(a+i)+j)) ;

}

printf("\n") ;

}

printf("\n") ;

return 0 ;

}

Example 6.16.4 上の例と間違えやすいものの例.

#include <stdio.h>

extern int print_p(int (*)[], unsigned int) ;

C7-2.tex,v 1.5 2001-09-25 20:21:00+09 naito Exp

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

int main()

{

int a[3] = {1,2,3} ;

int (*b)[3] ;

b = &a ;

print_p(b,3) ;

return 0 ;

}

int print_p(int (*b)[], unsigned int n)

{

unsigned int i ;

for(i=0;i<n;i++) printf(" %d", (*b)[i]) ;

printf("\n") ;

return 0 ;

}

6.16.5 main 関数の引数について

一番はじめに, main 関数は引数を持つと述べたが, ここで, その引数が何かを解説しよう. main は, 次の型を持つ.

int main(int argc, char **argv)

ここで, argc は, そのプログラムが実行された時の引数の数であり, argv は, その引数を格納する文字列へのポインタである. ここで, argv[0] には, そのプログラムの名前が入る. したがって,

% program -a -b a c

として program が実行された時には, argc = 5, argv[1] は -a などとなる. プログラムに空白文字を含む引数を渡したいときには,

% program -a -b "a c"

とすれば, a c で一つの引数となる.ここで,

char **argv ;

char *name[] = {echo, hello, world.} ;

char aname[][10] = {echo, hello, world.} ;

の違いをまとめておこう. それぞれを図式化すると次のようになる.

C7-2.tex,v 1.5 2001-09-25 20:21:00+09 naito Exp

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

argv

0

echo\0

hello\0

world.\0

echo\0

hello\0

world.\0

echo\0 hello\0 world\0

0 10 15

6.16.6 演習問題

Exercise 6.16.1 日付を表す unsigend int 型の変数 year, month, mday (それぞれ「西暦」, 「月」,「日」を表す)を引数とし, 1970年1月1日からの累積日数を表す関数を, 343 ページで定義した配列daytab を用いて書け. ただし, 1970年1月1日に対しては, 0 を返すこととする.

Exercise 6.16.2 次のプログラムの出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>

int main(int argc, char **argv)

{

while(*argv) printf("%s\n", *argv++) ;

}

Exercise 6.16.3 2つの N × N 行列のトレースを計算する関数を書け.

Exercise 6.16.4 2つの N × N 行列の和・積を計算するプログラムを書け.

Exercise 6.16.5 次のプログラムの出力結果がなぜそのようになるかを考えよ.

C7-2.tex,v 1.5 2001-09-25 20:21:00+09 naito Exp

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

#include <stdio.h>

int a[3][3] = { {1,2,3}, {4,5,6}, {7,8,9} } ;

int *pa[3] = { a[0], a[1], a[2] } ;

int *p = a[0] ;

int main()

{

int i ;

for(i=0;i<3;i++) printf("%X\t%X\t%X\n", a[i][2-i], *a[i], *(*(a+i)+i)) ;

for(i=0;i<3;i++) printf("%X\t%X\n", *pa[i], p[i]) ;

}

6.17 動的なメモリ確保とポインタ

C の配列はその定義において, 添字範囲を定数式にしなければならない. したがって, 実行時までどれだけの量の配列が必要になるかわからないときには, あらかじめ配列の大きさをきめて配列を定義することは出来ない. また, 配列の代りにポインタを利用するときには, その値を確保するためのメモリ領域を確保しなければならない. このように, 実行時に必要なだけのメモリ領域を確保する必要は, 大規模なプログラムや, 多重ポインタを利用するときには必ず必要となる. それを動的なメモリ確保と呼ぶ.動的なメモリ確保をするために, malloc 関数(または類似の各種の関数87)を用いて, ヒープ (heap) 領

域88のメモリを利用するか, alloca 関数を用いてスタック領域のメモリを確保する必要がある.

6.17.1 malloc を用いた動的なメモリ確保

malloc 関数は,

#include <stdlib.h>

void *malloc(size_t size);

と定義される標準ライブラリ関数であり, size で指定されるバイト数のメモリをヒープ領域に確保して,そのメモリの先頭のアドレスをポインタとして返す. ここで, size t 型とは, その処理系でのメモリ全体を表すのに十分な符号なし整数を表す型であり, unsigned int または unsigned long 型と同じである89.malloc 関数で確保した領域が不必要になったときには, free 関数でその領域を開放しなければならない.

Example 6.17.1 int 型のオブジェクト100個分のメモリをヒープに確保し, 値を代入した後に, メモリを開放する.

#include <stdio.h>

int main(int argc, char **argv)

{

int *p ;

87malloc に類似の関数に関しては, malloc のオンライン・マニュアルを参照. calloc のように初期化も同時に行ってくれる関数もある.

88ヒープ領域とは, スタックセグメント内で, スタックと逆方法にメモリを利用する領域である. したがって, 最悪の場合, スタックとヒープが重なって, メモリ不足が起きる可能性がある.

89正確には, 整数型のいずれかに typedef されている.

C7-3.tex,v 1.6 2001-07-19 17:12:30+09 naito Exp

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

int i ;

if ((p = (int *)malloc(100*sizeof(int))) == NULL) {

printf("Could not allocate memory!\n") ;

return -1 ;

}

for(i=0;i<100;i++) *(p+i) = i ;

free(p) ;

return 0 ;

}

malloc 関数は, メモリの確保に失敗すると NULL を返す.

Example 6.17.2 malloc 関数で確保したメモリを開放しないため, メモリが確保できなくなる例.

#include <stdio.h>

void foo(void)

{

int *p ;

if ((p = (int *)malloc(0x10000000)) == NULL) {

printf("Could not allocate memory!\n") ;

exit(-1) ;

}

printf("%p\n", p) ;

return ;

}

int main(int argc, char **argv)

{

int i ;

for(i=0;i<0x1000;i++) foo() ;

return 0 ;

}

また, malloc 関数で確保した領域が足らなくなった場合には, realloc 関数で領域を追加確保もできる.

#include <stdio.h>

int main(int argc, char **argv)

{

int i ;

int *p ;

if ((p = (int *)malloc(100*sizeof(int))) == NULL) exit(-1) ;

printf("%p\n", p) ;

if ((p = (int *)realloc(p, 200*sizeof(int))) == NULL) exit(-2) ;

printf("%p\n", p) ;

C7-3.tex,v 1.6 2001-07-19 17:12:30+09 naito Exp

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

free(p) ;

return 0 ;

}

Remark 6.17.1 なお, alloca 関数はヒープ領域の代りにスタック領域で動的にメモリを確保するために用いる. スタック領域でメモリを確保することと, ヒープ領域で確保することの違いは, ヒープ領域のメモリは, malloc 関数(またはその類似物)を呼び出した関数の実行が終わっても, その領域は確保されたままであるが, alloca 関数でスタック領域にメモリを確保すると, そのメモリが利用できるのは, 呼び出した関数の終了までの間に限られる. したがって, スタック領域の動的メモリの有効範囲が狭くなるが, 一方では, free によってメモリを開放する必要がないので, すなわち, 関数の実行が終了すれば, スタック領域は自動的に開放されるので, free を忘れることによる弊害を防ぐことが出来る.動的に確保したヒープメモリを開放せずに使い続けて, メモリを大量消費している状態をメモリ・リーク

(memory leak) とよび, プログラムが正常に動作していない状態の一つである.

6.17.2 動的なメモリ確保を利用する場面

動的なメモリ確保を必要とする場面は各種考えられるが, ここでは, 取りあえずいくつかの例を紹介しておこう.

6.17.2.1 標準入力からの文字列の読み込み

標準入力から文字列を読むには, fgets 関数を用いるのがよい. 1行の最大の文字数が決まっているときには,

#include <stdio.h>

#define MAX_LEN 1024

int main(int argc, char **argv)

{

char str[MAX_LEN], *p ;

while((!feof(stdin))&&((p = fgets(str, MAX_LEN, stdin)) != NULL))

printf("%s", str) ;

return 0 ;

}

とすればよい. MAX LEN は, プリプロセッサ命令で 1024 に置き換えられる. feof 関数はファイルの終端

かどうかを判定する関数である. このプログラムを

#include <stdio.h>

#define MAX_LEN 1024

int main(int argc, char **argv)

{

char str[MAX_LEN], *p ;

while(!feof(stdin)) {

fgets(str, MAX_LEN, stdin) ;

printf("%s", str) ;

C7-3.tex,v 1.6 2001-07-19 17:12:30+09 naito Exp

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

}

return 0 ;

}

とすると, 最終行を2度表示することになる. (詳細は fgets のオンライン・マニュアルを参照.)また, このプログラムをわかりやすくしたければ,

#include <stdio.h>

#define MAX_LEN 1024

int main(int argc, char **argv)

{

char str[MAX_LEN], *p ;

while(!feof(stdin)) {

if ((p = fgets(str, MAX_LEN, stdin)) != NULL)

printf("%s", str) ;

}

return 0 ;

}

としても良い. ここで, 式

((!feof(stdin))&&((p = fgets(str, MAX_LEN, stdin)) != NULL))

では, && (と ||)は他の演算子と異なり, 優先順位と結合順序にしたがって, 式の評価と副作用の完了が行われる. すなわち, feof(stdin) の返す値が 0 であれば, 後半の fgets 関数の呼出しは一切行われない.この2つの式の順序を交換すると, 標準入力が EOF に達しているにも関わらず fgets 関数による読み出し

が行われる. これは, 実行時エラーを発生したり, 予期しない動作をする可能性がある.この fgets 関数の使い方では, MAX LEN を越えた文字数を持つ行を str に一度に格納することは出来な

い. 例えば,

#include <stdio.h>

#define MAX_LEN 10

int main(int argc, char **argv)

{

char str[MAX_LEN], *p ;

while((!feof(stdin))&&((p = fgets(str, MAX_LEN, stdin)) != NULL))

printf("***%s", str) ;

return 0 ;

}

として, 実行すると何が起こっているのかがわかる. これでは, str に格納した1行を何かの関数に渡して文字列処理をすることが出来ない. これを解決するために, 動的なメモリ確保を行ってみよう.

#include <stdio.h>

#include <string.h>

#include <strings.h>

#define MAX_LEN 10

extern void error_jmp(void) ;

C7-3.tex,v 1.6 2001-07-19 17:12:30+09 naito Exp

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

int main(int argc, char **argv)

{

char *str, *temp_str, *p ;

size_t max = MAX_LEN ; /* その時点での文字列の最大の長さを格納する */

size_t len ; /* その時点での str の文字列の長さ */

if (((str = (char *)malloc(MAX_LEN+1)) == NULL)

||((temp_str = (char *)malloc(MAX_LEN+1)) == NULL))

error_jmp() ;

bzero((char *)str, MAX_LEN+1) ; /* str の内容をゼロクリアする */

len = strlen(str) ; /* 0 になっているはず */

/* 標準入力からの読み込み */

while((!feof(stdin))

&&((p = fgets(temp_str, MAX_LEN, stdin)) != NULL)) {

/* 文字列の長さが足りない!

* 領域の再確保 */

if (max < len+strlen(temp_str)+1) {

if ((str = (char *)realloc(str, len+strlen(temp_str))) == NULL) error_jmp() ;

max = len+strlen(temp_str)+1 ;

}

strcat(str,temp_str) ; /* 文字列の連結 */

len = strlen(str) ;

/* 改行文字が見つかったので, 文字列を表示する */

if (str[strlen(str)-1] == ’\n’) {

printf("%s", str) ;

bzero(str, len+1) ;

}

}

return 0 ;

}

void error_jmp(void)

{

printf("Could not allocate memory!\n") ;

exit(-1) ;

return ;

}

このプログラムでは, 改行文字を読むまでは, str に文字列を連結している. もし, str に確保した領域が足らなくなった場合には, realloc で領域を再確保している. なお, bzero は領域をゼロクリアする関数,strlen は文字列の長さを返す関数である. strlen は文字列終端文字の先頭からのポインタのオフセットを返すので, str[strlen(str)-1] は str の最後の文字を表すことになる. このプログラムでは, 一旦確保した文字列領域は最後まで利用するので, free の呼出しの必要はない.

C7-3.tex,v 1.6 2001-07-19 17:12:30+09 naito Exp

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

6.17.2.2 二重ポインタの指し示す先を確保する

C では, 一般のサイズの多重配列を関数に渡す手段は存在しない. そのかわり, 多重ポインタを渡すことによって多重配列を渡すことに代えることが多い. 例えば, main 関数の char **argv という引数は, 呼出し側のシェルから複数の文字列を得るための手段として用いられている.main 関数の char **argv の場合には, それが指し示すメモリ領域はシェルからプログラムが起動され

る段階で確保されているが, プログラム内で二重ポインタを利用して複数の文字列を扱うためには, それらの文字列を格納する領域を動的に確保しなければならない.ポインタのポインタを利用して, 長さの異なる文字列を扱う例を考えてみよう. ここでは, 標準入力から

入力されたテキストファイルの各行90を一つの文字列と思い, それらを二重ポインタで指し示す91.

#include <stdio.h>

#include <string.h>

#include <strings.h>

#define MAX_LEN 1024

extern void error_jmp(void) ;

int main(int argc, char **argv)

{

int i = 1 ;

char *str, *p ;

char **q ;

char temp_str[MAX_LEN] ;

/* q のための領域を取りあえず1行を指し示す分だけ確保 */

if ((q = (char **)malloc(sizeof(char *)*i)) == NULL) error_jmp() ;

/* 1行分の文字列領域を確保 */

if ((str = (char *)malloc(MAX_LEN+1)) == NULL) error_jmp() ;

while((!feof(stdin))

&&((p = fgets(temp_str, MAX_LEN, stdin)) != NULL)) {

strcpy(str, temp_str) ;

*(q+i-1) = str ;

i += 1 ;

/* q のための領域をさらに1行を指し示す分だけ確保 */

if ((q = (char **)realloc(q, sizeof(char *)*i)) == NULL) error_jmp() ;

/* 次の1行分の文字列領域を確保 */

if ((str = (char *)malloc(MAX_LEN+1)) == NULL) error_jmp() ;

}

/* ファイルの先頭から11行めを表示 */

printf("%s", q[10]) ;

return 0 ;

}

90簡単のため, 1行の最大文字数は 1024 としておこう.91いささか人為的な例であることは仕方ない.

C7-3.tex,v 1.6 2001-07-19 17:12:30+09 naito Exp

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

void error_jmp(void)

{

printf("Could not allocate memory!\n") ;

exit(-1) ;

return ;

}

q は読み込んだ文字列を格納する領域を指し示すポインタの列で, 1行を読み込むごとに, 次の1行を格納する領域と, それを指し示す領域を確保しながら q を構成している.

演習問題

Exercise 6.17.1 Section 6.17.2.2 の例で, 1行の文字数を制限しなくても良いようにプログラムを書換えよ.

Exercise 6.17.2 Section 6.17.2.2の例で,プログラム終了直前に, mallocで確保したすべての領域を free

で開放するようにプログラムを書換えよ.

6.18 データ構造

6.18.1 構造体

構造体とは, 複数のオブジェクトを一つにまとめて, あたかもそれらが一つの変数であるように見せるためのものである. 例えば, 複素数のように二つの実数の組のようなオブジェクトは構造体で表現するのが望ましい.

6.18.1.1 構造体の定義

構造体を定義する構文は,

{\tt struct} 識別子 (opt) {

メンバー宣言

}

という形である. ここで, 「メンバー宣言」とは, 構造体に含まれる要素(それをメンバー (member)と呼ぶ)の宣言であり, 構造体メンバーは, 関数型と不完全型遺体であれば, どのような型のものでもよい.すなわち, 構造体メンバーにその構造体自身を含んではならない. しかし, その構造体へのポインタを含むことは出来る. struct の直後に書かれた識別子(オプション)は構造体タグと呼ばれるものである.

Example 6.18.1 次は, double 型の2つの変数の組が構造体になったものである.

struct complex {

double real ;

double imaginary ;

} x ;

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

ここで, complex が構造体タグであり, double 型の2つのメンバー real と imaginary を持つ構造体と

して定義されている. さらに, 識別子 x は struct complex という型を持ったオブジェクトとして定義さ

れる.

realimaginary struct complex

Example 6.18.2 上の例 (Example 6.18.1) と同じ構造体を定義するが, 構造体タグを持たないもの:

struct {

double real ;

double imaginary ;

} x ;

Example 6.18.1 との違いは, 構造体タグを持たないことであるが, この場合には, この後でこの型の構造体を持つ変数を定義する際に,

struct {

double real ;

double imaginary ;

} y ;

と繰り返し書かなくてはいけなくなる. 一方, Example 6.18.1 のように, 構造体タグを利用すれば, その後同じ型の変数を定義する際などに, struct complex y とだけ書けば良い.

構造体メンバーそれ自身が構造体になってもかまわない.

Example 6.18.3 次は複素平面上の円を表す構造体である. 中心を表す複素数と半径を表す実数(倍精度浮動小数点数)の組からなる構造体である.

struct sphere {

struct complex center ;

double radius ;

}

radius

realimaginary stuct complex struct sphere

このように定義された構造体のメンバーは, 先に書かれた順にメモリ内に格納される.

6.18.1.2 構造体のメンバー参照

上のように定義した構造体変数のそれぞれのメンバーを参照するには, . という演算子(構造体メンバー演算子)を利用する.

Example 6.18.4 構造体 complex に値を代入する.

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

struct complex {

double real ;

double imaginary ;

} ;

int main(int argc, char **argv)

{

struct complex z ;

z.real = 1.0 ;

z.imaginary = 1.0 ;

}

すなわち, 構造体変数 struct_name に対して, struct_name.member_name によって, その構造体メンバーmember_name を表すことができる. したがって, 上で定義した構造体 struct sphere 型の変数 s に対し

て, その中心と半径を代入するには,

s.center.real = 0.0 ;

s.center.imaginary = 0.0 ;

s.radius = 1.0

とすれば良いことが分かる.

6.18.1.3 構造体の初期化と代入

構造体を初期化するには, そのメンバーに値を代入しても良いが, 一方で,

struct sphere s0 = {{0.0, 0.0}, 1.0} ;

struct sphere s1 = {0.0, 0.0, 1.0} ;

という初期化も可能である92. また, 構造体の変数への一括代入も可能である.

struct sphere s0 = {{0.0, 0.0}, 1.0} ;

struct sphere s1 ;

s1 = s0 ;

によっても, s0 の内容を s1 に代入することが可能である.

6.18.1.4 構造体と関数

構造体を引数にとる関数, 構造体を戻り値とする関数定義することができる93.

92当たり前だが, 上の初期化の方が何をしているかは分かりやすい.93Kernighan-Ritchie の初版(いわゆる traditional なC)では, 構造体への一括代入, 構造体を引数とする関数, 構造体を戻り値とする関数などは許されてはいなかった. したがって, traditional C でこのようなことを行うためには, すべて, 構造体のポインタを受け渡しする必要があった. また, traditional C では, 自動構造体(関数内部での自動変数となる構造体)の初期化も許されていなかった. ANSI 規格(Kernighan-Ritchie 第2版)では, これら構造体の扱いが簡単になったことが大きな改訂部分である.

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

Example 6.18.5 はじめに, struct complex 型の変数に対して, そのノルムを返す関数 complex_norm

をつくってみよう.

double complex_norm(struct complex x)

{

if (x.real != 0) {

return abs(x.real)*sqrt(1 + (x.imaginary/x.real)*(x.imaginary/x.real)) ;

}

return abs(x.imaginary) ;

}

ここで, 複素数 x +√−1y のノルムを

√x2 + y2 と計算してしまうと, x2 + y2 を計算する時点でオーバ

フローが発生するかもしれない. そのため, わざわざ√

x2 + y2 = |x|√1 + (y/x)2 と計算していることに注意.

Example 6.18.6 次に, 構造体を戻り値とする関数として, 2つの複素数の和を求める関数をつくる.

struct complex complex_add(struct complex z, struct complex w)

{

z.real += w.real ;

z.imaginary += w.imaginary ;

return z ;

}

Remark 6.18.1 これらの関数 complex_norm, complex_add をプロトタイプ宣言なしに利用すると, ともに戻り値が int ではないため, コンパイラが正しく関数を判断できない. したがって, それらを利用する前に, プロトタイプ宣言

extern double complex_norm(struct complex) ;

extern struct complex complex_add(struct complex, struct complex) ;

を行う必要がある.

このように構造体を引数とする関数, または, 構造体を戻り値とする関数では, 関数呼び出し, およびそこからの復帰の際に, 構造体のデータすべての値のコピーが行われる. したがって, 巨大な構造体に対するこれらの操作には, 関数呼び出しのオーバーヘッドが大きくなってしまう. それを避けるには, 構造体へのポインタを利用することが望ましい. ポインタを利用すれば, その指し示す先が何であっても, そのデータは(OSに依存した)一定のデータ量に過ぎない. (cf. Remark 6.18.4.)

6.18.1.5 構造体へのポインタと関数

はじめに, 構造体へのポインタをつくってみよう. 上で定義した struct complex 型の構造体へのポイ

ンタと, ポインタを経由したメンバーへの参照は次の例のようになる.

Example 6.18.7 はじめに, 構造体変数と, そのポインタを作成する.

struct complex z, *pz ;

pz = &z ;

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

これにより, 構造体へのポインタ pz は構造体変数 z の先頭アドレスを示すこととなる. この時, pz を経由して, z のメンバーへアクセスするための方法としては,

(*pz).real ;

pz->real ;

の2種類が考えられる. ここで, 演算子 -> は構造体ポインタの指し示す構造体のメンバーへの参照を表

す演算子であり, 実は上の2つの式は等価である.

Remark 6.18.2 ここで, (*pz).real のかわりに *pz.real と書いたとすると, これは演算子の優先順位より *(pz.real) を表すが, pz.real はポインタではないので, 誤りとなる.

Remark 6.18.3 もし,

struct sphere s, *ps ;

ps = &s ;

と定義されているとき,

s.center.real

ps->center.real

(s.center).real

(ps->center).real

は等価である. これは, ., -> は最も優先順位が高く, その結合法則が左から右となっているからである.

このような構造体へのポインタを利用して, 上でつくった complex_add のポインタ版をつくってみよう.

Example 6.18.8 一つの例は, 引数として求めるべき値を入れてしまう方法である.

void complex_add(struct complex *z, struct complex *w, struct complex *x)

{

x->real = z->real + w->real ;

x->imaginary = z->imaginary + w->imaginary ;

return ;

}

この場合, 呼び出し方法は,

complex_add(&z,&w,&x)

とすれば良い.

Example 6.18.9 もう一つの例として, 結果の入っている静的変数へのポインタを返すこともできる.

struct complex *complex_add(struct complex *z, struct complex *w)

{

static struct complex x ;

x.real = z->real + w->real ;

x.imaginary = z->imaginary + w->imaginary ;

return &x ;

}

この場合, 呼び出し方法は,

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

struct complex w,z,*px ;

px = complex_add(&z,&w) ;

とすれば良い.

Remark 6.18.4 構造体を戻り値とする関数と, 構造体のポインタを戻り値とする関数に関する比較をしてみよう. ここでは, 次のような3つの例を考えてみる.

1. 有理数を表す構造体

struct fractional

{

int n ; /* 分子 */

int d ; /* 分母 */

} ;

に対して,

struct fractional

frac_add(struct fractional a,

struct fractional b)

{

struct fractional c ;

c.n = a.d*b.n + b.d*a.n ;

c.d = a.d*b.d ;

return c ;

}

struct fractional

*frac_add(struct fractional a,

struct fractional b)

{

static struct fractional c ;

c.n = a.d*b.n + b.d*a.n ;

c.d = a.d*b.d ;

return &c ;

}

を考えよう.

2. 複素数を表す構造体

struct complex

{

double re ; /* 実部 */

double im ; /* 虚部 */

} ;

に対して,

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

struct complex

complex_add(struct complex a,

struct complex b)

{

struct complex c ;

c.re = a.re + b.re ;

c.im = a.im + b.im ;

return c ;

}

struct complex

*complex_add(struct complex a,

struct complex b)

{

static struct complex c ;

c.re = a.re + b.re ;

c.im = a.im + b.im ;

return &c ;

}

を考えよう.

3. さらに, もっと極端な例として, 構造体

struct test_str {

char a ;

} ;

に対して,

struct test_str

test_func(struct test_str a)

{

struct test_str b ;

b.a = 2*a.a ;

return b ;

}

struct test_str

*test_func(struct test_str a)

{

static struct test_str b ;

b.a = 2*a.a ;

return &b ;

}

を考えよう.

これら3つの構造体に関するそれぞれ2種類の呼出しに掛る時間を計測すると, およそ次のような結果が得られる94.

値を返す アドレスを返す

有理数 4.19 s 3.79 s

複素数 3.19 s 2.28 s

char 1.13 s 1.64 s

この環境では, 有理数の構造体のサイズは 64 ビット, 複素数の構造体は 128 ビット, 最後の例の構造体は8 ビットであり, ポインタのサイズは 32 ビットである. 戻り値として利用されるメモリサイズが実行時間に反映されていることに注意しよう. すなわち, 構造体のサイズが大きくなると, 「ポインタを戻り値とする関数」の方が, 実行時間の上からは有利に働く.しかしながら, 「ポインタを戻り値とする関数」では, そのポインタは関数内部の静的ポインタである

ので,94これは, 10000000 回呼出しを行った結果を time コマンドで計測した結果である. 環境は, Solaris 2.6, gcc 2.95.1, UltraSparc

400MHz である.

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

struct complex w,z,*px ;

px = complex_add(z,w) ;

のようにして得た結果は,

px = complex_add(z,w) ;

z = *px ;

として保存しておかなければならない. すなわち,

px = complex_add(z,w) ;

px = complex_add(px,w) ;

とすると, 2度めの呼出しの際に, メモリ領域が破壊されるため, 正しい結果を得ることが出来ない.

6.18.1.6 構造体の配列

構造体はそれ自身を配列にしたり, 構造体のフィールドに既に定義されている構造体を用いることができる. 構造体を配列にするには, 以下のような定義をすれば良い.

struct complex z[10] ;

これは, struct complex 型の構造体の10個の配列を定義している.また, 構造体変数がどれだけのメモリ量を利用しているかを知るには, sizeof 演算子を利用すれば良い.

sizeof(struct complex)

とすると, complex というタイプの構造体変数の占めるメモリ量を知ることができる95.

Remark 6.18.5 たとえば,

struct {

char c ;

int n ;

} ;

によって定義された構造体は, int が4バイトの時, 必ずしも5バイトを占めるわけではない. 実際, 多くの場合8バイトとなるだろう. これは, int 型の変数は(多くのアーキテクチャで)ワード境界に整列されるという性質があるからである. sizeof 演算子は正しくそのバイト数を返す.

Example 6.18.10 構造体として,

• 氏名

• 学籍番号

• 試験の得点

95この例の場合, sizeof(struct complex) と sizeof z では, 返す値が異なっている. sizeof(struct complex) は complex

構造体のバイト数を返すのに対し, sizeof z は, 変数 z の占めるバイト数を返す. したがって, この場合には sizeof(struct

complex)*10 が sizeof z に等しい.したがって, sizeof z / sizeof(struct complex) とすることにより, 配列の要素数を得ることができる. これは, sizeof z

/ sizeof z[0] としても同じである.

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

をメンバーに持つものを作成し, その構造体を型に持つ要素数3の配列を作成する. さらに, その配列を試験の得点の高い順に並び替える. もし, 試験の得点が同じであれば, 学籍番号を文字列の辞書式順序にしたがって, 順序が小さいものを前にする.

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

struct personal_data {

char name[40] ;

char number[10] ;

int point ;

} ;

extern int compare(struct personal_data *, struct personal_data *) ;

int main(int argc, char **argv)

{

struct personal_data pd[3] =

{

{"内藤久資", "0010011", 80},

{"藤原一宏", "0010012", 100},

{"木村芳文", "0010013", 100}

} ;

int i ;

qsort((struct personal_data *)pd,

sizeof(pd)/sizeof(struct personal_data),

sizeof(struct personal_data),

(int (*)(const void *, const void *))compare) ;

for(i=0;i<3;i++)

printf("%s %s %d\n",

(pd+i)->name,

(*(pd+i)).number,

pd[i].point) ;

return 0 ;

}

int compare(struct personal_data *a, struct personal_data *b)

{

if (a->point > b->point) return -1 ;

else if (a->point < b->point) return 1 ;

return strcmp(a->number,b->number) ;

}

結果表示のところで, 各メンバーへの参照に3種類の方法を用いている. C の標準関数 qsort は

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

#include <stdlib.h>

void qsort(void *base, size_t nel, size_t width,

int (*compar) (const void *, const void *));

と定義され, base で参照されるポインタを先頭にする配列を, compar 関数値によってソート(並び替え)を行う. この時, 一つの配列要素の大きさは width で表され, 配列要素数は nel で表される.

6.18.1.7 演習問題

Exercise 6.18.1 構造体の構成がどのようなものであっても, その構造体変数2つの内容を入れ替える関数を書け.

Exercise 6.18.2 Example 6.18.1 で作った複素数を表す構造体を利用して, かけ算, 割算を計算する関数を作れ.

Exercise 6.18.3 Example 6.18.2 で作った複素平面内の円を表す構造体を利用して, 与えられた複素数が円の内部にあるかどうかを判定する関数を書け.

Exercise 6.18.4 次のプログラムの出力結果がなぜそうなるのかを考えよ.

#include <stdio.h>

int main()

{

struct S1 { char c[4], *s ; } s1 = {"abc", "def" } ;

struct S2 { char *cp ; struct S1 ss1 ; }

s2 = { "ghi", { "jkl", "mno" }},

s3 = { "pqr", { "stu", "vwx" }} ;

struct S3 { struct S1 *sp1[2] ; }

s4 = { &s2.ss1, &s3.ss1} ;

struct S1 *sp2 = s4.sp1[0] ;

printf("s1.c[0] = %c\t*s1.s = %c\n",

s1.c[0], *s1.s) ;

printf("s1.c = %s\ts1.s = %s\n",

s1.c, s1.s) ;

printf("s2.cp = %s\ts2.ss1.s = %s\n",

s2.cp, s2.ss1.s) ;

printf("++s2.cp = %s\t++s2.ss1.s = %s\n",

++s2.cp, ++s2.ss1.s) ;

printf("s4.sp1[0]->c = %s\ts4.sp1[0]->s = %s\n",

s4.sp1[0]->c, s4.sp1[0]->s) ;

printf("s4.sp1[0]->c[0] = %c\ts4.sp1[1]->s[1] = %c\n",

s4.sp1[0]->c[0], s4.sp1[1]->s[1]) ;

printf("*(s4.sp1) = %s\t*(s4.sp1+1) = %s\n",

*(s4.sp1),*(s4.sp1+1)) ;

}

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

6.18.2 構造体を利用したデータ構造

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

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

6.18.2.1 リスト

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

6.18.2.2 ツリー

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

6.18.2.3 自己参照構造体

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

Example 6.18.11 80 文字からなる文字列と, 次のデータへのポインタを持った構造体は以下のように定義できる.

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

struct data {

char str[80] ;

struct data *next ;

} data ;

このように定義した構造体を利用して, リストを実現することができる.

Example 6.18.12 Example 6.18.11 で定義した構造体を初期化する. 即ち, 一番始めのデータには何も入れない.

strcpy(data.str,"") ;

data.next = NULL ;

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

Example 6.18.13 ここでは, ポインタのつなぎ変えと, 次のデータ領域の取得をしている.

struct data *p ;

if ((p = (struct data *)malloc((unsigned int)(sizeof(struct data)))) != NULL) {

strcpy(p->str,"b") ;

p->next = NULL ;

data.next = p ;

}

ここで, malloc 関数は, 必要なメモリ領域を確保するための関数である. ここで確保したメモリ領域は,必要がなくなったら, free 関数で領域を開放する必要がある.

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

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

struct data *p ;

struct data *q ;

if ((q = (struct data *)malloc((unsigned int)(sizeof(struct data))))

!= NULL) {

q->next = p->next ;

p->next = q ;

}

ここで, p は, 挿入したい位置を示しているポインタである.

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

p

p

p->next

q->next

q

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

struct tree_data {

char c ;

struct tree_data *r ;

struct tree_data *l ;

} ;

Example 6.18.15 定義した二分木構造を実際に使ってみよう.

Example 6.18.16 ここでは, 以下の図のような二分木を構成する.

c

e

g

f

�� ��

d

��

b

i

l m

��

k n

����

j

�� ��

h

��

a

root

はじめに二分木を入力するための関数 enter を定義する.

char *enter(struct tree_data **t, char *str)

{

struct tree_data *p ;

printf("%c", *str) ;

if (*str == ’\0’) return str ;

if (*str != ’.’) {

if ((p = (struct tree_data *)malloc(sizeof(struct tree_data))) == NULL) {

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

printf("Could not allocate memory!\n") ; exit(-1) ;

}

p->c = *str ; p->l = NULL ; p->r = NULL ;

*t = p ;

str = enter(&(p->l),++str) ;

str = enter(&(p->r),++str) ;

}

return str ;

}

関数 enter は

struct tree_data *root ;

char str[] = "abc..de..fg...hi..jkl..m..n.." ;

と定義された変数に対して

enter(&root, str) ; printf("\n") ;

として呼び出すと, 二分木の構造にデータを格納し, そのデータを表示する.このようにして構成した二分木を3つの方法で巡回してみよう. はじめは, 左優先に探索し, 通ったノー

ドを順に印字するものである.

void preorder(struct tree_data *t)

{

if (t == NULL) return ;

printf("%c", t->c) ;

preorder(t->l) ;

preorder(t->r) ;

return ;

}

次に左優先に探索し, ノードを分岐するときに印字するものである.

void inorder(struct tree_data *t)

{

if (t == NULL) return ;

inorder(t->l) ;

printf("%c", t->c) ;

inorder(t->r) ;

return ;

}

最後は, 左優先に探索し, 戻るときにノードを印字するものである.

void postorder(struct tree_data *t)

{

if (t == NULL) return ;

postorder(t->l) ;

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

postorder(t->r) ;

printf("%c", t->c) ;

return ;

}

これら3つの関数を

enter(&root, str) ; printf("\n") ;

preorder(root) ; printf("\n") ;

postorder(root) ; printf("\n") ;

inorder(root) ; printf("\n") ;

として呼び出すと,

abc..de..fg...hi..jkl..m..n..

abcdefghijklmn

cegfdbilmknjha

cbedgfaihlkmjn

という結果を得る. これが「二分木の巡回」である.

Example 6.18.17 Kernighan & Ritchie の教科書 [2] の 6.5 章には, 二分木の興味ある応用例が述べられている. そこに述べられているプログラムを掲載しておこう. (ただし, 多少改変してある.)

#include <stdio.h>

#include <string.h>

#include <ctype.h>

#define MAXLINE 1024

#define FMT " .,"

struct tnode {

char *word ;

int count ;

struct tnode *left ;

struct tnode *right ;

} ;

extern struct tnode *addtree(struct tnode *, char *) ;

extern struct tnode *talloc(void) ;

extern void error_jmp(void) ;

extern void print_tree(struct tnode *) ;

int main(int argc, char **argv)

{

struct tnode *root ;

char *p, *q, buf[MAXLINE] ;

root = NULL ;

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

while((!feof(stdin))&&((p = fgets(buf, MAXLINE, stdin)) != NULL)) {

/* 入力のトークン分解と二分木への挿入 */

q = strtok(buf,FMT) ;

if (isalpha(q[0])) {

q[0] = tolower(q[0]) ;

root = addtree(root,q) ;

}

while((q = strtok(NULL,FMT)) != NULL) {

if (isalpha(q[0])) {

q[0] = tolower(q[0]) ;

root = addtree(root,q) ;

}

}

}

/* 二分木の出力 */

print_tree(root) ;

return 0 ;

}

void print_tree(struct tnode *p)

{

if (p == NULL) return ;

print_tree(p->left) ;

printf("%4d: %s\n", p->count, p->word) ;

print_tree(p->right) ;

return ;

}

struct tnode *addtree(struct tnode *p, char *s)

{

int str_cond ;

if (p == NULL) { /* 新しい単語? */

if ((p = talloc()) == NULL) /* 領域確保に失敗 */

error_jmp() ;

if ((p->word = strdup(s)) == NULL) /* 領域確保に失敗 */

error_jmp() ;

p->left = NULL ; p->right = NULL ;

p->count = 1 ;

}

else if ((str_cond = strcmp(s,p->word)) == 0)

/* 同じ単語があるので, カウンタをインクリメント */

p->count += 1 ;

else if (str_cond < 0) /* 小さければ左に */

p->left = addtree(p->left,s) ;

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

else /* 大きければ右に */

p->right = addtree(p->right,s) ;

return p ;

}

/* p の領域を確保する */

struct tnode *talloc(void)

{

struct tnode *p ;

if ((p = (struct tnode *)malloc(sizeof(struct tnode))) != NULL)

return p ;

return NULL ;

}

/* s で与えられた文字列を duplicate する */

char *strdup(const char *s)

{

char *p ;

if ((p = (char *)malloc(strlen(s)+1)) != NULL) {

strcpy(p,s) ;

return p ;

}

return NULL ;

}

void error_jmp(void)

{

fprintf(stderr, "Could not allocate memory!\n") ;

exit(-1) ;

return ;

}

このプログラムは標準入力から入力されたテキストファイルを, FMT で与えられた文字列の要素を区切り子としてトークン分解し(すなわち, 単語に分解し), それを二分木構造に展開する. 二分木構造は, すべての葉に対して,

• 左部分木は, 単語の辞書式順序で葉よりも小さいものを,

• 右部分木は, 単語の辞書式順序で葉よりも大きいものを

格納する. 与えられた順序に対してこのような構造を持つ二分木を整列二分木 (heap) と呼ぶ. この二分木を inorder で巡回すると, 順序に対して整列された出力を得る.例えば, 以下のようなテキストファイルを入力する.

This is a test for a binary tree, which is called heap.

this is a test for a binary tree, which is called heap.

Thats are test for binary trees.

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

この場合の出力結果は

5: a

1: are

3: binary

2: called

3: for

2: heap

4: is

3: test

1: thats

2: this

3: tree

2: which

となる. これは確かに単語の辞書式順序となっている. (ただし, 各単語の先頭文字が大文字の時は, それを小文字に変更している.)

また, リストの特別な形式として, 双方向リスト, 循環リストという形式もある.

双方向リスト

循環リスト

これらのリストは, 単純に “自分の次” だけを示すのではなく, “自分の前” などを示すポインタを持っている.

6.18.2.3.1 typedef C の文法上は記憶クラス指定子となっている typedef を用いると,

struct data {

char str[80] ;

struct data *r_next ;

struct data *l_next ;

} data ;

といった構造体を記述する際に持っと簡単に書くことができる.

typedef [元の型] 新しい型の識別子

という形をとる. ここで定義された識別子は typedef 名と呼ばれる. 文法的には「元の型」はなくても良い.

Example 6.18.18 もっとも簡単なものは,

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

typedef int Length ;

というものであって, これ以後 Length という型の識別子が定義され, それは int と同じである.

Example 6.18.19 もう少し複雑なものは,

typedef struct tnode *treeptr ;

typedef struct tnode {

char str[80] ;

treeptr *r_next ;

treeptr *l_next ;

} treenode ;

これは, treeptr, treenode という2つの新しい型を定義している.

typedef と section 6.20 で述べる #define の違いは, typedef が型を定義しているのに対し, プリプロセッサ文の #define はコンパイル以前に展開されてしまうところにある. すなわち,

#define peach int

unsigned peach i ;

は peach が int にプリプロセッサで置き換えられるので, 問題なくコンパイル出来るが,

typdef peach int

unsigned peach i ;

は文法エラーとなる.一旦 typedef によって型を宣言してしまうと, その型はそれ以後で自由に利用可能である. したがって,

typedef struct complex Complex ;

によって Complexを定義すれば,それ以後はわざわざ struct complexと書かなくても良い. また, typedefでは同じ typedef 名をより狭いスコープで再宣言できるが, その場合には「元の型」を明示しなければ, 再宣言したことにはならない. (cf. [2, A8.9].)これまでに出てきた size t などの型は, typedef により, 処理系・OSによって適切な型に typedef さ

れている. 実際に typedef が有効に利用できるのは, このような処理系依存の部分を吸収する場合が多い.

6.18.3 その他の構造を持った型

6.18.3.1 共用体

余り使わないが, 共用体というものがある. 基本的には構造体と同じようなものであり, その定義方法も,struct の代りに, union という予約語を使って宣言を行う.

6.18.3.1.1 共用体の定義

Example 6.18.20 次は int, double の型を持つ2つのメンバーがある共用体の定義である.

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

union int_double {

double x ;

int n ;

} u ;

共用体と構造体との違いは, 各メンバーが重なり合うメモリ領域を共有していることである. すなわち, 共用体では, 各メンバーを同時に(意味のある方法では)アクセスすることができない. これ以外のことに関しては, 共用体は構造体と同じ性質を持つ.

double x

int n

struct int_double {

double x ;

int n ;

}

と定義すると, 2つのメンバーの領域は重ならない.

double x または int n

union int_double {

double x ;

int n ;

}

と定義すると,2つのメンバーの領域が重なってしまう.

Example 6.18.21 上の int_double 共用体では,

u.n = 1 ;

u.x = 1.0 ;

といったように, そのメンバーの識別子にしたがって, どの型でもとりうることができるが, 代入時と異なる型で参照したときの値は保証されない. すなわち, 共用体においては, そのメンバー変数がすべてメモリとして確保されるのではなく, (この例の場合は n と x )が同じメモリ領域に確保される.

6.18.3.1.2 共用体のメモリ内でのアロケート 共用体はメモリ内では, もっとも大きなメモリを必要とするフィールドの分だけ確保される. 例えば, Example 6.18.21 の場合は, int と double のメモリサイズ

の大きい方の分だけのメモリが使われ, その整合も各型に沿った方法で行われる.

6.18.3.1.3 共用体の初期化 共用体の初期化は, その最初のメンバーの型の値のみで行うことができる.すなわち, 上の int_double 共用体では, メンバー n を用いた初期化は行えるが, メンバー x を用いた初

期化は行うことができない. つまり,

union int_double x = {1} ;

とすると, x には正しく int 型の 1 という値が入るが,

union int_double x = {1.0} ;

は x には 1.0 を int に変換した 1 が入ることとなる.

6.18.3.1.4 共用体(その他) 共用体の配列, 共用体を構造体メンバーとすること, 共用体メンバーに配列, 構造体を使うことなどはすべて許される.実際に共用体を利用する場面は少ないが, 構造体メンバーとして, Pascal における「可変レコード」のよ

うに, 構造体のメンバーの一部の内容によって, その後のメンバー構成が変る時に利用される他に, 古くはIntel 社の 80286 CPU のレジスタを表現する構造体

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

union reg {

short x ;

struct {

char h ;

char l ;

} y ;

}

のような使い方もある. ここで, 80286 CPU は16ビットの演算レジスタを持ち, それぞれ上位バイト, 下位バイトを独立にレジスタとして利用することができた96. そこで, 16ビットを表すメンバーとして, xを利用し, その上位, 下位ビットを取り出す際には, y.h または y.l を利用するというアクセス方法が実現

できる97.

6.18.3.2 ビット・フィールド

ビット・フィールドとは, 構造体のフィールドにそのビット数を指定できることである. ただし, そのビット数の合計は1ワードを越えてはならない.

6.18.3.2.1 ビット・フィールド定義方法

Example 6.18.22

struct flag {

unsigned cf:1 ;

unsigned :5 ;

unsigned zf:1 ;

unsigned sf:1 ;

unsigned n:8 ;

} a ;

この ビットフィールドは, 次のように参照できる.

a.cf = 1 ;

a.zf = 0 ;

a.sf = 1 ;

a.n = 10 ;

また,

96これは Intel 社の8ビット CPU である 8086, または, Zailog 社の8ビット CPU である Z80 のコードの互換性を考慮した設計であった. そのため, 80286 のコードは複雑になる傾向が強かった.

97もちろん, このようなアクセスを行って, 最初にメンバー x に値を代入し, メンバー y.h にアクセスして, その上位ビットを取り出すことができる保証は一般にはない. しかし, CPU と処理系を特定することにより, その処理系では演算レジスタへのアクセスを実現することができた.

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

struct bit {

unsigned b0:1 ;

unsigned b1:1 ;

unsigned b2:1 ;

unsigned b3:1 ;

unsigned b4:1 ;

unsigned b5:1 ;

unsigned b6:1 ;

unsigned b7:1 ;

unsigned ubyte:8 ;

} ;

union body {

struct bit b ;

unsigned x ;

} a ;

a = 0xFFFF ;

のように一括代入もできる.

ここで, 無名のフィールドはパディング (padding) に利用され, ビット巾 0 のフィールドは次のワード境界に強制的に整合させることができる. ビット・フィールドには & 演算子は適用できない.

6.18.3.2.2 ビット・フィールドのメモリ内でのアロケート ビット・フィールドが宣言順に上位から並

ぶか下位から並ぶかは処理系依存である. 次の図は, Example 6.18.22 が下位ビットから並ぶ時の図である.

flag

15 7 0

sfzf cf

6.18.4 名前空間

識別子は次の4つの名前空間ごとに識別される. 即ち, 異なる名前空間の間では, 同じスコープであっても同じ識別子を利用できる. しかし名前空間が別だからといって, 同じスコープ内での同一識別子の乱用はつつしむべきである.

• 変数, 関数, 引数, 列挙定数, typedef 名.

• タグ名.

• (構造体・共用体)メンバー名.

• ラベル名.

Example 6.18.23 この例の person はすべて異なる名前空間に属する.

C8.tex,v 1.14 2001-09-26 15:38:45+09 naito Exp

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

struct person {

char person[80] ;

int age ;

} person ;

struct person の person は「タグ名」, char person[80] の person は「メンバー名」, “} person”の person は「変数名」である.

このように, 同じ識別子を異なった名前空間で利用することが可能であるが, 混乱の元となるので, やめた方がよい. ちなみに,

typedef struct foo {int x,y } foo ;

struct bar {int x,y } bar ;

は似ているが全く異なる内容である. typedef したものは次のように利用可能である.

struct foo a ; /* 構造体タグ名 foo を利用する */

foo b ; /* 構造体の型名 foo を利用する */

しかし, 後者は構造体タグ名 bar と変数名 bar を定義している. この時,

struct bar c ; /* 構造体タグ名 bar を利用する */

は許されるが,

bar d ;

は許されないので注意しよう.

6.18.5 演習問題

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

学籍番号: xxxxx

ID : xxxxx

氏名 : xxxxx

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

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

Example 6.18.24 Example 6.18.17のプログラムを元に,単語の出現頻度順で出力を得るように書換えよ.

Example 6.18.25 Example 6.18.17 のプログラムで, 行末が -(ハイフン)の時には, 次の行の先頭の単語と連結して, 単語の出現頻度順, または単語の辞書式順序で出力を得るように書換えよ.

C9.tex,v 1.10 2001-07-19 17:15:07+09 naito Exp

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

6.19 ファイルへの入出力

ファイルへの入出力とは, プログラム中でファイルからデータを読み取ったり, データをファイルに書き出したりすることをいう. Cでファイルの入出力を行なうには, 実際にファイルを利用する前に, ファイルをオープンし, 利用が終ったらクローズしなくてはならない. ファイルをオープン (open) するとは, システム(OS)に対して, どのようなファイルをどのように利用するかを知らせることであり, ファイルをクローズ (close) するとは, ファイルの利用が終わったことをシステムに通知することである. これらの操作には標準入出力関数と呼ばれる一連の関数を用いる. Cのプログラム中では入出力用のファイルをファイルストリーム (file stream) とよび, 一旦オープンされたファイルは入力用または出力用のバッファ(buffer)と呼ばれる領域を経由してデータがプログラムに渡されたり, プログラムからデータがOSに流れていく.ファイルのオープンに成功すると, プログラムはOSからファイル記述子 (file descripter) と呼ばれる

負でない整数を得ることができる98. 一方, ファイルをクローズするとは, 実際にはその読み出し, 書き込みなどのためのファイル記述子を再利用可能な状態にし, 書き込みを行ったときには, そのディスク領域を決定するなどの操作が行われる.実際にファイルのオープンやクローズをを行なうには, fopen, fclose 関数を利用する99. fopen 関数の

戻り値の型は FILE 構造体へのポインタであり, そこにはファイル記述子の他, ファイルへのアクセスのためのバッファなどのメンバーを持つ. FILE 構造体は, 標準ヘッダファイル stdio.h でその構造が定義され

ている.

Example 6.19.1 実際, a.data というファイルをオープンして, ファイルの内容を char 型変数として読

み込み, それをクローズするには以下のようにしておこなう.

FILE *fp ;

int c ;

if ((fp = fopen("a.data","r")) != NULL) {

while (!feof(fp)) {

c = fgetc(fp) ;

putc(c,stdout) ;

}

fclose(fp) ;

}

ここで, fopen の第一引数は, ファイル名を表す文字列であり, 第二引数は, ファイルをオープンするモードを表す文字列である100. また, feof は, ファイルに対する現在の読みとり(書き込み)位置が, EOF(ファイルの終端を表す特別な仮想的な文字)かどうかを判断する関数である.ここで, fgetc 関数の戻り値の型は char ではなく, int であることに注意. もし戻り値の型が char で

あるならば, EOF を表す値(#define EOF (-1) と定義されていることが多い. )と実際の文字とを区別できなくなるからである.また, このファイル中では FILE 型へのポインタ fpのための領域を確保していないが, この領域は fopen

98指定したファイルが存在しない, アクセス権がないなどの場合には, ファイルのオープンに失敗する. この場合, ファイル記述子に対応する値として, 負の値が戻ってくる仕様になっているものが多い.

99Cには「低レベル入出力」と呼ばれる関数群(UNIX の場合には, 実際にはシステムコール)があり, それらはファイル記述子を利用して, 入出力を制御する. fopen や fclose などの標準ファイル入出力関数は, その内部で低レベル入出力システムコールを利用している.100この場合は, 読みとり専用にオープンしている. 新規ファイルや, ファイルの内容を新しく書き込むためには w を指定したりする. 詳しくは fopen のオンラインマニュアルを参照.

C9.tex,v 1.10 2001-07-19 17:15:07+09 naito Exp

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

関数内で確保され, fclose 関数内で開放される.

また, fopen は指定したファイルがオープンできない時には NULL を返す. fopen 以後は, ファイルへのアクセスは, このポインタを利用する.また, ファイルに出力をする時には, fprintf 関数を利用することが多い. fprintf は, printf とほぼ

同様な利用法をする. すなわち, prinf 関数で

printf(fmt, ....)

としたものを, ファイルポインタ fp で示されるファイルに出力するには,

fprintf(fp, fmt, ...)

とする. printf 関数は標準出力 stdout への fprintf を行っているに過ぎない.これまでに利用してきた標準入出力も, ファイルとして定義されている. それらは, stdio.h に以下のよ

うに定義されている.

• stdin: 標準入力(ファイル記述子 0).

• stdout: 標準出力(ファイル記述子 1).

• stderr: 標準エラー出力(ファイル記述子 2).

これらは, fopen などを利用せずに使うことができる.ファイルへの入出力を行なう関数としては, fread, fwrite がある. fprintf がテキストのみを出力す

る関数であるのに対して, fwrite はどのようなデータでも出力することができる. 一般に, ファイルからデータを読みとる時には, fread を使うことが多い. どのような形式で格納されているわからないデータを読むには fread を使い, 次のようにする.

Example 6.19.2 a.data というファイルの内容を文字列としてとるには以下のようにしておこなう.

FILE *fp ;

char buf[80] ;

if ((fp = fopen("a.data","r")) != NULL) {

while (!feof(fp)) {

if (fread(buf,1,80,fp)!=0)

fprintf(stdout,"%s",buf) ;

bzero(buf,80) ;

}

fclose(fp) ;

}

ここで, fread の第一引数は, データを格納する領域, 第二引数は, 読みとるデータの数, 第三引数は最大どれだけのデータを読みとるかである. この時, buf を再利用するため, 出力した後には buf の中を bzero

関数でクリアしている.

また, 読み込むものが文字列とわかっている時には, 関数 fgets を利用するのが望ましい101.

101詳しくは, man 3 gets 参照.

C9.tex,v 1.10 2001-07-19 17:15:07+09 naito Exp

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

Example 6.19.3 実際, a.dataというファイルの内容を文字列としてとるには以下のようにしておこなう.

FILE *fp ;

char buf[80] ;

if ((fp = fopen("a.data","r")) != NULL) {

while (!feof(fp)) {

if (fgets(buf,80,fp) != NULL) {

fprintf(stdout,"%s",buf) ;

}

fclose(fp) ;

}

fread 関数ではストリーム内にあるデータを, その値が何であっても指定のバイト数だけ読み込みを行うのに対して, fgets 関数は, 指定のバイト数だけの文字列を改行文字まで読み込みを行う. すなわち, 現在のファイルポインタ (file pointer) の位置(読み込みを行っているファイル内での位置)から指定された文字数(上の例では80文字)以内に改行文字があれば, そこで読み込みを中断する. なお, fgets に良く似た gets という関数もあるが, こちらは, 読み込みの最大データ量を指定できないため, 読み込み領域を越えて読み込みが行われ, データが破壊される原因となるので使わない方がよい102. したがって, テキストデータの場合の読み込みには fgets 関数が適しているが, バイナリファイルは fread 関数で読み込む必要

がある. 同様に, fprintf 関数で書き出しを行うとテキストデータしか出力は出来ないが, fwrite 関数では, データそのものが書き出される.

Example 6.19.4 b.out というファイルに配列の内容をテキスト形式で書き出す.

FILE *fp ;

int a[10] = {0,1,2,3,4,5,6,7,8,9} ;

int i ;

FILE *fp ;

int a[10] = {0,1,2,3,4,5,6,7,8,9} ;

int i ;

if ((fp = fopen("b.out", "w")) != NULL) {

for(i=0;i<10;i++) fprintf(fp, "%d = %d\n", i, a[i]) ;

fclose(fp) ;

}

}

このプログラムの出力結果は,

0 = 0

1 = 1

2 = 2

102UNIX のいくつかのアプリケーションに見られる, “buffer overflow” によるセキュリティホールは, fgets を利用すべきところで, 不用意に gets を利用したことに起因するものがある.

C9.tex,v 1.10 2001-07-19 17:15:07+09 naito Exp

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

3 = 3

4 = 4

5 = 5

6 = 6

7 = 7

8 = 8

9 = 9

となる. この出力ファイルのように, fprintf 関数を用いて出力されたテキストファイルは, fscanf 関数で読み出すことが出来る.

FILE *fp ;

int a[10] ;

int i ;

if ((fp = fopen("b.out", "r")) != NULL) {

for(i=0;i<10;i++) fscanf(fp, "%d = %d", &i, &a[i]) ;

fclose(fp) ;

for(i=0;i<10;i++) fprintf(stdout, "%d = %d\n", i, a[i]) ;

}

fscanf, scanf 関数を使うのは, fprintf, printf 関数で定型のフォーマットで書き出したファイルを読む場合だけである.

Example 6.19.5 b.out というファイルに配列の内容をバイナリ形式で書き出す.

FILE *fp ;

int a[10] = {0,1,2,3,4,5,6,7,8,9} ;

if ((fp = fopen("b.out", "w")) != NULL) {

fwrite((int *)a, sizeof(int), sizeof(a)/sizeof(a[0]), fp) ;

fclose(fp) ;

}

このプログラムの出力結果を od コマンドで, od -x として見てみると,

0000000 0000 0000 0000 0001 0000 0002 0000 0003

0000020 0000 0004 0000 0005 0000 0006 0000 0007

0000040 0000 0008 0000 0009

となり, 4バイトの整数値が順に書かれていることがわかる. このように fwrite 関数で書き出したバイナ

リファイルは, fread 関数で読み出す.

FILE *fp ;

int a[10] ;

int i ;

if ((fp = fopen("b.out", "r")) != NULL) {

C9.tex,v 1.10 2001-07-19 17:15:07+09 naito Exp

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

fread((int *)a, sizeof(int), sizeof(a)/sizeof(a[0]), fp) ;

fclose(fp) ;

}

for(i=0;i<10;i++) printf("%d\n", a[i]);

上の int 型の数値を fwrite 関数で書き出した結果を, 例えば char 型で読み出すと, すなわち,

FILE *fp ;

char a[10] ;

int i ;

if ((fp = fopen("b.out", "r")) != NULL) {

fread((char *)a, sizeof(char), sizeof(a)/sizeof(a[0]), fp) ;

fclose(fp) ;

}

for(i=0;i<10;i++) printf("%d\n", a[i]);

として読み出すと, a[0] から順に

0 0 0 0 0 0 0 1 0 0

という値が入力される. これは, int 型が4バイトで, little endiean で書かれているため, a[0] から a[3]

までが int の 0 を読み, a[4] から a[7] までが int の 1 を読んだ結果である. このように, バイナリで出力した場合には, どのような型で, どのような順序(little endiean か big endiean か)で出力したかを管理しなければならない.

Example 6.19.6 プログラムの第一引数に与えられたファイル名を持つファイルから, 第二引数に与えられたファイル名を持つファイルにデータをコピーする.

#include <stdio.h>

int main(int argc, char **argv)

{

FILE *in, *out ;

char buf[1024] ;

size_t len ;

if (argc < 2) {

fprintf(stderr,"Usage: %s inputfile outputfile\n", argv[0]) ;

exit(-1) ;

}

if ((in = fopen(argv[1], "r")) == NULL) {

fprintf(stderr,"Could not open file %s\n", argv[1]) ;

exit(-1) ;

}

if ((out = fopen(argv[2], "w")) == NULL) {

fprintf(stderr,"Could not open file %s\n", argv[2]) ;

C9.tex,v 1.10 2001-07-19 17:15:07+09 naito Exp

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

exit(-1) ;

}

while((!feof(in))

&&(len = fread((char *)buf, sizeof(char), sizeof(buf)/sizeof(buf[0]), in))) {

fwrite((char *)buf, sizeof(char), len, out) ;

}

return 0 ;

}

このプログラムでは, 第二引数に与えられたファイルは w でオープンしているため, 既存のファイルがあっても, それを上書きする103.

6.19.1 演習問題

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

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

Exercise 6.19.3 標準入力から入力されたファイルの中の, 数字, 空白, その他の文字の出現頻度を数えるプログラムを書け. ただし, 空白とは, 空白文字, タブ, 改行の3種である.

Exercise 6.19.4 ファイルもしくは標準入力から, 空白で区切られた学籍番号, ID, 氏名の組を読み込み,それらをリスト形式として格納し, 標準出力もしくはファイルにに以下のフォーマットで出力するプログラムを書け.

学籍番号: xxxxx

ID : xxxxx

氏名 : xxxxx

その際, このプログラムを起動する時に与えた引数が読み込みもしくは書き込みのファイルとなるようにしなさい. 具体的には, 以下の通り. (実行コード名を prog とした).

% prog # このときには, 入出力とも標準入出力.

% prog - - # このときには, 入出力とも標準入出力.

% prog infile # このときには, 入力はファイル, 出力は標準出力.

% prog infile - # このときには, 入力はファイル, 出力は標準出力.

% prog infile outfile # このときには, 入出力ともファイル.

Exercise 6.19.5 ファイルもしくは標準入力から入力されたテキストファイルの行数, 文字数を標準出力に出力するプログラムを書け. その際, このプログラムを起動する時に与えた引数が読み込みもしくは書き込みのファイルとなるようにしなさい. 具体的には, 以下の通り. (実行コード名を prog とした).

% prog # このときには, 標準入力.

% prog - # このときには, 標準入力.

% prog infile # このときには, 入力はファイル.

103もし, 上書きをしたくない場合. すなわち, 同じファイル名(パス名)をもつファイルが存在する場合に何かの警告を出したいときには, stat システムコールなどで, そのパス名に対応するファイルの情報を得る必要がある.

C9-1.tex,v 1.6 2001-07-19 12:42:43+09 naito Exp

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

6.20 プリプロセッサ命令

C 言語の文法の最後として, プリプロセッサ命令を解説する.これまで, #include などを利用してきたが, これら, # からはじまる文は, コンパイラ以前に, プリプロ

セッサといわれるものが解釈をして, 必要な処理を行なった後コンパイラに渡される.プリプロセッサ命令として, 代表的なものは以下の通りである.

• #include 指定したファイルを取り込む.

• #define この後に続く一つのトークンをそれ以後のトークンに置き換える. もしくは, (トークンが一つの時)そのトークンが定義されたと宣言する.

• #ifdef と #endif の対. #ifdef で指定されたマクロ変数が定義されている場合にのみ, #endif までのプログラム列を有効にする.

例えば,

#define MAX 100

は MAX を 100 に置き換える.

Example 6.20.1 #define 命令の例は次の通りである.

#define MAX 100

int a[MAX] ;

int main(int arc, char **argv)

{

int i ;

for(i=0;i<MAX;i++)

a[i] = i ;

}

ここで,

#define MAX 100 ;

とするのは間違いである.

また,

#define UNIX

は UNIX というトークンが定義されていると宣言する. これは, 以下のようにして利用されることがある.

#ifdef UNIX

....

#else

....

#endif

これは, UNIX が定義されている時と, 定義されていない時にそれぞれどの部分をコンパイルするかを分岐している. これは, 移植性を高めるために用いられることが多い.最も標準的に利用されるマクロ定義は,

C9-1.tex,v 1.6 2001-07-19 12:42:43+09 naito Exp

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

#define ARRAY_MAX /* 配列の最大値 */

int a[ARRAY_MAX]

for(i=0;i<ARRAY_MAX;i++) a[i] = i ;

などと, 配列の大きさ, 文字列の長さなどを定義するために用いられるものである. しかし, この時に利用するマクロ名として, 安易に MAX 等とするのはやめよう. 何の MAX なのかすぐにわからなくなってしまう

ことがある.また, 複雑なマクロとしては, 次のようなことができる.

#define max(A,B) ((A) > (B) ? (A) : (B))

この時,

x = max(p+q,r+s)

は,

x = ((p+q) > (r+s) ? (p+q) : (r+s))

に置き換えられる. このようなマクロが有効であるのは, その引数がどのような型であっても良いからであるが, 一方,

max(i++,j++)

などとすると, 副作用が影響するので, 期待した動作をしないことがある.また, マクロの引数の直前に # がつくと, 対応する引数は " で囲まれ, # とパラメータ識別子は引数に置

き換えられる. 置き換えられた結果, 文字列が並ぶときにはそれらは連結される. したがって,

#define PR(fmt,val) printf(#val " = %" #fmt "\t", (val))

で PR(d,x1) とすると,

printf("x1 = %d\t", (x1))

と展開される.また, マクロ定義中に ## があると, パラメータ置換後に, 両側の空白文字とともに ## が削除され, 隣接

するトークンが連結される. しかし, これら # ## 演算子は, 展開の再スキャンの際に現れても置換されない. したがって,

#define cat(x,y) x ## y

に対して cat(var,123) を行うと var123 が現れるが, cat(cat(1,2),3) は不定となる. これは, 一度めの呼び出しの後の cat( 1 , 2)3 が正しいトークンを含まないからであり, これを避けるには, さらに

#define xcat(x,y) cat(x,y)

とし, xcat(xcat(1,2),3) とすることにより, 123 を得ることができる. これは, xcat が ## を含まない

ことによる.マクロの詳しい内容については, [2, 4.11, A12] を参照.

C10-0.tex,v 1.5 2002-03-20 11:18:41+09 naito Exp

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

6.21 デバッグの手法

ここでは, Cプログラムのデバッグの方法をいくつか簡単に述べておくことにしよう. ここに述べてあるものは, 初心者がおかしがちな間違いに対する処方箋であって, ある程度複雑なコードでは通用しないことが多いが, 最も基本的な間違いとして理解しておかなければならないことばかりである.

6.21.1 コンパイルエラー

コンパイラを通そうとした時点でエラーが発生したときの対処方法を考えよう. コンパイラの時点でのエラーには, 正確には2種類が考えられる.

1. コンパイラにおける文法エラー.

2. コンパイラの警告メッセージ.

3. リンクエラー.

文法エラーは文字通り, C言語の文法解釈でエラーが発生している場合であり, リンクエラーは, C言語の文法的にはエラーは生じていないが, 実行形式を作成する時点で, 解決できない問題が生じていることである. 警告メッセージは, 文法エラーではないが, Cプログラムとしては問題が生じている部分であり, 解決しておくことが望ましいものである.

6.21.1.1 文法エラー

頻繁にお目にかかる文法エラーには, 以下のようなものがある.

1. 文法構造自身に問題があるもの.

gcc では “parse error” と表示される.

2. 未定義の変数名などを利用しているもの.

gcc では “undeclared (first use in this function)” と表示される.

3. 配列サイズが不定になっているもの.

gcc では “array size missing” と表示される.

4. 代入の型が一致しないもの.

gcc では “incompatible type in assignment” と表示される.

5. 関数宣言の矛盾.

gcc では “implicit declaration of function” と表示される.

いずれのエラーであっても, エラーが発生したプログラム中の行番号がコンパイラから出力される.

Example 6.21.1 “parse error” の例

C10-0.tex,v 1.5 2002-03-20 11:18:41+09 naito Exp

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

#include <stdio.h>

int main(int argc, char **argv)

{

int i ;

for(i=0;i<10;i++) {

printf("i = %d\n", i) ;

return 0 ;

}

test.c: In function ‘main’:

test.c:9: parse error at end of input

エラーメッセージでは「9行目」でエラーが発生していると言っているが, for 文の括弧がとじられていないため, 9行目の「閉じ括弧」で for 文が終了したと認識し, それ以後に入力がないために “parse errorat end of input” エラーが発生している.

Example 6.21.2 “parse error” の例

#include <stdio.h>

int main(int argc, char **argv)

{

int i ;

for(i=0;i<10;i++) {

printf("i = %d\n", i)

}

return 0 ;

}

test.c: In function ‘main’:

test.c:7: parse error before ’}’

エラーメッセージでは「7行目」でエラーが発生していると言っているが, printf 関数に ; がないため,7行目の「閉じ括弧」で文法エラーとなる.

Example 6.21.3 “undeclared” の例

#include <stdio.h>

int main(int argc, char **argv)

{

int i ;

for(i=0;i<10;i++) {

printf("i = %d\n", j) ;

}

return 0 ;

}

test.c: In function ‘main’:

test.c:6: ‘j’ undeclared

(first use in this function)

言うまでもなく, 「6行目」に「未定義の変数」 j を使っている.

Example 6.21.4 “array size missing” の例

C10-0.tex,v 1.5 2002-03-20 11:18:41+09 naito Exp

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

#include <stdio.h>

int main(int argc, char **argv)

{

int i, a[] ;

for(i=0;i<10;i++)

a[i] = i ;

return 0 ;

}

test.c: In function ‘main’:

test.c:4: array size missing in ‘a’

言うまでもなく, 「4行目」の配列 a の定義が間違っている.

Example 6.21.5 “imcompatible type in assignment” の例

#include <stdio.h>

int main(int argc, char **argv)

{

int i, a[10] ;

a = i ;

return 0 ;

}

test.c: In function ‘main’:

test.c:5: incompatible types in assignment

配列として宣言されたオブジェクトは, 静的ポインタである.

6.21.2 警告メッセージ

C言語の警告メッセージは, バグの原因を指摘していることが多い. そのため, 警告メッセージを減らすことはバグを取り除くために有用な方法である. gcc において, 全ての警告を出力するには, コンパイル時に -Wall オプションをつける.頻繁にお目にかかる警告メッセージには, 以下のようなものがある.

1. 関数宣言に関わるメッセージ. (重大度:中)

gcc では “implicit declaration of function” と表示される.

戻り値が int である関数の場合には問題が生じないが, そうでない関数の場合にはエラーとなる潜在的危険性がある.

gcc では “implicit declaration of function” と表示されるエラーとなる場合もある. この場合には,

• “type mismatch with previous implicit declartion”

• “previouly implicitly declaration”

などという警告と同時に発生する.

2. 未使用の変数の存在. (重大度:小)

gcc では “unused variable” と表示される.

未使用の変数はメモリエリアを圧迫する可能性がある. また, プログラムコードが醜くなる.

C10-0.tex,v 1.5 2002-03-20 11:18:41+09 naito Exp

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

3. 関数引数の型の不一致. (重大度:大)

gcc では “incompatible type for argument” と表示される. また, “different type arg” と表示されることもある.

4. ポインタの型の不一致. (重大度:大)

gcc では “assignment makes pointer from *** without a cast” と表示される.

Example 6.21.6 “implicit declaration of function” の例.

int main(int argc, char **argv)

{

printf("\n") ;

return 0 ;

}

test.c: In function ‘main’:

test.c:3: warning: implicit declaration

of function ‘printf’

#include <stdio.h> がないため, printf 関数の前方宣言がない. そのため, 関数宣言に矛盾が生じている.

Example 6.21.7 “implicit declaration of function” でエラーとなる例.

#include <stdio.h>

int main(int argc, char **argv)

{

int i ;

a() ;

return 0 ;

}

void a()

{

return ;

}

test.c: In function ‘main’:

test.c:5: warning: implicit declaration

of function ‘a’

test.c:4: warning:

unused variable ‘i’

test.c: At top level:

test.c:10: warning: type mismatch with

previous implicit declaration

test.c:5: warning: previous implicit

declaration of ‘a’

test.c:10: warning: ‘a’ was previously

implicitly declared to return ‘int’

関数 aには前方宣言が存在していない. そのため,一旦は intと理解してコンパイルされるが, その後 void

と宣言されるため矛盾が生じる.

Example 6.21.8 “incompatible type for argument” の例.

#include <stdio.h>

#include <stdlib.h>

int main(int argc, char **argv)

{

double x,y;

x = atoi(y) ;

return 0 ;

}

test.c: In function ‘main’:

test.c:6: incompatible type for argument 1

of ‘atoi’

C10-0.tex,v 1.5 2002-03-20 11:18:41+09 naito Exp

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

atoi 関数は, 引数は char * である.

Example 6.21.9 “different type” の例.

#include <stdio.h>

int main(int argc, char **argv)

{

int i,j ;

printf("%d, %f\n", i,j) ;

return 0 ;

}

test.c: In function ‘main’:

test.c:5: warning: double format,

different type arg (arg 3)

printf 関数の第3引数と, 第2引数の % 表示が対応していない.

Example 6.21.10 ポインタの型の不一致の例.

#include <stdio.h>

int main(int argc, char **argv)

{

int i, *p ;

p = i ;

return 0 ;

}

test.c: In function ‘main’:

test.c:5: warning: assignment makes

pointer from integer without a cast

「5行目」のポインタ代入は明らかな間違いである. もし, int *p のかわりに int p[10] とするとエラー

となるが, ポインタ形式の場合にはエラーではなく, 警告となることに注意.ポインタ代入に関しては, 型が異ると常にこの警告が出てくる.

Example 6.21.11 -ansi -pedanticオプションをつけずにコンパイルしたときには問題がないが, -ansi-pedantic オプションをつけるとコンパイルできない例.

#include <stdio.h>

int main(int argc, char **argv)

{

int n, a[n] ;

return 0 ;

}

test.c: In function ‘main’:

test.c:5: warning: ANSI C forbids variable-size array ‘a’

ANSI C では可変サイズの配列は認められていない. C++ では可変サイズの配列が認められているため,-ansi -pedantic をはずすと警告が出てこない.

6.21.2.1 リンクエラー

リンク時のエラーは, コンパイルしたコードとライブラリを結合する際に, 全てのシンボル名(名前)を解決出来ないことが原因となる.

C10-0.tex,v 1.5 2002-03-20 11:18:41+09 naito Exp

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

Example 6.21.12

#include <stdio.h>

#include <math.h>

int main(int argc, char **argv)

{

printf("%f\n", sqrt(2.0)) ;

return 0 ;

}

gcc test.c

としてコンパイルすると,

Undefined first referenced

symbol in file

sqrt /var/tmp/ccx1zyDC.o

ld: fatal: Symbol referencing errors. No output written to a.out

collect2: ld returned 1 exit status

というリンクエラーが発生する.この例では, 数学関数 sqrt というシンボルが解決できていない. 数学関数を利用する場合には, リンク

時に -lm というオプションを最後につけなければならない.

gcc test.c -lm

とすればエラーは解決できる.

Example 6.21.13

/* test0.c */

#include <stdio.h>

int a=1 ;

int main(int argc, char **argv)

{

a = 2 ;

return 0 ;

}

/* test1.c */

#include <stdio.h>

int a=1 ;

int main(int argc, char **argv)

{

a = 2 ;

return 0 ;

}

gcc -c test0.c

gcc -c test1.c

gcc test0.o test1.o -o a.out

としてコンパイルすると,

C10-0.tex,v 1.5 2002-03-20 11:18:41+09 naito Exp

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

ld: fatal: symbol ‘a’ is multiply defined:

(file test.o and file test1.o);

ld: fatal: symbol ‘main’ is multiply defined:

(file test.o and file test1.o);

ld: fatal: File processing errors. No output writt

というリンクエラーが発生する.この例は2つエラーがあり,

• main 関数が2つのコードにあり, エントリポイントを決定できない.

• 静的変数 a がともに初期化されているため, 両方の変数 a のリンケージが決定できない.

第2の問題を解決するには, どちらかで初期化をやめることになる.

6.21.3 実行時のデバッグ

実行時にプログラムが意図しない動作をしている場合には, それを解決する必要があるが, 一般的な対応法は存在しない.よくあるバグとしては, 以下のようなものが考えられる.

1. 繰り返し文で1回繰り返しが余分または1回繰り返しが足りない.

2. 配列の範囲を逸脱して代入している.

3. ポインタの扱いの間違い.

このうち, ポインタの扱いの間違いについては, 警告レベルを最大にしてコンパイルし, 警告を受けた部分を直していけばある程度は回避出来る場合がある.上2つのパターンに関しては, 最も有効な方法は, printf 関数でメッセージや変数の値を出力し, 意図し

たように動作しているかどうかを確かめることが効果的である.

Example 6.21.14 1から10までの和を計算するため, 以下のようなプログラムを書いた.

#include <stdio.h>

int main(int argc, char **argv)

{

int i, j ;

j = 0 ;

for(i=0;i<10;i++) j += i ;

printf("%d\n", j) ;

return 0 ;

}

この結果は, 45 と出力される. これをデバッグするには,

C10-0.tex,v 1.5 2002-03-20 11:18:41+09 naito Exp

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

#include <stdio.h>

int main(int argc, char **argv)

{

int i, j ;

j = 0 ;

for(i=0;i<10;i++) {

j += i ;

printf("j = %d, i = %d\n", j, i) ;

}

printf("%d\n", j) ;

return 0 ;

}

と, ループの内部で変数の値を出力すれば, 間違いにはすぐに気が付く.

Example 6.21.15 配列代入の間違い.

#include <stdio.h>

int main(int argc, char **argv)

{

int i, a[10] ;

for(i=1;i<=10;i++) a[i] = i ;

return 0 ;

}

これは, a[10] にまで値を代入しているので明らかなバグが含まれている. しかし, これに気が付くことは難しい. もし, バグに気が付いたら,

#include <stdio.h>

int main(int argc, char **argv)

{

int i, a[10] ;

for(i=1;i<=10;i++) {

a[i] = i ;

printf("a[%d] = %d\n", i, a[i]);

}

return 0 ;

}

と, ループの内部で配列の添字と値を出力すれば, 間違いにはすぐに気が付く.

Example 6.21.16 配列代入の間違いの例.

C10-0.tex,v 1.5 2002-03-20 11:18:41+09 naito Exp

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

#include <stdio.h>

int main(int argc, char **argv)

{

int i ;

double x;

int a[16] ;

x = 1.0 ;

for(i=0;i<=16;i++) a[i] = -1 ;

printf("x = %.18f\n", x) ;

return 0 ;

}

このプログラムを Solaris 2.6 上の gcc 2.95.1 でコンパイル, 実行すると,

x = -NaN

という結果を得る. 配列代入部分にバグがあることがわかる.

Exercise 6.21.1 Example 6.21.16 でどうしてこのような出力が出てしまったのかを説明せよ. また, その理由は適切な情報を出力することで確認ができるか?

6.22 落ち穂拾い

ここでは, これまでに述べることが出来なかった重要な注意点などを列挙しておこう.

6.22.1 最適化について

C コンパイラでは, プログラムの最適化が行われることが多い. 一般に最適化とは, そのプログラムの実行時間, または必要なメモリ量, またはその両方を短縮または少量で済むように, 実行コードを作成することである.例えば, 次のプログラムを考えてみよう.

int main()

{

int i=1,j ;

i = 0 ;

j += i ;

}

このプログラムのアセンブル結果(主要部分のみ)は,

C10.tex,v 1.9 2001-07-19 17:01:44+09 naito Exp

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

mov 1, %o0

st %o0, [%fp-20]

st %g0, [%fp-20]

ld [%fp-24], %o0

ld [%fp-20], %o1

add %o0, %o1, %o0

st %o0, [%fp-24]

となり, 実際に変数に値が代入されていることがわかる. しかし, 現実にはそれらの変数は代入, 演算後何も利用されていないので, 最適化を行ってアセンブルすると,

nop

となり, 実際には何も実行されないようなコードが出力されることがわかる.このように C では処理系が実行速度の最適化やメモリ利用効率の最適化を行う. この最適化の方法によ

り実行結果が異なるようなプログラムを書いてはならない. 最適化の方法により実行結果が異なることは,オブジェクトのメモリ内での配置の様子を仮定したり, 文法上は不定となっている, 演算の結合規則, 評価順序などを仮定してしまうことが原因となることが多い.

6.22.2 コメント

C では /* から */ までのプログラム部分は, コメント(注釈)として扱われ, プリプロセッサによってコンパイラに渡される前に取り除かれる104. C ではコメントは入れ子に出来ないので,

/* /* これはテスト */

ここは取り除く */

となっていると, 最初の /* を見つけたあと, 次に見つかる */ までコメントと扱われ, 2行めはコメントとならないので, 構文エラーとなる. また, ポインタ参照を利用した演算式

int *p, *q ;

*p/*q

とすると, /* の部分がコメントの始まりとみなされるので注意すること.一部の本には C ではコメントとして, 行頭に // をおけば良いと書いてあるが, これは C++ の流れを

受けたもので, 正式な ANSI の規格ではないことに注意しよう. すなわち, gcc のように C++ コンパイラとしても利用できる処理系では, これをコメントとみなすことがありうるが, これをコメントとみなさない処理系も多い.プログラムをデバッグする際には変数の値の出力文を書くことが多く, その後それを消去したくない場

合には, その部分をコメントにしてしまうことが多い. 例えば,

for(i=0;i<10;i++) {

j += i ;

/* これは debug 用 */

/* printf("j = %d, i = %d\n", j,i) ; */

}104プリプロセッサ終了直後のコードの様子は, gcc -E test.c とすることで見ることが出来る.

C10.tex,v 1.9 2001-07-19 17:01:44+09 naito Exp

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

などとする. しかし, プログラムの完成時にはこれは見苦しくなるので,

for(i=0;i<10;i++) {

j += i ;

#ifdef DEBUG

printf("j = %d, i = %d\n", j,i) ;

#endif /* DEBUG */

}

としておき, デバッグ時には

#define DEBUG

をつけておく方法がある. これは, すべての DEBUG 部分を一度に制御できるため, デバッグ用の文のとり忘れがなく, 便利である.

[8, Section 8.3] では, この方法は奨励されておらず,

int DEBUG=1 ;

for(i=0;i<10;i++) {

j += i ;

if (DEBUG)

printf("j = %d, i = %d\n", j,i) ;

}

または,

enum {DEBUG=1} ;

for(i=0;i<10;i++) {

j += i ;

if (DEBUG)

printf("j = %d, i = %d\n", j,i) ;

}

という方法が紹介されている. #ifdef による「条件付きコンパイル」は, 条件を変更することにより, コンパイルに失敗する場合が考えられる. すなわち, コンパイラのチェックを受けないコードが存在する. そのようなことを避けるために, [8] では条件付きコンパイルを推奨していない. しかし, 多くのフリーウェアなどでは, 多様なプラットフォームに対応するコードを記述するため, 条件付きコンパイルが行われている. これは, Makefile からコンパイラに条件を渡すことが可能であるため, 多様なプラットフォーム上でのコンパイルが容易になるというメリットを採用しているためである.

6.22.3 実行時エラー

C で作成したプログラムなどを実行する際に,

Segmentation fault

Bus error

Floating exception

などというエラーが発生することがある.

C10.tex,v 1.9 2001-07-19 17:01:44+09 naito Exp

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

6.22.3.1 Segmentation fault

Segmentation fault というエラーは, 以下のように割り当てられていないメモリ領域に対するアクセスがあった場合に, 「アクセス違反」として発生する.

/* Segmentation fault を起こす

* 意味:非割り当てメモリへのアクセス.

*/

#include <stdlib.h>

#include <stdio.h>

int main(int argc, char **argv)

{

struct data {

char x ;

struct data *next ;

} *data_p, *p ;

data_p = (struct data *)malloc(sizeof(struct data)) ;

data_p->x = ’1’ ;

p = data_p ;

printf("%c\n",p->x);

p = p->next ;

printf("%c\n",p->x);

/* ここで割り当てられていないメモリにアクセスしている */

return 0 ;

}

6.22.3.2 Bus error

Bus error というエラーは, 以下のように, ワード境界に合わせられていないアドレスから, ワード単位で読み取りを行おうとした場合などに発生する.

C10.tex,v 1.9 2001-07-19 17:01:44+09 naito Exp

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

/* Bus error を起こす

* 意味:適切に境界を合わせていないアドレスからデータを読み取ろうとした.

* 原因:

* ハーフワード, ワード, ダブルワードの境界に合わせられていないアドレスから,

* それぞれ2バイト, 4バイト, 8バイトを読み取った.

*/

#include <stdlib.h>

int main(int argc, char **argv)

{

char *s = "hello world" ;

int *i = (int *)&s[1] ;

int j ;

j = *i ;

return 0 ;

}

6.22.3.3 Floating exception

Floating exception エラーは, 0 での除算を行おうとすると発生する.

/* 0 で除算を行う. */

#include <stdio.h>

int main(int argc, char **argv)

{

printf("%d\n",1/0) ;

return 0 ;

}

これの代りに

printf("%f\n",1.0/0.0) ;

とすると, Inf という答えが返ってくる.

6.22.3.4 実行時エラーのトラップ

UNIX では上のような実行時エラーはカーネルによって検出され, カーネルからプロセスに対してシグナル (signal) を送ることによって, プロセスはエラーの発生を知ることが出来る. C では, 標準ライブラリ関数 signal を利用することによって, 受け取ったシグナルの種類ごとにそのハンドラ (handler) を記述することが可能になっている. 上のそれぞれの実行時エラーに対して, プロセスが受け取るシグナルは,

• segmentation fault に対しては SIGSEGV,

• bus error に対しては SIGBUS,

C10.tex,v 1.9 2001-07-19 17:01:44+09 naito Exp

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

• floating exception に対しては SIGFPE

と定められている105. したがって,

/* 0 で除算を行う.

実行時エラーをトラップする */

#include <stdio.h>

#include <signal.h>

extern int signal_handler(void) ;

int main(int argc, char **argv)

{

signal(SIGFPE, (void (*)(int))signal_handler) ;

printf("%d\n",1/0) ;

return 0 ;

}

int signal_handler(void)

{

fprintf(stderr,"Floating exception が発生したので, 実行を停止します\n") ;

exit(-1) ;

return ;

}

として, ハンドラを記述すれば, 実行時エラーに対して, 適切な処理を行うことも可能である.

6.22.4 ライブラリ呼出しとシステムコール

これまでに各種の標準関数を利用してきたが, それらのほとんどはライブラリ関数の呼出しという手順で行われていた. これに良く似た概念でシステムコールと呼ばれるものが UNIX 上では存在する106. 例えば, ファイルをオープンする関数として fopen があるが, この関数内では実際にはシステムコール open が

用いられている. また, C のプログラム内からファイルを削除するためには, unlink システムコールが用いられる.このように, C のプログラム内から呼び出すことが出来る関数として, ライブラリコールとシステムコー

ルの2種類があることがわかる. ここでは, この2つの違いを簡単にまとめておこう.

• ライブラリコールは ANSI 規格で定められ, すべての処理系でその呼出し方法は同一であるが, システムコールは, OS によって異なる呼出し方法が異なる.

• ライブラリコールは, ライブラリ内にあるサブルーチンの呼出しであるが, システムコールは, サービスを受けるためのカーネル呼出しである.

• ライブラリコールは, プロセスのアドレス空間で実行されるが, システムコールは, カーネルの空間で実行される.

• 時間測定では, ライブラリコールは, 「ユーザ」時間になるが, システムコールは, 「システム」時間になる.

105これらのシンボルは signal.h 内で定義される整数定数である.106MS-DOS では, これに相当するのは BIOS コールと呼ばれるものがある.

C10.tex,v 1.9 2001-07-19 17:01:44+09 naito Exp

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

• ライブラリコールは呼出しに時間が掛らないが, システムコールは, 呼出しのオーバヘッドが大きい.

このように, 一見似ているが, ライブラリコールとシステムコールはその役割が異なり, 処理系に依存するシステムコールの部分を, ライブラリ関数によって吸収するという意味がある.

6.22.5 ANSI で定められた翻訳の最低基準

最後に, ANSI で定められた, 処理系に求められている最低基準を列挙しておこう. これらに挙げる数値は翻訳限界と呼ばれ,「各限界の出現をそれぞれ少なくとも一つ含むプログラムのうち少なくとも一つを翻訳および実行できなければならない」と定められている.

• 複合文, 繰り返し制御構造および選択制御構造に対する入れ子のレベル数 (15)

• 条件付き取り込みにおける入れ子のレベル数 (15)

• 一つの宣言中の一つの算術型, 構造体型, 共用体型または不完全型を修飾するポインタ, 配列および関数宣言子(の任意の組み合わせ)の個数 (12)

• 一つの完全宣言子における括弧に囲まれた宣言子の入れ子のレベル数 (21)

• 一つの完全式における括弧に囲まれた式の入れ子のレベル数 (32)

• 内部識別子またはマクロ名において意味のある先頭の文字数 (31)

• 外部識別子において意味のある先頭の文字数 (6)

• 一つの翻訳単位における外部識別子数 (511)

• 一つのブロックにおけるブロック有効範囲を持つ識別子数 (127)

• 一つの翻訳単位中で同時に定義されうるマクロ識別子数 (1024)

• 一つの関数定義における仮引数の個数 (31)

• 一つの関数呼出しにおける実引数の個数 (31)

• 一つのマクロ定義における仮引数の個数 (31)

• 一つのマクロ呼出しにおける仮引数の個数 (31)

• 一つの論理ソース行における文字数 (509)

• (連結後の)単純文字列リテラルまたはワイド文字列リテラル中における文字数 (509)

• (ホスト環境の場合)一つのオブジェクトのバイト数 (32767)

• #include で取り込まれるファイルの入れ子のレベル数 (8)

• 一つの switch 文(入れ子になった switch 文を除く)中における case 名札の個数 (257)

• 一つの構造体または共用体のメンバ数 (127)

• 一つの列挙体における列挙定数の個数 (127)

• 一つのメンバ宣言並びにおける構造体または共用体定義の入れ子のレベル数 (15)

C10.tex,v 1.9 2001-07-19 17:01:44+09 naito Exp

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

これらの翻訳限界を越えたプログラムは, 必ずしも他の処理系で翻訳できるとは限らないことに注意しよう.また, 標準ヘッダファイル limits.h には, 各算術型で格納できる限界の数が書かれている. ここでは, そ

のマクロ名と, ANSI 規格に定められた最低限の数値を書いておく.

• ビットフィールドでない最小のオブジェクト(バイト)におけるビット数

CHAR_BIT 8

• signed char のオブジェクトにおける最小値

SCHAR_MIN -127

• signed char のオブジェクトにおける最大値

SCHAR_MAX +127

• unsigned char のオブジェクトにおける最大値

UCHAR_MIN 255

• char のオブジェクトにおける最小値と最大値

CHAR_MIN CHAR_MAX

char のオブジェクトの値を符号付き整数として扱う場合, CHAR MIN の値は, SCHAR MIN と同じであ

り, CHAR MAX の値は, SCHAR MAX と同じでなければならない.

その他の場合, CHAR MIN の値は 0 でなければならず, CHAR MAX の値は UCHAR MAX と同じでなけれ

ばならない.

• サポートするロケールに体する多バイト文字の最大バイト数

MB_LEN_MAX 1

• short int のオブジェクトにおける最小値

SHRT_MIN -32767

• short int のオブジェクトにおける最大値

SHRT_MAX +32767

• unsigned short int のオブジェクトにおける最大値

USHRT_MAX 65535

• int のオブジェクトにおける最小値

INT_MIN -32767

C10.tex,v 1.9 2001-07-19 17:01:44+09 naito Exp

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

• int のオブジェクトにおける最大値

INT_MAX +32767

• unsigned int のオブジェクトにおける最大値

UINT_MAX 65535

• long int のオブジェクトにおける最小値

LONG_MIN -2147483647

• long int のオブジェクトにおける最大値

LONG_MAX +2147483647

• unsigned long int のオブジェクトにおける最大値

LONG_MAX 4294967295

この他にも ANSI 規格には float.h 内で定める, 浮動小数点型の特性も定められている.

C.tex,v 1.6 2002-03-04 15:19:40+09 naito Exp

409

References

[1] B. W. Kernighan and D. M. Ritchie. プログラミング言語C. 共立出版, 1981.

[2] B. W. Kernighan and D. M. Ritchie. プログラミング言語C(第2版). 共立出版, 1989.

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

[4] ANSI. ANSI C Rationale. Silicon Press, 1990.

[5] ANSI. ANSI C Rationale. ftp://ftp.uu.net/doc/standards/ansi/X3.159-1989, 1989.

[6] P. van der Liden. エキスパートCプログラミング. アスキー出版局, 1996.

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

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

[9] B. W. Kernighan and D. M. Ritchie. The C Programing Language (2nd Ed.). Addison-Wesley,1988.

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

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

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

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

[14] S. Oualline. C実践プログラミング(第3版). オライリー・ジャパン, 1998.

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

[16] A. R. Feuer. The C Puzzle Book (Revised Edition). Addison-Wesley, 1999.

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

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