Previous Next Contents Index Doc Set Home


ANSI C への移行

1


本書では次の移行事項について述べます。

基本モード 2 ページ
旧形式と新形式の関数の混用 4 ページ
可変する引数を持つ関数 11 ページ
拡張:符号なし保存と値保存 16 ページ
トークン化と前処理 24 ページ
const と volatile の使用方法 33 ページ
複数バイト文字とワイド文字 41 ページ
標準ヘッダーファイルと予約名 48 ページ
国際化 55 ページ
式中のグループ化と評価 61 ページ
不完全型 67 ページ
互換型と複合型 72 ページ


基本モード

ANSI C コンパイラでは、旧形式と新形式の両方の C コードが許可されます。以下の -X (大文字であることに注意) オプションにより、ANSI C 規格に対する準拠のレベルを選択することができます。デフォルトは、-Xa モードです。

-Xa

(a = ANSI) ANSI C に加えて K&R C との互換性を持たせます。ただし ANSI C による意味上の変更を受けます。K&R C と ANSI C が同じ構造の文に対して異なる意味を持つ場合、コンパイラはこの不一致について警告し、ANSI C の解釈を採用します。これは、デフォルトモードです。

-Xc

(c = conformance) K&R C との互換性を維持せず、ANSI 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 の解釈を採用します。


旧形式と新形式の関数の混用

C 言語に対する ANSI C の最も大きな変更は、C++ 言語から借りてきた関数プロトタイプです。関数ごとにその引数の数と型を指定すると、コンパイルするたびに各関数呼び出しに対する引数や引数のチェックが行われる (lint と同様) だけでなく、代入時に引数はその関数が予想する型に自動的に変換されます。ANSI C には旧形式と新形式の関数宣言の混用を制御する規則があります。プロトタイプを使用するために変換が必要な既存の C コードが数多く存在するからです。

新規コードの作成

新しくプログラムを書く場合、ヘッダーファイルでは新形式の関数宣言 (関数プロトタイプ) を使用し、ヘッダーファイル以外の C ソースファイルでは新形式の関数宣言と関数定義を使用します。ただし、ANSI C 以前のコンパイラを持つマシンにコードを移植する可能性がある場合は、ヘッダーファイルとソースファイルの両方においてマクロの __STDC__ (ANSI C コンパイラシステム専用に定義) を使用することをお勧めします。マクロの __STDC__ の例は、この項の後半を参照してください。

あるスコープ内に、 1 つのオブジェクトや関数に対して互換性を持たない 2 つの宣言が存在すると、ANSI C に準拠するコンパイラは常に診断を発行します。すべての関数がプロトタイプで宣言および定義されていて、該当するヘッダーファイルが正しいソースファイルにインクルードされていると、呼び出しはすべて関数の定義と一致します。これにより、最も多い C プログラミング上のミスの 1 つをなくすことができます。

既存コードの更新

既存のアプリケーションで関数プロトタイプを有効に利用したい場合、どのくらいコードを変更するかによって、次のような更新の方法が考えられます。

1. 変更せずに再コンパイルする。

コーディングの変更がなくても、コンパイラは -v オプションで呼び出されると引数の型と数における不一致について警告します。

2. ヘッダーファイルだけに関数プロトタイプを追加する。

大域関数に対するすべての呼び出しが対象になります。

3. 関数プロトタイプをヘッダーファイルに追加し、ソースファイルをその局所 (静的) 関数に対する関数プロトタイプで始める。

関数に対するすべての呼び出しが対象になりますが、これには各局所関数に対するインタフェースをソースファイルに 2 度入力することが必要です。

4. すべての関数宣言と関数定義を変更して、関数プロトタイプを使用する。

ほとんどのプログラマにとっては、費用と使いやすさの両面で 2 と 3 の方法が最善策になるでしょう。ただしこれらを選択するには、新旧形式を混用する際の規則を熟知している必要があります。

混用に関する留意点

関数プロトタイプ宣言を旧形式の関数定義で使用するには、どちらも機能的に同一のインタフェースを指定するか、または ANSI C に準拠した互換タイプを持つ必要があります。

引数の数が可変する関数では、ANSI C の省略記号の表記と旧形式の varargs() 関数定義は混用できません。引数の数が固定した関数の場合は、以前の実装で渡されたとおり引数の型を指定するだけです。

K&R C ではデフォルトの引数拡張規則に従い、それぞれの引数は、呼び出された関数に渡される直前に変換されていました。これらの引数拡張では、int より短い整数型はすべて int サイズに拡張され、float 引数は double に拡張されるように指定しました。これにより、コンパイラとライブラリの両方が簡素化されました。関数プロトタイプはさらに表現力があるため、指定された型の引数が関数に渡されます。したがって、既存 (旧形式) の関数定義に対して関数プロトタイプを書く場合、以下のどの型を持つ関数プロトタイプにも引数があってはなりません。

char signed char unsigned char float
short signed short unsigned short  

プロトタイプを書く上での複雑な問題がまだ 2 つ残っています。1 つは typedef 名と、もう 1 つは短い符号なし型のための拡張規則です。

旧形式の関数の引数を off_tino_t といった typedef 名で宣言している場合は、typedef 名がデフォルトの引数拡張の対象となる型を指定しているかどうかを知ることが必要です。off_tlong 型なので関数プロトタイプで使用するのが適切です。しかし ino_tunsigned short 型なので、関数プロトタイプで使用するとコンパイラが診断 (おそらくは致命的診断) を出すことになります。これは、旧形式の定義と関数プロトタイプが互換性を持たない別々のインタフェースを指定するためです。

もう 1 つの複雑な点は、unsigned short の代わりに何を使用するべきかということです。K&R C と ANSI C コンパイラの間で最も大きな違いは、unsigned charunsigned shortint 値に拡張するための拡張規則です (16 ページの「拡張:符号なし保存と値保存」を参照してください)。このような旧形式の引数に一致する引数型は、コンパイルするときのコンパイラモードに依存します。-Xs-Xt には unsigned int を、また -Xa-Xc には int をそれぞれ使用します。

この場合は旧形式の定義を変更して intunsigned int のどちらかを指定し、機能プロトタイプにおいて一致する型を使用することが最善と思われます (関数に入った後、必要に応じてより短い型を持つ局所変数にその値を代入することができます)。

プリプロセッサ (前処理系) の影響を受ける可能性のあるプロトタイプでの識別子の使用には注意してください。以下の例を考慮してください。

