エラーを起こさないために
C 言語でプログラムをする際の間違え易い点,注意すべき点について示す.
なお,C 言語の文法としてはANSI C を基準として考える.
ANSI C 標準は1989年12月14日に "ANSI‐プログラム言語 C, X3.159-1989"として制定され,1990年春に出版された.この標準は国際標準 ISO/IEC 9899:1990
として採用されている(通称 C90).
その後,1999年12月にISOで規格の改定が行われ ISO/IEC 9899:1999(E) Programming Language--C (Second Edition) が制定された(通称
C99).さらに,2011年に ISO/IEC 9899:2011 (通称 C11) が発行されている.
ここでは,C90 の規格をベースに留意すべき点を挙げる.
データ型,演算子,式
ブール式と変数
式
- インクリメント演算子とデクリメント演算子
インクリメント演算子++とデクリメント演算子--は,変数の値を変化させるが,その変数が同じ式の中で再び参照された場合,動作は未定義となり値は保証されない.
例えば,
a[i] = i++;
において,i++は i の値を増加させるが,i が同じ式の中で再び参照されている
ためこの式の動作は未定義となる.
また,次の式
int i = 7;
printf("%d\n", i++ * i++);
は,結果の値として何が表示されるか保証されない(49 または 56?).
i++ や i-- のようなポストインクリメント演算子やポストデクリメント演算子における変数の値の変化は,それが用いられている式全体の評価が終了する前に行なわれることが保証されているだけである.
上記の例では,乗算が行なわれる前にインクリメントが行なわれるか,乗算が行なわれた後にインクリメントが行なわれるかは処理系に依存する.
なお,式の値を用いない場合(値のインクリメント)は,i++ と ++i は全く等価でありどちらを用いてもよい.
C では演算子の被演算数に対する評価順序は規定していない.例外は,&&,||, ?: およびコンマ演算子である.これらの演算子は左から右への評価順序が保証される.
- 演算における型変換
以下のコードは正しい結果にならない可能性がある (int型のサイズによる).
int a = 1000, b = 1000;
long int c = a * b;
乗算は整数演算として行なわれ,結果が左辺に代入される前にオーバフローになるか切捨てが行なわれる.
以下のように演算がlong int型で行なわれるように明示的にキャストしなければならない.
long int c = (long int)a * b;
浮動小数点
- 一般に,ディジタル計算機においては浮動小数点演算を正確なシミュレーションにより行なっているわけではない.アンダフロー,累積誤差などの問題が起こり得る.
したがって,浮動小数点演算が完全に正確に行なわれることを仮定してはいけない.特に,2つの浮動小数点数が等しいか否かの比較を行なう場合は注意が必要である.
常に,<=や>=でテストし,正確な比較(!=や==)を用いてはいけない.
- 一般に数学ライブラリを使う場合は,数学ライブラリをリンクすることを明示的にコンパイル/リンク時に指定する必要がある.例えば,UNIX では -lm オプションを付ける.
- C ではべき乗演算子はない.数学ライブラリ(<math.h>)の pow()関数を用いる.指数の小さなべき乗演算に対しては,乗算を用いた方が良い場合が多い.
- 処理系によっては浮動小数点演算に関する機能 (ライブラリ)
をリンクするか否かを選択できる(あるいは処理系が自動的に判断する)ようになっている.これにより,浮動小数点演算を使わないプログラムではリンク時間の短縮が図れる.
printf や scanf の浮動小数点をサポートしない版では,%e, %fおよび %gを扱うコードを含まないことによりメモリの節約が図れる.
宣言
- 整数型の宣言
どの整数型を用いるかの基準は以下のとおりである.
- 32767以上または-32767以下の値を用いる場合は long型を用いる(16ビットマシン以上を想定).
- 32767以上または-32767以下の値を用いない場合,大きな配列や多くの構造体を用いるなどメモリの節約を重視するならば short型を用い,そうでなければ int型を用いる.
- 負の値を取らない数を扱うならば,対応する unsigned型を用いる.
但し,式において signed型 とunsigned型の混合に注意する必要がある.
char型および unsigned char型は小さい値の int型として用いることができるが,予期しない符合拡張やコードサイズの増加によりしばしばトラブルの原因となる.
- main関数
main関数は 0 または 2個の引数を持つ intを返す関数として宣言されねばならない.
- 以下のように新しいスタイルのプロトタイプ宣言と旧タイプの宣言を混在させてはいけない.
extern int func(float);
int func(x)
float x;
{...
- 旧い Cコンパイラ(およびプロトタイプ宣言のない ANSI C)では,すべての浮動小数点演算は倍精度で行なわれていた.したがって,float型の引数は暗黙のうちに double型に変換されて演算される.
新しいスタイルでは,以下のように定義する.
int func(float x) { ... }
また,旧いスタイルに合わせる場合には以下のようにする.
extern int func(double);
- 文字列へのポインタを返す関数へのポインタを返す関数へのポインタの配列
char *(*(*a[5])())();
これは次のように順番に型をtypedef宣言することによっても行える.
typedef char *pc; /* pointer to char */
typedef pc fpc(); /* function returning pointer to char */
typedef fpc *pfpc; /* pointer to above */
typedef pfpc fpfpc(); /* function returning... */
typedef fpfpc *pfpfpc; /* pointer to... */
pfpfpc a[5]; /* array of... */
- ANSI標準では外部変数名は6文字かつ1段(大文字か小文字)についてしか一義性は保証されていない.外部名はC言語の制御範囲外のアセンブラやローダでも使わるためである.
この制限は長さが6文字までの制限ではなく,最初の6文字が識別子として有効であるということである.
この制限は将来の版では緩和される方向にある.
- 外部変数の定義と宣言
外部変数の定義(定義宣言)はオブジェクトの生成(記憶領域の割り付けと場合によっては初期化)引き起こすが,宣言(参照宣言)ではそのようなことはない.ソースプログラムを構成するファイルの中で外部変数の定義はただ1つでなければならない.外部変数をアクセスする変数定義を含まない他のファイルではexternal宣言を指定する.定義を含んでいるファイルにも
external宣言はあってもよい.ANSI Cでは初期化子を伴わない宣言は同じ定義が複数存在してもよい.
外部変数の定義と宣言は以下のルールに従うのが良い.
- 外部変数の定義はプログラムの中心となる .cファイルに置き,その外部宣言(external宣言)をヘッダファイルに置く.その外部変数を使用する
.cファイルではヘッダファイルをincludeするようにする.外部変数の定義を含むファイルにもヘッダファイルをincludeすべきである.これにより,処理系が変数宣言の一致をチェックできる.
このルールは移植性を高めるとともに,ANSI Cの標準にも一致している.
また,外部変数をヘッダファイルにまとめて置き,プリプロセッサの処理により各ファイルにおいて定義宣言と参照宣言を適切に宣言するようにすることができる.
ヘッダファイルに変数を以下のような形式で定義する.
EXTERN char buf[100];
変数を定義したいファイルでは,ヘッダファイルをインクルードするとともに#include文の前に以下の定義を入れる.
#define EXTERN
一方,変数を参照するファイルでは,#include文の前に以下の定義を入れる.
#define EXTERN extern
- 関数へのポインタ
関数へのポインタはそれが使用される前に,以下の例のように実際の関数を指すように初期化されねばならない.
int r, func();
int (*fp)() = func;
r = (*fp)();
関数の名前が式に現れるが関数として呼ばれていない(すなわち"("が続かない)とき,配列の名前と同様にポインタ(関数のアドレス)を示す.
この場合,通常明示的な関数の外部宣言が必要である.
型"Tを返す関数"の式は,&演算子の被演算数として使われる場合を除き,"Tを返す関数へのポインタ"に変換される.
以下の式
r = fp();
は,fpが関数でも関数へのポインタでも正しく動作する.但し,旧タイプの処理系への移植性を考慮するならば,関数へのポインタを介した呼び出しには明示的に * を付けた方が良い.
制御構文
- if 文とswitch 文
if文は2分岐選択のための文でswitch文は多分岐選択のための文である.
switch文で記述する処理は入れ子にしてif文でも記述できる.
しかし,if文の入れ子の場合,記述した順に評価されるのに対し,switch文は記述した条件(case文)の順に評価される保証はない.
したがって,選択すべき複数の条件の中で各条件の成立する確率に偏りがある場合は,確率の高い順にif文により記述した方が効率が良くなる場合がある.
関数とプログラム構造
初期化
- プログラム領域
Cプログラムは一般的には以下のように5つの領域から構成されている.
・TEXTセグメント |
: |
コード領域 |
・DATAセグメント |
: |
初期化データ領域 |
・BSSセグメント |
: |
非初期化データ領域 |
・ヒープ領域 |
: |
動的確保領域 |
・スタック領域 |
: |
スタック領域 |
各セグメントの並びはアドレスの低番地から上記の順になっている.但し,処理系によってはスタック領域とヒープ領域が逆になる場合がある.
プログラムの外部変数と静的変数は,プログラム内で初期化されているとDATAセグメント,初期化されていないとBSSセグメントに置かれる.BSSセグメントは,main()が呼び出される前に全体が0で初期化される.
ヒープ領域は,malloc()などで動的に切り出されて使用されるためのメモリ領域である.
スタック領域は関数呼び出しに加えて,ローカルな自動変数領域として使われる.スタック領域は処理系により可変長の場合と固定長の場合がある.
- 変数の初期化
明示的な初期化がないとき,外部変数と静的変数は0に初期化されることが保証されている.したがって,これらの変数は例えばそれがポインタならば正しい型のnullポインタに,浮動小数点数ならば0.0に初期化される.
外部変数と静的変数では,初期値は定数式でなければならない.初期化はコンパイル時に一度だけ行なわれる.
自動変数とレジスタ変数は,明示的な初期化がなければ初期値は不定(ゴミ)である.mallocとreallocで確保されたメモリ領域もその値は不定(ゴミ)であり,呼び出し側で初期化されねばならない.一方,callocで確保されたメモリ領域は0で初期化される.
自動変数とレジスタ変数では,初期値は前もって決まっていて正しいものであれば,関数の呼出しをも含んだ任意の式でよい.
初期化は関数やブロックに入るごとに行なわれる.
- 共用体の初期化
ANSI Cでは共用体(union)の最初のメンバに対してのみ初期化が行なえるが,他のメンバの初期化を行なう標準的な方法はない.
プリプロセッサ
- プリプロセッサ指令
ANSI Cでは,プリプロセッサ指令を表す#は,その行の最初の空白文字でない文字でなければならない.
しかし,旧い処理系ではその行の最初の文字でなければならないものもある.したがって,旧い処理系への移植を考える場合はプリプロセッサ指令は行の最初から始めるべきである.
- トークン
ANSI Cでは,#if, #ifdef あるいは
#ifndefによって取り除かれるテキストもプリプロセッサの解釈する正当なトークンから構成されねばならない.これは,終端のないコメントや引用符および引用符内の改行があってはならないことを意味する.
- 文字列の連結
文字列の連結のための以下のマクロはANSI Cでは正しく動作しない.
#define Paste(a, b) a/**/b
上記のマクロは,コメントが完全に取り除かれることを前提にしているが,ANSI Cではコメントは単一の空白と置き換えられる.
ANSI Cでは##演算子がトークンの連結を行なうために用意されている.
##演算子があると,パラメータの置換後に両側の空白とともに##も削除されて隣接するトークンが連結される.使用例を以下に示す.
#define Paste(a, b) a##b
- ヘッダファイル
ヘッダファイルのネストは,定義箇所を分かり難くしたり,二重定義エラーを引き起こしたり,makeファイルの人手による保守を困難にしたりするため,できるだけ避けた方が良い.しかし,大規模なプログラムなどではプログラムのモジュール化を図るため,ヘッダファイルのネストが必要な場合もある.
このような場合,予期しないヘッダファイルの二重インクルードを防ぐため,以下のものを各ヘッダファイルに入れるのが一般的である.
#ifndef HEADER_FILE_NAME
#define HEADER_FILE_NAME
...header file contents...
#endif /* HEADER_FILE_NAME */
また,grepなどによる定義の確認やmakeファイルの自動生成ツールなどを用いる上記の問題を避けるようにすることができる.
- 標準ヘッダファイル
標準ヘッダファイルは処理系やOSに固有であり,他の処理系のものと混在させてはいけない.
- sizeof
プリプロセッサ指令中ではsizeof演算子は一般には使えない.プリプロセッサは型名が解析される前に処理を行なうためである.
<limits.h>であらかじめ定義されている定数を用いるようにする.
- cpp
cppは汎用的なプリプロセッサではない.特殊な前処理が必要な場合は,無理にcppで行なうよりは専用の簡易ツールを作成し,makeなどで自動的に起動させるようにした方が良い.
また,C言語以外の言語に対してはm4のような汎用のプリプロセッサを用いることもできる.
- #pragma指令
#pragma指令は処理系独自の制御や拡張,警告の抑止などを実現するために用いられる.認識できない#pragma指令は無視される.
pragmaは処理系によって解釈が異なり得ることもあるため,pragmaを用いる場合は,処理系依存として#ifdefで囲むべきである.また,ANSIでない処理系ではpragmaは
常に#ifdefで除外されなければならない.
- #, ##演算子
#演算子を使ってマクロを定義する場合,以下のようにする.
#define str(x) #x
#define xstr(x) str(x)
#define OP plus
char *opname = xstr(OP);
これは,opnameを"plus"にセットする.str(OP)では"OP"となる.
##演算子でも同様である.
- 定義済み名前
以下の識別子は前もって定義されており,未定義にしたり再定義してはならない.
__LINE__ __FILE__ __DATE__ __TIME__ __STDC__
ポインタと配列
NULLポインタ
C言語においては,各ポインタ型に対して"nullポインタ"とよばれる特別な値が定義されている.この値は他の全てのポインタ値とは区別され,どのようなオブジェクトも指さないものである.演算子 & は,nullポインタを決して生成しないことを意味する.
- nullポインタは,初期化されていないポインタとは概念が異なる.
nullポインタはどのようなオブジェクトも指さないものであり,一方初期化されていないポインタはどこかを指している.
-
上記の定義から,各ポインタ型に対してnullポインタが存在するが,その内部値はポインタ型ごとに異なり得る.nullポインタが内部でどのような値を取るかは処理系に依存する.
プログラマはnullポインタの内部値を知る必要はないが,処理系はどのnullポインタ型が要求されているかを知らねばならない.
-
言語定義により,ポインタのコンテキストにおける定数0はコンパイル時にnullポインタに変換される.
すなわち,ポインタコンテキストにおけるソースコードの"0"は,nullポインタを生成することが保証される.例えば,初期化,代入および比較において,一方がポインタ型の式または値であるとき,他方の0は対応したnullポインタ値に変換される.
したがって,次の文は正しい.
char *p = 0;
if (p != 0) { ... }
しかし,関数の引数として渡される場合は,必ずしもポインタのコンテキストと認識されない.
例えば,UNIXのシステムコールexeclは可変長のnullポインタで終了するchar型のポインタのリストを引数として持つ.
関数の呼び出しにおいてnullポインタを生成するためには,以下のように明示的にキャストする必要がある.
execl("/bin/sh", "sh", "-c", "ls", (char *)0);
上記において,キャストが指定されていない場合,コンパイラはnullポインタの代わりに整数の0を渡すことになる.
-
関数プロトタイプが宣言されている場合は,引数は代入のコンテキストになりキャストを省略しても多くの場合(引数が固定長の場合)問題はない.すなわち,コンパイラはどの型のポインタが要求されているかを知ることができ,0を正しいnullポインタに変換できる.
しかし,関数プロトタイプは可変長の引数のリストに対してはその型を知ることはできないため,このような引数に対してはキャストが必要である.
このような可変長の引数の問題や非ANSIコンパイラの使用を考慮すると,関数の引数に対しては常にキャストを指定することが安全である.
プリプロセッサマクロNULLが<stdio.h>または<$stddef.h>において値0または(void *)0として定義されている.
ANSI CではNULLの定義として
#define NULL ((void *)0)
も認めている.
この定義はNULLが不正に使用された場合(例えば,NUL文字を用いるべき場所
に用いた場合)を検出することができる.
-
プログラムコード上で整数の0とnullポインタの0を明示的に区別するため,nullポインタが要請される所では常に"NULL"を用いるべきである.また,NULLをポインタ以外の0が要求されている場合には,正しく動作する場合でも用いてはならない.ANSIのようにNULLが(void
*)0として定義されている場合には,ポインタのコンテキスト以外では正しくなくなる.
if, while文などにおいて式のブール値が評価されるとき,式の値が0に等しい場合偽を,その他の場合真の値が生成される.
したがって,以下の式
if (expr)
に対して,コンパイラは
if (expr != 0)
と全く同様に動作する.これはexprがポインタの場合も同様であり,正しくnullポインタと比較される.
また,!exprは expr ? 0 : 1 に等価である.
-
NULLと混同し易いものに,null文字とnull文字列がある.null文字はASCII文字(NUL)であり,'\0'を表す.null文字列は空の文字列でありnull文字を含むものである.
上記より,nullポインタの使用においては次の2つのルールに従う.
- 初期化,代入,比較においてNULLポインタを参照する場合は"NULL"(または"0")を用いる.
- 関数呼び出しの引数に"0"または"NULL"が用いられる場合は,その関数の引数に期待されるポインタ型にキャストする.
配列
- ポインタと配列
あるファイルで配列a[10]が定義されているとき,これを他のファイルから参照するには,extern char a[]とする(extern char *aではない).
型Tへのポインタと型Tの配列とは異なる.
以下の宣言
char a[] = "hello";
char *p = "world";
は,次のデータ構造を意味する.
+---+---+---+---+---+---+
a: | h | e | l | l | o |\0 |
+---+---+---+---+---+---+
+-----+ +---+---+---+---+---+---+
p: | *======> | w | o | r | l | d |\0 |
+-----+ +---+---+---+---+---+---+
x[3]のような参照はxが配列かポインタかによって異なるコードを生成する.
すなわち,xが配列であれば,位置xから3つ移動した位置の文字を取り出すコードを出力する.一方,xがポインタのときは,位置xにあるポインタ値を取り出しそれに3を加えた位置の文字を取り出すコードを出力する.
pはポインタであり変数であるが,aは配列名である.配列名は代入ができないという意味において定数であり,ポインタではない.したがって,a++や a=p のような構文は正しくない.
- ポインタと配列の等価性
ポインタと配列が等価であるとは,それらが交換できることを意味しているのではない.
等価性とは,型Tの配列型の式はその配列の最初の要素への定数ポインタとみなされるということである.結果のポインタは型Tへのポインタである.
但し,配列がsizeof()または&演算子のオペランドであるとき,あるいは文字配列に対するリテラルな文字列の初期化子であるときは例外である.
式 x[i] は,xがポインタか配列かによらず *((x)+(i))に等しい.
関数のパラメータとしては,配列とポインタは等価である.配列はポインタとして関数に渡される.
以下の関数宣言におけるパラメータは,コンパイラによってポインタのパラメータとして扱われる.
void func(char a[]);
- sizeof(array)
sizeof(array)は配列arrayが関数のパラメータであるとき,arrayの正しいサイズを返さない.コンパイラは配列パラメータをポインタのパラメータとみなすため,ポインタのサイズを返却する.
配列の参照 a[e]は *((a) + (e)) に等価である.この関係は,aとeの一方がポインタ表現であり,他方が整数であるかぎり成立する.そのため,5["abcdef"]のような表現も正しいものである.
関数のパラメータにおいては,配列の配列すなわち2次元配列は,ポインタへのポインタではなく配列へのポインタとして扱われる.
Cにおいては,2次元配列はその要素が配列となるような1次元配列である.
以下のように,パラメータとして2次元配列を関数に渡すとき,
int array[ROW][COL];
func(array);
関数の宣言は次のようでなければならない.
func(int a[][COL]) {...}
or
func(int (*ap)[COL]) {...} /* ap is a pointer to an array */
関数に渡されるのはCOL個の配列へのポインタであるため,行の次元(ROW)は無関係であり,省略できる.
配列へのポインタを宣言するには,Nが配列の大きさであるとき"int (*ap)[N]"のようにする.
- 配列
int realarray[10];
int *array = &realarray[-1];
上記のようにするとき,arrayを1をベースとする配列のように扱うことができる.
このテクニックは厳密にはCの標準には従っていない.
ポインタの演算は確保されたメモリ領域内でのみ定義されるものである.
上記の操作において,ポインタを--1することにより不正なアドレスをアクセスする可能性がある.
- ポインタの参照
以下のコードでは呼び出し側のポインタの内容は変化しない.
Cの引数は値渡しであり,ポインタのコピーに対して値が設定されるのみである.呼び出し側のポインタの内容を操作するには,ポインタのアドレス(ポインタのポインタ)を引数として渡さねばならない.
main()
{
int *ip;
func(ip);
return 0;
}
void func(int *ip)
{
static int dummy = 5;
*ip = dummy;
}
- キャスト演算子
キャスト演算子は型変換を行なうもので,その結果は値を代入したり値をインクリメントできる左辺式ではない.
pがchar *型のポインタであるとき,
((int *)p)++;
は不正である.
これは次のようにしなければならない.
p = (char *)((int *)p + 1);
or
p += sizeof(int);
- const
char const *p
は文字定数へのポインタである(文字を変えることはできない).一方,char * const p
は文字列への定数ポインタである(ポインタ値を変えることはできない).
メモリの確保
- ユーザ入力の取り込み
ユーザからの入力をバッファ領域に取り込むには,以下のようなコードが使われる.
#include <string.h>
#define BFSIZE 100
char word[BFSIZE], *p;
printf("Type something:\n");
fgets(word, BFSIZE, stdin);
if ((p = strchr(word, '\n')) != NULL)
*p = '\0';
printf("You typed "%s"\n", word);
入力バッファの大きさが指定されている場合は,バッファ領域以上の入力を防ぐためにgetsよりもfgetsを用いた方がよい.
但し,fgetsはgetsと異なり入力終端の改行コードを削除しないため,改行コードが不用の場合はそれを取り除く処理が必要である.
C言語では,ソースコードにおいて明示的にオブジェクトに対するメモリを確保しなければならない.そのため,文字列の連結のように空間が実行時に動的に必要になる場合は,配列の宣言やmallocの実行によりあらかじめメモリを確保しなければならない.
一般に,ポインタを使用する場合は常にポインタが示すメモリが存在し,そのアドレスがアクセスできるものか否かを意識しておく必要がある.ライブラリ関数のパラメータにポインタが使われる場合など,ポインタが指すメモリを確保するのはユーザの責任である.
- 関数の戻り値のポインタ
関数がポインタを返すとき,そのポインタは静的に確保された領域か呼び出し側で定義されている領域を指すものであり,ローカルな領域を指すものであってはならない.
- malloc/free
動的に確保したメモリ領域を解放した後に利用することは避けるべきである.
一般には,メモリを解放した後でもその内容は変わらないことが多いが,保証されたものではなく,ANSI C でも規定していない.
- realloc
realloc の第一引数に NULLポインタを指定すること,および第二引数に 0 を指定することは,ANSI C では認められている.
しかし,初期の実装ではサポートされていないものもあるため,移植性は良くない.
このような使い方は,メモリ確保のアルゴリズムを容易にするのには役に立つ.
- calloc
calloc は,malloc と異なり確保した領域を0で初期化する.
calloc(m, n) は本質的には以下と等価である.
p = malloc(m * n);
memset(p, 0, m * n);
すべての領域は全ビット0で初期化される.そのため,ポインタおよび浮動小数点値に対する 0 は保証されない.
malloc および calloc によって確保された領域は free によって解放される.
- alloca
alloca は,alloca を呼び出した関数がリターンするときに自動的に解放されるメモリ領域を確保する.すなわち,呼び出し側のスタックフレーム上に領域を確保する.
alloca は,ポータブルには実装できず,またスタックのないマシンでは実装が困難である.その利用においては,その戻り値を fgets(alloca(100), 100, stdin)
のように他の関数の引数に直接渡す場合に問題が起こる.
これらの理由により,alloca は高い移植性を要求するプログラムには用いるべきでない.
構造体
構造体,共用体,列挙型
- 列挙型
列挙型の識別子はint型の定数として宣言され,定数が必要なところならばどこへでも置くことができる.
列挙型の定数の値はユニークな値でなくともよい.
列挙型による宣言と#defineによる定数の宣言とは等価である.列挙型の利点は値が自動的に割り当てられることと,デバガが列挙型の変数をそのシンボリック値で表示できることである.
- 構造体の演算
構造体に対して許される演算は,代入(コピー),アドレスを求めること,およびそのメンバにアクセスすることである.代入には,関数に引数として渡すこと,および関数から値を返すことも含まれる.
関数の引数として構造体を渡すときには,構造体全体がスタックにコピー される.大きな構造体を渡す場合は,構造体に対するポインタを渡す方が効率的である.
- 自己参照型構造体
C の構造体にはそれ自身へのポインタを含むことができる.しかし,以下のような宣言はエラーとなる.
typedef struct {
char *item;
NODEPTR next;
} *NODEPTR;
nextフィールドが宣言された時点で NODEPTR の typedef宣言が完結していないことが問題である.
これに対する1つの対策は以下のようにすることである.
struct node {
char *item;
struct node *next;
};
typedef struct node *NODEPTR;
- 構造体の比較
2つの構造体を比較するには,比較を行なう独自の関数を作成する必要がある.
コンパイラにより構造体の比較を行なう合理的な方法がないためである.
すなわち,バイト単位の比較では構造体のパディングによる未使用領域の存在により正しい比較が行えない.また,フィールド単位の比較では,大きな構造体に対するインラインコードにおいては受け入れ難い繰り返し処理を必要とする.
- offsetof マクロ
ANSI Cでは構造体におけるフィールドのバイトオフセットを与えるマクロ offsetof を定義している(<stddef.h>を参照).
このマクロがない場合,以下の定義が使用できる.
#define offsetof(type, mem) ((size_t) \
((char *)&((type *) 0)->mem - (char *)((type *) 0)))
この実装は100%の移植性はない.処理系によっては受け入れられない場合がある.
構造体 a におけるフィールド b のオフセットは
offsetb = offsetof(struct a, b)
で求められる.
structp がこの構造体のインスタンスへのポインタであり,b が上記で計算されたオフセットを持つ int型のフィールドであるとき,b の値は以下のように設定することができる.
*(int *)((char *)structp + offsetb) = value;
- sizeof 演算子
構造体に対する sizeof演算子はフィールド境界のパディングのため,構造体のメンバ変数の合計サイズよりも大きな値を示す場合がある.
処理系によっては,このパディングの生成の有無をオプションにより制御できるものもある.