6.11 関数とは
6.11.1 関数の定義
6.11.1.1 関数の定義と例
C言語では関数は0個もしくは1個以上の引数(ひきすう)(parameter)を持ち,0個もしくは1個の 戻り値(もどりち)(return value)を持つ. 引数, 戻り値は, 数学における関数の独立変数, 関数の値に 相当すると考えられるが, C言語の関数は,引数が関数を呼び出した後に変化することも多い.30
関数は,以下の形をしている.
<戻り値の型> <関数の識別子> ( <関数の引数> ) 関数の本体となる複文
関数の引数として書かれた変数(これを仮引数(parameter)と呼ぶ)の識別子は,その関数内のみで有 効な識別子である.(Section 6.11.1.4参照.)また,関数を呼び出したときの引数を実引数(argument)と 呼ぶ.
Example 6.11.1 仮引数として int型の引数を2つ持ち,それらの和をint型で返す関数は以下のよう に書かれる.
int sum(int n, int m) {
return n + m ; }
このように,関数の戻り値を指定するにはreturn文を用いる.
return文に出会うと関数の実行は終了し,その後の部分は実行されない.
Example 6.11.2 次は,2つのint型の引数の小さくない方の値を返す関数.
int max(int n, int m) {
if (n < m) return m ; return n ;
}
int max(int n, int m) {
return (n < m) ? m : n ; }
関数の戻り値の型を省いた時には intであると解釈される. (cf. [3, X 3010 6.7.1, p. 1922], [2, A10]) したがって,戻り値を持たない関数の場合,戻り値の型はvoidであると明示的に宣言しなくてはならない.
関数の戻り値の型として配列をとることは出来ない.
また,引数を持たない関数も作ることができる.
Example 6.11.3 この例は,実際には余り意味がない.
30このようなことを副作用と呼ぶ.
void only_print(void) {
printf("Hello\n") ; return ;
}
文法上はonly_print()としても良いが,明示的にonly_print(void)とした方が良い.
一方, printfなどは,その場合により引数の数が異なる関数の例である. このような関数を可変引数を
持つ関数と呼ぶ.31
Example 6.11.4 このプログラムでは, 関数の引数の識別子(仮引数)と, 関数を呼び出している部分で 使われている変数(実引数)の識別子が同じものになっているが, それぞれの実体が違うことに注意. (→
Section 6.12).
#include <stdio.h>
int a,b,c ;
int sum(int b, int c) {
return b + c ; }
int main(int argc, char **argv) {
a = 0 ; b = 1 ; c = sum(a,b) ; return c ; }
mainもまた関数になっていることに注意. また,関数はプログラムファイル中の(他の関数内でなければ)
どこに書いても良い. さらに, Section 6.13で述べるように, Cでは一つのプログラムを複数のファイルに 分割することができ,関数全体が一つのファイル内にあれば,呼び出される関数が他のファイルにあっても 良い.
6.11.1.2 関数の引数について
関数の引数は,どのような型を持っても良い. 関数の引数として用いられた識別子を持つ変数は,その関 数内でのみ有効であることは,前に述べた. ここでは,関数に渡される引数がどのような型変換を受けるか を解説する.
C言語においては数の累乗は演算子では定義されていないので,関数を呼び出すことになる. 累乗を計算 するC言語の標準的な関数としてはpowがある. この関数は,以下のように定義されている.32
31可変引数の関数は後ほど述べる.
32man 3 pow参照.
double pow(double x, double y)
これは, xのy乗の値を返す関数である. ここで, 問題となるのは,全ての変数がdoubleで宣言してある ことである. この関数を次のように呼び出したらどうなるだろうか?
int n=2,m=3 ; pow(n,m) ; pow(3,5) ;
第一の呼び出しは,int型の変数を用いて呼び出しているし,第二の呼び出しは,整数の定数を用いて呼 び出している.
このように,関数の定義の異なる型の変数や定数を用いて関数を呼び出す時には,暗黙の型変換(integer
promotion など)が行なわれる. 実際, int型の変数は doubleに変換される. (もちろん,整数から浮動
小数点数への変換の規則が用いられる.)
Example 6.11.2で書いた,2つのint型の変数の小さくない方の値を返す関数を以下のように使ってみ
よう.
double x = 2.5, y = 0.3 ; int max(int n, int m) {
return (n < m) ? m : n ; }
int main(int argc, char **argv) {
printf("%f\n", max(x,y)) ; return 0 ;
}
これでは全く正しい結果が得られない. 実際には,2つの数の大きさを比較して小さくない方の値を表示 させるには,
#define max(A,B) (A < B) ? B : A printf("%f\n", max(1.0, 2.0)) ; とした方が良い.
Remark 6.11.1 関数の実引数の評価順序は不定であるので注意すること.
int v ;
func(v++,v++) ;
とすると,実引数としてどのような値が渡されるかはわからない.
Remark 6.11.2 演算においては,演算の優先順位と結合規則は,式の構造のみを決定するだけで,その評 価順序は不定であることに注意しよう. たとえば,
f() + g() * h()
としても,gとhの呼出しが先に行われる保証はない. Example 6.8.23の例,a/b*bおよびa*b/b ではど のような順序で式a,bの評価が行われても,その優先順位と結合規則にしたがって計算が行われていると 理解すれば十分であったが,関数呼出しのように副作用がある場合には,その副作用が完全に終了33するの は式全体の評価が終了した時点であることに注意しなければならない.
6.11.1.3 関数のプロトタイプ宣言とヘッダファイル
Section 6.11.1.1 では,関数の戻り値の型を省略した時にはintであると述べた. また,関数を書く場所
はどこでも良いと書いた. このことで,次のような2つの問題が起きる.
例えば,double型の引数と戻り値を持つような関数test fnを作り,以下のようにしたとする.
int main(int argc, char **argv) {
int n ;
n = test_fn(n) ; }
double test_fn(double a) {
. . . }
これは,正常にコンパイルできるだろうか?まず, 次のような問題があることに気が付く.
• main 関数内にある識別子test fnが,その時点で何を示すかわからない.
Cコンパイラは識別子test fnが実引数を伴っていることにより,関数であることを認識する. 関数シン ボル(識別子)の解決は,最終的にはリンカ(Section 6.13参照)で行うので, この問題は本質的な問題に はならない.
もう一つの問題は, 次のようなところにある. ANSI規格 [3, X 3010 6.7.1, p. 1922] および K&R [2, A10]によると,「関数定義の宣言子において省略されている型はintとみなす」とされている. すなわち, 戻り値の型や仮引数の型が省略されると,コンパイラはそれをintとみなして処理を進める. したがって, コンパイラは以下のような処理を行う.
• コンパイラがmain 内でtest fnを呼び出す時には, test fnの戻り値をintと解釈している. 同 時にtest fnの仮引数の型がintであると仮定して,処理を進める.
• 次に,test fnの部分をコンパイルする時には,戻り値や仮引数の型がdoubleとなっているので,矛 盾がありエラーとなる.
この様な,呼び出しと定義の部分の矛盾を避けるために,関数のプロトタイプ(prototype) 宣言を行なう 必要がある. プロトタイプ宣言とは,関数の持つ引数の型と戻り値の型のみを書いた文を,その関数が利用 される前に書いておくことである.
33式の評価において,副作用が完全に終了する場所を,副作用完了点と呼ぶ.この場合,副作用完了点は式全体の評価が終わった点 である.
上の例の場合には
extern double test_fn(double) ;
という一行を,main の前に書けば良い. 複数の引数を持つような関数のプロトタイプ宣言は extern double test_fn(double, double) ;
などと書く. すなわち,全体としては extern double test_fn(double) ; int main(int argc, char **argv) {
int n ;
n = test_fn(n) ; }
double test_fn(double a) {
. . . }
となる. ここで使われたexternは記憶クラス指定子(storage class specifier)の一つであり,詳しくは section 6.12で議論する.
Remark 6.11.3 上の例で用いた関数プロトタイプ宣言は int main(int argc, char **argv)
{
extern double test_fn(double) ; int n ;
n = test_fn(n) ; }
double test_fn(double a) {
. . . }
と書くことも可能である. この場合,関数test fnのプロトタイプが有効なのは,main 関数の内部に限ら れる.
C 言語のプログラムを書く際に, #include <stdio.h>などということを書いた. ここで使われたヘッ ダファイル stdio.hには, いくつかの関数(printf など)のプロトタイプ宣言などが書かれている. そ のような意味で, 標準的な関数を利用する際には, それぞれのプロトタイプ宣言を含むヘッダファイルを
#includeでインクルードしなくてはならない.
6.11.1.4 関数内の局所変数
関数内で局所的にしか利用できない変数を作ることができる. このような変数を定義するには,関数のブ ロック内に変数の定義をすることで,局所変数の定義となる.
局所変数はstatic宣言をしない限り34関数が呼び出されるごとに変数領域が確保され,しかるべき初期 化を受ける35.
また, 局所変数の識別子は,その関数内でのみ有効である. 局所変数の識別子と,関数引数の識別子は重 なってはいけない. 即ち,関数内で局所的に有効な変数は,関数引数と局所変数である.
Example 6.11.5 この例では,sum,mainの両方の関数内で局所変数を定義している.
#include <stdio.h>
extern int sum(int, int) ; int main(int argc, char **argv) {
int a,b,c ; a = 0 ; b = 1 ; c = sum(a,b) ; return c ; }
int sum(int b, int c) {
int a ; a = b + c ; return a ; }
ここで,mainの変数a,b,cと,sumの変数 a,b,cは異なるものであることに注意.
また,どの関数にも含まれない部分で定義された変数(このようなものを大域変数と呼ぶ.)は,そのファイ ル中の定義(宣言)以後はどこでも有効であるが,大域変数と局所変数もしくは関数引数の識別子が重なっ た時には,その識別子は関数内では局所変数のものと見倣される36.
Example 6.11.6 この例では,sumという関数内で局所変数を定義し,一方大域変数も利用している.
34staticに関しては後述する.staticも記憶クラス指定子の一つである.
35明示的に初期化をしない限り,初期値は不定である.
36このようなことを,変数のスコープと呼び, Section 6.12.2.3で詳しく述べる.
#include <stdio.h>
int a,b,c ;
extern int sum(int, int) ; int main(int argc, char **argv) {
a = 0 ; b = 1 ; c = sum(a,b) ; return c ; }
int sum(int b, int c) {
int a ; a = b + c ; return a ; }
ここで,sumの中では, 変数a,b,cは,大域変数ではなく, 局所変数, 関数引数で定義されたものが見え ていることに注意せよ.
局所変数に関しては, Section 6.12で詳しく議論する.
Exercise 6.11.1 次のプログラムの出力結果がなぜそうなるかを考えよ.
#include <stdio.h>
int a = 1, b = 2, c = 3 ; extern int foo(void) ;
int main(int argc, char **argv) {
int a = 0, b = 1 ;
printf("a = %d, b = %d, c = %d\n", a,b,c) ; foo() ;
foo() ;
printf("a = %d, b = %d, c = %d\n", a,b,c) ; return 0 ;
}
int foo(void) {
int a = 2, b = 0 ;