#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() の定義を含むファイルが旧形式と新形式の両方に存在するということです。まず先頭にヘッダーファイルをインクルードします。インクルードするべきヘッダーファイルは、コンパイラシステムに依存します。後で fprintf()vfprintf() を呼び出すので、stdio.h もインクルードしています。

#ifdef __STDC__
#include <stdarg.h>
#else
#include <varargs.h>
#endif
#include <stdio.h>

次に来るのは関数の定義です。識別子の va_alistva_dcl は旧形式の varargs.h インタフェースの一部です。

void
#ifdef __STDC__
errmsg(int code, ...)
#else
errmsg(va_alist) va_dcl /* 注:セミコロンなし! */
#endif
{
    /* 詳細は以下 */
}

旧形式の可変引数のしくみでは固定引数部分を指定できなかったので、可変部分の前に固定引数にアクセスするよう設定する必要があります。また、引数の "..." 部分に名前がないため、新しい va_start() マクロの 2 番目の引数には "..." の直前の引数名 (この例では code) を指定します。

拡張機能として、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);

FILENAMELINENUMBERWARNING といったマクロに対する定義は、通常 errmsg() の宣言と同じヘッダーファイルに含まれます。

たとえば errmsg() への呼び出しは、次のようにします。

errmsg(FILENAME, "<command line>", "cannot open: %s\n",
argv[optind]);


拡張:符号なし保存と値保存

次の内容は C 規格の草案に付属する Rationale (理由説明) セクションに示されています。

静かなる変更

符号なし保存算術変換に依存するプログラムは、おそらく警告を出さず、異なる動作をします。これは現在広まっている慣行に対して委員会が行なった最も重大な変更であると考えられます。

本節では、上記の変更が従来のコードにどのような影響を与えるかを検討します。

背景

K&R (『プログラミング言語 C』― 第 1 版) によれば、unsigned は正確に 1 つの型を指定していました。unsigned charunsigned shortunsigned long は存在しませんでしたが、ほとんどの C コンパイラはその後すぐにこれらを追加しました。一部のコンパイラは unsigned long を実装しませんでしたが、他の 2 つは含まれていました。当然これらの実装では、式の中で新しい型と他の型が混用された場合に、型拡張の別の規則を採用しました。

ほとんどの C コンパイラでは、さらに単純な規則、すなわち「符号なし保存」が使用されました。符号なし型を拡張する必要がある場合は、符号なし型に拡張されます。符号なし型と符号付き型が混在する場合、結果は符号なし型になります。

ANSI C により規定されたもう 1 つの規則は「値保存」というもので、結果の型はオペランドの型の相対的なサイズに依存します。unsigned charunsigned short を拡張する場合、int が小さいほうの型のすべての値を表わすのに十分大きければ、結果の型は int になります。それ以外の場合、結果の型は unsigned int になります。値保存規則では、ほとんどの式について最も妥当な算術結果が得られます。

コンパイラ動作

ANSI C コンパイラは、移行モードと ANSI 以前のモード (-Xt-Xs) でのみ符号なし保存拡張を使用します。他の 2 つのモード、すなわち適合モード (-Xc) と ANSI モード (-Xa) では、値保存の拡張規則が使用されます。

例 1:キャストを使用する

以下のコードでは、unsigned charint より小さいと想定しています。

int f(void)
{
    int i = -2;
    unsigned char uc = 1;

    return (i + uc) < 17;
}

この結果、-xtransition オプションを使用している場合は、コンパイラは次のような警告を出します。

line 6: 警告: ANSI C では "<" の意味が変わります。明示的なキャストを使用してください。

加算結果の型には、int (値保存) と unsigned int (符号なし保存) がありますが、ビットパターンはこれら 2 つの間で変化しません。2 の補数マシンでは、以下のようになります。

    i:    111...110 (-2)
+   uc:   000...001 ( 1)
===================
          111...111 (-1 または UINT_MAX)

このビット表現は int では -1 に、unsigned int では UINT_MAX に対応します。したがって、結果が int であれば符号付き比較が使用されて「より小さい」の評価は真になり、結果が unsigned int であれば符号なし比較が使用されて「より小さい」の評価は偽となります。

キャストを追加すると、2 つの動作のどちらを希望するかを指定するのに役立ちます。

値保存:
    (i + (int)uc) < 17

符号なし保存:
    (i + (unsigned int)uc) < 17

1 つのコードに対して受け取られる意味がコンパイラによって異なるため、この式はあいまいです。キャストの追加は、警告メッセージの削除と同じくらいユーザーにとって便利なものです。

ビットフィールド

ビットフィールド値の拡張にも同じ状況が考えられます。ANSI C では、intunsigned int のビットフィールドにおけるビット数が int のビット数に満たない場合、拡張される型は int になります。それ以外の場合、拡張される型は unsigned int になります。古い C コンパイラのほとんどでは、明示的に符号のないビットフィールドに対して拡張される型は unsigned int であり、それ以外の場合は int です。

キャストを同様に使用すれば、あいまいな状況をなくすことができます。

例 2:同じ結果になる

以下のコードでは、unsigned shortunsigned char の両方とも int より短いと想定します。

int f(void)
{
    unsigned short us;
    unsigned char uc;

    return uc < us;
}

この例では、自動的に intunsigned int のどちらかに拡張されるので、比較は符号なしと符号付きのどちらで行われるか定まりません。しかし、これら 2 つの選択の結果は同一であるため、C コンパイラは警告を出しません。

整数定数

式の場合と同様に、特定の整数定数の型についての規則が変更になりました。K&R C では、接尾辞なしの 10 進定数はその値が int に適合する場合だけ int 型であり、接尾辞なしの 8 進定数や 16 進定数は、その値が unsigned int に適合する場合だけ int 型でした。それ以外の場合、整数定数は long 型でした。そのため値が結果の型に適合しないことがありました。ANSI C では、定数の型は次のリスト中で最初に適合する型になります。

接尾辞なしの 10 進:

intlongunsigned long

接尾辞なしの 8 進または 16 進:

intunsigned intlongunsigned long

U 接尾辞付き:

unsigned intunsigned long

L 接尾辞付き:

longunsigned long

UL 接尾辞付き:

unsigned long

ANSI C コンパイラは、-xtransition オプションを使用している場合は、定数の型を決める規則に従って動作が変わる式について警告を出します。古い整数定数型の規則は、移行モードでのみ使用します。ANSI モードと適合モードは、新しい規則を使用します。

例 3:整数定数

