1990 ISO C 規格での最大の変更点は、C++ 言語の機能である関数プロトタイプを使用できることです。各関数にパラメータの数と型を指定することにより、すべての通常のコンパイルにおいて、関数呼び出しごとに (lint のように) 引数とパラメータが検査されるだけではなく、引数が (代入だけで) 自動的に関数が期待する型に変換されます。プロトタイプを使用するように変更できる (また、変更すべき) 既存の C コードが非常に多く存在するため、1990 ISO C 規格には、古い形式と新しい形式の関数宣言を併用する規則が含まれています。
まったく新しいプログラムを書くとき、ヘッダーでは、新しい形式の関数宣言 (関数プロトタイプ) を使用し、それ以外の C ソースファイルでは、新しい形式の関数宣言と関数定義を使用します。しかし、ISO C 以前のコンパイラを持つマシンにコードを移植する可能性がある場合は、ヘッダーとソースファイルの両方で、マクロ __STDC__ (ISO C コンパイルシステム専用に定義されている) を使用することをお勧めします。例については、「6.2.3 併用に関する考慮点」を参照してください。
同じオブジェクトまたは関数に対して 2 つの互換性のない宣言が同じスコープの中にある場合、ISO C 準拠のコンパイラは診断メッセージを発行しなければいけません。すべての関数がプロトタイプで宣言および定義され、適切なヘッダーが正しいソースファイルにインクルードされている場合、すべての呼び出しは関数の定義に従うはずです。この取り決めによって、起こりがちな C プログラミングの誤りを防ぐことができます。
既存のアプリケーションで関数プロトタイプを利用したい場合、どれくらいのコードを変更するかによって、更新方法が異なります。
変更せずに再コンパイルする
コードを変更しなくても、-v オプションでコンパイラを実行すると、パラメータの型と数の不一致について警告が発行されます。
ヘッダーだけに関数プロトタイプを追加する
大域的な関数へのすべての呼び出しが診断の対象になります。
ヘッダーには関数プロトタイプを追加し、各ソースファイルの先頭には局所 (静的な) 関数に対する関数プロトタイプを追加する
関数へのすべての呼び出しが診断の対象になります。ただしこの方法では、ソースファイル内で局所関数ごとに 2 回インタフェースを入力する必要があります。
すべての関数宣言と関数定義を、関数プロトタイプを使用するように変更する
結果として受ける恩恵とそのための負荷を考えると、ほとんどの場合、前述の 2 か 3 が適切な選択だと言えるでしょう。ただしこれらを選択する場合、古い形式と新しい形式を併用するための規則を詳細に知っておく必要があります。
関数プロトタイプ宣言と古い形式の関数定義がともに機能するためには、両方が機能的に同じインタフェースを指定しなければいけません。つまり、ANSI/ISO C の用語を使用する「互換形式」を持っていなければいけません。
可変引数を持つ関数の場合は、ANSI/ISO C の省略記号と古い形式の varargs() 関数定義を併用することはできません。 固定数のパラメータを持つ関数の場合、以前の実装で渡したとおりのパラメータの型を指定するだけです。
K&R C では、各引数は、呼び出された関数に渡される直前に、デフォルトの引数拡張に従って変換されました。 このような拡張では、int より狭いすべての整数型を int サイズに拡張し、また、任意の float 引数を double に拡張するように指定されていたため、コンパイラとライブラリの両方が単純化されていました。関数プロトタイプを使用すると、よりわかりやすく表現できます。つまり、指定したパラメータの型が、そのまま、関数に渡されるパラメータの型となります。
したがって、既存の (古い形式の) 関数定義用に関数プロトタイプを書く場合、関数プロトタイプに次の型のパラメータは使用できません。
char |
signed char |
unsigned char |
float |
short |
signed short |
unsigned short |
|
プロトタイプを書く際には、さらに 2 つの問題があります。typedef 名と、狭い unsigned 型の拡張規則です。
古い形式の関数内のパラメータが typedef 名で宣言されている場合 (off_t や ino_t など)、typedef 名がデフォルトの引数拡張によって影響を受ける型を指しているかどうかを確認することが重要です。前述の 2 つの typedef 名を例にすると、off_t は long です。したがって、関数プロトタイプで使用することは適切な使用方法です。しかし、ino_t は unsigned short であったため、プロトタイプで使用すると、古い形式の定義とプロトタイプが異なる互換性のないインタフェースを指定するため、診断メッセージが発行されます。
最後の問題は、unsigned short の代わりに何を使用するかです。K&R C と 1990 ANSI/ISO C コンパイラ間の最大の非互換性の 1 つは、unsigned char と unsigned short を int 値に広げるための拡張規則です (「6.4 拡張: 符号なし保存と値の保持」を参照)。このような古い形式のパラメータにあたる型は、コンパイル時に使用するコンパイルモードによって異なります。
-Xs と -Xt では unsigned int を使用する
-Xa と -Xc では int を使用する
最良の方法は、int または unsigned int のどちらかを指定するように古い形式の定義を変更して、一致する型を関数プロトタイプで使用することです。必要であれば、関数を入力したあとでも、より狭い型の値を局所変数に代入できます。
前処理によって影響を受ける可能性のあるプロトタイプでは、ID の使用に気を付けてください。次の例を考えてみましょう。
#define status 23 void my_exit(int status); /* 通常、スコープはプロトタイプで始まり、*/ /* プロトタイプで終わる */ |
関数プロトタイプは、狭い型を持つ古い形式の関数定義と併用できません。
void foo(unsigned char, unsigned short); void foo(i, j) unsigned char i; unsigned short j; {...} |
__STDC__ を適切に使用すれば、古いコンパイラと新しいコンパイラの両方で使用できるヘッダーファイルを作成できます。
header.h: struct s { /* . . . */ }; #ifdef __STDC__ void errmsg(int, ...); struct s *f(const char *); int g(void); #else void errmsg(); struct s *f(); int g(); #endif |
次の関数はプロトタイプを使用していますが、古いシステムでもコンパイルできます。
struct s * #ifdef __STDC__ f(const char *p) #else f(p) char *p; #endif { /* . . . */ } |
次の例は、更新したソースファイルです (選択肢 3 を使用したもの)。局所関数は古い形式の定義を使用していますが、新しいコンパイラ用にプロトタイプも含まれています。
source.c: #include “header.h” typedef /* . . . */ MyType; #ifdef __STDC__ static void del(MyType *); /* . . . */ static void del(p) MyType *p; { /* . . . */ } /* . . . */ |