Compaq Cではデータ・オブジェクトの型により,オブジェクトが表現できる値の範囲と種類, オブジェクトのために確保されるマシン記憶域のサイズ, およびオブジェクトで実行できる演算が決定されます。 また,関数にも型があり,関数の宣言に関数の返却値の型と仮引数型を指定することができます。
この章の各節では,次の内容について説明します。
次は派生型の種類です。
どんな言語においても,指定したオブジェクトまたは関数のデータ型の選択は基本的なプログラミング・ ステップの1つです。プログラム中の各データ・ オブジェクトまたは関数にはデータ型を指定する必要があり,明示的に割り当てるかまたは省略時の設定により割り当てられます( オブジェクトへのデータ型の割当てについては, 第4 章を参照してください)。Compaq Cはさまざまなデータ型を提供しています。 この多様性がCompaq Cの強力な機能ですが,慣れないうちは混乱するかもしれません。
混乱を避けるために,Compaq Cの基本型は数種類と考えてください。
それ以外の型はこの基本型を組み合わせたものです。それらのいくつかは複数の方法で指定できます。
たとえば,short
と short int
は同じ型です(本書では,
最も明瞭な最長の名前を使用します)。型は,宣言の一部として各オブジェクトまたは関数に割り当てます。
宣言についての詳細は,
第4章を参照してください。
表 3-1は,基本データ型の一覧です。 基本データ型には次の種類があります。
文字型は整数として格納されます。
汎整数型 | 浮動小数点型 |
---|---|
short int | float |
signed short int | double |
unsigned short int | long double |
int | float _Complex (Alpha 専用) |
signed int | double _Complex (Alpha 専用) |
unsigned int | long double _Complex (Alpha 専用) |
_Imaginary Compaq Cでは, | |
long int | |
signed long int | |
unsigned long int | |
long long int (Alpha 専用) | |
signed long long int (Alpha 専用) | |
unsigned long long int (Alpha 専用) | |
_Bool | |
汎整数文字型 | |
char | |
signed char | |
unsigned char |
汎整数型と浮動小数点型をまとめて,算術型と呼びます。整数値と浮動小数点値のサイズと範囲については, 第3.1 節を参照してください。
基本型からはさまざまな派生型を作成できます。派生型については, 第3.4節を参照してください。
基本型と派生型の他に,固有の型を指定するための
void
,enum
,および
typedef
の3つのキーワードがあります。
void
キーワードは値がないことを示す特別な型を指定するか,
またはポインタ演算子( * )とともに使用して汎用ポインタ型を示すことができる。
void
の使用法については,第3.5
節を参照のこと。
enum
キーワードは,プログラマが作成した整数型を指定する。
この指定は,整数定数値の定義済みセットに対してその型の許容値を指定することにより行う。
列挙型は整数として格納される。
列挙型の詳細については第3.6
節を参照のこと。
typedef
キーワードは,1つ以上の基本型または派生型から構成される型の同義語を指定する。
型定義の詳細については
第3.8節を参照のこと。
さらに,型修飾子キーワードがあります。
const
はオブジェクトへの書込みアクセスを防ぐために使用する(
第3.7.1
項を参照のこと)。
volatile
はオブジェクトの参照で行われる可能性がある最適化を禁止するために使用する(
第3.7.2項を参照のこと)。
__unaligned
(Alpha)は,ポインタ定義で使用され,指されているデータが,
正しいアドレスに適切に境界調整されていないことをコンパイラに示す。
__restrict
(ポインタ型の場合のみ)は,
ポインタが明確なオブジェクトを指していることを示すために使用され,
コンパイラによる最適化が行われるようにする(第3.7.4項を参照)。
オブジェクトの型宣言に修飾子キーワードを使用すると修飾型 になります。型修飾子の詳細については,第3.7節を参照してください。
このようにさまざまな型が存在するために,プログラムでは異なる型のオブジェクト上での演算が必要になる場合があります。 また,ある型の仮引数を別の仮引数型を返す関数に渡す必要が生じる場合もあります。 DEC Cでは異なる種類の変数を異なる方法で格納するため,最低1つのオペランドまたは実引数を変換して, そのオペランドまたは実引数の型をもう一方と一致させる必要があります。 変換は明示的にキャストを使用して行うか, またはコンパイラを介して暗黙に行うことができます。 データ型の変換については第6.11節を, 型の互換性については第2.7節を参照してください。
実行定義のデータ型については,プラットフォームに固有のDEC Cドキュメントを参照してください。
指定したデータ型のオブジェクトは,個別のサイズを持つメモリのセクションに格納されます。 異なるデータ型のオブジェクトには,それぞれ異なるメモリが必要です。 表 3-2は,基本データ型のサイズと範囲を示しています。
型 | サイズ | 範囲 |
---|---|---|
汎整数型 | ||
short intまたはsigned short int | 16 ビット | -32768〜32767 |
unsigned short int | 16ビット | 0 〜65535 |
intまたはsigned int | 32ビット | -2147483648〜2147483647 |
unsigned int | 32ビット | 0〜4294967295 |
long intまたはsigned long int (OpenVMS) | 32ビット | -2147483648〜2147483647 |
long intまたはsignedlong int (Tru64 UNIX) | 64ビット | -9223372036854775808〜9223372036854775807 |
unsigned long int (OpenVMS) | 32ビット | 0 〜4294967295 |
unsigned long int (Tru64 UNIX) | 64 ビット | 0〜18446744073709551615 |
signed long long int (Alpha 専用),
signed __int64 (Alpha 専用) | 64ビット | -9223372036854775808〜9223372036854775807 |
unsigned long long int (Alpha 専用),
unsigned __int64 (Alpha 専用) | 64ビット | 0〜18446744073709551615 |
汎整数文字型 | ||
charおよびsigned char | 8ビット | - 128〜127 |
unsigned char | 8 ビット | 0〜255 |
wchar_ t | 32ビット | 0〜4294967295 |
浮動小数点型(範囲は絶対値用) | ||
float | 32ビット | 1.1×10 -38 〜3.4×10 38 |
double | 64ビット | 2.2×10 -308 〜1.7×10 308 |
long double (OpenVMS, Alpha) | 128ビット | 3.4×10 -49321 〜1.2 ×10 -1049321 |
long double (OpenVMS VAX, Tru64 UNIX) | doubleと同様 | doubleと同様 |
派生型の場合は,さらにメモリ空間が必要になる可能性があります。
実行定義のデータ型のサイズについては,プラットフォームに固有の Compaq Cドキュメントを参照してください。
C言語では,次の汎整数型を宣言することができます。
汎整数型は次のとおりです。
char,
signed char,
unsigned char
- 8ビット
short int,
signed short int,
unsigned short int
- 16ビット
_Bool
-- 1バイト
int,
signed int,
unsigned int
- 32ビット
long int,
signed long int,
unsigned long int
- 32ビット
(OpenVMS)
long int
, signed long int
,
unsigned long int
- 64ビット
(Tru64 UNIX)
signed long long int
(Alpha 専用),
unsigned long long int
(Alpha 専用) -- 64ビット
signed __int64
(Alpha 専用),
unsigned __int64
(Alpha 専用) -- 64ビット
enum
- 32ビット
OpenVMSシステム上のCompaq Cでは,int
と long
の記憶域は同一です。
さらに,signed int
と
signed long
の記憶域も同一であり,
unsigned int
と
unsigned long
の記憶域も同一です。
Tru64 UNIXシステム上のCompaq Cでは,int
データ型の記憶域は32
ビットであり,long int
データ型の記憶域は64
ビットです。
64ビットの汎整数型signed long long int
および
unsigned long long int
と,これらと同等の
signed __int64
およびunsigned __int64
は,
Alphaプロセッサ上でのみ提供されています。
注意: __int64
データ型とlong long int
データ型(符号付きおよび符号なし)は,ポインタ演算の場合を除き,
取り替えて使用することができます。
ポインタ演算の場合は,ポインタの型が同じでなければなりません。
__int64 *p1; __int64 *p2; long long int *p3; . . . p1 = p2; // valid p1 = p3; // invalid
符号付き汎整数型には,それぞれに対応する同じ記憶容量を使用する
符号なし汎整数型が存在します。汎整数型に
unsigned
キーワードを指定すると,記憶域の正の値の範囲を拡げることで整数値の解釈方法を変更できます。
unsigned
キーワードを使用すると,ビットは異なる方法で解釈され,
符号なし型を持つ正の範囲を(負の範囲を狭めることによって)
拡げることができます。次にその例を示します。
signed short int x = 45000; /* ERROR -- value too large for short int */ unsigned short int y = 45000;/* This value is OK */
signed short int
型の値の範囲は -32,768〜32,767
です。unsigned short int
型の値の範囲は
0〜65,535です。
符号なしのオペランドを含む計算は,オーバフローすることはありません。
これは,unsigned
型の範囲外の結果は剰余算術の規則によってその型に合うように調整されるからです。
結果を整数型で表せない場合には,
その数が符号なし整数型で表すことができる最大値よりも大きいものとして結果を調整します。
つまり,下位ビットは保持され,結果の型に収まらない数値結果の上位ビットは破棄されます。
次にその例を示します。
unsigned short int z = (99 * 99999); /* Value of y after evaluation is 3965 */
Compaq Cでは,VAX Cおよび他のCコンパイラとの互換性のため,省略時の設定では単純なchar 型をsignedとして処理します。ただし,コマンド行オプションでこれを制御することが可能であり, 事前に定義されているマクロをテストして, 任意のコンパイルにおけるオプションの設定を判断することができます。Alpha システムでは,unsigned charを使用すると, 文字を多用する処理の効率が向上する場合があります。
nビットの符号なし整数は常に0〜2 n -1の範囲の変数で, 符号なしの2進表現に解釈されます。
C99規定の_Bool
データ型は,コンパイラのVAX Cモード,コモン・モード,
およびstrict ANSI89モード以外のすべてのモードで利用できます。
_Bool
オブジェクトの記憶域は1バイトで,
unsigned integer
として扱われます。
ただし,その値は0か1だけです。
_Bool
型として宣言できます。
_Bool
型に変換することができます。
_Bool
に変換する場合,値が0と等しければ
(たとえば,ポインタがNULL),結果は0です。
値が0と等しくなければ,結果は1です。
このことは,_Bool
型が他の整数型と違う点です。
次の例では,b
の値は0ですが,c
の値は1です。
double a = .01; int b = a; _Bool c = a;
_Bool
型は,新しい標準ヘッダ<stdbool.h>
と一緒に使用することを前提としています (ただし,必須ではありません)。
新しいヘッダの内容は,次のとおりです。
#define bool _Bool #define true 1 #define false 0 #define __bool_true_false_are_defined 1
第9.11節も参照してください。
文字型は,char
キーワードで宣言される汎整数型です。
char
オブジェクトは,非整数演算には使用しないようにしてください。
非整数演算に使用すると移植できなくなることがあります。
char
型として宣言するオブジェクトは,常にソース文字集合の最大メンバを格納することができます。
次に有効な文字型を示します。
char
signed char
unsigned char
wchar_t
wchar_t
ワイド文字型は,ASCII文字集合に含まれていない文字を表現するために提供されています。
wchar_
t
型は,<stddef.h>
ヘッダ・ファイルに
typedef
キーワードを使用して定義されています。定数または文字列に使用するワイド文字の直前には,
L
を付ける必要があります。次にその例を示します。
#include <stddef.h> wchar_t a[6] = L"Hello";
char
オブジェクトはすべて8ビットで格納されます。
wchar_t
オブジェクトは
unsigned int
オブジェクトと同様に32ビットで格納されます。
指定した文字の値は使用している文字集合で決定されます。本書に記載しているすべての例ではASCII
文字集合を使用しています。10進基数,8
進基数,および16進基数に対応するASCII値の一覧については,付録 C
を参照してください。
移植性を高めるために,算術に使用する char
オブジェクトは
signed char
または
unsigned char
として宣言します。次にその例を示します。
signed char letter; unsigned char symbol_1, symbol_2; signed char alpha = 'A'; /* alpha is declared and initialized as 'A' */
文字列はヌル文字 '\0'で終了する文字の配列です。 文字列使用の構文規則についての詳細は第1.9.3項を, 文字列リテラルの宣言については 第4章を参照してください。
浮動小数点型には,次の種類があります。
float
- 32ビット
double
- 64ビット
long double
(OpenVMS ALPHA) - 128ビット(
省略時の設定),64ビット(オプション)
long double
(Tru64 UNIX) - 64ビット(
Tru64 UNIXの最新バージョン)
long double
(VAX) - 64ビット
float _Complex
(Alpha 専用)
double _Complex
(Alpha 専用)
long double _Complex
(Alpha 専用)
浮動小数点型は,小数部がある変数,定数,または関数の返却値に使用します。 また,値が汎整数型で格納可能な範囲を超える場合にも浮動小数点型を使用します。 次に,浮動小数点型宣言(および初期化)の例を示します。
float x = 35.69; double y = .0001; double z = 77.0e+10; float Q = 99.9e+99; /* Exceeds allowable range */
C99規格では,Fortranの型に似た組み込み複素数データ型が,
3つのすべての精度(float _Complex
,
double _Complex
,および
long double _Complex
)で導入されました。
C99規格には,これに関連するヘッダ・ファイル<complex.h>
もあります。
<complex.h>
ヘッダ・ファイルでは,
"complex"というマクロを定義しています。
これらの型を参照するときには,このマクロを使用することをお勧め
します(第9.2節を参照)。
複素数型の表現および境界合わせの要件は,対応する実数型の要素を 2つ含む配列型と同じです。 1番目の要素は複素数の実数部と同じで,2番目の要素は虚数部と同じです。
この型の使用方法は,Fortranの場合と似ています。
定数には,特別な構文はありませんが,実数部が0で虚数部が1.0の
複素数値を持つ新しいキーワード_Complex_I
があります。
ヘッダ・ファイルには,_Complex_I
に展開されるマクロ I
が定義されています。
このため,実数部と虚数部が同じ2.0の複素数定数は,2.0 + 2.0*I
と
記述できます。
Compaq Cの複素数型に関しては,次の注意事項があります。
long double complex
以外の複素数型および関数は,
Version 7.3より前のOpenVMSで利用できます。
long double complex
型は,OpenVMS Version 7.3で
利用できます。
long double complex
変数の初期化付き宣言を使用すると,
コンパイラでマシン語リストを作成するときに,妥当性チェックのエラーが発生します。
cabs
,cabsf
,
およびcabsl
という関数は,2つの浮動小数点値を持つ
構造体表現を使用して,<math.h>
で
宣言されています。
この宣言は,複素数値を渡す呼び出し標準とは互換性がありません。
正しく動作するcabs関数にアクセスするには,
<math.h>
をインクルードする前に
<complex.h>
をインクルードしなければなりません。
C99規格では,Annex G(参考)に規定されている「純虚数」型という
実験的なオプションの機能とともに使用する型指定子として,
_Imaginary
キーワードが予約されています。
Compaq Cでは,_Imaginary
キーワードを使用すると
警告が出力されます。
この警告は,このキーワードを通常の識別子として扱うようにすると
解決します。
C言語の派生型には,次の5種類があります。
これ以降の各項では,この派生型の5種類について説明します。
派生型は,1つ以上の基本型の組み合わせから形成されています。 派生型を使用すれば,多くの新しい型を形成することができます。 配列型と構造体型をまとめて, 集成型と呼びます。集成型には共用体型は含まれませんが,共用体には集成体メンバを含めることが可能です。
関数型は指定された型の値を返す関数について記述します。関数が値を返さない場合には,
次のように「void
を返す関数」として宣言しなければなりません。
void function1 ();
次の例では,関数のデータ型は「int
を返す関数」です。
int uppercase(int lc) { int uc = lc + 0X20; return uc; }
一般的な宣言については,第4章で説明します。 関数については第5章で,その宣言, 仮引数,および実引数の引渡しについて説明します。
ポインタ型は,指定された型のオブジェクトのアドレスを表す値を記述します。 ポインタは,それが示すオブジェクトのアドレスを参照する整数値として格納されます。 ポインタ型は他の型から派生する型であり,ポインタの 被参照型と呼ばれます。次にその例を示します。
int *p; /* p is a pointer to an int type */ double *q(); /* q is a function returning a pointer to an object of type double */ int (*r)[5]; /* r is a pointer to an array of five elements */ /* (r holds the address to the first element of the array) */ const char s[6]; /* s is a const-qualified array of 6 elements */
ポインタ自体はどんな記憶域クラスでも持つことができますが,ポインタに示されるオブジェクトは
register
記憶域クラスを持つことはできません。
また,ビット・フィールドにすることもできません。
修飾済み互換型または未修飾の互換型へのポインタは,そのポインタが示す型と同じ表現および境界調整条件を持ちます。
これは互換型の場合のみであり,
他の型の場合にはあてはまりません。
void *
は汎用的な「void
へのポインタ型」を指定します。void *
を使用すれば,任意の型のオブジェクトを指定することができます。これは主に,
異なる型または不明の型(関数プロトタイプなど)を持つオブジェクトのアドレスを示すためにポインタが必要な場合に役に立ちます。
さらに,
void
へのポインタは別の型のポインタと変換することもでき,
文字型のポインタと同じ表現と境界調整条件を持ちます。
アドレス0へのポインタを空ポインタと呼びます。並びの次のメンバを示す際にポインタを使用しますが,
並びのメンバがそれ以上存在しないことを示す場合は空ポインタを使用します。
ただし,空ポインタを
*
または添字指定演算子で逆参照した場合の結果は保証できません。
ポインタ宣言の構文については,第4 章を参照してください。
配列型は,有効な完了型から形成することができます。配列型を完了するには,
配列メンバの数と型が明示的または暗黙に指定されていなくてはなりません。
メンバ型は,同じコンパイル単位または異なるコンパイル単位で完了できます。
配列は,
void
型または関数型にすることはできません。これは,
void
型は完了することができず,関数型は記憶域を必要とするオブジェクト型ではないからです。
一般的に,配列はいくつかの同種類の値で演算を実行するために使用します。 配列型のサイズは,配列のデータ型と要素数によって決定されます。 配列の各要素は同じ型を持っています。たとえば,次の定義では4文字の配列を作成します。
char x[] = "Hi!" /* Declaring an array x */;
要素のそれぞれが,char
オブジェクトのサイズ(8ビット)を持っています。
配列のサイズは初期化の時点で決定されます。この例では,
配列は3つの明示的な要素と1つのヌル文字を持っています。4つの8ビットの要素は,32
ビットのサイズを持つ配列になります。
配列は連続してメモリに割り当てられ,空(つまりメンバを持たない状態) にすることはできません。1つの配列は1次元でしか作成できません。「2 次元」の配列を作成するには,配列の配列を宣言してください。
サイズの不明な配列を宣言することができます。この宣言はサイズが指定されないため, 不完全配列宣言と呼びます。次の例は不完全な宣言を示しています。
int x[];
この方法で宣言する配列サイズは,プログラムのどこかで指定しなければなりません( 不完全配列と配列の初期化については,第4.7節を参照してください)。
文字列(文字列リテラル)は char
または
wchar_t
型の形式で格納され,ヌル文字'\0'
で終了します。
Compaq Cでは,1つの配列は1次元でしか作成できません。ただし,
配列の配列を宣言することにより,多次元配列を作成することができます。
これらの配列の要素は,右端の添字が最初に変更するように,
増加方向のアドレスに格納されます。これは行優先順序と呼ばれるもので,
車の走行距離計に似ています。たとえば,int
a[2][3];
として宣言した2つの配列では,次の順序で要素が格納されます。
a[0][0], a[0][1], a[0][2], a[1][0], a[1][1], a[1][2]
構造体型は,メンバと呼ばれる空以外のオブジェクトが連続して割り当てられている集合です。
構造体を使用すれば,異質のデータをグループ化することができます。
これはPascal言語のレコードと類似しています。
配列とは異なり,構造体の要素は同じデータ型の要素である必要はありません。
また,構造体の要素は名前でアクセスされ,
添字ではアクセスされません。次の例では,employee
構造体を宣言しています。
また,employee
という構造体型の2
つの構造体変数(ed
および
mary
)を宣言しています。
struct employee { char name[30]; int age; int empnumber; }; struct employee ed, mary;
構造体メンバには,void
型や関数型のような不完全型でなければ,
どんな型でも指定することができます。構造体にはそれ自体の型のオブジェクトへのポインタを含めることはできますが,
それ自体の型のオブジェクトをメンバとして含めることはできません。
このようなオブジェクトは,
不完全型を持つことになります。次に無効な例を示します。
/* This is invalid. */ struct employee { char name[30]; struct employee div1; /* This is invalid. */ int *f(); /* This is also invalid. */ };
次に有効な例を示します。
struct employee { char name[30]; struct employee *div1;/* Member can contain pointer to employee structure. */ int (*f)(); /* Pointer to a function returning int */ };
宣言された構造体メンバ名は,その構造体内では固有でなければなりません。 ただし,ネストされた構造体またはネストされていない別の構造体, あるいは異なるオブジェクトを参照している名前空間ではそのメンバ名を使用することができます。 次にその例を示します。
struct { int a; struct { int a; /* This 'a' refers to a different object than the previous 'a' */ } nested; };
拡張機能として,Compaq Cの緩和モードでは,
構造体または共用体でメンバの名前を指定せずに
(無名メンバと呼ぶ),
ネストされた構造体または共用体を宣言することができます。
この効果は,ネストされた無名の構造体または共用体のメンバを,
構造体または共用体にネストすることなく,
直接宣言したのと同じです。
そのため,前の例で,
内部のstruct
宣言から識別子
nested
を省略した場合,
内部の構造体のメンバa
と,
それが属している構造体のメンバa
との間で矛盾が発生するため,
エラーになります。
これは,(構造体と共用体の両方に許可されていることを除き)
variant_struct
および
variant_union
機能と類似しています。
構造体とその宣言については,第4 章を参照してください。
コンパイラは,メンバ宣言の順に構造体メンバの記憶域を割り当てます。 この順序では,後続のメンバが増加するメモリ・アドレスに割り当てられます。 最初のメンバは,必ずその構造体自体の開始アドレスから始まります。 後続のメンバは境界調整単位ごとに境界調整されますが, これは構造体のメンバ・サイズによって異なることがあります。 構造体は,そのメンバが正しく境界調整されるようにパディング( 未使用のビット)を含んでいることがあります。この場合,その構造体のサイズは, すべてのメンバに必要な記憶容量に境界調整のために必要なパディング領域用の記憶容量を追加したものになります。 構造体の境界調整および表現に関するプラットフォーム固有の情報については, ご使用のシステムの Compaq Cのマニュアルを参照してください。
あるプラットフォーム上の構造体の境界調整と,他のプラットフォーム上の境界調整とを一致させるためには, プラグマを使用します。プラグマについての詳細は, 第B.29 節を参照してください。
共用体型には,メモリの同じ位置に異なる型のオブジェクトを格納できます。 それぞれ異なる共用体メンバは,プログラム中の同じ位置を異なる時期に占有できます。 共用体の宣言には共用体のすべてのメンバを含めることができ, 共用体が保持できるすべてのオブジェクト型も入れることができます。 共用体には一度に1つのメンバしか入れることができません。共用体に他のメンバを続けて代入すると, その同じ記憶域にある既存のオブジェクトは上書きされます。
共用体には有効な識別子の名前を付けることができます。空の共用体を宣言することはできず,
共用体にそれ自体のインスタンスを含めることもできません。
共用体のメンバとして,void
型,関数型,または不完全型は指定できません。
共用体には,これらの型の共用体へのポインタを含めることができます。
見方を変えると,共用体とは異なる時期に異なる型のオブジェクトを表すことのできる単一オブジェクトであるといえます。 共用体によって,マシン依存の構成を使用せずに, プログラムの処理状況に応じて型とサイズを変更できるオブジェクトを使用することができます。 別の言語では,この概念を 可変レコードと呼んでいます。
共用体を定義するための構文は,構造体の構文と非常に類似しています。 各共用体型の定義は,それぞれ固有の型を作成します。共用体内では,各共用体メンバ名は固有でなければなりません。 しかし,ネストされた共用体やネストされていない他の共用体または名前空間では, 同じメンバ名を使用できます。 次にその例を示します。
union { int a; union foo { int a; /* This 'a' refers to a different object than the previous 'a' */ } nested; };
Compaq Cの拡張機能である緩和モードでは, C++言語と同じように無名の共用体メンバが許されることに注意してください。
共用体のサイズは,その最大のメンバに境界調整条件に必要なパディングを加えた記憶容量になります。
一度共用体を定義すると,その共用体に宣言されているオブジェクトに値を代入することができます。 次にその例を示します。
union name { dvalue; struct x { int value1; int value2; }; float fvalue; } alberta; alberta.dvalue = 3.141596; /* Assigns the value of pi to the union object */
この場合,alberta
は
double
値,struct
値,または
float
値を保持できます。プログラマは,共用体に含まれているオブジェクトの現在の型を追跡する必要があります。
代入式を使用して,
共用体に保持される値の型を変更できます。
ある型の値を格納するために共用体を使用して,その値を別の型でアクセスした場合の処理結果は定義されていません。 次にその例を示します。
/* Assume that `node' is a typedef_name for objects for which information has been entered into a hash table; `hash_entry' is a structure describing an entry in the hash table. The member `hash_value' is a pointer to the relevant `node'. */ typedef struct hash_entry { struct hash_entry *next_hash_entry; node *hash_value; /* ... other information may be present ... */ } hash_entry; extern hash_entry *hash_table [512]; /* `hash_pointer' is a union whose members are a pointer to a `node' and a structure containing three bit fields that overlay the pointer value. Only the second bit field is being used, to extract a value from the middle of the pointer to be used as an index into the hash table. Note that nine bits gives a range of values from 0 to 511; hence, the size of `hash_table' above. */ typedef union { node *node_pointer; struct { unsigned : 4; unsigned index : 9; unsigned :19; } bits; } hash_pointer;
void
型は,完了することができない不完全型です。
void
型には,次に示す重要な3つの使用方法があります。
次の例では,仮引数を持たず,値を返さない関数を定義するために
void
を使用する方法を示します。
void message(void) { printf ("Stop making sense!"); }
次の例では,1番目と2番目の実引数として,任意のオブジェクトへのポインタを受け付ける関数の関数プロトタイプを示します。
void memcopy (void *dest, void *source, int length);
void
型へのポインタは,文字型へのポインタと同じ表現および境界調整条件を持ちます。
void *
型は
void
を基本にした派生型です。
また,void
型をキャスト式に使用して明示的に値を破棄したり,
または値を無視することもできます。
int tree(void); void main() { int i; for (; ; (void)tree()){...} /* void cast is valid */ for (; (void)tree(); ;){...} /* void cast is NOT valid, because the */ /* value of the second expression in a */ /* for statement is used */ for ((void)tree(); ;) {...} /* void cast is valid */ }
void
式は値を持たないため,値が必要な場合はいかなる状況であっても使用できません。
列挙型は,定義済み並びからオブジェクトの許容値を指定するために使用します。 並びの要素は列挙定数と呼ばれます。列挙型の主な使用方法は, シンボル名を明示的に表すことです。したがって,値を整数値で表すことができるオブジェクトを意図しています。
列挙型のオブジェクトは signed int
型のオブジェクトとして解釈され,
他の汎整数型のオブジェクトと互換性があります。
コンパイラは,各列挙定数に整数値を(0から開始して)自動的に代入します。
次の例では,列挙定数の並びを持つ background_
color
列挙型オブジェクトを宣言しています。
enum colors { black, red, blue, green, white } background_color;
このプログラムでは,後で background_color
オブジェクトに値を代入できます。
background_color = white;
この例では,コンパイラが整数を自動的に代入します(
black
= 0,red
= 1,blue
=
2,green
= 3,white
= 4)。この代わりに,
列挙型の定義中に明示的に値を代入することもできます。
enum colors { black = 5, red = 10, blue, green = 7, white = green+2 };
この場合,black
は整数値5
,red
は10,blue
は11,green
は7,white
は9に等しくなります。blue
は直前の定数値(
red
)に1を加えた値に等しく,次の
green
はこの番号の連続からは外れます。
ANSI C規格では列挙型への代入については厳密ではないため,定義済み並びにない代入値でも何の警告もなしに受け付けられます。
型修飾子には,次の4つがあります。
const
volatile
__unaligned
(axp)
__restrict
(ポインタ型のみ)
型修飾子はANSI C規格によって導入されているものであり,コンパイラの最適化をある程度制御できるようにするものです。
const
およびvolatile
型修飾子は,任意の型に適用することができます。
__restrict
型修飾子はポインタ型にのみ適用できます。
__restrict
型修飾子は,1989 ANSI C規格に含まれていないため,
このキーワードには先頭に2つのアンダスコア(_)が付けられています。C
規格の次のバージョン(9X)では,この節の記述と同じ意味で,キーワード
restrict
が採用される予定です。
const
を使用するとオブジェクトへの書込みアクセスを制御でき,
そのオブジェクトを含む関数呼出しの間で起こり得る副作用を排除できます。
これは,通常は副作用によりオブジェクトの記憶域が変更されるのに対して
const
がこの変更を禁止するからです。
他のプロセスまたはハードウェアで変更される可能性のあるオブジェクトには,
volatile
を使用してください。
volatile
を使用すると,オブジェクトの参照に関する最適化が行われなくなります。
オブジェクトが
volatile
で修飾されている場合には,初期化されてから代入されるまでの間にそれが変更される可能性があるため最適化することはできません。
すべての関数の仮引数が1つの仮引数の型修飾を共有することはありません。 次にその例を示します。
int f( const int a, int b) /* a is const qualified; b is not */
配列識別子とともに型修飾子を使用すると配列の要素は修飾されますが, 配列型自体は修飾されません。
次の宣言と式は,型修飾子が配列型または構造体型を変更する場合を示しています。
const struct s { int mem; } cs = { 1 }; struct s ncs; /* ncs is modifiable */ typedef int A[2][3]; const A a = {{4, 5, 6}, {7, 8, 9}}; /* array of array of const */ /* int's */ int *pi; const int *pci; ncs = cs; /* Valid */ cs = ncs; /* Invalid, cs is const-qualified */ pi = &ncs.mem; /* Valid */ pi = &cs.mem; /* Violates type constraints for = operator */ pci = &cs.mem; /* Valid */ pi = a[0]; /* Invalid; a[0] has type "const int *" */
値を変更できないオブジェクトを修飾するには,
const
型修飾子を使用します。
const
キーワードで修飾したオブジェクトは,変更することはできません。
つまり,const
として宣言されているオブジェクトは,
その値を変更する演算時にオペランドとしての役割を果たすことはできません。
たとえば, ++
および -- 演算子は const
で修飾されるオブジェクトでは使用できません。オブジェクトに
const
修飾子を使用すると,記憶域を変更する操作によって引き起こされる副作用を防ぐことができます。
const
を修飾したオブジェクトの宣言は,非修飾型のものよりも多少複雑になる可能性があります。
次に,いくつかの例とその説明を示します。
const int x = 44; /* const qualification of int type. The value of x cannot be modified. */ const int *z; /* Pointer to a constant integer. The value in the location pointed to by z cannot be modified. */ int * const ptr; /* A constant pointer, a pointer that will always point to the same location */ const int *const p; /* A constant pointer to a constant integer: neither the pointer or the integer can be modified. */ const const int y; /* Illegal - redundant use of const */
次は,const
型修飾子に適用される規則です。
const
修飾子を使用して,構造体または共用体の単一メンバを含む任意のデータ型を修飾できる。
const
を指定すると,集成型のすべてのメンバは
const
で修飾されるオブジェクトとして処理される。
const
を使用して集成型の1つのメンバを修飾すると,
そのメンバのみが修飾される。次にその例を示す。
const struct employee { char *name; int birthdate; /* name, birthdate, job_code, and salary are */ int job_code; /* treated as though declared with const. */ float salary; } a, b; /* All members of a and b are const-qualified*/ struct employee2 { char *name; const int birthdate; /* Only this member is qualified */ int job_code; float salary; } c, d;
前の構造体のメンバは,すべて const
で修飾される。プログラムの後の部分で別の構造体を指定するために
employee
タグを使用すると,特に指定しない限り
const
修飾子は新しい構造体メンバに適用されない。
const
修飾子は
volatile
修飾子と一緒に指定できる。これは,ソース・
プロセスでは変わらないが,他のプロセスでは変更可能であるデータ・
オブジェクトの宣言に役立つ。また,実時間クロックなどのメモリ・
マップ入力ポートのモデルとしても役立つ。
const
オブジェクトのアドレスは,(明示的に
const
指定子を指定して) const
オブジェクトへのポインタに代入できる。ただし,そのポインタを使用してオブジェクトの値を変更することはできない。
次にその例を示す。
const int i = 0; int j = 1; const int *p = &i; /* Explicit const specifier required */ int *q = &j; *p = 1; /* Error -- attempt to modify a const- qualified object through a pointer */ *q = 1; /* OK */
const
修飾型へのポインタを使用して
const
オブジェクトを修正しようとすると,予測できない結果が生じる。
volatile
型修飾子を含むオブジェクトは,そのオブジェクトの参照または修正を変更するコンパイラの最適化の対象としてはならないことを示しています。
volatile
オブジェクトは,
特に副作用を引き起こす傾向があります(第2.5節を参照してください)。
volatile
を使用することによって無効になる最適化は,
次のように分類できます。
たとえば,オブジェクトの参照がプログラムの別の部分にシフトまたは移動する場合
たとえば,ループ・カウンタとして機能している変数をレジスタに格納し, メモリ参照の回数を少なくする場合
たとえば,変数参照を実際に排除するためループを導入する場合
volatile
指定子を持たないオブジェクトがコンパイラに対してこのような最適化を強制することはありません。
コンパイラは,プログラムの状況とコンパイラ最適化レベルによって,
最適化を自由に適用します。
volatile
修飾子は,コンパイラに
volatile
オブジェクトのメモリを割り当てさせて,そのオブジェクトを常にメモリからアクセスさせます。
この修飾子は,コンパイラを制御する方法以外の方法でオブジェクトをアクセスできるように宣言する際によく使用されます。
したがって,volatile
キーワードで修飾したオブジェクトは,
他のプロセッサまたはハードウェアによってそれぞれの方法で変更またはアクセスされる可能性があります。
さらに,副作用の影響を特に受けやすくなります。
次の規則は,volatile
修飾子を使用する場合に適用されます。
volatile
修飾子を使用して,文字列または共用体の単一メンバを含む任意のデータ型を修飾できる。
volatile
キーワードを重複して使用すると,警告メッセージが出される。
次にその例を示す。
volatile volatile int x;
volatile
を集成体宣言に使用すると,集成体のすべてのメンバが
volatile
で修飾される。集成体の中のあるメンバを修飾するために
volatile
を使用すると,そのメンバのみが修飾される。
次にその例を示す。
volatile struct employee { char *name; int birthdate; /* name, birthdate, job_code, and salary are */ int job_code; /* treated as though declared with volatile. */ float salary; } a,b; /* All members of a and b are volatile-qualified */ struct employee2 { char *name; volatile int birthdate; /* Only this member is qualified */ int job_code; float salary; } c, d;
このプログラムの後の部分で別の構造体を指定するために
employee
タグを使用すると,特に指定がない限り
volatile
修飾子は新しい構造体メンバに適用されない。
const
修飾子は
volatile
修飾子と一緒に使用できる。これは,ソース・
プロセスでは変わらないが,他のプロセスでは変更可能であるデータ・
オブジェクトの宣言に役立つ。または,実時間クロックなどのメモリ・
マップ入力ポートのモデルとしても役立つ。
volatile
オブジェクトのアドレスは,
volatile
オブジェクトへのポインタに代入できる。
次にその例を示す。
const int *intptr; volatile int x; intptr = &x;
同様に,volatile
オブジェクトのアドレスは非
volatile
オブジェクトへのポインタに代入できる。
ポインタ定義で,指しているデータが正しいアドレスで適切に境界調整されていないことをコンパイラに示すには, このデータ型修飾子を使用します。 適切に境界調整されるようにするには,オブジェクトのアドレスはその型のサイズの倍数でなければなりません。 たとえば,2バイトのオブジェクトは偶数のアドレスで境界調整される必要があります。
__unaligned
と宣言したポインタでデータをアクセスすると,
コンパイラはデータをコピーまたは格納するために必要な追加のコードを生成して,
境界調整のエラーが出ないようにします。境界調整の誤ったデータは一切使用しないのが良いのですが,
パックされた構造体にアクセスする必要やその他の理由から,
使用される場合があります。
次は,__unaligned
の一般的な使用例です。
typedef enum {int_kind, float_kind, double_kind} kind; void foo(void *ptr, kind k) { switch (k) { case int_kind: printf("%d", *(__unaligned int *)ptr); break; case float_kind: printf("%f", *(__unaligned float *)ptr); break; case double_kind: printf("%f", *(__unaligned double *)ptr); break; } }
ポインタについてコンパイラによる最適化を行うことを示すには,ポインタ型の宣言で
__restrict
型修飾子を使用します。限定ポインタは,ISO C
規格の9Xリビジョンに追加される予定です。限定ポインタを適切に使用すると,
多くの場合,コンパイラによるコード出力の質を改善することができます。
以降の各項では,限定ポインタをサポートする理由について説明します。
単に値をレジスタに保持しておくことからループの並列実行に至るまで, 多くのコンパイラの最適化を行うためには,2つの異なる左辺値が異なるオブジェクトを示しているかどうかを判断する必要があります。 オブジェクトが異なっていない場合, それらの左辺値はエイリアスであると言われます。2 つの左辺値がエイリアスであるかどうかをコンパイラが判断できない場合は, それらがエイリアスであると仮定して,さまざまな最適化をやめなければなりません。
単一の関数内,あるいは単一のコンパイル単位内でさえ,2つのポインタが同じオブジェクトを指しているかどうかを判断するために利用できる情報は十分でないため, ポインタを介したエイリアシングは大きな困難を伴います。 また,情報が十分に利用できたとしても,この分析にはかなりの時間とスペースを必要とします。 たとえば,関数の仮引数であるポインタが取り得る値を判断するには, プログラム全体を分析する必要があります。
2つの標準Cライブラリ関数memmove
とmemcpy
のCの実現において,どのように潜在的なエイリアシングが入るかについて考えてみます。
memmove
の使用に関する制限はなく,次に示す実現例では,
一時的な配列を経由してコピーすることにより,改訂ISO
C規格に記述のあるモデルに準拠している。
memcpy
は,相互に重なり合っている配列間でのコピーには使用できないため,
実現では直接コピーを行っていることがある。
次の例は,memcpy
関数とmemmove
関数の実現のサンプルを対比させています。
/* Sample implementation of memmove */ void *memmove(void *s1, const void *s2, size_t n) { char * t1 = s1; const char * t2 = s2; char * t3 = malloc(n); size_t i; for(i=0; i<n; i++) t3[i] = t2[i]; for(i=0; i<n; i++) t1[i] = t3[i]; free(t3); return s1; } /* Sample implementation of memcpy */ void *memcpy(void *s1, const void *s2, size_t n); char * t1 = s1; const char * t2 = s2; while(n-- > 0) *t1++ = *t2++; return s1; }
memcpy
の制限は,規格の説明には示されていますが,C
の実現において直接表すことはできません。このため,
memmove
での一時配列の使用を削除するというソース・
レベルの最適化はできますが,結果として得られる単一のループに対してのコンパイラの最適化は提供されません。
多くのアーキテクチャでは,バイトをブロック単位でコピーする方が,一 度に1バイトづつコピーするよりも速く処理することができます。
memmove
の実現では,malloc
を使用して一時配列を取得することにより,
確実に一時配列がソースおよび目的の配列から分離される。
このことから,コンパイラは,両方のループに対してブロック・
コピーが問題なく使用できることを推断することができる(
コンパイラがmalloc
を新しいメモリを割り当てる特殊な関数として認識する場合)
。
memcpy
の実現では,たとえば,
s1
とs2
が連続するバイトを指している可能性をコンパイラが除外する根拠を提供しない。
したがって,
ブロック・コピーを無条件に使用することは安全とは思われず,
memcpy
の単一のループに対して生成されたコードは,
memmove
の各ループに対して生成されたコードほど高速ではない可能性がある。
memcpy
の規格での説明にある制限では,相互に重なり合うオブジェクト
間でのコピーを禁止しています。オブジェクト
とはデータ記憶域の領域のことであり,ビット・フィールドを除いて,1
あるいはそれ以上のバイトの連続した並び(数,順番,およびエンコーディングは,
明示的に指定されているか,または実装者が定義する。)
で構成されます。
次の例について考えてみてください。
/* memcpy between rows of a matrix */ void f1(void) { extern char a[2][N]; memcpy(a[1], a[0], N); }
この例から,次のことがわかります。
N
バイトに決定される。
つまり,N
個の要素を持つ文字型の配列として処理される。
memcpy
の呼出しは,定義どおりに動作する。
では,次の例について考えてみましょう。
/* memcpy between halves of an array */ void f2(void) { extern char b[2*N]; memcpy(b+N, b, N); }
この例からは,次のことがわかります。
memcpy
では,配列内の連続する一連の要素は,
それ自身でオブジェクトと見なされることができる。
b
の重なり合っていない半分は,それ自身でオブジェクトと見なされる。
オブジェクトの長さは,さまざまな方法で決定されます。
mbstowcs
, wcstombs
, strftime
, vsprintf
,
sscanf
, sprintf
,および他のすべての同様の関数では,
オブジェクトと長さは動的に決定される。
memcpy
に関する制限と同様,エイリアシング制限も関数定義として表すことができるのであれば,
コンパイラは効果的にポインタのエイリアスの分析を行うために利用することができるでしょう。
__restrict
型修飾子は,ポインタの宣言で,ポインタが
malloc
への呼出しで初期化されたかのように,
ポインタが指し示すオブジェクトに対して,排他的な初期のアクセス
を提供することを指定することによって,これを実現します。
次に示すmemcpy
のためのプロトタイプは,必要な制限を表すとともに,
現在のプロトタイプと互換性があります。
void *memcpy(void * __restrict s1, const void * __restrict s2, size_t n);
次に示す限定ポインタの定義では,できるだけ多数の典型的な例におけるエイリアシング制限の式をサポートしています。 これは,既存のプログラムを変換して, 限定ポインタを使用する際に有用であり,また,新規のプログラムでは, より自由にスタイルを選択できるようになります。
このため,この定義では,限定ポインタについて次のことが可能です。
ポインタは,宣言で__restrict
型修飾子を指定することにより,
限定ポインタとして指定されます。
改訂ISO C規格に含まれる予定の限定ポインタの正式な定義は,次のとおりです。<blockquote>
Dは,オブジェクトPを限定修飾ポインタとして指定する方法を提供する通常の識別子の宣言とする。
Dがブロックの内部にあり,記憶域クラスexternを持たないとき,B はそのブロックを示すものとする。Dが関数定義の仮引数宣言の並びにあるとき,B はそれに関連するブロックを示すものとする。それ以外の場合には,B はmainのブロック(または,フリースタンディング環境において, プログラムの開始時に呼ばれる関数のブロック) を示す。
以降で,(ポインタ式Eを評価する前であってBを実行しているある評価順序点において) P
が以前指していた配列オブジェクトのコピーを指すようにP
を修正するとEの値が変わるとき,EはオブジェクトP
に基づいているという。つまり,Eは,Pを介して間接的に参照されるオブジェクトの値ではなく,P
の値自体に依存している。
たとえば,識別子p
が(int
** restrict)
という型を持つとすると,ポインタ式
p
およびp+1
は,p
で指定された限定ポインタ・オブジェクトに基づいているが,ポインタ式
*p
およびp[1]
は基づいていない。
Bの各実行中には,Oは,Pに基づくポインタ式を介するすべての参照によって動的に決定される配列オブジェクトとする。O の値へのすべての参照は,P に基づくポインタ式を介して行われなければならない。 さらに,Pが,別の限定ポインタ・オブジェクトP2に基づいているポインタ式E の値を割り当てられ,ブロックB2に関連付けられていれば,B2 のいずれかの実行は,Bの実行の前に開始されるか, あるいは,B2の実行はその割り当ての前に終了しなければならない。 この条件を満足しない場合,動作は未定義となる。
ここで,Bの実行とは,Bに関連付けられていて自動記憶域存続期間を持つオブジェクトのインスタンスのために記憶域の予約が保証されている間の, プログラムの実行の一部という意味である。値の参照とは, 値へのアクセスまたは値の変更のことである。Bの実行中, 実際に評価される参照だけが調べられる。(評価されない式での参照や, 可視識別子を使用するという意味において「利用可能」であるが, 実際にはBのテキストに現れない参照は除外される。)
翻訳プログラムは,限定ポインタの使用によるすべてまたは任意のエイリアシングを自由に無視することができる。</blockquote>
__restrict
型修飾子の正式な定義は分かりにくいものですが,
説明を簡略にすると不正確で不完全になってしまいます。この定義の本質は,
__restrict
型修飾子は,限定ポインタを介してメモリ・
アクセスが行われるときにはいつでもプログラマによってなされる表明であり,
コンパイラが考慮する必要のある唯一のエイリアスは,
同じポインタを介して行われる別のアクセスである,ということです。
複雑に感じられる原因の多くは,ポインタを介して行われるアクセスが何を意味するのかを厳密に定義したり( 基づくの規則),ブロック境界においてのみ起こりうるエイリアシングを制限しながら, 限定ポインタに別の限定ポインタの値を代入する方法を指定したりする点にあります。 限定ポインタについて理解する最善の方法は,例を参照することです。
次の例は,さまざまな文脈における限定ポインタの使用方法を示しています。
ファイル・スコープ限定ポインタは,非常に強い制限を受けます。このポインタは, プログラムが存続している間,単一の配列オブジェクトを指していなければなりません。 その配列オブジェクトは,限定ポインタを介して参照されてもならず, その宣言された名前(ある場合)または別の限定ポインタを介して参照されてもなりません。
これらの制限のため,ポインタを介した参照は,その宣言された名前で静的配列を参照するのと同様に効率的に最適化することができます。 したがって, ファイル・スコープ限定ポインタは,動的に割り当てられるグローバル配列へのアクセスを提供する際に有用です。
次の例では,コンパイラは__restrict
型修飾子から,名前
a
,b
,およびc
の間で潜在的なエイリアシングが存在しないことを推測することができます。
/* File Scope Restricted Pointer */ float * __restrict a, * __restrict b; float c[100]; int init(int n) { float * t = malloc(2*n*sizeof(float)); a = t; /* a refers to 1st half. */ b = t + n; /* b refers to 2nd half. */ }
関数init
では,割り当てられた記憶域の単一のブロックが2
つの別個の配列に分けられています。
限定ポインタは関数のポインタ仮引数としても非常に有用です。次の例について考慮してみてください。
/* Restricted pointer function parameters */ float x[100]; float *c; void f3(int n, float * __restrict a, float * const b) { int i; for ( i=0; i<n; i++ ) a[i] = b[i] + c[i]; } void g3(void) { float d[100], e[100]; c = x; f3(100, d, e); /* Behavior defined. */ f3( 50, d, d+50); /* Behavior defined. */ f3( 99, d+1, d); /* Behavior undefined. */ c = d; f3( 99, d+1, e); /* Behavior undefined. */ f3( 99, e, d+1); /* Behavior defined. */ }
関数f3
では,コンパイラは,変更されたオブジェクトのエイリアシングはないと推断して,
積極的にループの最適化を行う可能性があります。
f3
に入ると,限定ポインタa
はそれに関連する配列への排他的なアクセスを提供する必要があります。
特に,f3
の内部では,b
もc
もa
に基づくポインタ値を代入されていないため,どちらも
a
に関連する配列内を指していない可能性があります。
b
については,宣言のconst
修飾子から明らかですが,
c
については,f3
の本体の検査が必要になります。
g3
に示す2つの呼出しは,結果として,
__restrict
修飾子と矛盾するエイリアシングとなるため,
それらの動作は未定義となります。c
が,b
に関連している配列内を指すことは許されています。
また,この目的のため,特定のポインタに関連する「配列」は,そのポインタを介して実際に参照される配列オブジェクトの一部分のみを意味します。
ブロック・スコープ限定ポインタは,そのブロックに制限されたエイリアシング表明を行います。 これは,表明に関数スコープを持たせるよりも自然です。 たとえば,主要なループにのみ適用されるローカル表明ができるようにします。 また,関数をマクロに変換することにより関数をインライン化する際に同様の表明を行うことができるようにします。
次の例では,元の限定ポインタ仮引数は,ブロック・スコープ限定ポインタで表されています。
/* Macro version of f3 */ float x[100]; float *c; #define f3(N, A, B) \ { int n = (N); \ float * __restrict a = (A); \ float * const b = (B); \ int i; \ for ( i=0; i<n; i++ ) \ a[i] = b[i] + c[i]; \ }
構造体の限定ポインタ・メンバはエイリアシング表明を行います。この表明のスコープは, この構造体にアクセスするために使用される通常の識別子のスコープです。
したがって,次の例では,構造体の型はファイル・スコープで宣言されていますが,
f4
の仮引数の宣言によって行われている表明は,(
関数の)ブロック・スコープを持ちます。
/* Restricted pointers as members of a structure */ struct t { /* Restricted pointers assert that */ int n; /* members point to disjoint storage. */ float * __restrict p; float * __restrict q; }; void f4(struct t r, struct t s) { /* r.p, r.q, s.p, s.q should all point to */ /* disjoint storage during each execution of f4. */ /* ... */ }
typedef
内の__restrict
修飾子は,
オブジェクトへのアクセスを提供する通常の識別子の宣言で
typedef
名を使用する際にエイリアシング表明を行います。
構造体のメンバに関しては,typedef
名のスコープではなく,
通常の識別子のスコープが,エイリアシング表明のスコープを決定します。
次の例について考慮してみてください。
/* Pointer expressions based on p */ #include <stdlib.h> #include <string.h> struct t { int * q; int i; } a[2] = { /* ... */ }; void f5(struct t * __restrict p, int c) { struct t * q; int n; if(c) { struct t * r; r = malloc(2*sizeof(*p)); memcpy(r, p, 2*sizeof(*p)); p = r; } q = p; n = (int)p; /* - - - - - - - - - - - - - - - - - - - - - - - Pointer expressions Pointer expressions based on p: not based on p: ------------------- ------------------- p p->q p+1 p[1].q &p[1] &p &p[1].i q q->p ++q (char *)p (char *)(p->i) (struct t *)n ((struct t *)n)->q - - - - - - - - - - - - - - - - - - - - - - - - */ } main() { f5(a, 0); f5(a, 1); }
この例では,限定ポインタ仮引数p
は,2つの構造体の元の配列のコピーを指すように潜在的に調整されます。
定義により,
その後のポインタ式は,その値がこの調整によって変更される場合に限り,
p
に基づくと言われます。
コメント部分の説明は次のとおりです。
p
に基づいている。
p
に基づいていない。
この式に関する適切なprint文を追加し,main
内での
f5
の2つの呼出しによって生成された値を比較することにより,
このことを確認することができます。
「基づく」の定義は,処理系定義の動作に依存する式に適用されます。
これについては例を参照してください。例では,後ろに(struct t
*)
が続く(int)
のキャストは,元の値を提供すると想定しています。
最初の限定ポインタが関連付けられているブロックの実行を,2番目のポインタに関連付けられているブロックの後に開始した場合, 一方の限定ポインタがもう一方の限定ポインタより「新しい」と想定します。 このとき, 正式な定義では,新しい限定ポインタには,古い限定ポインタに基づく値を代入することができます。 これにより,たとえば,限定ポインタ仮引数を持つ関数を, 限定ポインタである実引数を指定して呼び出すことができます。
逆に,古い限定ポインタには,新しい限定ポインタに関連付けられているブロックの実行が終了した後にのみ, 新しい限定ポインタに基づく値を代入することができます。 これにより,たとえば,関数にローカルな限定ポインタの値を関数が返した後, その返却値を別の限定ポインタに代入することができるようになります。
プログラムに,これら2つのカテゴリのいずれにも当てはまらない2つの限定ポインタ間での代入が含まれている場合, このプログラムの動作は未定義となります。 次の例を参照してください。
/* Assignments between restricted pointers */ int * __restrict p1, * __restrict p2; void f6(int * __restrict q1, * __restrict q2) { q1 = p1; /* Valid behavior */ p1 = p2; /* Behavior undefined */ p1 = q1; /* Behavior undefined */ q1 = q2; /* Behavior undefined */ { int * __restrict r1, * __restrict r2; ... r1 = p1; /* Valid behavior */ r1 = q1; /* Valid behavior */ r1 = r2; /* Behavior undefined */ q1 = r1; /* Behavior undefined */ p1 = r1; /* Behavior undefined */ ... } }
次の例のように,限定ポインタの値は,非限定ポインタに代入することができます。
/* Assignments to unrestricted pointers */ void f7(int n, float * __restrict r, float * __restrict s) { float * p = r, * q = s; while(n-- > 0) *p++ = *q++; }
Compaq Cコンパイラは,ポインタ値を追跡して,限定ポインタ
r
とs
が直接使用された場合と同様に,
効率的にループを最適化します。これは,この場合,p
はr
に基づき,q
はs
に基づいていると簡単に判断できるためです。
限定ポインタと非限定ポインタをより複雑に組み合わせて使用すると,難しすぎてコンパイラが分析できないため, あまり効率的ではありません。 パフォーマンスが重要な場合には,プログラミングのスタイルをコンパイラの機能に合わせる必要があります。 保守的な方法では,同じ関数内では, 限定ポインタと非限定ポインタの両方を使用しないようにします。
正式な定義で特別に記述している場合を除いて,
__restrict
修飾子はconst
および
volatile
と同様に動作します。
特に,関数の返却値の型またはキャストの型名を修飾することは制約違反ではありませんが, 関数呼出し式およびキャスト式は左辺値ではないため, 修飾子は何の影響も及ぼしません。
このため,次の例でf8
の宣言に__restrict
修飾子が指定されていても,f8
を呼び出す関数内のエイリアシングについては何の表明も行いません。
/* Qualified function return type and casts */ float * __restrict f8(void) /* No assertion about aliasing. */ { extern int i, *p, *q, *r; r = (int * __restrict)q; /* No assertion about aliasing. */ for(i=0; i<100; i++) *(int * __restrict)p++ = r[i]; /* No assertion */ /* about aliasing. */ return p; }
同様に,2つのキャストは,ポインタp
および
r
を介した参照のエイリアシングについて,何の表明も行いません。
ポインタ型でないオブジェクト型を限定修飾することや,関数へのポインタを限定修飾することは, 制約違反です。次の例を参照してください。
/*__restrict cannot qualify non-pointer object types: */ int __restrict x; /* Constraint violation */ int __restrict *p; /* Constraint violation */ /* __restrict cannot qualify pointers to functions: */ float (* __restrict f9)(void); /* Constraint violation */
typedef
は型の同義語を定義するために使用します。
この定義では,識別子はオブジェクトの代わりに
型の名前を指定します。この定義方法によって,長い型定義や間違えやすい型定義に省略名を定義できます。
型定義では新しい基本データ型を作成しません。これは,基本型または派生型の別名を作成します。 たとえば,次のコードを使用すると,プログラムの後の部分で使用するオブジェクトのデータ型を説明するときに便利です。
typedef float *floatp, (*float_func_p)();
この場合,floatp
型は「float
値へのポインタ」型であり,
float_func_p
型は「float
を返す関数へのポインタ」型です。
型定義は,完全な型名が通常使用されている(通常の型名を使用できる)場所であれば, どこでも使用できます。型定義は変数と同じ名前空間を共有し, 定義した型はそれに等しい型と完全に互換性があります。修飾型として定義された型はその型修飾を引き継ぎます。
他の型定義から型定義を作成することもできます。次にその例を示します。
typedef char byte; typedef byte ten_bytes[10];
型定義は変数または関数に適用できます。型定義を他の指定子と合わせて使用することはできません。 次にその例を示します。
typedef int *int_p; typedef unsigned int *uint_p; unsigned int_p x; /* Invalid */ uint_p y; /* Valid */
型定義を使用して,関数型を宣言することもできます。ただし,型定義は関数定義に使用することはできません。 関数の返却値の型は型定義を使用して指定できます。 次にその例を示します。
typedef unsigned *uint_p; /* uint_p has type "pointer to unsigned int" */ uint_p xp; typedef uint_p func(void); /* func has type "function returning pointer to */ /* unsigned int */ func f; func b; func f(void) /* Invalid -- this declaration specifies a */ /* function returning a function type, which */ { /* is not allowed */ return xp; } uint_p b(void) /* Legal -- this function returns a value of { /* type uint_p. */ return xp; }
次の例では,関数定義は typedef
名から引き継ぐことができないことを示しています。
typedef int func(int x); func f; func f /* Valid definition of f with type func */ { return 3; } /* Invalid, because the function's type is not inherited */
この例を有効な形式に変更すると,次のようになります。
typedef int func(int x); func f; int f(int x) /* Valid definition of f with type func */ { return 3; } /* Legal, because the function's type is specified */
typedef
名に仮引数名を含めて,プロトタイプ情報を取り込むことができます。
また,第2.3
節で説明したスコープ規則に従って,内部スコープに
typedef
名を再定義することもできます。