以下のコードで、int が 16 ビットであると仮定してください。

int f(void)
{
    int i = 0;

    return i > 0xffff;
}

16 進定数の型は int (2 の補数マシンにおいて -1 という値を持つ) か、unsigned int (65535 という値を持つ) のどちらかなので、比較は -Xs-Xt モードでは真となり、-Xa-Xc モードでは偽となります。

ここでも以下のようにコードを適切なキャストによって明確にすれば、ANSI C コンパイラは警告を出さなくなります。

-Xt, -Xs モードで:
    i > (int)0xffff

-Xa, -Xc モードで:
    i > (unsigned int)0xffff
            または
    i > 0xffffU

接尾辞文字 U は ANSI C の新しい機能であり、旧コンパイラではエラーメッセージが出ることがあります。


トークン化と前処理

C の以前のバージョンで最も不明確と思われる点に、各ソースファイルを文字の集合から一連のトークンへ変換して構文解析の準備を行うという操作があります。この操作には、空白 (注釈を含む) の認識、連続文字のトークン化、前処理指令行の処理、マクロの置換などが含まれますが、各処理の順序付けはまったく保証されていませんでした。

ANSI C 翻訳段階

以下に示す翻訳段階の順序は ANSI C により規定されています。

1. ソースファイルにおけるすべての 3 文字表記シーケンスが置き換えられます。

ANSI C には 9 個の 3 文字表記シーケンスがあり、これらは欠陥のある文字セットの救済策としてのみ考案されたもので、ISO 646-1983 文字セットにない文字を表現する 3 文字の列です。

表 1-1 3 文字表記シーケンス

3 文字表記シーケンス 変換結果 3 文字表記シーケンス 変換結果
??= # ??< {
??- ~ ??> }
??( [ ??/ \
??) ] ??' ^
??! |    

これらのシーケンスは ANSI C コンパイラにより解釈されますが、使用されないことをお勧めします。ANSI C コンパイラでは、-xtransition オプションを使用していて、移行 (-Xt) モード中に 3 文字を置き換える場合 (注釈を含む) は、ユーザーに警告します。

/* comment *??/
/* still comment? */

この例では ??/ はバックスラッシュになります。この文字とその次の復帰改行 (newline) は削除されます。結果の文字は次のようになります。

/* comment */* still comment? */

2 行目の最初のスラッシュ (/) は注釈の終了記号です。次のトークンは * です。

2. すべてのバックスラッシュと復帰改行文字の組み合わせが削除されます。

3. ソースファイルは前処理トークンと空白シーケンスに変換されます。1 つの注釈行は 1 つのスペース文字に置き換えられます。

4. すべての前処理指令が処理され、マクロの置換が行われます。#include 指令は指定されたソースファイルに対して、その内容が指令行と置き換わる前に段階 1 〜 4 を実行します。

5. 文字定数と文字列リテラルにおけるすべてのエスケープシーケンスが解釈されます。

6. 隣接する文字列リテラルが連結されます。

7. すべての前処理トークンが通常のトークンに変換されます。コンパイラはこれらを正しく構文解析し、コードを生成します。

8. すべての外部オブジェクトと関数参照が解決され、最終的なプログラムになります。

旧 C 翻訳段階

以前の C コンパイラはこのような段階の単純な順序には従わず、これらの段階が適用された場合の保証もありませんでした。独立した前処理プログラムは、マクロを置換して指令行を処理するのと同時にトークンと空白を認識しました。コンパイラ本体は出力を完全に再びトークン化し、言語を構文解析してコードを生成しました。

前処理プログラム内でのトークン化プロセスにおいて、マクロ置換は文字単位 (トークン単位ではない) の操作として行われたので、トークンと空白は前処理中に非常に多くのバリエーションを持つことがありました。

これら 2 つの方法から生じる相違点は多数存在します。本節の残りの部分では、行の継ぎ合わせ、マクロ置換、マクロ置換中に起こる文字列化およびトークン貼付けにより、コードの動作がどのように変化するかについて述べています。

論理ソース行

K&R C では、バックスラッシュと復帰改行の組み合わせは、指令、文字列リテラル、または文字定数を次の行に続ける手段としてのみ許可されています。ANSI C ではこの機能が拡張され、バックスラッシュと復帰改行の組み合わせの後はどんな行でも次の行に続けられるようになりました。これが論理ソース行です。したがって、バックスラッシュと復帰改行の組み合わせの両側にあるトークンを別々に認識して得られるコードは、通常とは異なる動作をとることがあります。

マクロ置換

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 つの方法がありました。以下の例における呼び出しでは、どちらも x1 の 2 つのトークンから単一の識別子 x1 が生成されました。

#define self(a) a
#define glue(a,b) a/**/b 
self(x)1
glue(x,1)

ここでも、ANSI C はどちらの方法も許可していません。ANSI C では、上記のどちらの呼び出しでも x1 の 2 つのトークンが生成されます。上記の 2 つの方法のうち後者は、マクロ置換演算子の ## を使用することによって次のように ANSI C 用に書き直すことができます。

#define glue(a,b) a ## b
glue(x, 1)

### は、__STDC__ が定義されている場合にのみマクロ置換演算子として使用してください。## は実際の演算子なので、定義と呼び出しのどちらも空白の扱い方は自由です。

これら 2 種類の旧形式の貼付け方式のうち、前者は呼び出し時に貼付けの負担をかけるためほとんど使用されませんでした。


const と volatile の使用方法

予約語の const は ANSI C へ引き継がれた C++ 機能の 1 つです。類似の予約語 volatile が ANSI C 委員会により考案されたとき、「型修飾子」というカテゴリが発見されました。これはまだ ANSI C のあいまいな部分になっています。

左辺値専用の型

constvolatile は識別子の型の一部であって、識別子の記憶クラスではありません。しかし、これらはオブジェクトの値が式の評価において読み出された時に左辺値が右辺値になると、多くの場合は型の最上位部分から削除されます。これらの用語は、代入 "L=R" でよく使用されることに由来しています。左側はまだオブジェクト (左辺値) を直接参照しなければなりませんが、右側は値 (右辺値) であるだけでよいからです。したがって、左辺値となっている式だけが constvolatile、またはその両方によって修飾されます。

派生型における型修飾子

型修飾子は、型名と派生型を変更することができます。派生型というのは C の宣言の一部であるポインタ、配列、関数、構造体、および共用体のことです。これらはさらに複雑な型を構築するために何度も適用できます。関数を除く一方もしくは両方の型修飾子を使用して、派生型の動作を変更することができます。例を以下に示します。

