Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion...

51
314 数理解析・計算機数学特論 #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.10 関数とは , ある一 をさせるため プログラム ある. これま してきた, printf, getchar, random , てあらかじめ まれた . よう ように,C 多く 意されている. , 々が るプログラ ムにおいて , するこ によって, プログラム をわかり すく きる がある.C よう があるか , [2, B] かれている. 6.10.1 関数の定義 6.10.1.1 関数の定義と例 C しく 引数(ひきすう) (parameter) , しく 戻り値りち) (return value) . , , 学における , する えられるが,C , した に変 するこ 多い. 58 , をしている. < >< >(< >) 58 よう 副作用 . C5.tex,v 1.24 2005-06-21 19:21:11+09 naito Exp

Transcript of Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion...

Page 1: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

#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.10 関数とは

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

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

6.10.1 関数の定義

6.10.1.1 関数の定義と例

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

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

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

関数の本体となる複文

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

C5.tex,v 1.24 2005-06-21 19:21:11+09 naito Exp

Page 2: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

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

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

に書かれる.

int sum(int n, int m)

{

return n + m ;

}

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

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

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

int max(int n, int m)

{

if (n < m) return m ;

return n ;

}

int max(int n, int m)

{

return (n < m) ? m : n ;

}

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

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

void only_print(void)

{

printf("Hello\n") ;

return ;

}

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

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

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

59可変引数の関数は後ほど述べる.

C5.tex,v 1.24 2005-06-21 19:21:11+09 naito Exp

Page 3: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

#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.12 で述べるように, C では一つのプログラムを複数のファイルに分割することができ, 関数全体が一つのファイル内にあれば, 呼び出される関数が他のファイルにあっても良い.

6.10.1.2 関数の引数について

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

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

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 に変換される. (もちろん, 整数から浮動小数点数への変換の規則が用いられる.)

60man 3 pow 参照.

C5.tex,v 1.24 2005-06-21 19:21:11+09 naito Exp

Page 4: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

Example 6.10.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)

{

double z ;

z = max(x,y) ;

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

return 0 ;

}

これでは全く正しい結果が得られない. 関数 max の2つの引数は int 型であるにも関わらず, 呼び出し側の引数は double 型であるため, 実際の呼び出し時には double 型の値 2.5 と 0.3 がそれぞれ int 型

への(暗黙の)型変換を受けてから評価される.61 したがって, 関数 max には 2, 0 という値が渡され, その結果の値は int 型の 2 となる. 従って, z には 2.0 が代入される. さらに, printf 関数では, その値を浮動小数点数として表示すると指示されているため, 第1の値は 2.00000が表示されるが, 第2の値は, 多くの場合 0.00000 と表示される. (結果は処理系に依存する.)

Remark 6.10.5 実際には, 2つの数の大きさを比較して小さくない方の値を表示させるには,

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

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

とした方がよい. この例のようにプリプロセッサマクロを利用することにより, 演算子(この例では3項演算子)の多重定義を利用して, 型に依存しない手続きを記述することが可能である.

Remark 6.10.6 上の例のプリプロセッサマクロを

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

と書いてはいけない. 強引な例であるが, このように書いたプリプロセッサマクロに対して,

max(5|3, 2&1)

としてみよう. これは, 2&1, 5|3 の小さくない方, すなわち, 0, 7 の小さくない方であるので, 7 が答えとなるはずである. しかし, 実際に実行してみると, 0 という答えを得る.これは, マクロ展開の時点で

(5|3 < 2&1) ? 2&1 : 5|3

61Cでは, 浮動小数点型の値が整数型に変換される場合には, 小数点以下は無視される. この時, 値がその整数型で表現不可能な場合には, 振る舞いは不定となる.

C5.tex,v 1.24 2005-06-21 19:21:11+09 naito Exp

Page 5: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

と展開され, 5|3 < 2&1 は 5|(3<2)&1 と評価されるので, 5|0&1 = 5|0 = 0 という結果となる.したがって,「プリプロセッサマクロ」の引数は, その定義内では括弧をつけることが必要不可欠である.

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

int v ;

func(v++,v++) ;

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

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

f() + g() * h()

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

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

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

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

Section 6.10.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.12 参照)で行うので, この問題は本質的な問題にはならない.

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

C5.tex,v 1.24 2005-06-21 19:21:11+09 naito Exp

Page 6: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

もう一つの問題は, 次のようなところにある. ANSI 規格 [3, X 3010 6.7.1, p. 1922] および K&R [2,A10] によると,「関数定義の宣言子において省略されている型は int とみなす」とされている. すなわち,戻り値の型や仮引数の型が省略されると, コンパイラはそれを int とみなして処理を進める. したがって,コンパイラは以下のような処理を行う.

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

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

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

extern double test_fn(double) ;

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

extern double test_fn(double, double) ;

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

extern double test_fn(double) ;

int main(int argc, char **argv)

{

int n ;

n = test_fn(n) ;

}

double test_fn(double a)

{

.

.

.

}

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

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

int main(int argc, char **argv)

{

extern double test_fn(double) ;

int n ;

63この extern 指定子はなくてもよい.

C5.tex,v 1.24 2005-06-21 19:21:11+09 naito Exp

Page 7: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

n = test_fn(n) ;

}

double test_fn(double a)

{

.

.

.

}

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

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

Remark 6.10.10 「数学関数」の多くは, 実引数として double 型の値を取る. 数学関数のプロトタイプ宣言は “math.h”で定義されているが, “math.h”をインクルードせずに数学関数を利用するとどのようなことが起るかを考えてみよう.

#include <stdio.h>

int main(int argc, char **argv)

{

printf("%f\n", pow(2.0,1.5)) ;

return 0 ;

}

#include <stdio.h>

#include <math.h>

int main(int argc, char **argv)

{

printf("%f\n", pow(2.0,1.5)) ;

return 0 ;

}

右の例は “math.h”を正しくインクルードしている. すなわち, pow 関数のプロトタイプ宣言が正しく行われている. この場合には, 2.828427” という「正しい」値が出力される. 一方, 左の例は “math.h” をインクルードしていない. すなわち, pow 関数のプロトタイプ宣言が行われていない. この場合には, 多くの処理系において 0.000000” などと出力され, 正しい計算が行われていないことがわかる.これは, pow 関数のプロトタイプ宣言が無いために, pow の戻り値が int と解釈され, 実際の実行時にも,

