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

6.11

N/A
N/A
Protected

Academic year: 2021

シェア "6.11"

Copied!
86
0
0

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

全文

(1)

#include <stdio.h>

int x ;

int main(int argc, char **argv) {

x = 0 ;

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

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

}

6.11 関数とは

関数とは,ある一定の処理をさせるための部分的なプログラムのことである.

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

6.11.1 関数の定義

6.11.1.1 関数の定義と例

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

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

<戻り値の型> <関数の識別子> ( <関数の引数> ) 関数の本体となる複文

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

(2)

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

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

int sum(int n, int m) {

return n + m ; }

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

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

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

int max(int n, int m) {

if (n < m) return m ; return n ;

}

int max(int n, int m) {

return (n < m) ? m : n ; }

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

関数の戻り値の型として配列をとることは出来ない.

また,引数を持たない関数も作ることができる.

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

void only_print(void) {

printf("Hello\n") ; return ;

}

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

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

持つ関数と呼ぶ.36

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

Section 6.12).

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

(3)

#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がある. この関数は,以下のように定義されている.37

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に変換される. (もちろん,整数から浮動

小数点数への変換の規則が用いられる.)

37man 3 pow参照.

(4)

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 ;

}

これでは全く正しい結果が得られない. 関数maxの2つの引数はint型であるにも関わらず,呼び出し 側の引数は double型であるため,実際の呼び出し時にはdouble 型の値2.5と 0.3がそれぞれ int型 への(暗黙の)型変換を受けてから評価される.38 したがって,関数maxには2,0という値が渡され, そ の結果の値はint型の2となる. しかし,printf関数では, その値を浮動小数点数として表示すると指示 されているため,結果は処理系に依存するが,多くの場合0.00000と表示される.

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

#define max(A,B) ((A) < (B)) ? (B) : (A) printf("%f\n", max(1.0, 2.0)) ;

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

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

と展開され,5|3 < 2&1は5|(3<2)&1と評価されるので,5|0&1=5|0=0という結果となる.

したがって,「プリプロセッサマクロ」の引数は,その定義内では括弧をつけることが必要不可欠である.

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

(5)

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

int v ;

func(v++,v++) ;

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

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

f() + g() * h()

としても,gとhの呼出しが先に行われる保証はない. Example 6.8.25の例,a/b*bおよびa*b/bではど のような順序で式a,bの評価が行われても,その優先順位と結合規則にしたがって計算が行われていると 理解すれば十分であったが,関数呼出しのように副作用がある場合には,その副作用が完全に終了39するの は式全体の評価が終了した時点であることに注意しなければならない.

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とみなす」とされている. すなわち,

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

(6)

戻り値の型や仮引数の型が省略されると,コンパイラはそれを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.12で議論する.

Remark 6.11.9 上の例で用いた関数プロトタイプ宣言は int main(int argc, char **argv)

{

extern double test_fn(double) ; int n ;

n = test_fn(n) ; }

(7)

double test_fn(double a) {

. . . }

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

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

#includeでインクルードしなくてはならない.

6.11.1.4 関数内の局所変数

関数内で局所的にしか利用できない変数を作ることができる. このような変数を定義するには,関数のブ ロック内に変数の定義をすることで,局所変数の定義となる.

局所変数はstatic宣言をしない限り40関数が呼び出されるごとに変数領域が確保され,しかるべき初期 化を受ける41.

また, 局所変数の識別子は,その関数内でのみ有効である. 局所変数の識別子と,関数引数の識別子は重 なってはいけない. 即ち,関数内で局所的に有効な変数は,関数引数と局所変数である.

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

40staticに関しては後述する.staticも記憶クラス指定子の一つである.

41明示的に初期化をしない限り,初期値は不定である.

(8)

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

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

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

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

(9)

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

#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 ;

(10)

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)

となる43. これらの引数については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 関数呼び出しの実際

関数が呼び出された時に,プログラムはどのように動作しているかを考えてみよう.