const int five = 5;

これは const int 型を持つオブジェクトを宣言し、初期化します。この値はプログラムが正しければ変更されません (予約語の順序は C 言語にとって重要ではありません。たとえば、以下の 2 つの宣言は上記の宣言と事実上同一です)。

int const five = 5;



const five = 5;

次の宣言は const int へのポインタ型を持つオブジェクトを宣言し、このオブジェクトは直前に宣言したオブジェクトを最初に指します。

const int *pci = &five;

このポインタそのものは修飾された型を持っていません。ポインタは修飾された型を指し、プログラムの実行中にどの int でも指すように変更することができます。以下のようにキャストを使用しない限り、pci を使用してこのポインタが指すオブジェクトを変更することはできません。pci が実際に const オブジェクトを指す場合は、このコードの動作は未定義です。

*(int *)pci = 17;

以下の宣言は、int に対する const ポインタ型を持つ大域オブジェクトの定義が、プログラムのどこかに存在するということを表わしています。

extern int *const cpi;

この場合、cpi の値はプログラムが正しければ変更されませんが、それが指すオブジェクトを変更する際に使用することができます。const は上記の宣言中、* の後に来る点に注意してください。以下の一対の宣言は同じ効果があります。

typedef int *INT_PTR;
extern const INT_PTR cpi;

これらの宣言は以下の宣言のように結合することができます。この場合、オブジェクトは const int に対する const ポインタ型を持つと宣言されています。

const int *const cpci;

const は "読み取り専用" を意味する

結果的に、予約語 const の名前は readonly (読み取り専用) の方が適しているかもしれません。このように const を理解すれば、以下のような宣言では 2 番目の引数が文字の値を読み取るだけなのに対し、最初の引数はそれが指す文字を上書きする、ということを意味することがわかります。

char *strcpy(char *, const char *);

さらに上の例で cpci の型は const int へのポインタであるにもかかわらず、それが指すオブジェクトの値を変更することができます (それが const int 型で宣言されたオブジェクトを実際に指していない場合に限ります)。

const の使用例

const の主な使用方法は 2 つあります。1 つは、コンパイル時の大きな情報初期化テーブルを変更なしとして宣言する方法で、もう 1 つはポインタ引数がそれらの指すオブジェクトを変更しないと指定する方法です。

最初の使用方法では同じプログラムの他の並列呼び出し時にプログラムが使用するデータの一部を共有することができます。この不変のデータを変更しようとすると、そのデータがメモリーの読み取り専用部分に常駐しているために、ある種のメモリー保護違反によりすぐに検出されます。

2 番目の使用方法は、稼働中にメモリーアクセス違反を起こす前に、潜在的なエラーの場所を探し出すのに最も役立ちます。たとえば、文字列の中央に null 文字を一時的に挿入する関数に、そのように変更できない文字列へのポインタが渡されていると、コンパイル時に検出されます。

volatile

これまでの説明や例では、内容をわかりやすくするためにすべて const を使用してきました。では、volatile とは何でしょう。コンパイラの作成者にとって volatile は 1 つの意味、すなわち volatile 修飾子を持つオブジェクトにアクセスするためのコードの最適化を抑止するという意味しか持ちません。ANSI C では、volatile 修飾子で特殊な性質を持つオブジェクトを宣言するのはプログラマの役割です。

volatile の使用例

volatile オブジェクトの一般的な例を 4 つ示します。

1. メモリーに配置された入出力ポートであるオブジェクト。

2. 複数の並列プロセス間で共有されるオブジェクト。

3. 非同期シグナルハンドラにより変更されるオブジェクト。

4. setjmp を呼び出し、その値が setjmp への呼び出しと対応する longjmp への呼び出しとの間で変更されるように宣言された自動記憶期間オブジェクト。

最初の 3 つはすべて特有の性質を持つオブジェクトの例です。その値はプログラムの実行中いつでも変更することができます。したがって、一見無限だと思われる次のループは、flagvolatile 修飾型を持っている限り有効です。ある非同期イベントが将来 flag をゼロに設定することが考えられます。

flag = 1;
while (flag)
    ;

それ以外の場合、flag の値はループの本体内では変更されないので、コンパイラは上記のループを flag の値を完全に無視する真の無限ループに変更してもかまいません。

4 番目の例はさらに複雑で、setjmp を呼び出す関数に局所的な変数を含みます。setjmplongjmp の動作を詳細に調べてみると、4 番目の場合のオブジェクトの値については何の保証もないことがわかります。setjmp を呼び出す関数と longjmp を呼び出す関数との間のあらゆるスタックフレームを longjmp で調べて、退避されたレジスタ値を得るというのが最も望ましい動作です。スタックフレームは非同期的に生成されることがあるため、この操作がさらに困難なものになっています。

自動オブジェクトが volatile 修飾型で宣言されると、コンパイラはプログラマが書いた内容と完全に整合性のとれたコードを生成しなければならないことを認知します。したがって、そのような自動オブジェクトの最近の値は常にメモリーに入っており (レジスタではない)、longjmp が呼び出されたときは最新版であることが保証されます。


複数バイト文字とワイド文字

当初、ANSI C の国際化はライブラリ関数だけに影響を与えました。しかし、国際化の最終段階の複数バイト文字とワイド文字は、言語そのものにも影響を与えました。

アジア言語での複数バイト文字の必要性

アジア言語でのコンピュータ環境における基本的な難しさは、入出力のために必要な表意文字が膨大な数になるという点です。普通のコンピュータアーキテクチャの制約内で作業するには、こういった表意文字は複数バイトのシーケンスとして符号化されます。オペレーティングシステム、アプリケーションプログラム、および端末は、これらのバイトシーケンスを個別の表意文字として理解します。さらに、これらの符号化はすべて通常の単一バイト文字と表意文字バイトシーケンスを相互に混在させることができます。個々の表意文字を識別する難しさは、使用する符号化方式によって変わります。

複数バイト文字という用語は ANSI C で定義され、どの符号化方式を採用するかにかかわらず、表意文字を符号化するバイトシーケンスを意味しています。すべての複数バイト文字は、いわゆる拡張文字セットのメンバーです (通常の単一バイト文字は複数バイト文字の特殊なケースにすぎません)。符号化に課せられた唯一の本質的な必要条件は、複数バイト文字はその一部に null 文字を使用できないということです。