呼び出し側からは int 型の戻り値として値を解釈した結果である.

6.10.1.4 関数内の局所変数

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

化を受ける65.

64static に関しては後述する. static も記憶クラス指定子の一つである.65明示的に初期化をしない限り, 初期値は不定である.

C5.tex,v 1.24 2005-06-21 19:21:11+09 naito Exp

Page 8: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

また, 局所変数の識別子は, その関数内でのみ有効である. 局所変数の識別子と, 関数引数の識別子が重なった場合には, 引数側にアクセスができなくなる. 即ち, 関数内で局所的に有効な変数は, 関数引数と局所変数である.

Example 6.10.11 この例では, 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 は異なるものであることに注意.

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

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

66このようなことを, 変数のスコープと呼び, Section 6.11.2.3 で詳しく述べる.

C5.tex,v 1.24 2005-06-21 19:21:11+09 naito Exp

Page 9: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

#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.11 で詳しく議論する.

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

#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.24 2005-06-21 19:21:11+09 naito Exp

Page 10: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

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.10.1.5 main 関数の引数

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

int main(int argc, char **argv)

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

6.10.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.12 で詳しく議論する.

6.10.1.7 関数呼び出しの実際

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

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

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

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

C5.tex,v 1.24 2005-06-21 19:21:11+09 naito Exp

Page 11: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

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

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

6.10.2 再帰的関数呼びだし

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

Example 6.10.14 次は, 帰納的に定義された数列 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])再帰的関数呼び出しの詳細については Section 6.12.4.2で詳細に調べる.

6.10.3 可変引数を持つ関数

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

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

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

69非常に複雑な再帰定義のものでは, 再帰を使わなくては計算できないものがある.70これらのマクロに関しては, [2, B7] 参照.

C5.tex,v 1.24 2005-06-21 19:21:11+09 naito Exp

Page 12: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

#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) することが必要になるかもしれない.

Remark 6.10.16 K&RによるCの定義 [1] では, char 型を引数としてもつ関数の実引数は int 型へ型

変換されてから関数へ渡されると定義されていた. (cf. [1, 7.1].) しかし, ANSI のCの定義 [2] では, 関数宣言のパラメータリストで型を含んで宣言すれば, 必ずその型の値として渡されると定義されている. (cf.[1, A7.3.2, A10.1].) 関数宣言のパラメータリストで型を含まない宣言が行われている場合には, 整数型に関しては, 整数への格上げを行い, float 型に関しては double への変換を行う. これは,

int foo(char a)

と宣言されている関数の実引数は char 型の値として渡されるが,

int foo(a)

char a ;

と定義した場合には char 型ではなく int 型の値が渡されるという意味である. すなわち, ここであらわれた可変引数関数の引数のうちの char 型や short 型の実引数は, 実際に関数に渡される時点では整数への格上げを受け, int 型の値として評価されることを意味している.

C5.tex,v 1.24 2005-06-21 19:21:11+09 naito Exp

Page 13: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

6.10.4 関数のエラー処理

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

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

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

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

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

Example 6.10.17 実引数に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.10.17 のコードの実行結果として, Solaris 2.6 の場合には, errno として, EINVAL (値は 22) を返し, 結果として Invalied argimentという出力をするのが良い.

6.10.5 演習問題

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

int mul(int n, int m)

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

C5.tex,v 1.24 2005-06-21 19:21:11+09 naito Exp

Page 14: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

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

int div(int n, int m)

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

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

int mod(int n, int m)

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

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

int pow_int(int n, int m)

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

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

double pow_d(double n, int m)

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

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

int is_upper(int c)

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

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

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

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

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

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

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

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

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

C5.tex,v 1.24 2005-06-21 19:21:11+09 naito Exp

Page 15: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

int fib(unsigned int n)

{

if (n == 0) return 0 ;

if (n == 1) return 1 ;

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

}

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

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

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

6.11 識別子

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

6.11.1 識別子とは

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

6.11.1.1 識別子の分類

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

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

• 型定義名,

• ラベル名,

• マクロ名,

• マクロ仮引数.

6.11.1.2 名前空間

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

• ラベル名.

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

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

Page 16: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

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

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

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

6.11.2 基本概念

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

6.11.2.1 定義と宣言

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

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

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

Example 6.11.1

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

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

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

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

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

Page 17: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

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

extern int x = 1 ;

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

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

/* file1.c */

int x = 1 ;

/* file2.c */

extern int x = 1 ;

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

6.11.2.2 翻訳単位

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

6.11.2.3 スコープ

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

1. ファイル・スコープ

2. 関数・スコープ

75より正確には, プリプロセッシングを終了した翻訳フェーズにおけるファイルが翻訳単位となる.

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

Page 18: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

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

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

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

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

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

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

• 関数定義の仮引数.

• 関数の先頭部分.

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

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

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

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

#include <stdio.h>

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

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

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

{

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

{

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

k = 0 ;

}

return 0 ;

}

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

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

76これは余り利用しないが, 有効に利用できる場合がある. 変数を関数内で極めて局所化したい場合に利用することがある. これは, デバック (debug) を行う場合などで利用することがある. 恒久的なコードでこの手法を用いると, 思わぬ変数の隠蔽が起きる可能性があるので, デバック時などの一時的なコードにのみ用いる方がよいだろう.

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

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

Page 19: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

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

#include <stdio.h>

int i, l ;

int main(int argc,char **argv)

{

int j ;

j = 0 ;

{

int k ;

k = 0 ;

}

return 0 ;

}

#include <stdio.h>

int i ;

extern int l ;

int main(int argc,char **argv)

{

int j ;

j = 0 ;

{

int k ;

k = 0 ;

}

return 0 ;

}

int l ;

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

Example 6.11.6 文法上許されない変数宣言の例.

int main(int argc,char **argv)

{

int j ;

j = 0 ;

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

k = 0 ;

return 0 ;

}

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

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

int i ;

int main(int argc, char **argv)

