...

第6章 C 言語入門

by user

on
Category: Documents
26

views

Report

Comments

Transcript

第6章 C 言語入門
数理解析・計算機数学特論
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 という名前で作成することを指示している. もし, -o
hello という部分を省くと, コンパイラは a.out という名前で実行コード3 を作成する.
ここで作成した hello を実行するには
% ./hello
とする. ここで, ./ をわざわざ指定していることに注意せよ4 .
6.4.1
C のプログラムのコンパイル方法の詳細
gcc を利用して ANSI 規格の C 言語のプログラムをコンパイルする時には, 単に
% gcc hello.c -o hello
とするだけではなく, gcc のオプションをより詳しく設定すべきである. 具体的には,
% gcc -Wall -ansi -pedantic -O hello.c -o hello
ここで, 新しく付け加えたオプションの意味は次の通りである.
-Wall 文法エラーではない「警告」を軽微なレベルまで出力する.
-ansi -pedantic ANSI 規格の C としてコンパイルする.
-O オブジェクトコードの最適化を行う.
2 これは
mule の “c-mode” の特徴である.
UNIX 上の処理系では, コンパイラが出力する実行コードのデフォールトの名前は a.out になる. これは, Assembra
Output の略. 最近の LINUX, FreeBSD では elf という名前になるものがある. これら実行形式の名前の違いは, 実行形式の違い
でもある.
4 UNIX ではカレントディレクトリはデフォールトではコマンドサーチパスには入っていないし, 入れない方が望ましい.
3 多くの
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
6.5.1
C 言語の基本的な注意
C の基本的な構造
C のプログラムは, コメント (注釈) (comment), プリプロセッサ命令 (preprosessing), 文 (statement), 関数 (function) の集まりで構成されている. それぞれの文は, ; で終るか, { } で囲まれた文の集
まりである複文 (compound statement) で構成されている. 関数自身もまた文である. 実際には, main
という名前の特別な関数がはじめに実行され, そこに記述されている順序にしたがって実行される.
6.5.2
C で利用できる文字
C では, 英文字, 数字, 空白文字(スペース, タブなど), 記号文字, 改行文字などが利用できる. 記号文
字には特別な意味があることが多いので, 注意すること. また C では, 大文字と小文字は区別される. C の
コンパイラにおいて, 日本語が利用できるといっても, 変数名などに日本語が利用できるわけではないので
注意すること. 日本語が利用できるのは, 文字列に日本語が利用できるという程度の意味であり, 今回利用
する処理系では, このコードは EUC でなければならない.
また, 以下のものは全て空白と見なして無視される.
• 空白文字, 改行文字, タブ, 改ページ記号, コメント.
行末に \ を書くと, 行の連結を表し, 1 行として扱われる.
6.5.2.1
行
C 言語では「行」という概念は存在しない. 改行文字は空白文字と見なされるが, 改行文字の直前に \ が
ある場合には, 行の連結を表し, 処理系によって \ と改行文字の連続は一つの空白文字に置き換えられる.
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 「リテラル」
6 正確には,
(“literal”) とは, 「文字通りの」という意味である.
• 内部識別子またはマクロ名においては意味のある先頭の文字数は 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
unsigned
double
signed
int
void
long
typedef
enum
auto
float
register
short
extern
static
volatile
union
struct
const
sizeof
if
else
while
switch
do
case
continue
default
goto
break
return
for
6.5.5
文
文 (statement) とは, C 言語のプログラムの基本的な単位になるもので, それぞれの文は ; によって
終了する. 一つの文が複数行にわたっても良い.
Example 6.5.1 次の各行は全て文である.
int n ;
printf("Hello World.\n") ;
x + y ;
a = b ;
;
最後の行は空文と呼ばれる.
また, {, } によって文の集まりを一つの文(複文 (compound statement))にすることができる.
Example 6.5.2 次は一つの複文である.
{
a = b ;
c = d ;
}
6.5.6
式
式 (expression) とは, 計算を行なう最小単位のことで, それぞれの式は値を持つ. その値は, 次のよう
にして決まる.
• 計算式の場合は, その結果.
• 代入式の場合は, その左辺の値.
• 関数の場合は, その戻り値.
• 比較の場合, 真ならば 1, 偽ならば 0.
式に ; をつけることにより, 文にできる. それを式文 (expression statement) と呼ぶ.
C1.tex,v 1.13 2002-03-20 11:15:18+09 naito Exp
数理解析・計算機数学特論
223
Example 6.5.3 次のようなものは式文である.
/* 値は a
/* 値は x + y
a ;
x + y ;
*/
*/
a = b ;
/* 値は代入された a の値
printf("Hello World.\n") ; /* 値はこの関数の戻り値
a = b = c ;
a = b, c = d ;
/* 値は代入された a の値
/* 値は最後に代入された c の値
a < b ;
/* 値は真ならば 1, 偽ならば 0 */
6.6
6.6.1
*/
*/
*/
*/
用語
処理系とその動作
C において, 処理系 (implementation) とは, 特定の環境中の特定のオプションの下で, 特定の実行環境
用のプログラムに翻訳を行うソフトウェア群を指し, 処理系依存 (implementation-defined behavior)
とは, その処理系ごとにどのように振舞いかが規定されているものである. 一方, 不定(または未定義)
(undefined behavior) とは, 同じ処理系であっても, その振舞いが規定されていないものを指す. 特に,
最適化(オプティマイザ (optimizer))の指定によって振舞いが変わることが多い. 未規定 (unspecified
behavior) とは, 規格がその動作を一切指定しないものを指す. この他に, 処理系依存の項目の一部として,
文化圏固有動作 (locale-specific behavior) がある. これは, 処理系そのものに依存するのではなく, 処
理系動作またはプログラム動作中に与えられた文化圏情報 (locale) ごとに, 処理系が明示的に動作を規定
するものである.
Example 6.6.1 未規定の動作の例としては, 関数の実引数の評価順序. 不定(未定義)の動作の例として
は, 整数演算のオーバフローに対する動作. 処理系定義の例としては, 符号付き整数を右シフトした場合の
最上位ビットの伝播. 文化圏固有動作の例としては, islower 関数が26個の英小文字以外の文字に関し
て, 真を返すかどうかがある.
ANSI の規格にしたがった処理系とは, ANSI の規格で規定されているすべての動作を受理しなくてはなら
ない.
6.6.2
その他
ANSI 規格で定められているその他の用語として, バイト, 文字, オブジェクトがある. バイト (byte) と
は, 実行環境中の基本文字集合の任意の要素を保持するために十分な大きさを持つデータ記憶領域の基本単
位と定められ, 1バイトのビット数は処理系依存, バイトは連続するビット列からなる. 文字 (character)
とは, 1バイトに収まるビット表現. オブジェクト (object) とは, その内容によって, 値を表現できる実行
環境中の記憶領域. ビットフィールド以外のオブジェクトは, 連続する一つ以上のバイトの列からなる. ま
た, オブジェクトを参照する場合, オブジェクトは特定の型を持っていると解釈して良い.
6.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 ここの解説は本当は正しくない。この関数はもっと多くの引数をとり、最初の引数も文字列リテラルである必要はない.
9 main
関数が明示的な戻り値を持たない場合には, シェル(ホスト環境)に返される値は不定となる.
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 there
is 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
11 void,
(単精度)浮動小数点型
double
倍精度浮動小数点型
long double
長い倍精度浮動小数点型
void
何もない型
enum
列挙型
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
const int
cn = n ; n
n ;
cn ;
= cn ;
この中で代入が許されるのはどの場合かを考えてみる. 当然 n = cn は許される. しかし, cn = n は gcc の場合に
は, assignment of read-only variable ‘cn’ という警告が出される. Sun の C コンパイラでは, left operand
must be modifiable lvalue というエラーとなる.
しかし, 次の例はどうだろうか?
char
*cp ;
const char *ccp ;
ccp = cp ; cp
= ccp ;
こちらは ccp = cp が許され, cp = cpp の代入では, gcc では assignment discards qualifiers from pointer
target type という警告が出される. これでは何を言っているかわからないので, Sun の C コンパイラに通してみる
と, assignment type mismatch: pointer to char "=" pointer to const char という警告が出る.
まず, cpp = cp が許される理由を考えてみよう. 実は, const char * という型指定は, 「const char へのポイン
タ」という意味であり, ccp 自身を const 宣言しているのではなく, ccp が指し示すオブジェクトが const と言って
いるのである (cf. [2, A.8.6.1]). したがって, 次のような例は警告対象となる.
const char *ccp="abc" ;
*ccp =’b’ ;
しかし,
char
cp[4] ;
const char *ccp="abc" ;
ccp
= cp ; *cp =’b’ ;
のように, 一旦 const 修飾子がついていないオブジェクトを経由して, const 宣言を行ったオブジェクトへのアクセ
スを行うことは, 文法上問題は発生しない. しかし, const 修飾子は, 「読み出し専用」のメモリ領域にオブジェクト
を配置して良いことをコンパイラに知らせるという役目も持ち, そのような場合も含めて, この例の結果は不定である
と考えるべきである. なお, const ポインタを宣言するには, char *const ccp とする. すなわち,
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 という代入が行われることに相当し, 上で述べた単純代入の規約を満たす. しかし, const
char ** の例では, 仮引数 p は「const char * へのポインタ」であり, 実引数 argv は「char * へのポインタ」で
あるため, 単純代入の規約を満たさない.
なお, char * を仮引数とする多くの標準ライブラリ関数(例えば, strcpy など)は, 仮引数として const char *
を宣言している. これは, 関数内で明示的に仮引数の指し示す値を変更しないための措置である.
6.8.3.2
変数と記憶領域
変数は宣言と同時に対応する記憶領域が確保される. しかしながら, それぞれの型に対して, どれだけの
記憶領域が確保されるかは処理系依存である.
それぞれの型がどれだけの記憶領域をとるかを調べるには, sizeof 演算子を使う. sizeof 演算子の利
用法は以下の通りである.
sizeof (型) ;
sizeof オブジェクト ;
ここで, その結果として得られる値は, 符号なし整数で表現され13 , その意味は, char 型の何倍の記憶領
域が確保されるかを表す. 即ち, sizeof(char) の結果は処理系によらず 1 である.
それでは, char 型がどれだけの記憶領域を使うかを知るには, どのようにすれば良いのだろうか. それ
には, C 言語の標準的なヘッダ・ファイルを見れば良い. 実際, limits.h に定義されている CHAR BIT と
いうマクロ14 の値が char 型のビット数である. Sun Sparc Station の C コンパイラの場合,
#define CHAR_BIT
13 正確には
0x8
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
n
l
(padding)
16 bits
c
n
l
16 bits
c n
n(続き) l
d
d
d
(a)
(b)
(c)
この中で (c) のメモリ・アロケーションはアライメント (alignment, 境界調整) に適合していない環境が
多いため, ほとんどこのようなアロケーションは行われない.
Example 6.8.3 変数に値を代入する操作とは, 変数に対して与えられたメモリに数値を書き込むことに他
ならない. 例えば, (int が16ビットの場合)
int n ;
n = 1 ;
とすることは,
16 bits
n 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
16 bits
n
=⇒
または
16 bits
n 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0
8 bits
と値を代入することになる. 下のように上位バイトと下位バイトを入れ替えて数値を表現する処理系 (CPU)
を big endian, 上のように数値を表現する処理系 (CPU) を little endian と呼び, 8080, Z80 などの Intel
社の CPU は big endian になっていることが多く, 68000, SPARC などの CPU は little endian になって
いる.
15 ANSI
の規格書によれば, char 型の占めるビット幅を1バイトと定義している.
C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp
数理解析・計算機数学特論
6.8.3.3
231
文字型と整数型
文字型 (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
int
2
4
long
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
4
double
long double
8
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]
17 C
の定義によれば, 「符号なし型はより広い符号なし型に変換される」とされているので注意すること.
の定義によれば, 「標準文字セットのすべての文字は正の値を持つ」となっている.
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 = 1.0/b ;
/* x の値は 0 である. */
/* x の値は 0.5 になる. 算術変換. */
a = -7 ; b = 2 ;
c = a/b ;
/* この値が -3 となるか -4 となるかは処理系依存. */
n = 1 ;
a = n ;
/* sizeof(int) = 4, char が符号付きの時,
* a = 0xFFFFFF01 となるか (符号拡張)
* a = 0x00000001 となるかは処理系依存. */
この値を正しく計算するには, 後述する型変換を使う必要がある.
6.8.4.8
K&R での算術変換
K&R の C 言語, すなわち [1] で定義されている, traditional C と ANSI C とでは, 算術変換の方法が全
く異なる. K&R では,
まず, char または short 型の任意の被演算数が int に変換され,
float 型は double に変換される.
次に, どちらかの被演算数が double なら他方も double に変換さ
れ, それが結果の型となる.
そうでなく, 一方の被演算数が long なら他方も long に変換され,
それが結果の型となる.
そうでなく, 一方の被演算数が unsigned なら他方も unsigned に
変換され, それが結果の型となる.
そうでないときには, 両方の被演算数が int でなければならず, そ
れが結果の型となる.
と書かれている. ANSI C では「値を保存」する方向に変換が行われるが, K&R C では「unsigned を保
存」する方向に変換が行われる.
Example 6.8.9 次のコード19 は, ANSI C と K&R C では異なった結果を出力する20 .
19 [6]
からの引用
K&R の規約では, unsigned char は存在しない. unsigned, short, long は int につく限定詞と定義されている. しか
し, 古い ANSI 規格ではない処理系の多くで unsigned char が利用できる.
20 実は,
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 は規定されていない.
後述するポインタにも作用する.
23 もちろん, /, % の第2被演算数は 0 であってはならない. /, % の第2被演算数が 0 の場合には結果は不定となる. 一般には「 0
除算による例外割り込み」が発生する.
22 加法演算子は,
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 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
↓&
1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0
↓|
1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 1
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
0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1
↓ <<1
0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 0
↓ >>1
0 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0
このように正の数のシフトには何の問題も生じない.
オブジェクトが signed の場合
1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0
1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0
↓ >>1 (算術シフト)
1 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1
↓ >>1 (論理シフト)
0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1
負の数のシフトで, どちらが起きるかは処理系依存
Example 6.8.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
↓ >>1 (算術シフト)
1111111111111111
0xFFFF
6.8.5.6
↓ >>1 (論理シフト)
0111111111111111
0x8FFF
格上げ
0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0
↓ >>1
0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1
0x0077
論理演算
論理演算には, 比較(関係演算子), 等値演算子, 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
数理解析・計算機数学特論
6.8.5.7
243
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
数理解析・計算機数学特論
6.9.1
247
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
long
b = -1 ;
c = 10L ;
unsigned int d = 2U ;
char
s[3] = "ab" ;
double
double
x = 1.0e-4 ;
y = 1.0e-5 ;
long double
z = 1.0L ;
printf("a = %c\n", a) ;
printf("a = %x\n", a) ;
a=a
a=61
printf("b = %d, c = %ld\n",b, c) ;
printf("b = %d, c = %+ld\n",b, c) ;
b=-1,c=10
b=-1,c=+10
printf("b = % .3d, c = % .3ld\n",b, c) ;
printf("b = %0.3d, c = %0.3ld\n",b, c) ;
b=-001,c=010
b=-001,c=010
printf("c = %3ld\n",c) ;
c=10
printf("d = %u\n", d) ;
printf("d = %0.5u\n", d) ;
d=2
d=00002
printf("d = %X\n", d) ;
printf("d = %.3x\n", d) ;
d=2
d=002
printf("d = %#.3x\n", d) ;
printf("s = %p\n", (void *)s) ;
d=0x002
s=effff9c8
printf("x = %f\n", x) ;
printf("y = %e\n", y) ;
x=0.000100
y=1.000000e-05
printf("x = %G\n", x) ;
printf("y = %g\n", y) ;
x=0.0001
y=1e-05
printf("z = %LE\n", z) ;
z=1.000000E+00
C3.tex,v 1.25 2002-03-04 14:46:52+09 naito Exp
数理解析・計算機数学特論
6.9.2
249
プログラムの演習
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>
#include <stdio.h>
int i ;
int i ;
int main(int argc, char **argv)
{
int main(int argc, char **argv)
{
for(i=0;i<10;i++) {
printf("%d\n",i) ;
for(i=0;i<10;i++)
printf("%d\n",i) ;
}
return 0 ;
return 0 ;
}
}
この例では, はじめに i = 0 が実行され, i < 10 である間は, { } の部分が実行される. 各繰り返しが終
ると, i++ が実行される. i が 9 になると, 繰り返しは実行されるが, それが終了した後, i++ が実行され,
i は 10 となる. したがって, i < 10 が 1 となり, 繰り返しの文は実行されずに, for は終る.
Example 6.10.2 この例は, 式1から式3が省かれたもので, 無限ループを実現している.
#include <stdio.h>
int main(int argc, char **argv)
{
for(;;) ;
return 0 ;
}
式2が省かれた場合は, 常にその値が non-zero と認識される.
Example 6.10.3 この例は, 1 から 10 までの和を計算している.
#include <stdio.h>
int i, j ;
int main(int argc, char **argv)
{
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>
#include <stdio.h>
int i=0;
int i=0;
int main(int argc, char **argv)
{
int main(int argc, char **argv)
{
while (i < 10) {
printf("%d\n",i++) ;
while (i < 10)
printf("%d\n",i++) ;
}
return 0 ;
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 までの整数を順に印字するものである.
28 char
c ではなく, int c としている理由は, Section 6.19 を参照.
C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp
数理解析・計算機数学特論
259
#include <stdio.h>
#include <stdio.h>
int i=0;
int i=0;
int main(int argc, char **argv)
{
int main(int argc, char **argv)
{
do {
printf("%d\n",i++) ;
do
} while (i < 10) ;
while (i < 10) ;
printf("%d\n",i++) ;
}
}
この例では, はじめに i = 0 が実行される. 繰り返しの部分は, 1回目は無条件に実行され, その後に i<10
が評価される. 各繰り返し中では, i++ が実行されているので, 繰り返し毎に i は 1 だけ増加する. i が 9
になると, 繰り返しは実行されるが, 繰り返しの文中で, i++ が実行され, 繰り返しが終ると, i は 10 とな
る. したがって, i < 10 が 1 となり, 繰り返しの文は実行されずに, do は終る.
do は while とは異なり, 少なくとも1回は繰り返しの文が実行される.
Example 6.10.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)
#include <stdio.h>
int main(int argc, char **argv)
{
{
double x ;
double x ;
for(x=0.0;x<0.5;x+=0.1)
printf(‘‘%f\n’’, x) ;
for(x=0.0;x<1.0;x+=0.1)
printf(‘‘%f\n’’, x) ;
return 0 ;
}
return 0 ;
}
この2つのプログラムは, それぞれ, 0.1 の4回, 9回の和を計算するという意図を持つ. それぞれのプログ
ラムは正しく実行できるだろうか?
Solaris 2.6 上の gcc 2.95.1 を用いて実行すると, それぞれ
0.000000
0.100000
0.000000
0.100000
0.200000
0.300000
0.200000
0.300000
0.400000
0.400000
0.500000
0.600000
0.700000
0.800000
0.900000
1.000000
という出力を得る. 5回の和は期待通りに動作しているが, 10回の和は余分な動作が含まれている.
Example 6.10.11 while 文を用いて 0.1 の和を計算する.
#include <stdio.h>
#include <stdio.h>
int main(int argc, char **argv)
int main(int argc, char **argv)
{
{
double x=0.0 ;
double x=0.0 ;
while(x != 0.5) {
x += 0.1 ;
while(x != 1.0) {
x += 0.1 ;
printf(‘‘%f\n’’, x) ;
}
printf(‘‘%f\n’’, x) ;
}
}
return 0 ;
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.000000
0.100000
0.200000
0.100000
0.200000
0.300000
0.400000
0.300000
0.400000
0.500000
0.500000
0.600000
0.700000
0.800000
0.900000
1.000000
1.100000
.
.
.
となり, 右のプログラムは終了しない.
これら2つのプログラムは, 浮動小数点数の誤差に起因し, 正しく条件判断が行われていないことが原因と
なる.
これらを正しく動作させるにはいくつかの方法がある.
Example 6.10.12 繰り返し回数を整数型変数を用いて制御する.
#include <stdio.h>
#include <stdio.h>
int main(int argc, char **argv)
{
int main(int argc, char **argv)
{
int
i
double x=0.0;
int
i=0 ;
double x=0.0 ;
for(i=0;i<10;i++) {
x += 0.1 ;
while(i != 10) {
x += 0.1 ; i += 1 ;
printf("%f\n", x) ;
}
printf("%f\n", x) ;
}
}
return 0 ;
return 0 ;
}
Example 6.10.13 浮動小数点数の誤差を見込んで制御する.
C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp
数理解析・計算機数学特論
262
#include <stdio.h>
#include <math.h>
#include <stdio.h>
#include <math.h>
#define EP 1.0E-12
#define
EP 1.0E-12
int main(int argc, char **argv)
{
int main(int argc, char **argv)
{
double x=0.0 ;
double x ;
while(fabs(x)< 1.0+EP) {
for(x=0.0;fabs(x)<= 1.0+EP;x+=0.1)
printf("%f\n", x) ;
printf("%f\n", x) ;
x+=0.1 ;
return 0 ;
}
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
数理解析・計算機数学特論
6.10.5
263
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
数理解析・計算機数学特論
6.10.8.1
265
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 ;
if (n > 0) {
if (a > b) z = a ;
}
else z = b ;
else z = b ;
}
C4.tex,v 1.10 2002-03-04 15:18:31+09 naito Exp
数理解析・計算機数学特論
6.10.8.3
267
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") ;
case 1:
break ;
printf("n は 1 だよ\n") ;
case 0:
break ;
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()
利用法は, 以下の通り.
Control + D
Example 6.10.24 標準入力から1文字を読んで, それを出力する. プログラムを終了するには, を入力する.
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
数理解析・計算機数学特論
6.11.1
関数の定義
6.11.1.1
関数の定義と例
273
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)
{
int max(int n, int m)
{
if (n < m) return m ;
return n ;
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 可変引数の関数は後ほど述べる.
32 man
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 という関数内で局所変数を定義し, 一方大域変数も利用している.
34 static
に関しては後述する. static も記憶クラス指定子の一つである.
初期値は不定である.
36 このようなことを, 変数のスコープと呼び, Section 6.12.2.3 で詳しく述べる.
35 明示的に初期化をしない限り,
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 システムによっては,
38 Section
より多くの引数(たとえば, 環境変数など)を取ることもある.
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
数理解析・計算機数学特論
6.12
285
識別子
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 宣言の趣旨とは矛盾する.)
41 ANSI 規格書によれば, ファイルスコープのオブジェクトを, 初期化子を使わず, 記憶クラス指定子なしか 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 */
/* file2.c */
int x = 1 ;
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 より正確には,
プリプロセッシングを終了した翻訳フェーズにおけるファイルが翻訳単位となる.
有効に利用できる場合がある. 変数を関数内で極めて局所化したい場合に利用することがある. これ
は, デバック (debug) を行う場合などで利用することがある. 恒久的なコードでこの手法を用いると, 思わぬ変数の隠蔽が起きる可
能性があるので, デバック時などの一時的なコードにのみ用いる方がよいだろう.
46 正確には, 上の「関数・スコープ」は「ブロック・スコープ」の一部であり, ANSI 規格に定める「関数・スコープ」とは, goto
文にあらわれるラベル名だけが適用対象であり, このラベル名は関数内のどこからでも参照可能である. なお, ラベル名の識別子は構
文の出現とともに暗黙に宣言される.
このノートでは「関数・スコープ」と「ブロック・スコープ」を便宜上区別しよう.
45 これは余り利用しないが,
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 ; /* ファイルスコープ */
識別子は(ラベル名を除いて)すべて宣言以後でないと可視でないことに注意する. したがって, Example
6.12.4 の例の識別子 l のスコープを, ファイル全体にしたい場合には, 以下の例のいずれかに書き直す必要
がある.
Example 6.12.5 Example 6.12.4 の識別子 l のスコープをファイル全体にする.
#include <stdio.h>
#include <stdio.h>
int i, l ;
int i ;
extern int l ;
int main(int argc,char **argv)
{
int main(int argc,char **argv)
{
int j ;
int j ;
j = 0 ;
{
j = 0 ;
{
int k ;
k = 0 ;
int k ;
k = 0 ;
}
return 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 初期化宣言とは,
49 C
オブジェクトの定義とともに初期化を行うこと.
の識別子の概念の中で, もっともわかりにくいのがリンケージであり, その定義はほとんどメチャクチャと思える.
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)
void foo(void)
{
{
return ;
foo() ;
}
return 0 ;
}
今度は, file1.c に foo の関数プロトライプ宣言を入れた. この場合にも, この関数プロトタイプ宣言は外
部リンケージを持ち, file2.c の関数 foo の宣言も外部リンケージを持ち, 外部リンケージとはファイルを
跨がって識別子を結び付ける操作であるので, この2つの識別子の宣言は同じ実体を表すことになる. ここ
で, file1.c の関数プロトタイプ宣言では extern がなくても良い. もちろん, 通常は関数プロトタイプ宣
言を書くのが望ましく, その場合には, 「関数プロトタイプ宣言」であることを明示する意味でも, extern
をつけた方が良い.
ところが, 次の例はどうなるだろうか?
int main(int argc, char **argv)
{
static void foo(void)
{
foo() ;
return 0 ;
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 宣言って一体何に使うの?」ってのに対する答えは何通りも考えられる. 「プログラム
を複数のプログラマで開発する際に, 自分の担当するファイル中だけで利用したい関数は内部リンケージに
しておく」というのが, 最もよくある通常の答え.
51 file1.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)
int i ;
void foo(void)
{
{
i = 0 ;
foo() ;
return 0 ;
i = 1 ;
return ;
}
}
この場合, file1.c で利用している変数 i と, file2.c で利用してる変数 i は同一の実体を持つ. なぜな
ら, file1.c, file2.c のそれぞれのオブジェクト i の宣言は外部リンケージを持ち, 外部リンケージとは
ファイルを跨がって識別子を結び付ける操作であるため, その実体は同じものとなる.
もちろん, どれか一つの宣言を除いて, 他の宣言には extern をつけるのが望ましい. すなわち,
int i ;
extern int i ;
int main(int argc, char **argv)
{
void foo(void)
{
i = 0 ;
foo() ;
return 0 ;
i = 1 ;
return ;
}
}
とするのが良い. したがって, 次のような初期化は明らかな文法エラーとなる.
int i = 0 ;
extern int i = 1 ;
int main(int argc, char **argv)
void foo(void)
{
{
i = 0 ;
i = 1 ;
foo() ;
return 0 ;
return ;
}
}
つまり, 同一の実体を表すオブジェクトを2ヶ所で初期化宣言することは出来ない.
オブジェクトの場合も, やはり static 宣言は, 他のプログラムファイルからそのオブジェクトを隠蔽す
るために用いられる. すなわち,
C6.tex,v 1.18 2001-07-19 17:11:45+09 naito Exp
数理解析・計算機数学特論
297
int i = 0 ;
static int i = 1 ;
int main(int argc, char **argv)
{
void foo(void)
{
i = 0 ;
foo() ;
i = 1 ;
return ;
return 0 ;
}
}
とすることで, 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 ;
int main(int argc,char **argv)
{
{
int i ;
int i ;
int i ;
return 0 ;
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 foo(void)
{
{
int a = 0, b = a ;
int b = a, a = 0 ;
...
...
}
}
この例は正しく動作する. すなわち, オブジェ この例は文法違反となる. すなわち, オブジェ
クト a が定義された後, b を定義し, それを a クト 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 ここで用いられている
“,” は, 「コンマ演算子」ではなく, 「識別子並び」中の「区切り子」であることに注意.
X 3010, 6.5.4 宣言子, p. 1904] の「意味規則」によると, 「各宣言子は一つの識別子を宣言する. 式の中にその宣言子と同
じ形式のオペランドが現れた場合, そのオペランドは, 宣言子指定子列が指示する有効範囲, 記憶域期間及び型を持つ関数またはオブ
ジェクトを指し示す.」とあり, 複数の識別子を一つの宣言に並べたとしても, それは複数の宣言子が与えられたと解釈すべきであり,
Cの文法規則により, 左から右に解釈される.
54 [3,
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
file1.c
file2.c
compile
compile
file1.o
file2.o
link
library
exec code
単一のプログラムファイルから実行形式を作成するための手順
% 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>
extern int k ;
int k = 0 ;
extern int i ;
extern int i ;
static int l = 0 ;
extern int add(int) ;
int add(int j)
int main(int argc,char **argv)
{
{
i += 1 ; l += 1 ; k += 1 ;
int j ;
static int l = 0 ;
return j + 1 ;
}
i = j = 0 ;
add(j) ;
static void foo(void)
{
printf("%d\n", i) ;
printf("%d\n", j) ;
k = 0 ; l = 0 ;
return ;
return 0 ;
}
}
このプログラムコードをコマンド 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
.size
l.3,#object
l.3,4
l.3:
.uaword 0
.section
".rodata"
.align 8
.LLC0:
.asciz
.section
"%d\n"
".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
st
%i0, [%fp+68]
%i1, [%fp+72]
sethi
or
%hi(i), %o1
%o1, %lo(i), %o0
st
%g0, [%fp-20]
st
ld
%g0, [%o0]
[%fp-20], %o0
call
nop
add, 0
sethi
or
%hi(i), %o0
%o0, %lo(i), %o1
sethi
or
%hi(.LLC0), %o2
%o2, %lo(.LLC0), %o0
ld
call
[%o1], %o1
printf, 0
nop
sethi
%hi(.LLC0), %o1
or
ld
%o1, %lo(.LLC0), %o0
[%fp-20], %o1
call
printf, 0
nop
mov
0, %i0
b
nop
.LL2
.LL2:
ret
restore
.LLfe1:
.size
.ident
main,.LLfe1-main
"GCC: (GNU) 2.95.1 19990816 (release)"
############## file2.c のアセンブラコード
.file
"file2.c"
gcc2_compiled.:
.section
".data"
.align 4
.type
.size
l,#object
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
sethi
%i0, [%fp+68]
%hi(i), %o1
or
sethi
%o1, %lo(i), %o0
%hi(i), %o2
or
ld
%o2, %lo(i), %o1
[%o1], %o2
add
st
%o2, 1, %o1
%o1, [%o0]
sethi
or
%hi(l), %o1
%o1, %lo(l), %o0
sethi
or
%hi(l), %o2
%o2, %lo(l), %o1
ld
add
[%o1], %o2
%o2, 1, %o1
st
%o1, [%o0]
sethi
or
%hi(k), %o1
%o1, %lo(k), %o0
sethi
or
%hi(k), %o2
%o2, %lo(k), %o1
ld
add
[%o1], %o2
%o2, 1, %o1
st
ld
%o1, [%o0]
[%fp+68], %o1
add
mov
%o1, 1, %o0
%o0, %i0
b
nop
.LL2
.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
or
%hi(k), %o1
%o1, %lo(k), %o0
st
sethi
%g0, [%o0]
%hi(l), %o1
or
%o1, %lo(l), %o0
st
b
%g0, [%o0]
.LL3
nop
.LL3:
ret
restore
.LLfe2:
.size
.ident
foo,.LLfe2-foo
"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]
[2]
|
|
0|
0|
0|SECT |LOCL |0
0|SECT |LOCL |0
|.comment
|.text
|
|
[3]
[4]
|
|
0|
0|
0|SECT |LOCL |0
0|SECT |LOCL |0
|.data
|.bss
|
|
[7]
[8]
|
|
0|
0|
0|SECT |LOCL |0
0|NOTY |LOCL |0
|.rodata
|ABS
|
|*ABS*
[13]
[1]
|
|
0|
0|
0|NOTY |GLOB |0
0|FILE |LOCL |0
|UNDEF
|ABS
|add
|file1.c
[5]
[12]
|
|
0|
0|
0|NOTY |LOCL |0
0|NOTY |GLOB |0
|.text
|UNDEF
|gcc2_compiled.
|i
[10]
[6]
|
|
0|
4|
4|OBJT |GLOB |0
4|OBJT |LOCL |0
|.data
|.data
|k
|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]
[4]
|
|
0|
0|
0|SECT |LOCL |0
0|SECT |LOCL |0
|.data
|.bss
|
|
[9]
[8]
|
|
0|
0|
0|SECT |LOCL |0
0|NOTY |LOCL |0
|.comment
|ABS
|
|*ABS*
[10]
[1]
|
|
0|
0|
120|FUNC |GLOB |0
0|FILE |LOCL |0
|.text
|ABS
|add
|file2.c
[7]
[5]
|
|
120|
0|
44|FUNC |LOCL |0
0|NOTY |LOCL |0
|.text
|.text
|foo
|gcc2_compiled.
[11]
|
0|
0|NOTY |GLOB |0
|UNDEF
|i
[12]
[6]
|
|
0|
0|
0|NOTY |GLOB |0
4|OBJT |LOCL |0
|UNDEF
|.data
|k
|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 と定義されていることに対応する.
内)で, data セクション内に定義され, 0 で初期化されていることに相当している.
59 アセンブラコード中でも, 内部自動変数と同じ扱いを受けている.
58 アセンブラコード内(file1.s
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
sin
in file
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] にも書かれている通り, これは「警告」対象とはならない. せめて警告くらいはしてくれる仕様にして欲しいのは誰でも思う
ことなのだが...
69 islower は ctype.h で宣言されている.
70 FreeBSD 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
数理解析・計算機数学特論
6.13.3
311
メモリ配置
ここでは, C で書かれたプログラムが実行される場合に, 各種のオブジェクトがどのようにメモリ上に配
置されていくかを調べてみよう. これによって, 関数呼び出しの場合の引数の評価, 実行時のエラー (Section
6.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
[61]
|
67200|
116|FUNC |GLOB |0
|.text
Name
|_start
[73]
[62]
|
|
133784|
133780|
4|OBJT |WEAK |0
4|OBJT |GLOB |0
|.bss
|.bss
|environ
|i
[63]
|
133688|
4|OBJT |GLOB |0
|.data
|j
[50]
[47]
|
|
133776|
133692|
0|OBJT |LOCL |0
4|OBJT |LOCL |0
|.bss
|.data
|k
|l
[79]
[33]
|
|
67600|
133728|
72|FUNC |GLOB |0
24|OBJT |LOCL |0
|.text
|.bss
|main
|object.11
[48]
[49]
|
|
133760|
133696|
0|OBJT |LOCL |0
4|OBJT |LOCL |0
|.bss
|.data
|s.3
|t.4
となる. 前にも述べた通り, 変数 i, j, k, l, s, t がシンボルテーブル上に表れ, 変数 n, m は内部自動変数
であるので, シンボルテーブルには表れない.
ここで, UNIX のメモリ管理のセグメント (segment) という概念が必要となる. UNIX におけるセグメ
ント72 とは, 実行形式に割り当てられた主記憶の区切りのことであり, UNIX では
71 これは,
nm -s a.out により生成した.
におけるセグメントとは異なるので注意すること. MS-DOS におけるセグメントとは, 80286 CPU のアドレス管理
方法に依存したもので, 16 ビットアドレス管理で管理可能なメモリの区切りを指す. ちなみに 80286 CPU は 20 ビットアドレス線
を持ち, 上位から 16 ビットと下位から 16 ビットのオフセットとセグメントによるメモリ管理を行っていた.
72 MS-DOS
C6-1.tex,v 1.4 2001-07-19 17:08:40+09 naito Exp
数理解析・計算機数学特論
312
• text セグメント.
実行可能形式のコマンド列を格納する部分.
• data セグメント.
実行可能形式の大域的なオブジェクトのうち, 明示的な初期化が与えられたオブジェクトが格納され
る部分.
• bss セグメント.
実行可能形式の大域的なオブジェクトのうち, 明示的な初期化が与えられていないオブジェクトが格
納される部分.
• stack セグメント.
実行中に自動変数や関数呼び出し, 動的なメモリ割り当てで利用する部分.
の4つに分けられる. これらのセグメントは, 次の図のように割り当てられるのが普通である.
スタック
n (main 呼出し時に)
m (main 呼出し時に)
text セグメント, data セ
?
グメントは, 主記憶への配
置時に, 実行ファイル中に
stack セグメント
ある値で埋め尽くされる.
bss セグメントは実行開始
時に 0 でクリアされる.
stack セグメントは, 関
6
数呼び出しに伴い, 上位メ
モリから順に利用される.
(スタックの利用)また, 動
的メモリ割り当てのうち
ヒープ
alloca 関数では, スタック
の利用が可能である.
bss セグメント
s
k
i
実行中の動的メモリ割り当
て (malloc 関数の呼出し
など) では, stack セグメ
ントが下位メモリから順に
data セグメント
t
l
j
text セグメント
利用される. (ヒープ領域
の利用)
命令の列
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
filesize
unlimited
unlimited
datasize
stacksize
2097148 kbytes
8192 kbytes
coredumpsize
vmemoryuse
0 kbytes
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, 値 1
main の m, 値 2
add の戻り値を格納する部分
add の a, 値 1
add の a, 値 2
main に戻ってきたとき
main の n, 値 1
main の m, 値 2
戻り値 3
値のコピー
呼出し前
main の n, 値 1
main の m, 値 2
関数終了直前
main の n, 値 1
main の m, 値 2
値3
add の a, 値 1
add の a, 値 2
関数呼出し終了
main の n, 値 3
main の 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 に戻ってきたとき
main の n, 値 1
戻り値 2
関数呼出し終了
main の n, 値 1
関数終了直前
main の n, 値 1
値2
add の a, 値 2
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 ;
}
というプログラムの場合には,
呼出し前
main の n, 値 1
呼び出した後
main の n, 値 1
add の戻り値を格納する部分
add の a, 値 1
add の b, 値 1
return 文実行直前
main の n, 値 1
add の戻り値を格納する部分
add の a, 値 1
add の b, 値 2
k, 値 0
k, 値 0
k, 値 1
関数終了直前
main の n, 値 1
戻り値 2
add の a, 値 2
add の b, 値 2
main に戻ってきたとき
main の n, 値 1
戻り値 2
関数呼出し終了
main の n, 値 2
k, 値 1
k, 値 1
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(2) の戻り値)
(func(2) の実引数) 2
func(1) 呼び出し後
(func(2) の戻り値)
(func(2) の実引数) 2
(func(1) の戻り値)
(func(1) の実引数) 1
func(0) 呼出し後
(func(2) の戻り値)
(func(2) の実引数) 2
(func(1) の戻り値)
(func(1) の実引数) 1
(func(0) の戻り値)
(func(0) の実引数) 0
func(0) 終了
(func(2) の戻り値)
(func(2) の実引数) 2
(func(1) の戻り値)
(func(1) の実引数) 1
(func(0) の戻り値) 0
func(1) 終了
(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 =
n =
10, c = effff9a7
9, c = effff92f
n =
n =
8, c = effff8b7
7, c = effff83f
n =
n =
6, c = effff7c7
5, c = effff74f
n =
n =
4, c = effff6d7
3, c = effff65f
n =
n =
2, c = effff5e7
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 i ;
extern int sub_function(void) ;
int i=0 ;
static int j ;
int sub_function()
int j=0 ;
int main()
{
i += 1 ; j += 1 ;
{
return 0 ;
sub_function() ;
}
printf("i=%d, j=%d\n",i,j) ;
return 0 ;
}
Exercise 6.13.2 次の2つのファイルからなるプログラムをコンパイル・リンクし, その出力結果がなぜ
そのようになるかを考えよ.
#include <stdio.h>
extern int sub_function(void) ;
extern int i ;
extern int j ;
int i=0 ;
int j=0 ;
int sub_function()
{
int main()
{
i += 1 ; j += 1 ;
return 0 ;
sub_function() ;
printf("i=%d, j=%d\n",i,j) ;
}
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 ;
int *pa ;
pa = &a[0] ;
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[1]
pa=a
a[2]
a[3]
a[4]
a[5]
pa+1
pa
もっと極端なことを書けば,
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 ;
と定義されていると仮定する.
方法1
80 Solaris
81 Solaris
2.6 では4バイト(32ビット)である.
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
数理解析・計算機数学特論
6.14.2.3
325
汎用データ・ポインタ
ここまでは, 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) ;
for(p=a;p<a+MAX;p++)
printf("*p=%d\t",*p) ;
printf("\n") ;
printf("\n") ;
for(i=0;i<MAX;i++)
printf("\n") ;
printf("*(a+i)=%d\t",*(a+i)) ;
}
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
数理解析・計算機数学特論
呼出し前
a
b
6.15.2
331
-
呼び出した後
a
b
sum の *a
sum の b
*a += b の実行
- a, ここが書き換えられる
b
sum の *a
sum の b
配列を引数とする関数
配列の識別子はそれ自身配列の先頭のアドレスを持つため, 配列を引数とする関数は, ポインタを引数と
していると考えて良い.
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
数理解析・計算機数学特論
6.15.3
333
文字列操作関数
また, ポインタを利用することにより, 文字列を関数の引数として渡すことができ, その戻り値にポイン
タを利用することもできる.
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);
83 NULL
とは, 何も指し示さないという特別なポインタである. NULL と 0 を示すポインタとは異なることに注意せよ.
man -s 3c strcpy を見よ.
84 その他の文字列操作関数については,
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
数理解析・計算機数学特論
6.16.1
343
多次元配列の定義
はじめに多次元配列の定義を行う.
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[0][0]
str[1]
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[0][0]
a[1]
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
ja1
二重ポインタ
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] のアドレス, 例えば 0xeeff
a[1] のアドレス, 例えば 0xef20
0xeeff 番地
*
0xef20 番地
j
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
int
print_matrix_2(int *[],
print_matrix_3(int **,
unsigned int, unsigned 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
数理解析・計算機数学特論
/* printf("
printf("
351
%d",*(a[i]+j)) ; */
%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("
/* printf("
printf("
%d",a[i][j]) ;
*/
%d",*(a[i]+j)) ; */
%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
echo\0
hello\0
world.\0
0
echo\0
hello\0
world.\0
6.16.6
echo\0
hello\0
0
10
world\0
15
演習問題
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 ;
87 malloc に類似の関数に関しては, 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
数理解析・計算機数学特論
6.17.2.2
359
二重ポインタの指し示す先を確保する
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 という型を持ったオブジェクトとして定義さ
れる.
real
imaginary
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 ;
}
real
imaginary
radius
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 当たり前だが,
上の初期化の方が何をしているかは分かりやすい.
の初版(いわゆる traditional なC)では, 構造体への一括代入, 構造体を引数とする関数, 構造体を戻り値
とする関数などは許されてはいなかった. したがって, traditional C でこのようなことを行うためには, すべて, 構造体のポインタ
を受け渡しする必要があった. また, traditional C では, 自動構造体(関数内部での自動変数となる構造体)の初期化も許されてい
なかった. ANSI 規格(Kernighan-Ritchie 第2版)では, これら構造体の扱いが簡単になったことが大きな改訂部分である.
93 Kernighan-Ritchie
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) ;
}
√
−1y のノルムを x2 + y 2 と計算してしまうと, x2 + y 2 を計算する時点でオーバ
フローが発生するかもしれない. そのため, わざわざ x2 + y 2 = |x| 1 + (y/x)2 と計算していることに
注意.
ここで, 複素数 x +
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
int
n ;
d ;
/* 分子 */
/* 分母 */
} ;
に対して,
struct fractional
struct fractional
frac_add(struct fractional a,
struct fractional b)
*frac_add(struct fractional a,
struct fractional b)
{
{
struct fractional
c ;
c.n = a.d*b.n + b.d*a.n ;
static struct fractional
c.n = a.d*b.n + b.d*a.n ;
c.d = a.d*b.d ;
return c ;
c.d = a.d*b.d ;
return &c ;
}
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
struct complex
complex_add(struct complex a,
struct complex b)
*complex_add(struct complex a,
struct complex b)
{
{
struct complex
c ;
static struct complex c ;
c.re = a.re + b.re ;
c.im = a.im + b.im ;
c.re = a.re + b.re ;
c.im = a.im + b.im ;
return c ;
return &c ;
}
}
を考えよう.
3. さらに, もっと極端な例として, 構造体
struct test_str {
char a ;
} ;
に対して,
struct test_str
test_func(struct test_str a)
struct test_str
*test_func(struct test_str a)
{
{
struct test_str b ;
static struct test_str b ;
b.a = 2*a.a ;
return 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
s1.c[0], *s1.s) ;
= %c\n",
printf("s1.c
s1.c, s1.s) ;
= %s\n",
= %s\ts1.s
printf("s2.cp
= %s\ts2.ss1.s
s2.cp, s2.ss1.s) ;
= %s\n",
printf("++s2.cp
= %s\t++s2.ss1.s
++s2.cp, ++s2.ss1.s) ;
= %s\n",
printf("s4.sp1[0]->c
= %s\n",
= %s\ts4.sp1[0]->s
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->next
p
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 ここでは, 以下の図のような二分木を構成する.
root
a
,e
, e
h
C
C
b
C
C
c d i
CC
e f
g
j
JJ
k n
C
C
lm
はじめに二分木を入力するための関数 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)
preorder(root)
; printf("\n") ;
; printf("\n") ;
postorder(root)
inorder(root)
; printf("\n") ;
; 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
extern void
error_jmp(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
double x または int n
int n
union int_double {
double x ;
int
n ;
}
struct int_double {
double x ;
int
n ;
}
と定義すると, 2つのメンバーの領域が重なっ
てしまう.
と定義すると, 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
6.18.4
sfzf
cf
7
0
名前空間
識別子は次の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 b ;
/* 構造体タグ名 foo を利用する */
/* 構造体の型名 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
数理解析・計算機数学特論
6.19
383
ファイルへの入出力
ファイルへの入出力とは, プログラム中でファイルからデータを読み取ったり, データをファイルに書き
出したりすることをいう. 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 指定したファイルが存在しない, アクセス権がないなどの場合には, ファイルのオープンに失敗する. この場合, ファイル記述子
に対応する値として, 負の値が戻ってくる仕様になっているものが多い.
99 Cには「低レベル入出力」と呼ばれる関数群(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
102 UNIX のいくつかのアプリケーションに見られる, “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
数理解析・計算機数学特論
6.20
389
プリプロセッサ命令
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
数理解析・計算機数学特論
6.21
391
デバッグの手法
ここでは, 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) ;
test.c: In function ‘main’:
test.c:9: parse error at end of input
return 0 ;
}
エラーメッセージでは「9行目」でエラーが発生していると言っているが, for 文の括弧がとじられてい
ないため, 9行目の「閉じ括弧」で for 文が終了したと認識し, それ以後に入力がないために “parse error
at 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++) {
test.c: In function ‘main’:
test.c:7: parse error before ’}’
printf("i = %d\n", i)
}
return 0 ;
}
エラーメッセージでは「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) ;
test.c: In function ‘main’:
test.c:6: ‘j’ undeclared
(first use in this function)
}
return 0 ;
}
言うまでもなく, 「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)
{
test.c: In function ‘main’:
test.c:4: array size missing in ‘a’
int i, a[] ;
for(i=0;i<10;i++)
a[i] = i ;
return 0 ;
}
言うまでもなく, 「4行目」の配列 a の定義が間違っている.
Example 6.21.5 “imcompatible type in assignment” の例
#include <stdio.h>
int main(int argc, char **argv)
{
test.c: In function ‘main’:
test.c:5: incompatible types in assignment
int i, a[10] ;
a = i ;
return 0 ;
}
配列として宣言されたオブジェクトは, 静的ポインタである.
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)
test.c: In function ‘main’:
{
printf("\n") ;
return 0 ;
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)
{
test.c: In function ‘main’:
test.c:5: warning: implicit declaration
of function ‘a’
int i ;
test.c:4: warning:
unused variable ‘i’
a() ;
return 0 ;
test.c: At top level:
test.c:10: warning: type mismatch with
}
void a()
previous implicit declaration
test.c:5: warning: previous implicit
{
declaration of ‘a’
test.c:10: warning: ‘a’ was previously
return ;
}
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)
test.c: In function ‘main’:
{
double x,y;
x = atoi(y) ;
return 0 ;
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)
test.c: In function ‘main’:
{
test.c:5: warning: double format,
different type arg (arg 3)
int i,j ;
printf("%d, %f\n", i,j) ;
return 0 ;
}
printf 関数の第3引数と, 第2引数の % 表示が対応していない.
Example 6.21.10 ポインタの型の不一致の例.
#include <stdio.h>
int main(int argc, char **argv)
test.c: In function ‘main’:
{
test.c:5: warning: assignment makes
pointer from integer without a cast
int i, *p ;
p = i ;
return 0 ;
}
「5行目」のポインタ代入は明らかな間違いである. もし, int *p のかわりに int p[10] とするとエラー
となるが, ポインタ形式の場合にはエラーではなく, 警告となることに注意.
ポインタ代入に関しては, 型が異ると常にこの警告が出てくる.
Example 6.21.11 -ansi -pedantic オプションをつけずにコンパイルしたときには問題がないが, -ansi
-pedantic オプションをつけるとコンパイルできない例.
#include <stdio.h>
int main(int argc, char **argv)
test.c: In function ‘main’:
{
test.c:5: warning: ANSI C forbids variable-size array ‘a’
int n, a[n] ;
return 0 ;
}
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
symbol
first referenced
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 ;
/* test1.c */
#include <stdio.h>
int a=1 ;
int main(int argc, char **argv)
{
a = 2 ;
return 0 ;
}
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
st
%o0, [%fp-20]
%g0, [%fp-20]
ld
ld
[%fp-24], %o0
[%fp-20], %o1
add
st
%o0, %o1, %o0
%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 これらのシンボルは
106 MS-DOS
signal.h 内で定義される整数定数である.
では, これに相当するのは 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.programm
1982.
Fly UP