ANSI C では、注釈、文字列リテラル、文字定数、およびヘッダーファイル名はすべて複数バイト文字のシーケンスであると規定しています。

符号化のバリエーション

符号化方式は 2 つのグループに分かれます。1 つは、それぞれの複数バイト文字がそれ自身独立しているものです。言い換えれば、複数バイト文字を対になっている任意の複数バイト文字の間に挿入することができます。

もう 1 つは特殊なシフトバイトによって後のバイトの解釈が変わるものです。例としては、線を引くモードに出入りするために、いくつかの文字端末を使用している方法があります。シフト状態に依存する符号化方式の複数バイト文字で書かれたプログラムの場合、ANSI C ではそれぞれの注釈、文字列リテラル、文字定数、ヘッダーファイル名がシフトされていない状態で始まり、かつ終らなければなりません。

ワイド文字

すべての文字が同じバイト数またはビット数であれば、複数バイト文字の処理は軽減されます。複数バイトの文字セットには何千または何万の表意文字が含まれていることがあるので、すべての文字を収容するには 16 ビットまたは 32 ビットの整数値を使用する必要があります (漢字には全部で 65,000 を超える表意文字があります)。ANSI C には、拡張文字セットのすべてのメンバーを収容するのに十分な大きさで、実装で定義された汎整数型の typedefwchar_t があります。

それぞれのワイド文字ごとに対応する複数バイト文字があり、またその逆も成立します。通常の単一バイト文字に対応するワイド文字は、null 文字を含めてその単一バイト値と同じ値を持つことが必要です。ただし、EOFchar として表すことができないことがあるように、マクロ EOF の値を wchar_t に格納できるという保証はありません。

変換関数

ANSI C は、複数バイト文字とワイド文字を管理する 5 つのライブラリ関数を備えています。

表 1-2 複数バイト文字変換関数

関数名 機能
mblen() 次に指定された複数バイト文字の長さ (バイト数) を返す
mbtowc() 複数バイト文字をワイド文字に変換する
wctomb() ワイド文字を複数バイト文字に変換する
mbstowcs() 複数バイト文字列をワイド文字列に変換する
wcstombs() ワイド文字列を複数バイト文字列に変換する

これらの関数の動作は使用しているロケールによって変わります (56 ページの「setlocale() 関数」を参照してください)。

この市場をターゲットとしてコンパイラシステムを提供するベンダーは、さらに多くの文字列関連の関数を供給して、ワイド文字列の処理を簡単にすることが期待されます。しかし、ほとんどのアプリケーションプログラムでは、複数バイト文字をワイド文字に変換したり、その逆を行う必要はありません。たとえば diff のようなプログラムは複数バイト文字の読み込みと書き出しを行い、バイト単位での正確な一致についてチェックするだけです。正規表現のパターン一致を使用するさらに複雑な (grep のような) プログラムは複数バイト文字を理解する必要があるかもしれませんが、このような知識は正規表現を管理する関数の共通セットのみに必要です。プログラム grep 自身についていえば、他の特殊な複数バイト文字処理を必要としません。

C 言語機能

アジア言語環境のプログラマにさらに柔軟性を与えるために、ANSI C はワイド文字定数とワイド文字列リテラルを備えています。これらは文字 L がすぐ前に付けられるという点を除いて、それぞれのワイド文字でない文字と同じ形を持っています。

'x' 通常文字定数
'' 通常文字定数
L'x' ワイド文字定数
L'' ワイド文字定数
"abcxyz" 通常文字列リテラル
L"abcxyz" ワイド文字列リテラル

複数バイト文字は通常文字とワイド文字のどちらでも有効です。表意文字 '' を生成するために必要なバイトのシーケンスは符号化方式に固有ですが、それが 2 バイト以上で構成されている場合の文字定数 '' の値は、'ab' の値がそうであるように実装で定義されます。エスケープシーケンスを除き、通常の文字列リテラルは引用符間に指定されたバイトを持ち、それぞれの指定された複数バイト文字を含みます。

コンパイラがワイド文字定数またはワイド文字列リテラルを検出すると、それぞれの複数バイト文字は mbtowc() 関数を呼び出して行うかのようにワイド文字に変換されます。このように、L'' の型は wchar_t であり、"abcxyz" の型は長さ 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 つの配列、xyz ならびに wp により指定された配列は同じ長さを持ち、すべて同一の値で初期化されます。

最後に、通常の文字列リテラルの場合のように、隣接するワイド文字列リテラルが連結されます。しかし、隣接する通常の文字列リテラルとワイド文字列リテラルとの連絡の結果は未定義です。このような連結を許可するかどうかは、コンパイラによって異なります。


標準ヘッダーファイルと予約名

標準化プロセスの非常に早い段階で、ANSI 規格委員会ではライブラリ関数、マクロ、ヘッダーファイルを ANSI C に含めることに決定しました。この決定は移植可能な C プログラムを書くために必要なものでしたが、予約名の数が膨大であるという点について一般から否定的な意見が聞かれます。

本節では、予約名のさまざまなカテゴリと予約名を予約するための根拠について説明します。また、予約名なしにプログラムを制御できる規則についても説明します。

バランスプロセス

ANSI 委員会では、既存の printfNULL といった名前を採用しました。しかし、C プログラムで自由に利用できる名前が減少してしまいました。

標準化以前には、予約語やヘッダーファイルの名前を自由に追加することができました。あるベンダーから別のベンダーの実装への移植だけでなく、あるリリースから別のリリースへの再コンパイルさえ保証できないことになります。

この結果 ANSI C 委員会では、すべての実装に対して特定の形式を持つ名前を除いて、余分な名前の使用を禁止しました。今日の C コンパイラシステムのほとんどに互換性があるのは、この決定によるものです。しかし、規格には 32 の予約語名と 250 ほどのヘッダーファイルの名前が含まれていますが、一定の命名規則に従っているわけではありません。

標準ヘッダーファイル

標準ヘッダーファイルは以下のとおりです。

表 1-3 標準ヘッダーファイル

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) は、fdopenstdio.h で宣言することを規定しています。これらの 2 つの規格が共存できるよう、POSIX はヘッダーファイルをインクルードする前にマクロ _POSIX_SOURCE#define 文で定義して、追加したこれらの名前の存在を保証するよう求めています。X/Open も『Portability Guide』において拡張にこのマクロ方式を使用してきました。X/Open のマクロは _XOPEN_SOURCE です。

