この付録の内容は、次のとおりです。
ANSI C コンパイラでは、古い形式と新しい形式の両方の C コードを使用できます。次の -X (大文字の X であることに注意) オプションは ANSI C 規格への準拠の度合いを指定します。デフォルトのモードは -Xa です。
-Xa
(a = ANSI) ANSI C に K&R C との拡張互換性を持たせます。ANSI C に従って意味解釈を変更します。同じ言語構造に対して K&R C と ANSI C の意味解釈が異なる場合は、相違に関する警告を発行した上で、ANSI C に準拠した解釈を行います。これは、デフォルトのモードです。
-Xc
(c = conformance) ANSI C に最大限に準拠します。K&R C との拡張互換性はありません。ANSI C にない構文を使用しているプログラムに対して、エラーと警告を発行します。
-Xs
(s = K&R C) コンパイルした言語には、ANSI 以前の K&R C と互換性を持つすべての機能が含まれます。ANSI C と K&R C の間で動作が異なるすべての言語構文に対して、警告を発行します。
-Xt
(t = transition) ANSI C に K&R C との拡張互換性を持たせます。ANSI C に従った意味解釈の変更は行いません。同じ構文に対して K&R C と ANSI C の意味解釈が異なる場合は、相違に関する警告を発行した上で、K&R C に準拠した解釈を行います。
ANSI C での最大の変更点は、C++ 言語の機能である関数プロトタイプを使用できることです。各関数にパラメータの数と型を指定することにより、すべての通常のコンパイルにおいて、関数呼び出しごとに (lint のように) 引数とパラメータが検査されるだけではなく、引数が (代入だけで) 自動的に関数が期待する型に変換されます。プロトタイプを使用するように変更できる (また、変更すべき) 既存の C コードが非常に多く存在するため、ANSI C には、古い形式と新しい形式の関数宣言を併用する規則が含まれています。
まったく新しいプログラムを書くとき、ヘッダーでは、新しい形式の関数宣言 (関数プロトタイプ) を使用し、それ以外の C ソースファイルでは、新しい形式の関数宣言と関数定義を使用します。しかし、ANSI C 以前のコンパイラを持つマシンにコードを移植する可能性がある場合は、ヘッダーとソースファイルの両方で、マクロ __STDC__ (ANSI C コンパイルシステム専用に定義されている) を使用することをお勧めします。例については、「併用に関する考慮点」を参照してください。
同じオブジェクトまたは関数に対して 2 つの互換性のない宣言が同じスコープの中にある場合、ANSI C 準拠のコンパイラは診断メッセージを発行しなければなりません。すべての関数がプロトタイプで宣言および定義され、適切なヘッダーが正しいソースファイルにインクルードされている場合、すべての呼び出しは関数の定義に従うはずです。この取り決めによって、もっともありがちな C プログラミングの過りを防ぐことができます。
既存のアプリケーションで関数プロトタイプを利用したい場合、どれくらいのコードを変更するかによって、更新方法が異なります。
変更せずに再コンパイルする
コードを変更しなくても、-v オプションでコンパイラを実行すると、パラメータの型と数の不一致について警告が発行されます。
ヘッダーだけに関数プロトタイプを追加する
大域的な関数へのすべての呼び出しが診断の対象になります。
ヘッダーには関数プロトタイプを追加し、各ソースファイルの先頭には局所 (静的な) 関数に対する関数プロトタイプを追加する
関数へのすべての呼び出しが診断の対象になります。ただしこの方法では、ソースファイル内で局所関数ごとに 2 回インタフェースを入力する必要があります。
すべての関数宣言と関数定義を、関数プロトタイプを使用するように変更する
結果として受ける恩恵とそのための負荷を考えると、ほとんどの場合、上記の 2 か 3 が適切な選択だと言えるでしょう。ただしこれらを選択する場合、古い形式と新しい形式を併用するための規則を詳細に知っておく必要があります。
関数プロトタイプ宣言と古い形式の関数定義がともに機能するためには、両方が機能的に同じインタフェースを指定しなければなりません。つまり、ANSI C の用語を使用する「互換形式」を持っていなければなりません。
可変引数を持つ関数の場合は、ANSI C の省略記号と古い形式の varargs() 関数定義を併用することはできません。固定数のパラメータを持つ関数の場合、以前の実装で渡したとおりのパラメータの型を指定するだけです。
K&R C では、各引数は、呼び出された関数に渡される直前に、デフォルトの引数拡張に従って変換されました。このような拡張では、int より狭いすべての整数型を int サイズに拡張し、また、任意の float 引数を double に拡張するように指定されていたため、コンパイラとライブラリの両方が単純化されていました。関数プロトタイプを使用すると、よりわかりやすく表現できます。つまり、指定したパラメータの型が、そのまま、関数に渡されるパラメータの型となります。
したがって、既存の (古い形式の) 関数定義用に関数プロトタイプを書く場合、関数プロトタイプに次の型のパラメータは使用できません。
表 E-1 関数プロトタイプに使用できないパラメータ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 と ANSI C コンパイラ間の最大の非互換性の 1 つは、unsigned char と unsigned short を int 値に広げるための拡張規則です (「拡張: 符号なし保存と値の保持」を参照)。このような古い形式のパラメータにあたる型は、コンパイル時に使用するコンパイルモードによって異なります。
-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 *); /* . . . */ #endif static void del(p) MyType *p; { /* . . . */ } /* . . . */
以前の実装では、関数が期待するパラメータの型を指定できませんでした。しかし、ANSI C でプロトタイプを使用すれば、これを指定できます。printf() などの関数をサポートするために、プロトタイプの構文では特別な省略記号 (...) が終了記号として使用されます。実装によっては可変引数を処理するために特別なことを行う必要があるため、ANSI C では、すべての宣言とこのような関数などの定義が末尾に省略記号を含むべきであると規定しています。
パラメータの「...」部分には名前がないため、stdarg.h に含まれている特別なマクロにはこれらの引数へアクセスするための関数が含まれています。初期のバージョンではこのような関数は varargs.h に含まれている同様なマクロを使用しなければなりませんでした。
次に、これから書こうとする関数が errmsg() というエラーハンドラであると仮定します。この関数は void を返し、固定パラメータとして、エラーメッセージの詳細を指定する int だけを持つと仮定します。このパラメータの後には、ファイル名または行番号 (あるいは、その両方) を指定できます。そして、ファイル名または行番号の後には、(printf() のような) エラーメッセージのテキストを指定する書式と引数を指定できます。
初期のコンパイラで上記例をコンパイルするには、ANSI C コンパイルシステム専用に定義されたマクロ __STDC__ を多く使用する必要があります。したがって、適切なヘッダーファイルにおける関数の宣言は次のようになります。
#ifdef __STDC__ void errmsg(int code, ...); #else void errmsg(); #endif
errmsg() の定義を持つファイルは、古い形式と新しい形式を併用できます。まず、インクルードするヘッダーはコンパイルシステムによって異なります。
#ifdef __STDC__ #include <stdarg.h> #else #include <varargs.h> #endif #include <stdio.h>
その後で fprintf() と vfprintf() を呼び出すため、stdio.h をインクルードしています。
次は関数の定義です。識別子 va_alist と va_dcl は古い形式の varargs.h インタフェースの一部です。
void #ifdef __STDC__ errmsg(int code, ...) #else errmsg(va_alist) va_dcl /* 注: セミコロンなし */ #endif { /* more detail below */ }
古い形式の変数引数メカニズムでは固定パラメータを指定できないため、固定パラメータは、可変部分の前でアクセスされるように配置しなければなりません。また、パラメータの「...」部分に名前がないため、新しい va_start() マクロは 2 番目の引数 (「...」の直前にあるパラメータの名前) を持ちます。
拡張として、Sun ANSI C では、固定パラメータなしで関数を宣言および定義できます。
int f(...);
このような関数の場合、va_start() は 2 番目の引数を空にして呼び出す必要があります。
va_start(ap,)
{ va_list ap; char *fmt; #ifdef __STDC__ va_start(ap, code); #else int code; va_start(ap); /* 固定引数を抽出する */ code = va_arg(ap, int); #endif if (code & FILENAME) (void)fprintf(stderr, "¥"%s¥": ", va_arg(ap, char *)); if (code & LINENUMBER) (void)fprintf(stderr, "%d: ", va_arg(ap, int)); if (code & WARNING) (void)fputs("warning: ", stderr); fmt = va_arg(ap, char *); (void)vfprintf(stderr, fmt, ap); va_end(ap); }
va_arg() と va_end() マクロは両方とも古い形式と ANSI C バージョンで同様に動作します。va_arg() は ap の値を変更するため、vfprintf() への呼び出しを次のようにすることはできません。
(void)vfprintf(stderr, va_arg(ap, char *), ap);
マクロ FILENAME、LINENUMBER、および WARNING の定義は、おそらく、errmsg() の宣言と同じヘッダーに含まれています。
errmsg(FILENAME, "<command line>", "cannot open: %s¥n", argv[optind]);
C 規格の草案の「Rationale (論理的根拠)」節に、次のような情報があります。
QUIET CHANGE
A program that depends on unsigned preserving arithmetic conversions will behave differently, probably without complaint. This is considered to be the most serious change made by the Committee to a widespread current practice.
メッセージなしの変更
符号なし保存演算変換に依存するプログラムは、おそらくはメッセージを発行せずに、異なる動作を行います。これは、現在広く行われている慣習に対して委員会が行なったもっとも重大な変更であると考えられます。
この節では、この変更がコーディングにどのように影響するかを説明します。
K&R の『The C Programming Language (First Edition)』によると、unsigned は 1 つだけの型を指定していました。つまり、unsigned char、unsigned short、unsigned long はありませんでした。しかし、ほとんどの C コンパイラにはすぐにこれらの型が追加されました。unsigned long を実装せず、残りの 2 つだけを実装するコンパイラもあります。当然、式の中でこれらの新しい型が他の型と併用されている場合、実装によって異なる型拡張規則が適用されました。
ほとんどの C コンパイラでは、より簡単な規則「符号なし保存」が使用されています。つまり、unsigned 型を拡張する必要があるときは unsigned 型に拡張します。そして、unsigned 型が signed 型と混合されているときも、unsigned 型に拡張されます。
ANSI C では、「値の保持」という規則も指定されています。この規則では、拡張結果の型は、オペランドの型の相対的なサイズによって異なります。unsigned char または unsigned short を拡張するとき、int がより小さい型の値をすべて表現できる大きさである場合は、拡張結果の型は int になります。それ以外の場合、unsigned int になります。この「値の保持」規則に従えば、ほとんどの式が無難な演算結果になります。
ANSI C コンパイラは、移行モード (-Xt) または ANSI 以前のモード (-Xs) では、符号なし保存拡張規則を適用します。準拠モード (-Xc) および ANSI モード (-Xa) では、値保持拡張規則を使用します。
次のコードでは、unsigned char が int より小さいと仮定します。
int f(void) { int i = -2; unsigned char uc = 1; return (i + uc) < 17; }
上記コードを使用すると、-xtransition オプションを使用したときに、次の警告が発行されます。
line 6: warning: semantics of "<" change in ANSI C; use explicit cast
加算の結果の型は int (値保持) または unsigned int (符号なし保存) です。しかし、どちらの場合でもビットパターンは同じです。2 の補数を使用するマシンでは、次のようになります。
i: 111...110 (-2) + uc: 000...001 ( 1) =================== 111...111 (-1 または UINT_MAX)
このビット表現は、int では -1 に対応し、unsigned int では UINT_MAX に対応します。したがって、結果の型が int の場合、符号付き比較が使用され、「より小さいか」の答えは真になります。結果の型が unsigned int の場合、符号なしの比較が行われ、「より小さいか」の答えは偽になります。
キャストの加算を使用すると、2 つの動作のうち、どちらを希望するかを指定できます。
value preserving: (i + (int)uc) < 17 unsigned preserving: (i + (unsigned int)uc) < 17
コンパイラが異なれば同じコードに対する解釈も異なるため、この式は曖昧になる可能性があります。キャストの加算を使用することにより、コードが読みやすくなると同時に、警告メッセージも発行されなくなります。
同じ動作が、ビットフィールド値の拡張にも適用されます。ANSI C では、int または unsigned int ビットフィールド内のビットの数が int 中のビットの数よりも少ない場合、拡張される型は int です。それ以外の場合、拡張される型は unsigned int です。ほとんどの古い C コンパイラでは、明示的な符号なしビットフィールドの場合、拡張される型は unsigned int です。それ以外の場合は int です。
この場合も、キャストを使用することにより、曖昧になることを防ぐことができます。
次のコードでは、unsigned short と unsigned char の両方が int よりも狭いと仮定します。
int f(void) { unsigned short us; unsigned char uc; return uc < us; }
この例では、2 つの自動変数は int または unsigned int のどちらかに拡張されます。したがって、比較対象は符号なしになることも、符号付きになることもあります。しかし、どちらを選んでも結果は同じなので、警告は発行されません。
式と同様に、ある整数定数の型の規則も変更されました。K&R C では、接尾辞なしの 10 進定数の型は、その値が int に収まる場合だけ int でした。接尾辞なしの 8 進定数または 16 進定数の型は、その値が unsigned int に収まる場合だけ int でした。それ以外の場合、整数定数の型は long でした。したがって、値が結果の型に収まらないことがありました。ANSI C では、定数の型は、次のリストのうち、値を格納できる最初の型となります。
接尾辞なし 10 進数:
int、long、unsigned long
接尾辞なし 8 進数または 16 進数:
int、unsigned int、long、unsigned long
接尾辞 U 付き:
unsigned int、unsigned long
接尾辞 L 付き:
long、unsigned long
接尾辞 UL 付き:
unsigned long
ANSI C コンパイラで -xtransition オプションを使用するとき、定数の型規則によって式の動作が異なる場合は警告が発行されます。古い整数定数の型規則は、移行モード (-Xt) だけで適用されます。ANSI モード (-Xa) と準拠モード (-Xc) では、新しい規則が適用されます。
次のコードでは、int が 16 ビットであると仮定します。
int f(void) { int i = 0; return i > 0xffff; }
16 進定数の型は int (2 の補数を使用するマシン上で -1 の値を持つ) または unsigned int (65535 の値を持つ) のどちらかです。比較結果は、ANSI 以前モード (-Xs) と移行モード (-Xt) では真で、ANSI モード (-Xa) と準拠モード (-Xc) では偽です。
この場合も、キャストを適切に使用することにより、コードが読みやすくなり、警告も発行されなくなります。
-Xt, -Xs modes: i > (int)0xffff -Xa, -Xc modes: i > (unsigned int)0xffff or i > 0xffffU
接尾辞 U 文字は ANSI C の新しい機能であるため、古いコンパイラではおそらくエラーメッセージが生成されます。
以前のバージョンの C でもっとも不明確な仕様は、各ソースファイルを文字の集合から一連のトークンに変換して構文解析できるようにするまでの操作でしょう。具体的には、空白 (コメントも含む) の認識、連続した文字のトークン化、前処理指令行の処理、およびマクロの置換などがあります。しかし、これら操作の優先順位は保証されていませんでした。
ANSI C では、このような翻訳段階の順番が指定されています。
ソースファイル内のすべての 3 文字表記シーケンスが置換されます。ANSI C は、9 つの 3 文字表記シーケンスを持っています。これらのシーケンスはもともと文字セットの不完全な点を補うために導入されました。しかし、現在では、この 3 文字シーケンスは ISO 646-1983 文字セットに含まれない文字を指定するために使用されています。
3 文字シーケンス |
変換後の文字 |
3 文字シーケンス |
変換後の文字 |
---|---|---|---|
??= | # | ??< | { |
??- | ‾ | ??> | } |
??( | [ | ??/ | ¥ |
??) | ] | ??' | ^ |
??! | | |
ANSI C コンパイラは上記シーケンスを理解するはずです。しかし、これらのシーケンスを使用することはお勧めしません。-xtransition オプションを使用したとき、移行モード (-Xt) では、ANSI C コンパイラは 3 文字シーケンスを置換するたびに警告を発行します (コメント内でも)。たとえば、次の例を考えてください。
/* コメント *??/ /* コメントの続き? */
??/ はバックスラッシュになります。この文字とそれに続く改行は削除されます。結果として、次のようになります。
/* コメント */* コメントの続き? */
2 行目の最初の / は、コメントの終わりです。次のトークンは * です。
バックスラッシュと改行文字の組み合わせがすべて削除されます。
ソースファイルが前処理トークンと空白文字のシーケンスに変換されます。各コメントは必要最低限の空白文字で置換されます。
すべての前処理指令が処理され、すべてのマクロ呼び出しが置換されます。#include でインクルードされた各ソースファイルは、内容が指令行に置換される前の初期段階で実行されます。
すべてのエスケープシーケンス (文字定数と文字列リテラル) が解釈されます。
隣接する文字列リテラルが連結されます。
すべての前処理トークンが通常のトークンに変換されます。コンパイラはこれらのトークンを適切に構文解析して、コードを生成します。
すべての外部オブジェクトと関数参照が解釈処理され、最終的なプログラムになります。
以前の C コンパイラは、このような単純な順番に従いませんでした。また、これらの段階がいつ適用されるかも保証されていませんでした。コンパイラとは別のプリプロセッサが、マクロを置換して指令行を処理するときに、トークンと空白を認識していました。そして、コンパイラがプリプロセッサの出力を適切に再トークン化し、言語を構文解析し、コードを生成していました。
プリプロセッサ内のトークン化処理は必要に応じて行われる操作で、マクロ置換は (トークンベースではなく) 文字ベースの操作として行われます。したがって、前処理中にトークンと空白は大きく変動する可能性がありました。
2 つの方法の間には、いくつか異なる点があります。この節の後半では、マクロ置換中に発生する行の連結、マクロ置換、文字列化、およびトークンの連結によって、コードの動作がどのように変化するかを説明します。
K&R C では、バックスラッシュと改行を組み合わせの次の行には、指令、文字列リテラル、文字定数しか指定できませんでした。ANSI C ではこの概念が拡張され、バックスラッシュと改行、組み合わせの次の行に、あらゆるものを指定できるようになりました。K&R では 1 行は 1 行でしたが ANSIC では複数行組み合わせて 1 行とでき、これが論理行です。したがって、バックスラッシュと改行の組み合わせのどちら側にあるかによってトークンの認識が異なるコードは、期待どおりに動作しません。
ANSI C 以前には、マクロ置換処理については詳細に定義されていません。この曖昧さのために、処理系に多くの差が生まれました。したがって、明白な定数置換や簡単な関数のようなマクロよりも複雑なものを持つコードは、おそらく完全には移植できません。このマニュアルでは、古い C と ANSI C 間のマクロ置換実装の違いをすべて説明することはできません。ほとんどすべてのマクロ置換の結果は、前とまったく同じトークンの連続になります。ただし、ANSI C マクロ置換アルゴリズムは、古い C ではできなかったことができます。次の例を見てください。
#define name (*name)
この例は、すべての name を name 経由の間接参照で置換します。古い C プリプロセッサは数多くの括弧とアスタリスクを生成し、ときには、マクロの再帰についてエラーを生成する場合もあります。
ANSI C によるマクロ置換方法の主な変更は、マクロ置換演算子 # と ## のオペランド以外のマクロ引数が要求であること、置換トークンリストでの置換前に再帰的に展開することです。ただし、この変更によって、実際に生成されるトークンに差が生じることは滅多にありません。
ANSI C では、-xtransition オプションを使用するとき、次の例 (キ印) を使用すると、古い機能を使用しているという警告が生成されます。移行モード (-Xt と -Xs) の場合のみ、結果は以前のバージョンの C と同じになります。
K&R C では、次のコードは文字列リテラル「x y!」を生成しました。
#define str(a) "a!"キ str(x y)
したがって、プリプロセッサは、文字列リテラルと文字定数の内部で、マクロパラメータのように見える文字を検索していました。ANSI C はこの機能の重要性を認識していましたが、トークンの部分にこの操作を行うことはできませんでした。ANSI C では、上記マクロは文字列リテラル「a!」を生成します。ANSI C で以前の効果を得るためには、# マクロ置換演算子と文字列リテラルの連結を使用してください。
#define str(a) #a "!" str(x y)
上記コードは、2 つの文字列リテラル「x y」と「!」を生成し、連結した後、同じ「x y!」を生成します。
文字定数用の操作を完全に代用するものはありません。この機能の主な使用方法は次のようなものでした。
#define CNTL(ch) (037 & 'ch') キ CNTL(L)
(037 & 'L')
これは、ASCII の Control-L 文字と同じです。最良の解決策は、このマクロを次のように変更することです。
#define CNTL(ch) (037 & (ch)) CNTL('L')
このコードの方が読みやすく、式にも適用できるため、より使いやすくなっています。
K&R C では、2 つのトークンを連結するために、少なくとも 2 つの方法がありました。次の 2 つの呼び出しは、2 つのトークン x と l から 1 つの識別子 xl を生成します。
#define self(a) a #define glue(a,b) a/**/b キ self(x)1 glue(x,1)
ANSI C では、どちらの方法も使用できません。ANSI C では、上記の呼び出しは、両方とも 2 つの別々のトークン x と l を生成します。しかし、上記の呼び出しの内 2 番目の方法については、## マクロ置換演算子を使用すれば、ANSI C 用に書き換えることができます。
#define glue(a,b) a ## b glue(x, 1)
# と ## は、__STDC__ が定義されているときだけ、マクロ置換演算子として使用しなければなりません。## は実際の演算子のため、定義と呼び出しの両方で空白をより自由に使うことができます。
上記の古い形式の連結方法のうち、最初の方法を直接代用できる方法はありません。しかし、この方法では呼び出し時に連結の処理が必要なため、他の方法に比べてあまり使用されることはありませんでした。
キーワード const は C++ の機能の 1 つで、ANSI C に取り入れられました。ANSI C 委員会が類似キーワード volatile を導入したとき、「型修飾子」カテゴリが作成されました。このカテゴリは、現在でも、ANSI C のあいまいな部分として残っています。
const と volatile は識別子の型の一部であり、記憶クラスの一部ではありません。ただし、この部分は多くの場合、式の評価中にオブジェクトの値が取り出されるとき (正確には、右辺値が左辺値になるとき) に、型の一番上の部分から削除されます。これらの用語はプロトタイプ代入式「L=R」から来ています。この意味は、左側がオブジェクト (lvalue) を直接参照しなければならず、右側が値 (rvalue) であるだけでよいということです。したがって、lvalue である式だけが const または volatile (あるいは、その両方) で修飾できます。
型修飾子は型名と派生型を変更します。派生型は C の宣言の一部であり、何度も適用することによって、より複雑な型 (ポインタ、配列、関数、構造体、共用体) を構築できます。関数を除き、1 つまたは両方の型修飾子を使用すると、派生型の動作を変更できます。
const int five = 5;
これは、型が const int であり、値が正しいプログラムによって変更されないオブジェクトを宣言し、初期化します。キーワードの順番は C にとって重要ではありません。たとえば、次を見てください。この 2 つの宣言の効果は上記宣言と同じです。
int const five = 5; const five = 5;
const int *pci = &five;
この宣言は、型が const int へのポインタである (つまり、以前宣言されたオブジェクトを指している) オブジェクトを宣言します。ポインタ自身は修飾型を持ちません。つまり、ポインタは修飾型を指すため、プログラムの実行中に任意の int を指すように変更できます。pci を使用して、pci が指すオブジェクトを変更することはできません。このためには、次のようにキャストを使用します。
*(int *)pci = 17;
pci が実際に const オブジェクトを指す場合、このコードの動作は未定義です。
extern int *const cpi;
この宣言は、プログラム内のどこかに、型が int への const ポインタである大域オブジェクトの定義があることを意味します。この場合、正しいプログラムでは cpi の値は変更されません。しかし、cpi を使用して、cpi が指すオブジェクトを変更することはできます。上記宣言において、const が * の後にあることに注意してください。次の 2 つの宣言の効果は同じです。
typedef int *INT_PTR; extern const INT_PTR cpi;
上記の宣言は、次の宣言のように連結できます。この場合、オブジェクトの型は const int への const ポインタであると宣言されます。
const int *const cpci;
なお、キーワードとしては通常 const よりも readonly を選択するほうが便利です。このように const を解釈すると、次のような宣言は簡単に理解できます。
char *strcpy(char *, const char *);
この宣言では、2 番目のパラメータは文字値を読み取るためだけに使用され、最初のパラメータはその値が指す文字を上書きすることを意味しています。さらに、上記例の事実に関わらず、cpi の型は const int へのポインタです。したがって、実際に型が const int で宣言されたオブジェクトを指していないかぎり、その値が指すオブジェクトの値は別の方法で変更できます。
const の 2 つの主な使用法は、コンパイル時に初期化された大きな情報テーブルが未変更であると宣言することと、ポインタパラメータが指しているオブジェクトを変更しないことを指定することです。
最初の使用法では、同じプログラムの他の並行呼び出しが、プログラムのデータ部分を共有可能にします。つまり、データはメモリーの読み取り専用部分にあるため、この不変データを変更しようとする試みを、ある種類のメモリー保護障害で即座に検出できます。
2 番目の使用法では、実行中にメモリー障害が発生する前に、潜在的なエラーを見つけることができます。たとえば、ヌル文字を挿入できない文字列に対して、ある関数が一時的にヌル文字を挿入しようとした場合、その関数は、コンパイル時、ヌル文字を挿入できない文字列へのポインタが渡されたときに検出されます。
これまでの例ではすべて const を使用してきましたが、これは const が概念的に簡単であるためです。しかし、volatile はどのような意味でしょうか。volatile という言葉は「揮発性の」、つまりすぐに変わってしまうという意味を持ちます。そのためコンパイラでは、コード生成時にこのようなオブジェクトにアクセスするためのショートカットは行われません。ANSI C では、オブジェクト宣言するかどうかはプログラマの責任であると規定しています。
volatile は、通常、次の 4 つのオブジェクトに使用します。
メモリーにマップされた入出力ポートであるオブジェクト
複数の並行プロセス間で共有されるオブジェクト
非同期シグナルハンドラによって変更されるオブジェクト
setjmp を呼び出す関数中で宣言され、その値が setjmp への呼び出しとそれに対応する longjmp への呼び出し間で変更される自動記憶オブジェクト
最初の 3 つの例はすべて、特定の動作を行うオブジェクトのインスタンスです。つまり、その値は、プログラムの実行中の任意の時点で変更できます。したがって、外見上は無限ループに見えます。
flag = 1; while (flag) ;
これは、flag が volatile 修飾型を持つ間は有効です。おそらく、ある非同期イベントが将来 flag をゼロに設定することもあります。それ以外の場合、flag の値はループ本体内では変更されないため、コンパイルシステムは上記ループを、完全に flag の値を無視する本当の無限ループに変更できます。
4 番目の例は、setjmp を呼び出す関数に対して局所的な変数を含んでいるため、より複雑です。setjmp と longjmp の動作についての細字部分には、4 番目の例に一致するオブジェクトの値は保証されないという注記があります。もっとも望ましい動作を行うためには、setjmp を呼び出す関数と longjmp を呼び出す関数の間で、longjmp がすべてのスタックフレームを検査して、保存されたレジスタ値と比較することが必要です。スタックフレームは非同期的に作成される可能性があるため、この作業はより難しくなります。
自動オブジェクトを volatile 修飾型で宣言したとき、コンパイルシステムは、プログラマが書いたものと完全に一致するコードを生成します。したがって、このような自動オブジェクトに対する最新の値は常に、レジスタではなく、メモリー内に存在します。そして、longjmp が呼び出されたときに最新であることが保証されます。
最初に、ANSI C の国際化はライブラリ関数だけに影響がありました。しかし、国際化の最終段階 (複数バイト文字とワイド文字) は言語属性にも影響します。
アジア言語を使用するコンピュータ環境における基本的な難しさは、膨大な数の表意文字を入出力しなければならないことです。通常のコンピュータアーキテクチャの制限内で動作するためには、このような表意文字はバイトシーケンスとして符号化します。関連するオペレーティングシステム、アプリケーションプログラム、および端末は、このようなバイトシーケンスを個々の表意文字として認識します。さらに、すべてのこのような符号化によって、通常の 1 バイト文字を表意文字のバイトシーケンスと混合できます。個々の表意文字を認識することがどのくらい困難であるかは、使用する符号化方式によって異なります。
「複数バイト文字」は、ANSI C の定義では、使用する符号化方式の種類に関係なく、表意文字を符号化するバイトシーケンスを示します。すべての複数バイト文字は「拡張文字セット」に属します。通常の 1 バイト文字は、単に複数バイト文字の特別なケースです。符号化に必要な唯一の条件は、どの複数バイト文字もヌル文字を符号化の一部として使用できないということです。
ANSI C では、プログラムのコメント、文字列リテラル、文字定数、およびヘッダー名がすべて複数バイト文字のシーケンスであると規定されています。
符号化方式は 2 つの種類に分けることができます。1 つは、各複数バイト文字が自己識別性を持つ方式がそうです。つまり、どの複数バイト文字も簡単に 2 つの複数バイト文字の間に挿入できます。
もう 1 つは、特別なシフトバイトの存在が後続のバイトの解釈を変更する方式です。たとえば、あるキャラクタ端末で行描画モードを切り替えるために使用する方式です。このシフト状態依存符号化による複数バイト文字で書かれたプログラムの場合、ANSI C では、コメント、文字列リテラル、文字定数、およびヘッダー名の始まりと終わりがすべてシフトなし状態でなければならないと規定しています。
複数バイト文字の処理で不都合が発生した場合は、すべての文字を一定のバイト数またはビット数にすることで解決できることがあります。このような文字セットには何千または何万もの表意文字があるため、これらすべてを保持するには、大きさが 16 ビットまたは 32 ビットの整数値を使用しなければなりません (完全な中国語には 65,000 以上もの表意文字があります)。ANSI C には、拡張文字セットのすべてを保持するために十分な大きさを持つ実装定義の整数型として、typedef 名 wchar_t があります。
各ワイド文字には、それに対応する複数バイト文字があります (その逆もあります)。つまり、通常の 1 バイト文字に対応するワイド文字は、その 1 バイト値と同じ値を持つ必要があります (ヌル文字も含む)。しかし、マクロ EOF が char として表現できないように、マクロ EOF の値が wchar_t に格納できるかどうかは保証されていません。
ANSI C では、複数バイト文字とワイド文字を管理するために、5 つのライブラリ関数を規定しています。
表 E-3 複数バイト文字の変換関数
mblen() |
次の複数バイト文字の長さ |
mbtowc() |
複数バイト文字からワイド文字に変換する |
wctomb() |
ワイド文字から複数バイト文字に変換する |
mbstowcs() |
複数バイト文字の文字列からワイド文字の文字列に変換する |
wcstombs() |
ワイド文字の文字列から複数バイト文字の文字列に変換する |
これらの関数のすべての動作は、ロケールによって異なります。「setlocale() 関数」を参照してください。
アジア市場向けにコンパイルシステムを提供するベンダーが、より多くの文字列用関数を提供し、ワイド文字の文字列の処理が簡単になることが期待されます。しかし、ほとんどのアプリケーションプログラムでは、複数バイト文字とワイド文字間の変換は必要ありません。たとえば、複数バイト文字で読み取りと書き込みを行うプログラム (diff など) は、バイト単位での正確な一致を検査することだけが必要です。正規表現によるパターン一致を使用するより複雑なプログラム (grep など) は、複数バイト文字を理解する必要があります。しかし、複数バイト文字を理解する必要があるのは、正規表現を管理する関数だけです。プログラム grep 自身には、他の特別な複数バイト文字処理は必要ありません。
アジア言語環境においてプログラマがより柔軟にプログラムを組むために、ANSI C では、ワイド文字定数とワイド文字列リテラルを提供しています。この 2 つの形式は、直前に文字「L」の接頭辞が付くことを除き、通常の (ワイドでない) バージョンと同じです。
'x' 通常の文字定数
'¥' 通常の文字定数
L'x' ワイド文字定数
L'¥' ワイド文字定数
|abc¥xyz| 通常の文字列リテラル
L|abcxyz| ワイド文字列リテラル
複数バイト文字は、通常とワイドの両方のバージョンで有効です。表記文字 ¥ を生成するために必要なバイトシーケンスは符号化によって異なります。しかし、文字定数 '¥' が複数のバイトから構成される場合、'ab' が実装により定義されるのと同様に、その値は実装により定義されます。エスケープシーケンスを除き、通常の文字列リテラルには、引用符の間に指定されたものと同じバイト数 (指定したすべての複数バイト文字のバイト数も含む) が含まれます。
コンパイルシステムがワイド文字定数またはワイド文字列リテラルを検出したとき、各複数バイト文字は (mbtowc() 関数を呼び出したように) ワイド文字に変換されます。したがって、L'¥' の型は wchar_t です。abc¥xyz の型は長さが 8 の wchar_t の配列です。通常の文字列リテラルと同様に、各ワイド文字列リテラルは、値がゼロの余分な要素が追加されます。しかし、この要素は、ゼロの値を持つ wchar_t です。
通常の文字列リテラルが文字配列初期化の簡単な方法として使用できるのと同様に、ワイド文字列リテラルも wchar_t 配列を初期化するために使用できます。
wchar_t *wp = L"a¥z"; wchar_t x[] = L"a¥z"; wchar_t y[] = {L'a', L'¥', L'z', 0}; wchar_t z[] = {'a', L'¥', 'z', '¥0'};
上記の例では、3 つの配列 x、y、z と、wp が指す配列の長さは同じです。すべての配列は同じ値で初期化されます。
最後に、通常の文字列リテラルと同様に、隣接するワイド文字列リテラルは連結されます。しかし、通常の文字列リテラルとワイド文字列リテラルが隣接する場合、その動作は定義されていません。このような連結が受け付けられない場合、コンパイラはエラーを発行する必要はありません。
標準化作業の初期の段階において ANSI 規格委員会は、ライブラリ関数、マクロ、およびヘッダーファイルを ANSI C の一部として含むことを選択しました。この決定は本当に移植可能な C プログラムを書くために必要でしたが、一方では、ユーザーから ANSI C に対してもっとも否定的な意見 (つまり、予約名が多すぎる) が出る理由となりました。
この節では、さまざまな予約名のカテゴリとその予約名が必要な基本的な理由を示します。最後には、プログラムで予約名を使用しないようにするための規則を示します。
既存の実装に一致させるため、ANSI C 委員会は printf や NULL などの名前を選択しました。しかしその結果、C プログラムで自由に使用できる名前が減りました。
一方、標準化以前では、実装者は自由に新しいキーワードをコンパイラに追加し、新しい名前をヘッダーに追加できました。したがって、どのプログラムもリリースが変わるだけでコンパイルできるかどうか保証されず、まして、異なるベンダーの実装間では移植できませんでした。
その結果、委員会は ANSI C に準拠する実装では余分な名前を使用してはならない (特定の形式の名前を除く) という厳しい決定を下しました。この決定には、ほとんどの C コンパイルシステムがほぼ準拠できます。しかし、標準ヘッダーには、32 個のキーワードと約 250 個の名前が含まれています。どのキーワードまたは名前も特定の命名パターンに従っているとは限りません。
標準ヘッダーは次のとおりです。
表 E-4 標準ヘッダーassert.h | locale.h | stddef.h |
ctype.h | math.h | stdio.h |
errno.h | setjmp.h | stdlib.h |
float.h | signal.h | string.h |
limits.h | stdarg.h | time.h |
ほとんどの実装では、さらに多くのヘッダーが用意されています。しかし、ANSI C に厳密に準拠するプログラムが使用できるのは、上記ヘッダーだけです。
これらヘッダーの一部の内容については、他の規格ではわずかに異なります。たとえば、POSIX (IEEE 1003.1) は、fdopen を stdio.h で宣言するように指定しています。これら 2 つの規格が共存するために、POSIX では、このような追加の名前が存在することを保証するためには任意のヘッダーをインクルードする前にマクロ _POSIX_SOURCE を #define で定義しなければならないと規定しています。X/Open の『Portability Guide』によると、X/Open もこのマクロ方式を使用して拡張しています。X/Open のマクロは _XOPEN_SOURCE です。
ANSI C は、標準ヘッダーがそれ自身だけで完結し、べき等 (何度指定しても同じ) であることを要求しています。どの標準ヘッダーも、その前後で他のヘッダーを #include でインクルードする必要はありません。標準ヘッダーは何度 #include でインクルードしても、問題は発生しません。ANSI C 規格では、安全なコンテキストにおいてのみ、標準ヘッダーを #include でインクルードすることを要求します。したがって、ヘッダーで使用される名前は変更されないことが保証されます。
ANSI C 規格は、標準ライブラリについて、より多くの制限を実装に課しています。過去において、ほとんどのプログラマは UNIX システムでは独自の関数に read や write などの名前を使用しないように学びました。ANSI C では、予約されている名前だけを実装内の参照で使用するように規定しています。
したがって ANSI C 規格では、実装で使用する可能性があるすべての名前のサブセットが予約されています。この名前のクラスは下線で始まり、もう 1 つの下線または大文字の英字が続く識別子から構成されます。この名前のクラスは、次の正規表現に一致するすべての名前を含みます。
_[_A-Z][0-9_a-zA-Z]*
厳密には、プログラムがこのような識別子を使用する場合、その動作は未定義です。したがって、_POSIX_SOURCE (または、_XOPEN_SOURCE) を使用するプログラムの動作は未定義です。
ただし、動作がどれぐらい未定義なのかは異なります。POSIX 準拠の実装で _POSIX_SOURCE を使用する場合、ユーザーのプログラムの未定義の動作が特定のヘッダー内に追加された特定の名前から構成されていることと、受け入れられる標準にユーザーのプログラムが準拠していることは予測できます。ANSI C 規格におけるこの故意の抜け道により、実装は外見上互換性のない仕様に準拠できます。一方、POSIX 規格に準拠しない実装は、_POSIX_SOURCE などの名前に遭遇したとき、任意の方法で動作できます。
ANSI C 規格では、下線で始まる他のすべての名前が (局所的なスコープではなく) ヘッダーファイルにおける通常のファイルのスコープの識別子として、および構造体と共用体のタグとして使用するために予約されています。従来通り、_filbuf と _doprnt という名前の関数によりライブラリの隠れた部分を実装することはできます。
明示的に予約されたすべての名前に加えて、ANSI C 規格は、次の特定のパターンに一致する名前を (実装と将来の規格用に) 予約しています。
表 E-5 拡張用の予約名
ファイル |
予約名のパターン |
---|---|
errno.h | E[0-9A-Z].* |
ctype.h | (to|is)[a-z].* |
locale.h | LC_[A-Z].* |
math.h |
<現在の関数名> [fl] |
signal.h | (SIG|SIG_)[A-Z].* |
stdlib.h | str[a-z].* |
string.h | (str|mem|wcs)[a-z].* |
上記リストにおいて、大文字の英字で始まる名前はマクロで、関連するヘッダーがインクルードされるときだけ予約されます。残りの名前は関数を示し、大域的なオブジェクトや関数を指定する場合には使用できません。
ANSI C の予約名と衝突しないようにするためには、次の 4 つの簡単な規則に従う必要があります。
すべてのシステムヘッダーは、ユーザーのソースファイルの最初に #include でインクルードする (_POSIX_SOURCE または _XOPEN_SOURCE (あるいは、その両方) の #define 行がある場合は、その後でインクルードする)
下線で始まる名前は定義または宣言しない
すべてのファイルスコープのタグと通常名の最初の数文字では、下線または大文字の英字を使用する。stdarg.h または varargs.h 内の va_ 接頭辞に注意する
すべてのマクロ名の最初の数文字では、数字または小文字の英字を使用する。 errno.h を #include でインクルードする場合、E で始まるほとんどすべての名前は予約されています。
ほとんどの実装はデフォルトで標準ヘッダーに名前を追加しているため、これらの規則は一般的なガイドラインに過ぎません。
「複数バイト文字とワイド文字」の節では、標準ライブラリの国際化を紹介しました。この節では、国際化の影響を受けるライブラリ関数について説明し、これらの機能を利用するにはどのようにプログラムを書けばいいかのヒントを提供します。
C プログラムは常に、現在のロケール (国、文化、および言語に適切な規約を記述した情報の集まり) を持っています。ロケールは文字列の名前を持っています。標準化されたロケール名は、"C" と "" の 2 つだけです。どのプログラムも "C" ロケールから始まります。つまり、すべてのライブラリ関数は従来どおりに動作します。"" ロケールは、各処理系がプログラムの呼び出しに最適であると推測する規約セットです。"C" と "" の動作は同じになることもあります。他のロケールは各処理系によって提供されます。
実用性と便宜上の目的により、ロケールはカテゴリに分類されます。プログラムは、ロケール全体を変更することも、1 つまたは複数のカテゴリを変更することもできます。一般的に各カテゴリは、他のカテゴリが影響を与える関数とは関係なく、複数の関数に影響を与えます。したがって、一時的に 1 つのカテゴリを変更することにも意味があります。
setlocale() 関数は、プログラムのロケールとのインタフェースです。一般的に、国の規約を呼び出して使用するプログラムは、プログラムの実行パスの前のほうで、次のような呼び出しを行わなければなりません。
#include <locale.h> /*...*/ setlocale(LC_ALL, "");
LC_ALL は 1 つのカテゴリではなく、ロケール全体を指定するマクロであるため、この呼び出しによって、プログラムの現在のロケールが適切なローカルバージョンに変更されます。次に、標準的なカテゴリを示します。
表 E-6 標準的なカテゴリLC_COLLATE |
ソート情報 |
LC_CTYPE |
文字分類情報 |
LC_MONETARY |
通貨の出力情報 |
LC_NUMERIC |
数値の出力情報 |
LC_TIME |
日付と時刻の出力情報 |
上記の任意のマクロを setlocale() への最初の引数として渡すことによって、そのカテゴリを指定できます。
setlocale() 関数は、特定のカテゴリ (または、LC_ALL) に対する現在のロケールの名前を返します。2 番目の引数がヌルポインタの場合は、照会専用として機能します。したがって次のようなコードを使用すると、制限された期間だけロケール (または、その一部) を変更できます。
#include <locale.h> /*...*/ char *oloc; /*...*/ oloc = setlocale(LC_<カテゴリ名>, NULL); if (setlocale(LC_<カテゴリ名>, "new") != 0) { /* use temporarily changed locale */ (void)setlocale(LC_<カテゴリ名>, oloc); }
ほとんどのプログラムではこの機能は必要ありません。
変更が適切で可能である場合、既存のライブラリ関数はロケールに依存する動作を含むように拡張されました。これらの関数は、次の 2 つのグループに分類できます。
ctype.h ヘッダーで宣言される関数 (文字の分類と変換)
数値を出力可能な形式から内部的な形式に (または、その逆に) 変換する関数 (printf() や strtod() など)
すべての ctype.h 述語関数 (isdigit() と isxdigit() を除く) は、現在のロケールの LC_CTYPE カテゴリが "C" 以外の場合に、追加の文字に対してゼロでない (真の) 値を返すことができます。スペイン語ロケールでは isalpha は真になります。同様に、文字変換関数 tolower() と toupper() は、isalpha() 関数で識別される特別な英字を適切に処理できます。ctype.h 関数は、ほとんどの場合、文字引数による索引付きテーブル検索を使用して実装されるマクロです。これらの関数の動作を変更するには、テーブルを新しいロケールの値に再設定します。したがって、パフォーマンスに影響はありません。
出力可能な浮動小数点値を書き込んだり解釈したりする上記の関数は、現在のロケールの LC_NUMERIC カテゴリが "C" 以外の場合に、ピリオド (.) 以外の小数点文字を使用するように変更できます。千単位区切り型文字で数値を出力可能な形式に変換するための規定はありません。出力刷可能な形式から内部的な形式に変換するときにも、実装では、"C" 以外のロケールの場合に、このような追加の形式を受け入れることが許可されています。小数点文字を使用する関数は、printf() と scanf() のグループ、 atof()、および strtod() です。実装での定義を拡張できる関数は、atof()、atoi()、atol()、strtod()、strtol()、strtoul()、および scanf() のグループです。
新しい標準関数として、特定のロケールに依存する機能が追加されました。ロケール自身を制御する setlocale() 以外にも、ANSI C 規格には次の新しい関数が導入されました。
表 E-7 新しい標準関数
localeconv() |
数値/通貨の規約 |
strcoll() |
2 つの文字列の照合順序 |
strxfrm() |
照合のために文字列を変換する |
strftime() |
書式化された日付/時刻の変換 |
さらに、複数バイト関数 mblen()、mbtowc()、mbstowcs()、wctomb()、および wcstombs() があります。
localeconv() 関数は、現在のロケールの LC_NUMERIC と LC_MONETARY カテゴリに適切な、書式化された数値および通貨の情報に便利な情報を含む構造体へのポインタを返します。この関数は、動作が複数のカテゴリに依存する唯一の関数です。数値の場合、構造体は、小数点文字、千単位区切り文字、および区切り文字を置く場所を記述します。通貨値を書式化する方法を記述する構造体のメンバーは、他にも 15 個あります。
strcoll() 関数は、strcmp() 関数と似ていますが、現在のロケールの LC_COLLATE カテゴリに従って、2 つの文字列を比較するところが異なります。strxfrm() 関数は、変換後の 2 つの文字列を strcmp() に渡すと、変換前の 2 つの文字列を strcoll() に渡した場合に返される順番と似た順番が返されるように、文字列を別の文字列に変換します。
strftime() 関数は、struct tm に値を持つ sprintf() で使用される書式化と似た書式化と、さらに、現在のロケールの LC_TIME カテゴリに依存する日付と時刻の書式を提供します。この関数は、UNIX System V リリース 3.2 の一部としてリリースされた ascftime() 関数に基づいています。
C の設計において Dennis Ritchie が行なった選択の 1 つとして、式の中で数学的に交換可能で結合可能な演算子が隣接する場合、括弧が存在する場合でも、その式を再配置する権利をコンパイラに与えました。このことは、Kernighan と Ritchie 著の『プログラミング言語 C』の付録に明示的に記載されています。しかし、ANSI C は、この権利をコンパイラに与えませんでした。
この節では、上記 2 つの C の定義間の違いを説明します。また、次のコードにおける式文を考えることによって、式の副作用、グループ化、および評価の間の区別を明らかにします。
int i, *p, f(void), g(void); /*...*/ i = *++p + f() + g();
式の副作用とは、メモリーへの変更と、volatile 修飾オブジェクトへのアクセスのことです。上記式の副作用とは、i と p の更新と、関数 f() と g() 内に含まれる任意の副作用です。
式のグループ化とは、値を他の値や演算子と結合させる方法です。上記の式のグループ化は、主に加算を実行する順番です。
式の評価には、その結果の値を生成するために必要なすべてが含まれます。式を評価するためには、指定したすべての副作用が以前のシーケンスポイントから次のシーケンスポイントまでの間で発生しなければならず、指定した演算が特定のグループ化で実行されなければなりません。上記の式の場合、i と p の更新は、以前の文からこの式文の ; までの間に発生しなければなりません。関数への呼び出しは、以前の文からその戻り値が使用されるまでの間に、任意の順番で発生できます。特に、メモリーを更新する演算子には、演算の値が使用される前に新しい値を代入しなければならないという制約はありません。
上記の式では加算が数学的に交換可能で、また結合可能であるため、K&R C の再配置の権利が上記式に適用されます。通常の括弧と実際の式のグループ化を区別するために、左右の中括弧でグループ化を示します。この式の場合、次の 3 つのグループ化が考えられます。
i = { {*++p + f()} + g() }; i = { *++p + {f() + g()} }; i = { {*++p + g()} + f() };
上記すべてのグループ化は、K&R C の規則であれば有効です。さらに、たとえば、次のように式を書き換えた場合でも、上記すべてのグループ化は有効です。
i = *++p + (f() + g()); i = (g() + *++p) + f();
オーバーフローによって例外が発生するか、あるいは、オーバーフローで加算と減算が逆にならないアーキテクチャ上でこの式が評価される場合、加算の 1 つがオーバーフローしたとき、上記 3 つのグループ化の動作は異なります。
このようなアーキテクチャ上では、K&R C では、式を分割することによって強制的にグループ化するしか方法がありません。次に、上記 3 つのグループ化を強制的に行うために式を分割した例を示します。
i = *++p; i += f(); i += g(); i = f(); i += g(); i += *++p; i = *++p; i += g(); i += f();
ANSI C では、数学的に交換可能で結合可能であるが、対象となるアーキテクチャ上では実際にそうではない演算を再配置することは許可されていません。したがって、ANSI C の文法の優先度と結合規則では、すべての式のグループ化が完全に記述されています。つまり、すべての式は、構文解析されるとおりにグループ化されなければなりません。上記の式は、次の方法でグループ化されます。
i = { {*++p + f()} + g() };
このコードでもなお「f() が g() よりも前に呼び出されなければならない」、あるいは、「g() が呼び出されるよりも前に p が増分されなければならない」ということはありません。
ANSI C では、予想外のオーバーフローが発生しないように式を分割する必要があります。
ANSI C では、不充分な理解と不正確な表現のために、括弧の信頼性と括弧に従った評価について、間違って記述されることがしばしばあります。
ANSI C の式は構文解析で指定されるグループ化を持つため、括弧は、どのように式が構文解析されるかを制御する方法としてだけ機能します。つまり、式の自然な優先度と結合規則が括弧とまったく同じ重要さを持ちます。
i = (((*(++p)) + f()) + g());
グループ化と評価に与える影響は、括弧を使用しない場合と同じです。
K&R C の再配置規則には、いくつかの理由がありました。
再配置によって、より多くの最適化の機会が生まれること (たとえば、コンパイル時の定数折り畳み)
ほとんどのマシンにおいて、再配置によって整数型の式の結果が変わらないこと
すべてのマシンにおいて、いくつかの演算が数学的にも演算的にも交換可能で結合可能であること
ANSI C 委員会は、記述される対象アーキテクチャに適用されるときに、再配置規則は as if 規則のインスタンスになるものであると、最終的に確信しました。ANSI C の as if 規則は、有効な C プログラムの動作を変更しない限り、実装が必要に応じて抽象マシン記述から離れることを一般的に許可しています。
したがって、すべてのビット単位の 2 項演算子 (シフトを除く) は任意のマシンで再配置できます。これは、このような再グループ化を確認できる方法がないためです。2 の補数を使用するマシンでオーバーフローが発生しない場合は、いくつかの理由のため、乗算または加算を含む整数式は再配置できます。
したがって、C におけるこの変更は、ほとんどの C プログラマには重要な影響を与えません。
(C の当初から内在し)、C の基本的な部分であるがまだ真価を認められていない部分を正式なものとするために、ANSI C 規格は「不完全な型」を導入しました。この節では、不完全な型がどこで許可されるかと、なぜ便利であるかを説明します。
ANSI は C の型を、関数、オブジェクト、および不完全の 3 つに区分しました。関数型の定義は明白です。オブジェクト型は、サイズが不明なオブジェクトを除く、その他すべてのものを示します。ANSI C 規格は、明示されるオブジェクトのサイズが既知でなければならないことを指定するために、「オブジェクト型」を使用します。しかし、void 以外の不完全な型もオブジェクトを指すことは十分に理解してください。
不完全な型には、void、不特定長の配列、および不特定内容の構造体と共用体の 3 つの種類しかありません。型 void は、完成させることができない不完全な型であるという点で他の 2 つとは異なります。そして、特別な関数の戻り型とパラメータ型として機能します。
不完全な配列型を完全なものにするには、同じオブジェクトを示す同じスコープ内にある後続の宣言で、配列のサイズを指定します。同じ宣言でサイズが不明な配列 (不特定長の配列) が宣言および初期化されるとき、その配列は、宣言の終わりから初期化の終わりまでの間だけ、不完全な型になります。
不完全な構造体型または共用体型を完成させるには、同じタグの同じスコープ内にある後続の宣言で、構造体型または共用体型の内容を指定します。
不完全な型を使用できる宣言もありますが、完全なオブジェクト型が必要な宣言もあります。オブジェクト型を必要とする宣言は、配列要素、構造体または共用体のメンバー、および関数に局所的なオブジェクトです。他のすべての宣言は、不完全な型を許可します。特に、次の構造が許可されています。
不完全な型へのポインタ
不完全な型を返す関数
不完全な関数パラメータ型
不完全な型の typedef 名
関数の戻り型とパラメータ型は特別です。このような方法で使用される不完全な型 (void を除く) は、関数が宣言または呼び出されるときまでに完全にならなければなりません。void の戻り型は、値を返さない関数を指定します。また、void の単一のパラメータ型は、引数を受け入れない関数を指定します。
配列と関数のパラメータ型はポインタ型に書き換えられるため、配列のパラメータ型は外見上不完全ですが、実際には不完全ではありません。典型的な main の argv (つまり、char *argv[]) の宣言は、不特定長の文字ポインタの配列として、文字ポインタへのポインタとして書き換えられます。
ほとんどの式演算子では完全なオブジェクト型が必要ですが、例外が 3 つあります。単項 & 演算子、コンマ演算子の最初のオペランド、および ?: 演算子の 2 番目と 3 番目のオペランドです。ポインタのオペランドを受け入れるほとんどの演算子は、ポインタ演算が要求されない限り、不完全な型へのポインタも許可します。この中には、単項 * 演算子も含まれます。たとえば、次の例を見てください。
void *p
&*p は、この例を使用する有効な式の一部です。
なぜ不完全な型が必要なのでしょうか。void を除いて、C では他の方法で扱えない不完全な型の唯一の機能は、構造体と共用体の前方参照です。たとえば、2 つの構造体がお互いを指すポインタを必要とする場合、これを実現するためには、不完全な型を使用しなければなりません。
struct a { struct b *bp; }; struct b { struct a *ap; };
異なる形式のポインタや異なる種類のデータ型を持つ、強力な型依存プログラミング言語には、すべて上記のようなケースを処理するための方法が用意されています。
不完全な構造体型や共用体型には typedef 名の定義が役立ちます。データ構造が複雑な (お互いへのポインタを多数持つような) 場合は、構造体への typedefs のリストを前方に (中心となるヘッダーに) 指定することによって、宣言が簡単になります。
typedef struct item_tag Item; typedef union note_tag Note; typedef struct list_tag List; . . . struct item_tag { . . . }; . . . struct list_tag { List *next; . . . };
さらに、内容がプログラムの残りで使用できてはいけない構造体や共用体に対しては、内容なしのタグをヘッダーに宣言できます。プログラムの他の部分は、何の問題もなく不完全な構造体や共用体へのポインタを使用できます。ただし、そのメンバーは使用できません。
不特定長の外部配列は不完全な型として頻繁に使用されます。一般的に、配列の内容を使用するために、配列の大きさを知る必要はありません。
K&R C では (ANSI C の場合はさらに顕著ですが)、同じ要素を参照する 2 つの宣言を別のものとして扱うことができます。ANSI C は、このような「ある程度似ている」型を示すために、「互換型」という用語を使用します。この節では、この互換型と、2 つの互換型を結合した「複合型」を説明します。
C プログラムにおいて各オブジェクトまたは関数の宣言が 1 度しか許されていないのであれば、互換型は必要ないはずです。しかし、同じ要素を参照する複数の宣言を許可するリンク、関数のプロトタイプ、および分割コンパイルには、このような機能が必要です。複数の翻訳単位 (ソースファイル) 間では、型の互換性の規則は 1 つの翻訳単位内のものとは異なります。
各コンパイルでは別々のソースファイルを参照するため、分割コンパイル間の互換型に対して、ほとんどの規則の内容は次のように構造化されています。
一致するスカラー (整数、浮動小数点、およびポインタ) 型は、同じソースファイル内にある場合のように、互換性を持たなければならない。
一致する構造体、共用体、および列挙型のメンバー数は同じでなければならない。一致する各メンバーは (分割コンパイルという意味で) 互換型を持たなければならない (ビットフィールド幅も含む)。
一致する構造体のメンバーの順番は、同じでなければならない。共用体と列挙型のメンバーの順番は問題にならない。
一致する列挙型のメンバーの値は、同じでなければならない。
さらに、構造体、共用体、および列挙型のメンバーの名前 (名前なしメンバーに名前がないということ) も一致しなければなりません。しかし、それぞれのタグは必ずしも一致する必要はありません。
同じスコープ内の 2 つの宣言が同じオブジェクトまたは関数を記述するとき、この 2 つの宣言は互換型を指定しなければなりません。これら 2 つの型は次に、最初の 2 つと互換性を持つ、1 つの複合型に結合されます。複合型については後で説明します。
互換型は再帰的に定義されます。一番下は型指定子のキーワードです。これらの規則は、unsigned short は unsigned short int と同じであり、型指定子なしの型は int を持つ型であることを示します。他のすべての型は、派生元の型が互換性を持つときだけ、互換性を持ちます。たとえば、修飾子 const と volatile が同じであり、未修飾型が互換性を持つ場合、2 つの修飾型は互換性を持ちます。
2 つのポインタ型が互換性を持つためには、この 2 つのポインタが指す型が互換性を持ち、2 つのポインタが同じように修飾されていなければなりません。ポインタの修飾子は * の後に指定されることを念頭に置いて、次の例を見てください。
int *const cpi; int *volatile vpi;
上記 2 つの宣言は、同じ型 int を指すが修飾が異なる 2 つのポインタを宣言しています。
2 つの配列型が互換性を持つためには、この 2 つの配列の要素の型が互換性を持たなければなりません。両方の配列の型のサイズが指定されている場合は、両方のサイズも一致しなければなりません。つまり、不完全な配列型 (「不完全な型」を参照) は、他の不完全な配列型とも、サイズが指定されている配列型とも互換性を持ちます。
関数が互換性を持つためには、次の規則に従わなければなりません。
2 つの関数型が互換性を持つためには、その戻り型が互換性を持たなければなりません。どちらか、あるいは両方の関数型がプロトタイプを持つ場合、規則はより複雑になります。
プロトタイプを持つ 2 つの関数型が互換性を持つためには、(省略記号 (...) も含む) パラメータの数が同じで、対応するパラメータもパラメータ互換でなければなりません。
古い形式の関数定義がプロトタイプを持つ関数型と互換性を持つためには、プロトタイプの最後のパラメータが省略記号 (...) であってはなりません。プロトタイプの各パラメータは、デフォルトの引数拡張の適用後、対応する古い形式のパラメータとパラメータ互換でなければなりません。
古い形式の関数宣言 (定義ではない) が、プロトタイプを持つ関数型と互換性を持つためには、プロトタイプの最後のパラメータが省略記号 (...) であってはなりません。プロトタイプのすべてのパラメータは、デフォルトの引数拡張で影響を受けない型でなければなりません。
2 つの型がパラメータ互換になるためには、これら 2 つの型は、1 番上に修飾子があればそれが削除された後、そして、関数型または配列型が適切なポインタ型に変換された後に、互換性を持たなければなりません。
signed int は int と同じように動作します。ただし、ビットフィールドで通常の int が unsigned 動作を示す数になる場合を除きます。
また、列挙型は同じ整数型と互換性を持たなければなりません。移植可能なプログラムの場合、これは、列挙型が別の型であることを意味します。一般的に、ANSI C 規格はこのように列挙型を扱います。
2 つの互換型から 1 つの複合型への作成も再帰的に定義されます。不完全な配列型や古い形式の関数型を使用することにより、互換型をお互いに異なるようにできます。同様に、複合型の最も簡単に記述するには、元の両方の型 (元の型のすべての使用可能な配列サイズとすべての使用可能なパラメータリストも含む) と型の互換性を持たせればよいでしょう。