プログラム中で関数が呼び出された時には,次のような手続きが行なわれて,関数が呼び出される.

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

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

その際に,関数内のローカルな変数44はスタック内に確保される.

43システムによっては,より多くの引数(たとえば,環境変数など)を取ることもある.

44Section 6.12で述べる.

(11)

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

Pascalの varを利用した変数の渡し方(call by address(参照呼び出し))と同様なことをC言語で

実現するには,ポインタが必要となる.

6.11.2 再帰的関数呼びだし

関数はそれ自身をその内部で呼び出すことができる. これを再帰的関数呼び出し(recursive function

callまたは,再帰(recursion)と呼ぶ. 再帰的関数呼び出しを利用すると,帰納的に定義されたものを計算

することが容易になる. 数学的には,再帰で計算できるものの多くは再帰を利用しなくても計算できること が証明されている45が, 再帰で計算するとプログラムが簡潔になるという利点がある. 一方, 関数や手続き 等を呼び出す際には, 多くの処理系において呼び出しの手順として時間がかかることが多い. したがって, 再帰には時間がかかることが多い.

Example 6.11.13 次は,帰納的に定義された数列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.13.4.2 で詳細に調べる.

6.11.3 可変引数を持つ関数

printf に代表される,可変個の引数を持つ関数の定義方法と性質について述べておこう. ここでは, 後

に解説する「ポインタ」や「文字列」を利用している.

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

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

45非常に複雑な再帰定義のものでは,再帰を使わなくては計算できないものがある.

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

(12)

#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.11.15 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型の値として評価されることを意味している.

(13)

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以外の数値をセットすることで,

呼出し側の関数にエラー発生を伝えることが出来る.

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

6.11.5 演習問題

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

int mul(int n, int m)

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

(14)

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

int div(int n, int m)

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

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

int mod(int n, int m)

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

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

int pow_int(int n, int m)

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

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

double pow_d(double n, int m)

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

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

int is_upper(int c)

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

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

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

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

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

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

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

Exercise 6.11.29 再帰的関数呼び出しを用いて,n×n行列の行列式を求めるプログラムを書け. (注意:

この関数を作るためには,配列を必要とする.)

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

(15)

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.31 次のようなprintf関数の類似の関数を作れ. (ただし,関数内部でprintfを利用し ても良い.)通常のprintfの記述子の他に,bとして, 引数の2進数による表示を行なう. ただし, フィー ルド幅の指定子はサポートしなくても良い.

6.12 識別子

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

6.12.1 識別子とは

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

6.12.1.1 識別子の分類

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

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

型定義名,

ラベル名,

マクロ名,

マクロ仮引数.

6.12.1.2 名前空間

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

ラベル名.

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

(16)

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

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

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

6.12.2 基本概念

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

6.12.2.1 定義と宣言

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

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

このように「定義」と「宣言」を定義すると,どれが識別子の「定義」で,どれが「宣言」かわからなく なるのだが,一つのオブジェクトや関数に対して「定義」はただ一度だけである. 関数の「定義」は関数本 体とともに宣言された時に行われる. それ以外のものはすべて「定義」を伴わない「宣言」である. オブ ジェクトの「定義」は通常extern指定子を伴わない「宣言」で行われる50. extern宣言の詳細について は後に解説する.

なお,変数の宣言と同時に初期化を行うことが出来るが,初期化を行うと,その宣言は定義とみなされる.

Example 6.12.1

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

48ANSI規格書によれば,ファイルスコープのオブジェクトを,初期化子を使わず,記憶クラス指定子なしかstaticで宣言する場

合を,仮宣言(tentative definition)と呼んでいる. これは,オブジェクトコードのリンク時に最終的にリンケージが決定される

ことを意味している. (cf. [3, X 3010 6.7.2, p. 1924])

49正しくは, traditionalな形式の宣言も許されている.すなわち,仮引数の型を伴わない宣言も許されるが,バグを引き起こす原因 となる.

# 何でこんなのを許す仕様を残しておいたのだろう?

50つまり,これまでオブジェクトを宣言してきたものは,すべてオブジェクトの定義となっている. オブジェクトの宣言にすべて

externをつけてもエラーとはならない.リンク時にそれらの宣言のうちのいずれか一つを「定義」とみなす.これが「リンケージ」

というやつ.

# 要するに,extern宣言とはきちんと使わなければ,「メチャメチャ」になるものの典型的なものである.

(17)

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宣言の趣旨とは矛盾する.)