ANSI C では標準ヘッダーファイルが自己充足的で、「べき等的」であることが必要です。つまり、標準ヘッダーファイルはその前後に他のヘッダーファイルが組み込まれることを必要とせず、それぞれの標準ヘッダーファイルは問題なく 2 度以上インクルード可能です。規格では、ヘッダーファイルで使用される名前が変更されないことを保証するため、ヘッダーファイルを安全な状況でのみインクルードすることも求めています。

実装用に予約された名前

規格はさらにライブラリに関する実装にも制限を設けています。以前は UNIX システム上で独自の関数として readwrite といった名前を使用しないと学びましたが、ANSI C では規格によって予約された名前だけを実装内の参照により取り入れるよう求めています。

したがって規格では、実装で使用する可能性のあるすべての名前のサブセットを予約しています。これらは下線で始まり、別の下線あるいは大文字に続く識別子から成ります。つまり、この種の名前は次の正規表現に一致するすべての名前を含みます。

_[_A-Z][0-9_a-zA-Z]*

厳密に言えば、プログラムでこのような識別子を使用する場合の動作は未定義です。したがって、_POSIX_SOURCE (または _XOPEN_SOURCE) を使用するプログラムの動作は確定できません。

しかし、不確定の動作の程度はさまざまです。POSIX に適合した実装で _POSIX_SOURCE を使用する場合、ある名前を特定のヘッダーファイルに追加するとプログラムの不確定動作が生じることは判明していますが、プログラムはまだ規格に適合しています。ANSI C 規格におけるこういった意図的な抜け道により、実装は一見不適合と思われる仕様にも適合することができます。一方、POSIX 規格に準拠しない実装は、_POSIX_SOURCE のような名前を検出した場合にどのように動作してもかまいません。

規格は、下線で始まる他のすべての名前も予約しています。これらはヘッダーファイルの中 (ただし局所的なスコープを除く) で通常のファイルのスコープ識別子、もしくは構造体と共用体のためのタグとして使用されます。関数を _filbuf_doprnt と名付けて、ライブラリの隠れた部分を実装するといった一般的な慣例が認められています。

拡張用に予約された名前

明示的に予約されたすべての名前に加えて、規格は (実装と将来の規格のため) 特定のパターンに一致する名前も予約しています。

表 1-4 予約名

ファイル名 予約名のパターン
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 つあります。

1. #include 文によって、すべてのシステムヘッダーファイルをソースファイルの最上部にインクルードする。_POSIX_SOURCE_XOPEN_SOURCE、またはその両方の #define 文がある場合は例外で、その後に #include 文を挿入する。

2. 下線で始まる名前を定義または宣言してはいけない。

3. すべてのファイルをスコープとするタグと通常の名前の中で、最初の 2、3 文字内のどこかに下線または大文字を使用する (stdarg.h または varargs.h にある接頭辞 "va_" には注意してください)。

4. すべてのマクロ名の中で、最初の 2、3 文字内のどこかに大文字以外の文字または数字を使用する (errno.h がインクルードされている場合、E で始まるほとんどすべての名前は予約済みなので注意してください)。

これらの規則は単なるガイドラインに過ぎず、ほとんどの実装ではデフォルトで標準ヘッダーファイルに名前を追加し続けます。


国際化

標準ライブラリの国際化については、41 ページの「複数バイト文字とワイド文字」で紹介しました。本節では、国際化の影響を受けるライブラリ関数を説明し、それらの機能を利用するためのプログラムを書くヒントを示します。

ロケール

C プログラムには、常に特定の国、文化、および言語に合った慣例を記した情報群があります。これをロケールといいます。ロケールには文字列の名前があります。標準化されたロケールの名前は "C" と "" の 2 つだけです。すべてのプログラムは "C" ロケールで始まり、どのライブラリ関数もこのロケールに従って動作します。"" ロケールはプログラムの起動に際し、実行環境における適切なロケールを使用します。"C" と "" がまったく同じ動作をしたり、実装によって他のロケールが提供されることもあります。

実用性と迅速化のため、ロケールはいくつかのカテゴリに分類されます。プログラムによってロケール全体を変更したり、1 つまたはいくつかのカテゴリだけを変更することもできます。一般に各カテゴリは、他のカテゴリに影響される関数との共通点がない関数の集合に影響を与えます。したがって、1 つのカテゴリを一時的に変更することにも意味があります。

setlocale() 関数

setlocale() 関数は、プログラムのロケールとのインタフェースです。起動側の国の規約を使用したいプログラムは、プログラムの実行における早い段階で次のような呼び出しを行う必要があります。

#include <locale.h>
/*...*/
setlocale(LC_ALL, "");

これによって、プログラムの現在のロケールが適切な国のものに変わります。これは LC_ALL が 1 カテゴリではなく、ロケール全体を指定するマクロだからです。標準カテゴリは、以下の表のとおりです。

LC_COLLATE ソート情報
LC_CTYPE 文字分類情報
LC_MONETARY 通貨出力情報
LC_NUMERIC 数字出力情報
LC_TIME 日付と時刻の出力情報

特定のカテゴリを指定するために、マクロのうち任意のものを第 1 の引数として setlocale() に渡すことができます。

setlocale() 関数は、指定されたカテゴリ (または LC_ALL) に対する現ロケール名を返し、第 2 の引数が NULL ポインタであれば照会のみ行います。このようにして、以下のようなコードを使用し、ロケール全体またはその一部を一時的に変更することができます。

#include <locale.h>
/*...*/
char *oloc;
/*...*/
oloc = setlocale(カテゴリ, NULL);
if (setlocale(カテゴリ, "ロケール") != 0)
{
            /* 一時的に変更されたロケールを使用 */
    (void)setlocale(カテゴリ, oloc);
}

ほとんどのプログラムは、この機能を必要としません。

変更された関数

既存のライブラリ関数は、適切で可能な場合は必ずロケールに従った動作をするように拡張されました。これらの関数は 2 つのグループに分けられます。ctype.h ヘッダーファイル (文字分類と変換) で宣言される関数と、印字可能形式と内部形式の数値からの変換、またはそれらの形式への変換を行う関数 (たとえば printf()strtod()) です。

現ロケールの LC_CTYPE カテゴリが "C" 以外であれば、isdigit()isxdigit() 以外の ctype.h 述語関数は、追加文字についてゼロ以外 (真) を返すことができます。ロケールがスペインの場合、isalpha('') は真です。同様に、tolower()toupper() といった文字変換関数は、isalpha() 関数が認識する追加のアルファベット文字を適切に取り扱います。ctype.h 関数は通常、文字引数が指すテーブル参照を使用して実現されるマクロです。この関数の行動は、テーブルを新しいロケール値に設定し直すと変わるので、動作性能への影響はありません。