{

int i ;

{

78要するに, Example 6.11.4 の int l の宣言は, 文法上可能というだけのことである.

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

Page 20: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

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.11.2.3.2 関数名のスコープ 関数名は通常はファイル・スコープを持つが, 次のような例では関数宣言がブロックスコープとなる.

int main(int argc,char **argv)

{

int a ;

extern int foo(int) ;

foo(a) ;

return 0 ;

}

double foo(int a)

{

...

}

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

6.11.2.4 寿命

オブジェクトの寿命とは, 正しくは記憶域期間 (storage duration) とは, プログラム実行状態において,そのオブジェクトの記憶域が存在する期間を指す. C における記憶域期間は静的 (static) と自動 (auto)

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

Page 21: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

の2種類がある. 寿命という概念は記憶域と関わる概念であるので, 識別子に対する概念ではなく, オブジェクトに対する概念である.オブジェクトが静的であるとは, プログラム実行の開始から終了までの期間, そのオブジェクトの記憶域

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

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

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

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

79初期化宣言とは, オブジェクトの定義とともに初期化を行うこと.

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

Page 22: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

static 宣言されて, 明示的に初期化宣言がされているオブジェクトは, プログラム実行開始時にただ1度だけ, その値により初期化が行われる. (cf. Example 6.11.11)

6.11.2.5 リンケージ

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

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

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

• 無結合 (no linkage)

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

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

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

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

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

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

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

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

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

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

80C の識別子の概念の中で, もっともわかりにくいのがリンケージであり, その定義はほとんどメチャクチャと思える.

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

Page 23: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

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

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

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

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

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

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

#include <stdio.h>

int i ;

int l = 2 ;

static int k ;

int k ;

extern void foo(void) ;

int main(int argc,char **argv)

{

int i ;

extern int l ;

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

foo() ;

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

return 0 ;

}

void foo(void)

{

l = 0 ;

return ;

}

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

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

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

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

Page 24: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

l = 2

l = 0

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

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

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

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

#include <stdio.h>

extern void bar(void) ;

int main(int argc,char **argv)

{

extern void foo(void) ;

foo() ;

return 0 ;

}

int bar(void)

{

foo() ;

}

void foo(void)

{

return ;

}

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

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

ない.

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

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

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

Page 25: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

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

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

int main(int argc, char **argv)

{

foo() ;

return 0 ;

}

void foo(void)

{

return ;

}

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

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

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

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

extern void foo(void) ;

int main(int argc, char **argv)

{

foo() ;

return 0 ;

}

void foo(void)

{

return ;

}

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

int main(int argc, char **argv)

{

foo() ;

return 0 ;

}

static void foo(void)

{

return ;

}

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

Page 26: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

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

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

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

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

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

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

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

int i ;

int main(int argc, char **argv)

{

i = 0 ;

foo() ;

return 0 ;

}

int i ;

void foo(void)

{

i = 1 ;

return ;

}

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

int i ;

int main(int argc, char **argv)

{

i = 0 ;

foo() ;

return 0 ;

}

extern int i ;

void foo(void)

{

i = 1 ;

return ;

}

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

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

Page 27: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

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

int i = 0 ;

int main(int argc, char **argv)

{

i = 0 ;

foo() ;

return 0 ;

}

extern int i = 1 ;

void foo(void)

{

i = 1 ;

return ;

}

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

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

int i = 0 ;

int main(int argc, char **argv)

{

i = 0 ;

foo() ;

return 0 ;

}

static int i = 1 ;

void foo(void)

{

i = 1 ;

return ;

}

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

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

int i ;

int i ;

int main(int argc,char **argv)

{

int i ;

return 0 ;

}

int i ;

int main(int argc,char **argv)

{

int i ;

int i ;

return 0 ;

}

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

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

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

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

Page 28: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

6.11.2.6 内部静的変数の利用法

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

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

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

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

int function()

{

static int i = 0 ;

i += 1 ;

.....

}

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

6.11.3 初期化

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

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

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

int a = 1, b = a ;

int main(int argc, char **argv)

{

....

}

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

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

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

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

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

Page 29: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

int foo(int a)

{

int b = a ;

...

}

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

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

int foo(void)

{

int a = 0, b = a ;

...

}

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

の値で初期化している.

int foo(void)

{

int b = a, a = 0 ;

...

}

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

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

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

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

int a ;

foo(void)

{

int a = a ;

...

}

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

int a = 1 ;

int foo(void)

{

int b = a, a = 0 ;

...

}

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

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

Page 30: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

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

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

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

int foo(int n)

{

static int a = n ;

...

}

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

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

6.11.4 演習問題

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

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

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

Page 31: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

#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.12 コンパイルとリンク

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

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

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

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

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

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

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 32: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

link

file2.ofile1.o

compilecompile

exec code

library

file1.c file2.c

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

% gcc file.c -o target

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

6.12.1 オブジェクトコード

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

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

% gcc -c file1.c

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

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

% gcc -S file1.c

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

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

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

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 33: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

#include <stdio.h>

int k = 0 ;

extern int i ;

extern int add(int) ;

int main(int argc,char **argv)

{

int j ;

static int l = 0 ;

i = j = 0 ;

add(j) ;

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

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

return 0 ;

}

extern int k ;

extern int i ;

static int l = 0 ;

int add(int j)

{

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

return j + 1 ;

}

static void foo(void)

{

k = 0 ; l = 0 ;

return ;

}

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

以下のようになる87.

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

.file "file1.c"

gcc2_compiled.:

.global k

.section ".data"

.align 4

.type k,#object

.size k,4

k:

.uaword 0

.align 4

.type l.3,#object

.size l.3,4

l.3:

.uaword 0

.section ".rodata"

.align 8

.LLC0:

.asciz "%d\n"

.section ".text"

.align 4

.global main

.type main,#function

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

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 34: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

.proc 04

main:

!#PROLOGUE# 0

save %sp, -120, %sp

!#PROLOGUE# 1

st %i0, [%fp+68]

st %i1, [%fp+72]

sethi %hi(i), %o1

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

st %g0, [%fp-20]

st %g0, [%o0]

ld [%fp-20], %o0

call add, 0

nop

sethi %hi(i), %o0

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

sethi %hi(.LLC0), %o2

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

ld [%o1], %o1

call printf, 0

nop

sethi %hi(.LLC0), %o1

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

ld [%fp-20], %o1

call printf, 0

nop

mov 0, %i0

b .LL2

nop

.LL2:

ret

restore

.LLfe1:

.size main,.LLfe1-main

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

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

.file "file2.c"

gcc2_compiled.:

.section ".data"

.align 4

.type l,#object

.size l,4

l:

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 35: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

.uaword 0

.section ".text"

.align 4

.global add

.type add,#function

.proc 04

add:

!#PROLOGUE# 0

save %sp, -112, %sp

!#PROLOGUE# 1

st %i0, [%fp+68]

sethi %hi(i), %o1

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

sethi %hi(i), %o2

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

ld [%o1], %o2

add %o2, 1, %o1

st %o1, [%o0]

sethi %hi(l), %o1

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

sethi %hi(l), %o2

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

ld [%o1], %o2

add %o2, 1, %o1

st %o1, [%o0]

sethi %hi(k), %o1

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

sethi %hi(k), %o2

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

ld [%o1], %o2

add %o2, 1, %o1

st %o1, [%o0]

ld [%fp+68], %o1

add %o1, 1, %o0

mov %o0, %i0

b .LL2

nop

.LL2:

ret

restore

.LLfe1:

.size add,.LLfe1-add

.align 4

.type foo,#function

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 36: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

.proc 020

foo:

!#PROLOGUE# 0

save %sp, -112, %sp

!#PROLOGUE# 1

sethi %hi(k), %o1

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

st %g0, [%o0]

sethi %hi(l), %o1

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

st %g0, [%o0]

b .LL3

nop

.LL3:

ret

restore

.LLfe2:

.size foo,.LLfe2-foo

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

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

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

file1.o:

[Index] Value Size Type Bind Other Shname Name

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

[2] | 0| 0|SECT |LOCL |0 |.text |

[3] | 0| 0|SECT |LOCL |0 |.data |

[4] | 0| 0|SECT |LOCL |0 |.bss |

[7] | 0| 0|SECT |LOCL |0 |.rodata |

[8] | 0| 0|NOTY |LOCL |0 |ABS |*ABS*

[13] | 0| 0|NOTY |GLOB |0 |UNDEF |add

[1] | 0| 0|FILE |LOCL |0 |ABS |file1.c

[5] | 0| 0|NOTY |LOCL |0 |.text |gcc2_compiled.

[12] | 0| 0|NOTY |GLOB |0 |UNDEF |i

[10] | 0| 4|OBJT |GLOB |0 |.data |k

[6] | 4| 4|OBJT |LOCL |0 |.data |l.3

[11] | 0| 108|FUNC |GLOB |0 |.text |main

[14] | 0| 0|NOTY |GLOB |0 |UNDEF |printf

file2.o:

[Index] Value Size Type Bind Other Shname Name

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 37: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

[2] | 0| 0|SECT |LOCL |0 |.text |

[3] | 0| 0|SECT |LOCL |0 |.data |

[4] | 0| 0|SECT |LOCL |0 |.bss |

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

[8] | 0| 0|NOTY |LOCL |0 |ABS |*ABS*

[10] | 0| 120|FUNC |GLOB |0 |.text |add

[1] | 0| 0|FILE |LOCL |0 |ABS |file2.c

[7] | 120| 44|FUNC |LOCL |0 |.text |foo

[5] | 0| 0|NOTY |LOCL |0 |.text |gcc2_compiled.

[11] | 0| 0|NOTY |GLOB |0 |UNDEF |i

[12] | 0| 0|NOTY |GLOB |0 |UNDEF |k

[6] | 0| 4|OBJT |LOCL |0 |.data |l

コマンド nm の出力の最右行にある “Name” は, シンボル名と呼ばれ, “Bind” で示されるスコープを持つ.また, “Shndx” で UNDEF とされたシンボルは, リンク時にその配置が決定されることを示す.

関数 file1.c の中では, 関数 printf, add が利用されているが, これらの関数は, アセンブラコード中で,call printf, call add という形で, サブルーチン呼び出しとして書かれていることに注意しよう.

また, file1.c で定義されている関数 main と, file2.c で定義されている関数 add は, 外部リンケージを持つので, それぞれのオブジェクトコード中で, Type が FUNC, Bind が GLOB と定義されて

いる88. しかし, 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バイトであると明示されている89.

外部リンケージを持ち初期化宣言されている変数 file1.c の k が該当する.

シンボルテーブルでは, GLOB な OBJT とされ, 4バイトであることがわかる.

外部リンケージを持ち初期化宣言されていない変数 file1.c と file2.c の i が該当する. これら2つの識別子は外部リンケージで同じオブジェクトを指していることに注意.

シンボルテーブルでは, GLOB な NOTY (No Type) とされ, その配置は UNDEF となっている90.

内部リンケージを持つ大域変数 file2.c の l が該当する.

88アセンブラコード中で, text セクション内で global と定義されていることに対応する.89アセンブラコード内(file1.s 内)で, data セクション内に定義され, 0 で初期化されていることに相当している.90アセンブラコード中でも, 内部自動変数と同じ扱いを受けている.

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 38: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

シンボルテーブルでは, LOCL な OBJT とされ, 4バイトであることがわかる91.

なお, シンボルテーブル中の関数に対する “Size”の値は, その関数の実行コードサイズをあらわし, “Value”はオブジェクトコード中における先頭からのバイト数を表している92.このように, オブジェクトコードは, 未解決なシンボル名を含む, 環境に依存した命令の列やオブジェク

トの配置情報を含んだデータである. これらの未解決シンボルとオブジェクトの配置は, 複数のオブジェクトコードの結合を行うリンカによって解決される.

6.12.2 リンク

プログラムファイルからコンパイラを利用して生成したオブジェクトコードを結合して, シンボルを解決して, オブジェクトを配置することによって, 実行可能コードを作成することが出来る. この操作をリンク(link) とよぶ.だが, ちょっと待った!Section 6.12.1.2 での例を見ればわかるように, file1.o と file2.o を結合した

だけでは, シンボル printfが解決できない. 関数 printf は C の標準ライブラリ関数であるため, この関数の実体を含むライブラリ (library) もついでに結合しておかなければ, すべてのシンボルを解決し, その実体を明らかにすることが出来ない. つまり, リンクとは必要であれば, ライブラリも結合するという操作を含むことになる.

6.12.2.1 実行形式の作成

Section 6.12.1.2 での例で作成した2つのオブジェクトコード file1.oと file2.o,さらに, C の標準ライブラリをリンクするには,

gcc file1.o file2.o -o target

とする. ここで, -o target に書かれた target が, リンカが出力する実行形式のファイル名となる.でも, この命令では「標準ライブラリ」を指定していないが?通常のリンカは「標準ライブラリ」を必

ず結合するようになっているため, 明示的に標準ライブラリを指定しなくても良い. 標準関数ライブラリはUNIX の場合, 通常 /usr/lib/libc.aというファイルである.しかし, C の標準関数の中には, 「数学関数」と呼ばれる関数群があり93, これらの実体は標準関数には

入っていない. 数学関数ライブラリは UNIX の場合には通常は /usr/lib/libm.a であり, 数学関数ライブラリ94を必要とする場合には,

gcc file1.o file2.o -o target -lm

91オブジェクトコード(アセンブラコード)中での扱いは, 内部静的変数とほとんど同じ扱いであることに注意. したがって, C プログラムのレベルでは static の意味が多少異なるが, 生成するオブジェクトコードレベルになると, 内部リンケージを持つファイルスコープの変数と, 内部静的変数は全く同じ扱いになることに注意しよう.# だから, 同じ static という記憶クラス指定子を持つ.92正確には, そのオブジェクトコード中の “text” セグメントの先頭からの「オフセット」と呼ばれる値である.93例えば, 三角関数の値を求める sin や, 対数関数 log がある.94なぜ数学関数ライブラリのリンクを明示的に指定しなければいけないのだろうか?数学関数ライブラリは, ユーザの目的によっては, その精度や速度に問題がある可能性が否定できない. 標準的な数学関数ライブラリの場合には, 精度と速度が適切になるようなコードから生成されていることが多く, より高い精度や, より高速な実行を求める場合には, 必要に応じて, 異なった数学関数ライブラリを用いることが考えられる. そのため, 数学関数ライブラリが標準関数ライブラリから独立していると考えられる.しかし, Darwin (MacOS X) の Public Beta Version では, 数学関数は標準関数ライブラリに組み込まれていた. Darwin の元

となった NeXTSTEP でどのような構成になっていたかは, 私は良く知らないが, まあ, Darwin では数値計算はするなということなのだろう.

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 39: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

のように, -lm というオプションをリンカに渡す必要がある95. 最後に, なぜ -lm オプションを最後につけ

るかという理由を考えてみよう. 例えば, file1.o は関数 sin の呼び出しを含むとき,

gcc -lm file1.o file2.o -o target

としても, リンカは正しく動作する. しかし,

Undefined first referenced

symbol in file

sin file1.o

ld: fatal: Symbol referencing errors.

というエラーを出力するだろう. これは, 先に libm.a のシンボルが評価され, file1.o にある未解決シンボル sin の解決が出来なくなっていることを表している. したがって, ライブラリの指定は一番最後にしなくてはならない. また, 同様にライブラリの指定の順序によっては, シンボルの解決が出来ない場合がありうるので注意が必要である.

6.12.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.12.2.2.1 ライブラリの作成方法 オブジェクトコードを静的リンクライブラリとしてまとめる(アー

カイブ (archive) するという)時には, コマンド ar を用いて,

ar -q libx.a file2.o file3.o

とすれば, file2.o, file3.o を libx.a にアーカイブでき, リンク時に -lx オプションで静的にリンクで

きる96. また, オブジェクトコードを動的リンクライブラリにアーカイブするときには,95-l の後に空白なしに指定した文字を XXXX とすると, リンカは指定されたディレクトリから libXXXX.a という名前のライブラリを探し, それをリンクする. 指定されたディレクトリとは, 通常は /usr/lib であり, それ以外のディレクトリもライブラリの検索対象としたい場合には, -L/usr/local/lib のように -L オプションで明示的にディレクトリを指定する必要がある.

96静的リンクライブラリの拡張子 .a は archive の略であるのは明らかだろう.

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 40: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

gcc -o libx.so -G file2.o file1.o

とすればよい9798.

6.12.2.3 インターポジショニング

インターポジショニング (interpositioning) とは, ライブラリ中で定義されている関数を自前の関数で置き換えてしまうことを指す. C では, 標準関数の識別子名は予約 (reserve) されているが, その識別子を使ってはいけないという意味ではない99. 例えば, C の標準関数 islower を考えてみよう. 次のようなプログラムを書いたら何が起こるかということである100.

#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関数を利用している関数101を利用したらどうなるのだろうか?この場合には, 標準ライブラリの islowerではなく, このプログラムファイル中にある islowerが利用される. ということは, 悲惨な結末を向かえることになるのは明らかである.このように, 標準関数内で定義されているシンボル名を関数名に利用してはいけない.

97動的リンクライブラリの拡張子 .so は shared object の略である. また, あるプログラムがどのような動的リンクライブラリを用いているかは, ldd コマンドで知ることが出来る. ldd /usr/bin/cp としてみるとよい.

98しかし, Solaris 2.xの動的リンクライブラリには少々面倒なところがあり, 実行時の動的リンクライブラリの検索パスを指定するために, プログラムのリンク時に -R オプションにより明示的に動的リンクライブラリを指定するか, シェルの環境変数 LD LIBRARY PATH

で動的リンクライブラリのあるディレクトリを指定する必要がある. SunOS 4.x などでは, 動的リンクライブラリのリンクキャッシュld.so があり, そこに動的リンクライブラリのハッシュテーブルを構成できた. 個人的にはこちらの方が好みなのだが, ld.so をつぶしてしまうと悲惨なことが起きるという欠点がある.# 実際, 私は ld.so を間違って消してしまった経験がある.99[6] にも書かれている通り, これは「警告」対象とはならない. せめて警告くらいはしてくれる仕様にして欲しいのは誰でも思うことなのだが...100islower は ctype.h で宣言されている.101FreeBSD 4.2 RELEASE のライブラリ群のソースコード (/usr/src/lib 以下) を見てみると, libc/net/inet network.c などで利用されている.

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 41: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

6.12.3 メモリ配置

ここでは, C で書かれたプログラムが実行される場合に, 各種のオブジェクトがどのようにメモリ上に配置されていくかを調べてみよう. これによって,関数呼び出しの場合の引数の評価,実行時のエラー (Section6.21.3 参照) の意味が明確になってくる.

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

}

このコードをコンパイルして, 実行形式を作成し, そのシンボルテーブル(一部省略)を見てみると102,

a.out:

[Index] Value Size Type Bind Other Shname Name

[61] | 67200| 116|FUNC |GLOB |0 |.text |_start

[73] | 133784| 4|OBJT |WEAK |0 |.bss |environ

[62] | 133780| 4|OBJT |GLOB |0 |.bss |i

[63] | 133688| 4|OBJT |GLOB |0 |.data |j

[50] | 133776| 0|OBJT |LOCL |0 |.bss |k

[47] | 133692| 4|OBJT |LOCL |0 |.data |l

[79] | 67600| 72|FUNC |GLOB |0 |.text |main

[33] | 133728| 24|OBJT |LOCL |0 |.bss |object.11

[48] | 133760| 0|OBJT |LOCL |0 |.bss |s.3

[49] | 133696| 4|OBJT |LOCL |0 |.data |t.4

となる. 前にも述べた通り, 変数 i, j, k, l, s, t がシンボルテーブル上に表れ, 変数 n, m は内部自動変数であるので, シンボルテーブルには表れない.ここで, UNIX のメモリ管理のセグメント (segment) という概念が必要となる. UNIX におけるセグメ

ント103とは, 実行形式に割り当てられた主記憶の区切りのことであり, UNIX では

102これは, nm -s a.out により生成した.103MS-DOS におけるセグメントとは異なるので注意すること. MS-DOS におけるセグメントとは, 80286 CPU のアドレス管理方法に依存したもので, 16 ビットアドレス管理で管理可能なメモリの区切りを指す. ちなみに 80286 CPU は 20 ビットアドレス線を持ち, 上位から 16 ビットと下位から 16 ビットのオフセットとセグメントによるメモリ管理を行っていた.

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 42: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

• text セグメント.

実行可能形式のコマンド列を格納する部分.

• data セグメント.

実行可能形式の大域的なオブジェクトのうち, 明示的な初期化が与えられたオブジェクトが格納される部分.

• bss セグメント.

実行可能形式の大域的なオブジェクトのうち, 明示的な初期化が与えられていないオブジェクトが格納される部分.

• stack セグメント.

実行中に自動変数や関数呼び出し, 動的なメモリ割り当てで利用する部分.

の4つに分けられる. これらのセグメントは, 次の図のように割り当てられるのが普通である.

text セグメント

data セグメント

bss セグメント

stack セグメント

スタック

ヒープ

命令の列

jlt

iks

n (main 呼出し時に)m (main 呼出し時に)

text セグメント, data セグメントは, 主記憶への配置時に, 実行ファイル中にある値で埋め尽くされる.bss セグメントは実行開始

時に 0 でクリアされる.stack セグメントは, 関数呼び出しに伴い, 上位メモリから順に利用される.(スタックの利用)また, 動的メモリ割り当てのうち

alloca関数では,スタックの利用が可能である.実行中の動的メモリ割り当

て (malloc 関数の呼出しなど) では, stack セグメントが下位メモリから順に

利用される. (ヒープ領域の利用)

size コマンドで, a.out の各セグメントの大きさを調べると,

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 43: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

% size a.out

2288 + 360 + 68 = 2716

となり, text セグメントが 2288 バイト, data セグメントが 360 バイト, bss セグメントが 68 バイトで

あることがわかる.仮にプログラム内部でこれらのセグメントを越えてメモリのアクセスを行う104と, “segmentation fault”

という実行時エラーを発生して, プログラムの実行が停止する.stack セグメントは, 次に述べる関数呼び出し手順の中で利用され, stack セグメントにどれだけの大き

さが割り当てられるかは, 実行形式を呼び出したシェルの環境に依存する. stack セグメントの大きさは

limit コマンドで表示される値

cputime unlimited

filesize unlimited

datasize 2097148 kbytes

stacksize 8192 kbytes

coredumpsize 0 kbytes

vmemoryuse unlimited

descriptors 64

で知ることが出来る.

6.12.4 関数呼び出しの手順

Section 6.10.1.7 では, 関数呼出しを行った場合のプログラムの動作の様子を考察し, 関数への実引数は「値渡し」が行われることを述べた. ここでは, それがメモリ内で何をしていることになるのかを考察してみよう.

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

{

104これは, 「ポインタ」を用いると容易に実現できる.

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 44: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

return a + b ;

}

このプログラム中で, 関数 add を呼び出す直前, 呼び出した後, add の終了時のメモリ内の様子は, 以下の通りとなる.【注意】n, m は内部自動変数なので, メモリはすべてスタックセグメントが用いられる.

呼出し前 呼び出した後 関数終了直前main の n, 値 1main の m, 値 2

main の n, 値 1main の m, 値 2

add の戻り値を格納する部分add の a, 値 1add の a, 値 2

�値のコピー

�値のコピー

main の n, 値 1main の m, 値 2

値 3add の a, 値 1add の a, 値 2

main に戻ってきたとき 関数呼出し終了main の n, 値 1main の m, 値 2戻り値 3

main の n, 値 3main の m, 値 2

この図のように, 関数呼出しを行うと, 関数の実引数はスタック内に新しい記憶領域が確保され, そこへ実引数の値がコピーされる. したがって,

extern int add(int) ;

int main(int argc, char **argv)

{

int n ;

n = 1 ; add(n) ;

return 0 ;

}

int add(int a) ;

{

return ++a ;

}

というプログラムでは,

呼出し前 呼び出した後 関数終了直前main の n, 値 1 main の n, 値 1

add の戻り値を格納する部分add の a, 値 1

main の n, 値 1値 2

add の a, 値 2

main に戻ってきたとき 関数呼出し終了main の n, 値 1戻り値 2

main の n, 値 1

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 45: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

となり, n の値が変化しない理由は明らかとなる. しかし, ファイルスコープのオブジェクトは静的であり,データセグメントまたは bss セグメントに格納されるため, それらを関数内で変更すると, その変更は永続的となる.

extern int add(int) ;

int k = 0 ;

int main(int argc, char **argv)

{

int n = 1 ;

n = add(n) ;

return 0 ;

}

int add(int a) ;

{

int b = 1 ;

b += 1 ; k += 1 ;

return ++a ;

}

というプログラムの場合には,

呼出し前 呼び出した後 return 文実行直前main の n, 値 1

k, 値 0

main の n, 値 1add の戻り値を格納する部分

add の a, 値 1add の b, 値 1

k, 値 0

main の n, 値 1add の戻り値を格納する部分

add の a, 値 1add の b, 値 2

k, 値 1

関数終了直前 main に戻ってきたとき 関数呼出し終了main の n, 値 1戻り値 2

add の a, 値 2add の b, 値 2

k, 値 1

main の n, 値 1戻り値 2

k, 値 1

main の n, 値 2

k, 値 1

となり, k の値は変更されている. (k はデータセグメントに格納されている.)

Remark 6.12.1 関数呼出しの時には, ここで説明したものよりも多くのデータがスタックに積まれる. 関数呼出しの時には, その時点でのレジスタ情報, 関数終了時にプログラム制御が戻るべきテキストセグメント内のアドレス(プログラム・カウンタ)など, 多くの情報がスタックに積まれ, その後に戻り値領域, 関数実引数領域が確保される.また, 関数実引数がスタック上に積まれる順序は処理系依存である. 実際には, オペレーティングシステ

ムとライブラリ, 処理系などで整合性のある渡し方が行われる105.105多くの処理系では後ろに書かれた実引数が先にスタックに積まれることが多い. また, Pascal, Fortran などの言語では, スタックに積まれる順序が指定されていて, それらで書かれたライブラリを使う場合には, 処理系依存のオプションを利用することにより,スタックに実引数を積む順序を指定できることが多い.

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 46: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

6.12.4.2 再帰的関数呼び出し

ここでは, 再帰的関数呼び出しを詳細に考察してみよう. はじめに, フィボナッチ数列

an+2 = an+1 + an, a0 = 0, a1 = 1

を求める関数を再帰的に定義してみよう. これは, 容易に書くことができて,

int fib(int n)

{

if (n == 0) return 0 ;

if (n == 1) return 1 ;

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

}

となる. この関数を fib(4) として呼び出すと,

fib(4) = fib(3) + fib(2)

= (fib(2) + fib(1)) + (fib(1) + fib(0))

= ((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))

と計算される. このことから容易に想像がつくように, fib(4) の呼び出しでは fib(2), fib(1), fib(0)の呼び出しが複数回行われ, 極めて効率が悪いことが想像できる. 実際, fib(n) を計算するために必要な関数 fib の呼び出し回数は, fib(n) の値とほぼ同じであることが容易に証明できる.一方, Example 6.10.14 で用いた, 帰納的に定義された数列 an+1 = an + 2, a0 = 0 の an を求める関数

を利用しよう.

int func(unsigned int n)

{

if (n == 0) return 0 ;

return func(n-1) + 2 ;

}

この関数を func(3) として呼び出すと

func(3) = func(2) + 2

= (func(1) + 2) + 2

= ((func(0) + 2) + 2)

となり, 各 func(2), func(1), func(0) は高々1回のみ呼び出されている. このように関数 func の呼

び出し func(a) が高々1回新しい func の呼び出しを行うとき, 関数 func の定義が線形再帰 (linearrecursive) であると呼ばれる.

6.12.4.2.1 線形再帰関数の呼出しの様子 次に線形再帰関数を呼びだす様子を見てみよう. 再び上で用いた Example 6.10.14の帰納的に定義された数列 an+1 = an +2, a0 = 0の an を求める関数を利用しよう.

int func(unsigned int n)

{

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 47: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

if (n == 0) return 0 ;

return func(n-1) + 2 ;

}

この関数を func(3) として呼び出すと

func(3) = func(2) + 2

= (func(1) + 2) + 2

= (((func(0) + 2) + 2) + 2)

となり, func(2)+1の演算は func(2)の値が決まるまでは実行できないことがわかる. 同様に, func(1)+2は func(1)の値が決まるまでは実行できず, func(0) を呼び出し, その値が 0 と確定してから, func(1)+2の演算が実行される. すなわち,途中に呼び出された func(3), func(2), func(1)における演算は, func(0)の呼び出しが終了した後に実行される. その結果,

func(3) = func(2) + 2

= (func(1) + 2) + 2

= (((func(0) + 2) + 2) + 2)

= (((0 + 2)2) + 2)

= ((2 + 2) + 2)

= (4 + 2)

= 6

という結果を得る.この場合の関数呼び出しのスタックの様子は以下のようになる.

呼出し前 func(2) 呼び出し後 func(1) 呼び出し後(func(2) の戻り値)

(func(2) の実引数) 2(func(2) の戻り値)

(func(2) の実引数) 2(func(1) の戻り値)

(func(1) の実引数) 1

func(0) 呼出し後 func(0) 終了 func(1) 終了(func(2) の戻り値)

(func(2) の実引数) 2(func(1) の戻り値)

(func(1) の実引数) 1(func(0) の戻り値)

(func(0) の実引数) 0

(func(2) の戻り値)(func(2) の実引数) 2(func(1) の戻り値)

(func(1) の実引数) 1(func(0) の戻り値) 0

(func(2) の戻り値)(func(2) の実引数) 2(func(1) の戻り値) 2

func(2) 終了 関数呼出し 終了(func(2) の戻り値) 4

各 func() 終了時には,

return func(n-1) + 2

が行われ, 直前の戻り値に 2 を加えたものがその関数の戻り値となる. このことから, 再帰的な関数呼出しがスタックを順に利用していることがわかる.

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 48: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

6.12.4.2.2 末尾再帰 再帰的に定義された関数が再帰を利用せず値を返すか, 単に再帰的に定義された関数の結果の値のみを返すとき, その関数は末尾再帰 (tail recursive) であるという.

Example 6.12.2 次の関数は末尾再帰である.

int g(int n, int a)

{

return n?g(n-1,n*a):a ;

}

この関数は, 以下のものに等しい

g(n, a) =

{a if n = 0,

g(n − 1, na) その他.

すなわち, g(n, a) = a ∗ n! である.この関数を g(3, 1) として呼び出すと,

g(3, 1) = g(2, 3) = g(1, 6) = g(0, 6) = 6

となることがわかる.

末尾再帰となっている関数は, その駆動の終了段階での演算が単純であり, 極めて効率が良いことがわかる.また, 末尾再帰で定義された関数は, 再帰を利用せずループを利用した形式に書き直すことが極めて容易

である. 実際上の例の関数は

int g(int n, int a)

{

while(1) {

if (n == 0) return a ;

else { a *= n ; n -= 1 ; }

}

と書き直すことができる.さて, なぜ末尾再帰が有効なのかを考えてみよう. 末尾再帰でない例では, 再帰呼び出しから戻ったあと

にさらに計算を行わなければならない. 上の例では,

return f(n-1)+2 ;

となっているため, 計算待ちの列が残っていることとなり, スタックを破棄することができない.一方, 末尾再帰となっている関数では, 再帰呼び出しから戻ったあとには関数からの戻り値を戻すだけの

操作しか残っていないため, 呼び出し側のスタックを破棄して, 再帰として呼び出した関数が利用するスタックで上書きすることが可能である. すなわち, 以下のような呼び出しが可能である. いま, g(2, 3) を呼び出してみよう.

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 49: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

呼出し前 g(2.3) 呼び出し後 g(1,6) 呼び出し後(g(2,3) の戻り値)

(g(2,3) の実引数) 2, 3(g(1,6) の戻り値)

(g(1,6) の実引数) 1, 6

g(0,6) 呼出し後 g(0,6) 終了 g(1,6) 終了(g(0,6) の戻り値)

(g(0,6) の実引数) 0, 6(g(1,6) の戻り値) (g(2,3) の戻り値)

g(2,3) 終了 関数呼出し 終了(g(2,3) の戻り値)

としてスタックを利用することが可能となる.

6.12.4.2.3 再帰的関数呼出しでスタックをあふれさせる さて, 再帰的関数呼出しを実行して, スタック領域が使い尽くされていくことを実感するために, 以下のような「荒っぽい」ことをしてみよう.上で利用した関数 func を大量に呼び出して, スタックセグメントが使い尽くされると何が起こるだろう

か?まず, 以下のプログラムを実行してみよう.

#include <stdio.h>

extern int func(unsigned int) ;

int main(int argc, char **argv)

{

func(10) ;

return 0 ;

}

int func(unsigned int n)

{

char c ;

printf("n = %3d, c = %p\n",n,&c) ;

if (n == 0) return 0 ;

return func(n-1) + 2 ;

}

ここで, c = %p の出力は, &c, すなわち, c のアドレスを出力する. したがって, この値はその時点でのおおよそのスタックの先頭のアドレスを示していることになる. 実行結果は,

n = 10, c = effff9a7

n = 9, c = effff92f

n = 8, c = effff8b7

n = 7, c = effff83f

n = 6, c = effff7c7

n = 5, c = effff74f

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 50: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

n = 4, c = effff6d7

n = 3, c = effff65f

n = 2, c = effff5e7

n = 1, c = effff56f

n = 0, c = effff4f7

となり, 1回の呼出しで120バイトのスタックを利用していることがわかる. そこで, スタックセグメントを小さくするために,

limit stacksize 1

とする. これにより, スタックセグメントは1Kバイトに制限される. そのうえ, func(100) を呼び出してみる. この結果はシステムに依存するが, 多くの場合, 途中で Segmentation fault というエラーを出し

て実行が停止する. これは, スタックセグメントが足らなくなって, 実行が停止する例となっている.また, 上の fib も1回の呼び出しで120バイトのスタックを必要とする. fib(n) の呼び出しにはほぼ

fib(n) 回の呼び出しが必要となる. そのおおよそのオーダは 1√5(1+

√5

2 )n ∼ 1.6n である. もし, 1Gバイト(= 109 バイト)のスタックがあったとしても, 呼び出し可能な回数 N は

1.6N = 109/100 = 107, N = 7 log1.6 10 ∼ 35

となり, 高々35回程度しか呼び出すことができない. 一方, 線形再帰関数の場合には, 明らかに

N = 107

となる.

6.12.5 演習問題

Exercise 6.12.3 次の2つのファイルからなるプログラムをコンパイル・リンクし, その出力結果がなぜそのようになるかを考えよ.

#include <stdio.h>

extern int sub_function(void) ;

int i=0 ;

int j=0 ;

int main()

{

sub_function() ;

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

return 0 ;

}

extern int i ;

static int j ;

int sub_function()

{

i += 1 ; j += 1 ;

return 0 ;

}

Exercise 6.12.4 次の2つのファイルからなるプログラムをコンパイル・リンクし, その出力結果がなぜそのようになるかを考えよ.

C6-1.tex,v 1.8 2003-06-15 14:07:38+09 naito Exp

Page 51: Grad. Sch. of Math., Nagoya Univ. - 関数とはnaito/lecture/2005_SS/...promotion など)が行なわれる. 実際, int 型の変数はdouble に変換される. (もちろん,

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

#include <stdio.h>

extern int sub_function(void) ;

int i=0 ;

int j=0 ;

int main()

{

sub_function() ;

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

return 0 ;

}

extern int i ;

extern int j ;

int sub_function()

{

i += 1 ; j += 1 ;

return 0 ;

}

6.13 配列とポインタ(その1)

6.13.1 配列

配列 (array) とは, 特定の型の変数をひとまとまりにして, 利用できる構造である.

6.13.1.1 配列の定義と宣言

配列を利用するには, 配列としての宣言をしなくてはならない. 例えば, int 型の 10 個の配列 digit は

次のように宣言される.

int digit[10] ;

この時, 識別子の名前は digit であり, 「演算子」 [ ] は配列宣言演算子と呼ばれ, 配列の要素数を表す.配列の定義において, 配列の要素数を表すものは 0 より大きい値を持つ整数定数式でなければならない. したがって, 配列の要素の数は, unsigned long で表せる範囲内であれば良いことがわかる. また, 任意の型,及びその派生型に対して配列を定義することができる.上のようにして定義された配列の要素は digit[0], ..., digit[9]のように [ ] の中(添字)に整数

式を代入することで要素を参照することが出来る. 注意すべきことは, 添字は必ず 0 から始まり, 定義された添字の範囲を越えて配列の参照を行った場合の動作は不定となる106.

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

配列の各要素は, メモリ内では連続した部分にアロケートされる.配列の宣言では,

extern int digit[] ;

という宣言が可能であり, これは, プログラム中の他のどこかで定義される配列を表し, この配列の定義においてはじめて配列のサイズ(要素数)が決定される. このように, 配列の要素数が決まっていなかったり,(後の配列の初期化で述べるように)配列要素の成分のすべてが決定できないような配列を, 不完全な配列と呼ぶ.

106他のオブジェクトを参照してしまうかも知れないし, 実行時エラーをおこすかもしれない.

C7.tex,v 1.27 2001-07-19 17:13:15+09 naito Exp