Example 6.12.3 2つのファイルからなるプログラムで以下のようなことをすると,リンク時にエラーと

なる. (分割コンパイルに関しては, Section 6.13 を参照.)

/* file1.c */

int x = 1 ;

/* file2.c */

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

6.12.2.2 翻訳単位

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

6.12.2.3 スコープ

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

1. ファイル・スコープ 2. 関数・スコープ

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

(18)

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

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

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

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

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

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

関数定義の仮引数.

関数の先頭部分.

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

3. ブロックに入った直後52. これは「ブロック・スコープ」となり,対応する}の出現で終了する53. 4. 関数プロトタイプ宣言の仮引数. この場合,「関数プロトタイプ・スコープ」となり, スコープはそ

の宣言内のみとなる.

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のスコープを,ファイル全体にしたい場合には,以下の例のいずれかに書き直す必要

がある.

52これは余り利用しないが,有効に利用できる場合がある. 変数を関数内で極めて局所化したい場合に利用することがある. これ

は,デバック(debug)を行う場合などで利用することがある.恒久的なコードでこの手法を用いると,思わぬ変数の隠蔽が起きる可

能性があるので,デバック時などの一時的なコードにのみ用いる方がよいだろう.

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

このノートでは「関数・スコープ」と「ブロック・スコープ」を便宜上区別しよう.

(19)

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

#include <stdio.h>

int i, l ;

int main(int argc,char **argv) {

int j ; j = 0 ; {

int k ; k = 0 ; }

return 0 ; }

#include <stdio.h>

int i ;

extern int l ;

int main(int argc,char **argv) {

int j ; j = 0 ; {

int k ; k = 0 ; }

return 0 ; }

int l ;

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

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

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 ; {

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

(20)

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

言がブロックスコープとなる.

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)

(21)

の2種類がある. 寿命という概念は記憶域と関わる概念であるので,識別子に対する概念ではなく,オブジェ クトに対する概念である.

オブジェクトが静的であるとは,プログラム実行の開始から終了までの期間,そのオブジェクトの記憶域 が記憶領域内に存在することをいう. オブジェクトが自動であるとは,プログラム実行中のある期間にのみ, そのオブジェクトの記憶域が記憶領域内に存在することをいう.

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

なお,関数プロトタイプ宣言内で宣言された仮引数宣言は,定義ではないため,寿命とは無関係である.

Example 6.12.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.12.2.4.1 オブジェクトの初期化 オブジェクトの初期化の手続きは,寿命と関連している. オブジェク

トが明示的に初期化宣言55されていない場合,静的オブジェクトはプログラム実行開始時に記憶領域がビッ トパターン0で初期化される. 自動オブジェクトは記憶領域確保時に初期化は行われない.

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

(22)

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

6.12.2.5 リンケージ

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

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

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

無結合 (no linkage)

の3種類に分類される. 外部リンケージを持つ同じ名前の識別子がプログラム内に複数回現れた場合には, それらは同じオブジェクトまたは関数を表し, 内部リンケージを持つ同じ名前の識別子が一つの翻訳単位

(プログラムファイル)中に複数回現れた場合には, それらは同じオブジェクトまたは関数を表す. 無結合 を持つ同じ名前の識別子は,それぞれが一意に決まる実体を持つ.