現ロケールの LC_NUMERIC カテゴリが "C" 以外であれば、印字可能の浮動小数値を書き込んだり解釈する関数は、ピリオド (.) 以外の小数点文字を使用するように変更することができます。任意の数値を 3 桁ごとの分離記号を付けたプリント可能形式に変換する機能はありません。プリント可能形式を内部形式に変換するとき、"C" ロケール以外ではそのような追加形式を受け入れる実装が可能です。小数点文字を使用する関数は printf()scanf() の系列、atof()strtod() です。実装時の定義により拡張が可能な関数は、atof()atoi()atol()strtod()strtol()strtoul()scanf() 系列です。

新しい関数

新しい標準関数として、ロケール依存型の関数が追加されました。標準関数には、ロケールそのものを制御する setlocale() 以外に、規格では以下の関数が追加されました。

localeconv() 数字/金額慣例
strcoll() 2 つの文字列の照合順序
strxfrm() 照合のための文字列翻訳
strftime() 日付/時刻形式の変換

これらの他に、複数バイト関数 mblen()mbtowc()mbstowcs()wctomb()wcstombs() があります。

localeconv() 関数は、構造体を指すポインタを返します。その構造体の中には、現ロケールの LC_NUMERICLC_MONETARY カテゴリに対応した、数字と金額の書式作成に役立つ情報が含まれています (動作が 2 つ以上のカテゴリに依存する唯一の関数です)。数値については、構造体が小数点文字、3 桁ごとの分離記号、分離記号の位置を記述します。金額の書式を記述する構造体の要素は、他に 15 あります。

strcoll() 関数は、2 つの文字列が現ロケールの LC_COLLATE カテゴリに従って比較される以外は、strcmp() 関数と同様です。strxfrm() 関数を使用して文字列を変換し、変換後の 2 つの文字列を strcmp() に引き渡すと、strcoll() が変換前の 2 つの文字列を受け取ったときに返すものと同様の順序付けを行うことができます。

strftime() 関数は struct tm 中の値について、sprintf() で使用するものと同様の書式にし、現ロケールの LC_TIME カテゴリに従った日付と時刻の表記も行います。この関数は、UNIX System V リリース 3.2 の一部として提供された ascftime() 関数にもとづいています。


式中のグループ化と評価

Dennis Ritchie 氏は C 言語を設計する段階で、隣接する演算子が数学的に交換および結合できれば、括弧があっても式の再配置を許可するよう決めました。このことは、Kernighan と Ritchie 両氏による『プログラミング言語 C』の付録に明記されています。ただし、ANSI C ではこの機能を許可していません。

本節では C に関するこれら 2 つの定義の相違点を説明し、以下に示す式の文を例にとって、式の副作用であるグループ化と評価の相違点を示します。

int i, *p, f(void), g(void);
/*...*/
i = *++p + f() + g();

定義

式の副作用は、メモリーの変更と volatile 修飾付きオブジェクトへのアクセスです。上記の式での副作用は、ip が更新されることと関数 f()g() が与える影響です。

値を他の値や演算子と組み合わせることを式のグループ化と言います。上記の式のグループ化は、主に加算が行われる順に行われています。

結果の値を得るには、式の評価が必要です。式を評価するには、(その式の直前と直後の文の間のどこかで) 指定された副作用がすべて発生している必要があります。その結果指定の演算がグループ化に従って行われます。上記の式では、前の文とこの式の ; の間で、ip が更新される必要があります。関数呼び出しは、どちらが先でも前の文の後、関数からの戻り値が使われる前であればいつでも出すことができます。特にメモリーを更新させる演算子は、その演算の値が使われる前に新しい値を設定する必要がないので注意してください。

K&R C での再配置の自由

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();

オーバーフローが例外になるか、あるいはオーバーフローの前後の加算と減算が逆でないアーキテクチャでこの式が評価された場合、加算のどれかでオーバーフローになれば、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 では、数学的に交換および結合できても、ターゲットになるアーキテクチャで実際に交換や結合できない演算子は再配置を許可しません。したがって、ANSI C の文法における優先度と結合規則によって、すべての式のグループ化が決定されます。すべての式は構文解析に従ってグループ化される必要があります。式のグループ化の例を以下に示します (ただし、g() の前に f() を呼ばなければならない、または g() を呼ぶ前に p をインクリメントしなければならないという意味ではありません)。

i = { {*++p + f()} + g() };

ANSI C では、意図しないオーバーフローの対策として式を分割する必要はありません。

括弧の扱い

ANSI C は、括弧を尊重するとか、括弧に従って評価するといった間違った解釈をされることがあります。

ANSI C の式では構文解析によって指定されるグループ化があるだけで、括弧は式の構文解析を助ける手段として使われます。通常の式の優先度と結合規則は、括弧とまったく同じ重みを持っています。

上記の式を次のように書き換えてもグループ化は変わらず、評価も変わりません。

i = (((*(++p)) + f()) + g());

as if 規則

K&R C の再配置規則には、いくつかの正当な理由がありました。

その後 ANSI C 委員会は、再配置規則を特定のターゲットアーキテクチャに適用した場合の as if (「あたかも...のように」) 規則の一例であると認識するようになりました。ANSI C の as if 規則は、正しい C プログラムの動作が変更されない限り、実装と抽象的に記述されたマシンの動作が完全には一致しないことを、大筋では許可しています。

したがって、2 進数のビット単位の (シフトを除く) 演算子はすべて、どんなマシン上でも再配置することができます。これは再配置によるグループ化の変更に気付く方法がないからです。オーバーフローが自然に桁送りになる、普通の 2 の補数を基準とする装置では、同様の理由で乗算または加算を含む整数式を再配置することができます。

C 言語中でこのような変更が行われても、ほとんどの C プログラマにとって大きな影響はありません。


不完全型

ANSI C 規格では不完全型 (incomplete type) という用語を導入し、C 言語の最初から不明確で誤解されていた基本的な部分を公式なものにしました。本節では、不完全型がどういう場合に許容され、なぜ有用なのかを説明します。

ANSI は C の型を 3 つの異なる集合に分けます。関数、オブジェクト、不完全型です。関数型は明らかです。オブジェクト型はその他全部ですが、オブジェクトのサイズが未知のときは除きます。ANSI C 規格は、指定されたオブジェクトは既知のサイズでなければならないことを示すために、オブジェクト型という用語を使います。しかし、void 以外の不完全型もオブジェクトを参照しますので注意してください。

