第3回 関数

Cの関数に関する仕様は、細かな点でC++との違いがいろいろあります。C++ではできてCではできないこともあれば、CではできてC++ではできないこともあります。今回は、それらについて順番に解説していきます。なお、Cのバージョンによって異なる内容もありますので、ここではあくまでもC90を対象にしていることを念押ししておきます。

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

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

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

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

省略時実引数は使えない

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

インライン関数は使えない

C++では、inline 関数指定子を指定することで、関数のインライン置換をコンパイラに示唆することができました。しかし、Cではインライン関数を使うことはできません。Cではテンプレートを使うこともできませんので、ヘッダファイルで関数定義を記述することはCでは皆無であると考えてよいでしょう。

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

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

関数宣言は必須ではない

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

double foo(); /* 関数原型なし */

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

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

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

分離形式

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

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

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

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

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