すなわち,同じ名前の識別子が異なったプログラムファイル中に現れ,それらが内部リンケージを持てば, それらは別々の実体を表し,同じ名前の識別子が同じファイル中にあっても, それらが無結合であれば, そ れらは別々の実体を表す.

識別子の宣言で記憶クラス指定子externまたはstaticを指定することにより,リンケージを変えるこ とが出来る. そのルールは以下の通りである.

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

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

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

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

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

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

6.12.2.5.1 プログラムが単一のファイルからなる場合 まず,外部リンケージは,プログラムが複数の翻

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

この場合に上の規約を要約すると,

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

(23)

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

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

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

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

Example 6.12.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はファイルスコープを持つ. つまり,このプログラムはコン パイル可能であり,実行すると,

(24)

l = 2 l = 0

という出力を得る.

その他のオブジェクト,すなわち,ファイルスコープのiと関数スコープのiは無結合であり,それぞれ は異なる実体を持つ.

オブジェクト kの staticを含む宣言は内部リンケージを持つ仮定義であり, 次の宣言int k と記憶 クラスが矛盾し,動作は不定となることに注意. これをextern int k とすれば, 正しい宣言となり,k は

static記憶クラスに属することになる.

Example 6.12.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.12.2.5.2 プログラムが複数のファイルからなる場合 元々, リンケージとはプログラムが複数のファ

イルからなる場合に,それぞれのファイルで宣言された識別子を結び付けるために考えられた概念である.

ここでは,関数とオブジェクトの識別子に関して別々に考えよう57

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

(25)

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 ; }

この場合,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 ; }

(26)

この例では,file2.cで関数fooをstatic宣言している. したがって,file2.cの関数fooは内部リン ケージとなり,file1.cの関数fooの呼び出しは,file2.cの関数fooを呼び出すわけではない58.

すると2つの疑問が生じる.

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

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

まず,「staticで定義した関数の関数プロトタイプ宣言はどうするの?」という疑問に対する答えは,「関 数プロトタイプ宣言にもexternなしでstaticをつける」というのが答えです. staticをつけても内部 リンケージは残るので,これで問題は解決.

「関数のstatic宣言って一体何に使うの?」ってのに対する答えは何通りも考えられる. 「プログラム

を複数のプログラマで開発する際に,自分の担当するファイル中だけで利用したい関数は内部リンケージに しておく」というのが,最もよくある通常の答え.

6.12.2.5.2.2 オブジェクトのリンケージ オブジェクトに関しては, ファイルスコープを持つものだけ

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

次のような2つのファイルからなるプログラムを考えよう. この部分では左のファイルを file1.c,右の ファイルをfile2.cとする.

int i ;

int main(int argc, char **argv) {

i = 0 ; foo() ; return 0 ; }

int i ;

void foo(void) {

i = 1 ; return ; }

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

もちろん,どれか一つの宣言を除いて,他の宣言にはexternをつけるのが望ましい. すなわち,

int i ;

int main(int argc, char **argv) {

i = 0 ; foo() ; return 0 ; }

extern int i ; void foo(void) {

i = 1 ; return ; }

58file1.cで呼び出される関数fooがどのようになるかは,この2つのプログラムファイルのオブジェクトコードをリンクするま

ではわからない. このことについては, Section 6.13で詳細に議論する.

(27)

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

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.12.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つのオブジェクトが存在する.

(28)

6.12.2.6 内部静的変数の利用法

内部リンケージまたは無結合である静的変数(オブジェクト)は,簡単に内部静的変数と呼ばれることが ある.

ファイルスコープを持つ内部静的変数の利用方法は,上に述べた通り,他のファイルからのオブジェクト の隠蔽であったが,ブロックスコープ(関数スコープ)を持つ内部静的変数は,ブロック外への変数の隠蔽 という効果の他に重要な役割を果たす.

すなわち,関数内で定義した変数にstaticをつけて宣言すると,その変数に対する寿命は大域的となる.

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

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

int function() {

static int i = 0 ; i += 1 ;

...

}

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

6.12.3 初期化

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