不完全型は 3 種類しかありません。void、長さ指定なしの配列、そして内容指定なしの構造体と共用体です。void 型は、完全化できない不完全型という点で他の 2 つとは異なります。この型は、関数の特殊な戻り値と引数の型として役立ちます。

不完全型を完全にする

配列型は同じスコープの中のそれに続く宣言で、配列のサイズを指定することによって完全化されます (また、サイズなしの配列が同じ宣言の中で宣言と初期化が行われると、その配列はその宣言子の終わりと初期化子の終わりの間だけ不完全型となります)。

不完全な構造体または共用体は、同じタグに関する同じスコープの中で、後に続く宣言で内容を指定することによって完全化されます。

宣言

宣言には不完全型を使用することができるものがありますが、(完全な) オブジェクト型が必要なものもあります。オブジェクト型が必要な宣言は、配列の要素、構造体や共用体のメンバー、関数に対して局所的なオブジェクトです。他の宣言には、不完全型を使用することができます。特に以下のものが許可されます。

関数の戻り値と引数の型は特殊です。void を除くと、この種の使い方をする不完全型は、その関数を定義するかまたは呼び出すときまでに完成させる必要があります (戻り値型の void は値を返さない関数を指定し、単一引数型の void は引数を受け付けない関数を指定します)。

配列と関数引数の型はポインタ型になるように書き直されるため、外見上不完全な配列引数型は、実際は不完全ではないことに注意してください。文字ポインタの長さを指定しない配列として、mainargv の典型的宣言 (すなわち char *argv[]) は、文字ポインタを指すポインタに書き直されます。

ほとんどの式の演算子には (完全な) オブジェクト型が必要です。単項 & 演算子、コンマ演算子の最初のオペランド、?: 演算子の第 2 と第 3 のオペランドの 3 つだけは例外です。ポインタオペランドを受け付ける演算子のほとんどは、ポインタ算術演算が必要でないかぎり、不完全型を指すポインタも許可します。また、単項の * 演算子も受け付けます。たとえば次の式を使う &*p は有効な式です。

void *p

不完全型の必要性

なぜ不完全型が必要なのでしょうか。void 以外に、不完全型が提供する機能で C が他の方法では扱えないのは 1 つだけです。それは構造体と共用体への順方向参照に関係します。構造体が 2 つあってお互いを指すポインタを必要とする場合、それを実現するには不完全型を使用しなければなりません。

struct a { struct b *bp; };
struct b { struct a *ap; };

なんらかの形のポインタと、異種データ型を持つ強力に型付けされたプログラム言語では、上記のような場合の処理方法を提供しています。

不完全型の構造体と共用体について typedef 名を定義することはとても有用です。複雑なデータ構造体や相互間のポインタが数多く存在する場合は、最初から (中心になるヘッダーファイルに) 構造体を指す typedef のリストがあれば宣言を簡単にできます。

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 では、同じ実体を参照する 2 つの宣言が同一ではないことがあります。ANSI C の場合、その確率はさらに高くなります。ANSI C では、型に十分な類似性があることを表わす際に、互換型という用語を使います。本節では、互換型と、2 つの互換型を組み合わせた結果の複合型について説明します。

多重宣言

C プログラムで、各オブジェクトまたは関数の宣言が 1 回しかできなければ、互換型は必要ないでしょう。しかし、(同じ実体を参照する 2 つ以上の宣言を可能にする) リンケージ、関数プロトタイプ、ソース毎に分離したコンパイルには互換性が必要です。分離した翻訳単位 (ソースファイル) には、型について単一の翻訳単位とは異なった互換性の規則があります。

分離コンパイルでの互換性

コンパイルごとに異なるソースファイルを取り扱うため、別々のコンパイル間での互換型に関する大半の規則は構造的なものです。

構造体、共用体、および列挙型では、(名前のないメンバーについては、名前がないことも含めて) メンバー名も対応する必要がありますが、それぞれのタグはその必要がありません。

単一コンパイルでの互換性

同じスコープ内で 2 つの宣言が同じオブジェクトや関数を記述するときは、その 2 つの宣言は互換性のある型を指定しなければなりません。これら 2 つの型は組み合わされて、最初の 2 つと互換がある 1 つの複合型になります。複合型については、後で詳しく説明します。

互換型は再帰的に定義されます。その根底にあるのは、型を指定する予約語です (これらは、unsigned shortunsigned short int と同じであり、型指定のない型は int のある型と同じとする規則です)。他のすべての型は、派生してくる元の型が互換可能である場合にのみ互換性を持ちます。たとえば、2 つの修飾型は、修飾子 (constvolatile) が同一で、修飾子のない基本型が互換可能である場合は互換性を持ちます。

互換性のあるポインタ型

2 つのポインタの型が互換性を持つには、それらが指す型が互換であり、2 つのポインタの修飾子が同一であることが必要です。ポインタの修飾子が * の後に指定されることを思い出してください。したがって、次の 2 つの宣言は、同じ型 int を指す修飾子の異なる 2 つのポインタを宣言しています。

int *const cpi;
int *volatile vpi;

互換性のある配列型

2 つの配列の型が互換性を持つには、それらの要素の型が互換でなければなりません。両方の配列の型のサイズが指定されている場合は、サイズが一致している必要があります。これは、不完全な配列の型 (67 ページの「不完全型」を参照) には、別の不完全な配列の型とサイズが指定された配列の型の両方に互換があることを意味します。

互換性のある関数型

関数に互換性を持たせるには、次の規則に従わなければなりません。

特殊な事例

signed intint と同じ動作をします。ただし単なる int が符号なしの数量を意味するビットフィールドには、適用できないこともあります。

もう 1 つの興味深い点は、どの列挙型も汎整数型のどれかと互換性を持たなければならないことです。移植性のあるプログラムの場合、これは列挙型は独立した型であることを意味しています。通常、ANSI C 規格は列挙型をこのように扱います。

複合型

2 つの互換型から作られた複合型の構造も再帰的に定義されます。互換型が相互に異なる場合があるのは、不完全配列または旧形式の関数の型のためです。したがって、複合型を最も簡単に説明すると、元の型から利用できる配列のサイズと引数リストを含め、双方の元の型と互換性を持つ型であるということが言えます。




Previous Next Contents Index Doc Set Home