[C99] 第3回 関数

C99の関数に関する仕様は、細かな点でC++との違いがいろいろあります。C++ではできてC99ではできないこともあれば、C99ではできてC++ではできないこともあります。今回は、それらについて順番に解説していきます。

関数の多重定義はできない

C++では、引数の型や個数によって、同名の関数を多重定義(オーバーロード)することができました。しかし、C99ではそのようなことはできません。(可変個引数や関数原型なしの場合をのぞき)引数の型や個数が異なれば、別の名前にしなければなりません。

これは、C99の関数は、C++においてC結合を用いた場合と同じだからです。すなわち、単に多重定義ができないだけでなく、仮に引数の型や個数に矛盾があったとしても、リンク時にエラーを検出できないことを意味しています。関数のシグニチャの整合性は、プログラマの責任で保障しなければなりません。

なお、C99では演算子の多重定義を行うこともできません。

省略時実引数は使えない

C++では、実引数を省略した場合にデフォルトで関数に渡される省略時実引数を指定することができました。しかし、C99では省略時実引数を使うことができません。したがって、必要な実引数は、関数を呼び出す際に明示的に指定しなければなりません。

インライン関数

C++では、inline 関数指定子を指定することで、関数のインライン置換をコンパイラに示唆することができました。C99にも inline 関数指定子がありますが*1、その意味はC++とはずいぶん異なります。

C99の inline 関数指定子は、その関数の呼び出しを可能な限り高速にすることを示唆するものです。つまり、より実行サイクルが少ないアドレッシングモードを選択するといった方法で高速にしてもよく、必ずしもインライン置換を示唆するものではありません。

また、C99のインライン関数は、static や extern といった記憶クラス指定子の有無で振る舞いがかなり変わってきます。インライン関数に static 記憶クラス指定子を付けた場合、つまり内部結合にした場合、とくに制限はなく、ふつうの関数と同じように扱うことができます。

static 指定子を付けないインライン関数は外部結合になります。明示的に extern 指定子を付けない場合、そのインライン関数の定義はインライン定義になり、外部定義を生成しません。こうしたインライン定義が外部定義の代わりとして使われるか、それとも外部定義が使われるかは未規定ですので、インライン定義の関数を参照する場合は必ず別の翻訳単位に外部定義がなければなりません。

また、外部結合を持つ関数のインライン定義では、関数内部で静的記憶域期間を持つオブジェクトを定義することはできません。C++にはこのような制限はありませんでした。外部結合を持つインライン関数から内部結合を持つ識別子を参照できないのはC++と同じです。

extern 指定子を付けて関数の宣言を行った場合、その関数の定義がインライン関数として行われている場合でも外部定義が生成されます。当然のことながら、複数の翻訳単位で同じ関数の外部定義を生成させると衝突してしまいます。

inline void foo(void) { ... }
inline void bar(void) { ... }

extern void foo(void);  /* 外部定義を生成 */

int func(void)
{
  foo();
  bar();
}

上の例では、foo は外部定義が生成されますが、 bar は外部定義が生成されません。したがって、他の翻訳単位のどこかで、bar の外部定義が必要になります。

仮引数並びを省略した場合の振る舞い

C++では、関数の仮引数並びを省略した場合、void を指定したものとして扱われました。しかし、C言語では仮引数並びを省略すると、関数原型(プロトタイプ)がないものとみなされます。そして、関数呼出し式において、実引数の型や個数はまったくチェックされません。関数原型のない関数に渡された実引数は、可変個の実引数の場合と同様に、既定の実引数拡張が行われます。

関数原型は必須ではない

C++では、多重定義を解決しなければならない事情もあり、関数を呼び出す際は、先立って関数原型(プロトタイプ)が必要でした。しかし、C99では関数原型は必須ではありません。関数原型は必須ではありませんが、関数の宣言自体は必須です*2。つまり、仮引数並びがなくてもよいので、呼び出しに先立って宣言だけは行う必要があるのです。関数原型がない場合、返却型は宣言された型に、仮引数並びは省略されたものとみなされます。

double foo();  /* 関数原型なし */
 
int main(void)
{
  foo(1, 2.0, "abc");  /* 関数原型のない関数の呼出し */
  bar(1.0, 2L);        /* 宣言のない関数の呼出し */
  return 0;
}

ただし、可変個の実引数を受け取る関数を関数原型なしで呼び出した場合の動作は未定義になりますので、注意が必要です。

関数原型は、C言語にはもともとなかった仕様です。C90の標準化の際に、C++からC言語にバックポートされたものです。昔ながらのC言語の標準関数には、char 型や short 型や float 型を受け取るものがありませんが、これはもともと関数原型の仕様がなかったことが原因と考えられます。

分離形式

先ほど、関数原型はC++からバックポートされた仕様だと書きました。昔は関数原型の仕様がありませんでしたので、関数定義の際の仮引数の記述のしかたもちょっと違っていました。

int main(argc, argv)
  int argc;
  char *argv[];
{
  ...
  return 0;
}

のように、関数名の直後のカッコ内には仮引数名のみを並べ、関数本体のブロックの前に各仮引数の型を指定するための宣言を記述したのです。このような書き方を「分離形式」といいます。この仕様は、(主に互換性のために)標準Cでも有効です。古いCコンパイラとの互換性を保つ以外の理由で分離形式を使うことはまずありませんが、C言語ではこうした記述も可能だということは知っておくべきでしょう。

なお、分離形式で仮引数を記述した場合、これは関数原型にはなりません。こうした関数定義のあとで呼び出す場合、実引数の型や個数はチェックされませんし、既定の実引数拡張が行われることになります。

ちなみに、こうした分離形式に対して、C++と同じような関数原型を兼ねる仮引数の記述方法は「一括形式」といいます。


*1 C90には inline 関数指定子はありません。
*2 C90では 関数宣言も省略できます。その場合、返却値の型はint型とみなされます。C99では関数宣言がなければ未定義の動作になりますが、現実には、互換性のためか、C90と同様の動作になる場合が多いようです。

この記事のトラックバックURL:

http://www.kijineko.co.jp/trackback/775