X 3010, 6.5.7, p. 1910]を参照しよう. そこには,「静的記憶域期間を持つオブジェクトの初期化子,また

は集成体型もしくは共用体型を持つオブジェクトの初期化子並びにおいて,全ての式は定数式でなければな らない」とある. 逆にいえば, 自動記憶域期間をもつ任意のオブジェクトを初期化することができる. (cf.

[3, X 3010, 6.5.7, p. 1910, Footnote 74].) すなわち,静的記憶期間を持ち, 初期化子を持つオブジェクト

は,プログラムの実行開始時にdataセグメントに配置され,定数式によって与えられた初期化式により決 まる値が代入される. したがって,次のような例は文法違反となる.

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

int a = 1, b = a ;

int main(int argc, char **argv) {

....

}

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

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

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

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

(29)

int foo(int a) {

int b = a ; ...

}

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

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

int foo(void) {

int a = 0, b = a ; ...

}

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

int foo(void) {

int b = a, a = 0 ; ...

}

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

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

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

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

}

60ここで用いられている“,”は,「コンマ演算子」ではなく,「識別子並び」中の「区切り子」であることに注意.

61[3, X 3010, 6.5.4宣言子, p. 1904]の「意味規則」によると,「各宣言子は一つの識別子を宣言する.式の中にその宣言子と同

じ形式のオペランドが現れた場合,そのオペランドは,宣言子指定子列が指示する有効範囲,記憶域期間及び型を持つ関数またはオブ ジェクトを指し示す.」とあり,複数の識別子を一つの宣言に並べたとしても,それは複数の宣言子が与えられたと解釈すべきであり, Cの文法規則により,左から右に解釈される.

(30)

この例では,int b = a の段階で,関数スコープのaは定義されていないため,int b = aの初期化式a はファイルスコープのaの値を参照することとなる.

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

int foo(int n) {

static int a = n ; ...

}

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

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

6.12.4 演習問題

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

(31)

#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. プログラムファイル中に書かれたマクロ定義などの処理を行うプリプロセッサ(preprosessor62. 2. プログラムファイルをオブジェクトコード(object code)と呼ばれる,機械が認識可能な命令の列

に置き換えるコンパイル(compile). コンパイルを行う一連の処理系をコンパイラ (compiler) と 呼ぶ.

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

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

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

(32)

link

file2.o file1.o

compile compile

exec code

library

file1.c file2.c

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

% gcc file.c -o target

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

6.13.1 オブジェクトコード

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

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

% gcc -c file1.c

というコマンドを利用する. これによってオブジェクトコードfile1.oが生成される.

オブジェクトコードの作成は,アセンブラコードの作成とアセンブラコードの翻訳という2段階にわか れる. プログラムファイルからアセンブラコードを出力させるためには,

% gcc -S file1.c

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

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

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

参照

関連したドキュメント

 我が国における肝硬変の原因としては,C型 やB型といった肝炎ウイルスによるものが最も 多い(図

ここで, C ijkl は弾性定数テンソルと呼ばれるものであり,以下の対称性を持つ.... (20)

現行の HDTV デジタル放送では 4:2:0 が採用されていること、また、 Main 10 プロファイルおよ び Main プロファイルは Y′C′ B C′ R 4:2:0 のみをサポートしていることから、 Y′C′ B

しかし , 特性関数 を使った証明には複素解析や Fourier 解析の知識が多少必要となってくるため , ここではより初等的な道 具のみで証明を実行できる Stein の方法

つまり、p 型の語が p 型の語を修飾するという関係になっている。しかし、p 型の語同士の Merge

としても極少数である︒そしてこのような区分は困難で相対的かつ不明確な区分となりがちである︒したがってその

いてもらう権利﹂に関するものである︒また︑多数意見は本件の争点を歪曲した︒というのは︑第一に︑多数意見は

・私は小さい頃は人見知りの激しい子どもでした。しかし、当時の担任の先生が遊びを