2章. 構造体、共用体、列挙型
2.1:
以下の二つの宣言の違いは。
struct x1 { ... };
typedef struct { ... } x2;
A:
最初の形は「構造体タグ」を宣言している。二つ目の形は「typedef」
を宣言している。主な違いは二番目の宣言のほうが少しだけ抽象デー
タ型らしいことである。つまり使う側は それが構造体であることを
知っている必要がない。また変数を宣言する際にstructというキーワー
ドを使う必要がない。
2.2:
なぜ以下のコードがうまく動かないのか。
struct x { ... };
x thestruct;
A:
CはC++ではない。typedefした名前は構造体タグとして自動的に生成
されるわけではない。上の質問2.1も参照のこと。
2.3:
構造体は自身へのポインターをメンバーとして含んでいてもいいのか。
A:
もちろん。質問1.14も参照のこと。
2.4:
Cであいまいな(抽象)データ型を実装する一番よい手は。
A:
よい方法の一つは使う側が構造体へのポインターを(たぶんtypedefで
隠すことまでして)使うことである。そのポインターが公には定義し
てない構造体データ型を指す。
2.6:
配列namestrが複数の要素を持つように振る舞わせるために構造体を
以下のように宣言して、巧妙にメモリ割り当てをしている以下のよう
なコードを見かけた。このコードは許されるのか。移植性は高いのか。
- struct name {
- int namelen;
- char namestr[1];
- };
A:
この技巧は人気がある。ただしDennis Ritchieは「Cの実装への根拠
のない馴れ馴れしさ」と呼んだ。公式な解釈によると上の技巧は Cの
規格に厳密には準拠していないと考えられる。(この技法が正しいか
どうかを取り巻く議論を尽くすことは、このFAQの守備範囲を越えて
いる。) しかし世の中に知られている全てのコンパイラの実装で、こ
の方法は移植性が高いようである(配列の境界を注意深くチェックす
るコンパイラは警告を出すかもしれない)。
別のやり方としては、可変長の要素の大きさを、小さく取るより、非
常に大きく取ることが考えられる。上の例でいえば、
...
char namestr[MAXSIZE];
...
ここでMAXSIZEは配列namestrに保存されうるどんな名前よりも大きい。
しかしながらこの技法も、規格の厳密な解釈によると許されないよう
だ。
References:
Rationale Sec. 3.5.4.2.
2.7:
構造体を、変数に代入することも、関数に引数として渡すことも、関
数の戻り値としても使うこともできると聞いた。けれどK&R初版には
できないと書いてある。
A:
K&R初版に書いてあるのは、構造体への演算の制限は将来のコンパイ
ラでは取り除かれるだろうということである。実際K&Rの初版が発行
された時点のDennis Ritchieのコンパイラには、構造体の代入も構造
体の引数渡しも用意されていた。初期のCコンパイラの中には対応し
ていないものも存在したが、現在のコンパイラはすべて対応している
し、ANSI C規格の一部でもある。よって使うことをためらうことはな
い。
(構造体に代入したり引数として渡したり関数の戻り値とするときに
は、データの複写は一枚板に行われる。ポインターフィールドがあっ
たとしてもその指す先はコピーされない。
References:
K&R1 Sec. 6.2 p. 121; K&R2 Sec. 6.2 p. 129; ANSI
Sec. 3.1.2.5, Sec. 3.2.2.1, Sec. 3.3.16; ISO Sec. 6.1.2.5,
Sec. 6.2.2.1, Sec. 6.3.16; H&S Sec. 5.6.2 p. 133.
2.8:
なぜ構造体を比較することはできないのか。
A:
構造体の比較を、低レベルの操作もできるというC言語の味わいに矛
盾することなくコンパイラにやらせる単純で、よくできた方法はない。
バイト単位の比較は、構造体の中の使われていない"穴"(このような
埋め草は、後ろのフィールドの配置が正しくなるように使われる)に
どんなビットパターンが来るかもしれないことを考えると使えない。
フィールド単位の比較は、大きな構造体が対象のときに、とんでもな
い量のくり返しのインラインのコードを必要とする。
二つの構造体を比較したいのなら、フィールド単位に比較する関数を
自分自身で書かなければならない。
References:
K&R2 Sec. 6.2 p. 129; ANSI Sec. 4.11.4.1 footnote
136; Rationale Sec. 3.3.9; H&S Sec. 5.6.2 p. 133.
2.9:
どんな仕組みで構造体を引数で渡したり、関数の戻り値に使うことが
できるのか。
A:
構造体が関数の引数として渡されるとき、構造体全体が、必要なだけ
のワードを使ってスタックに積まれる(プログラマーは、このオーバー
ヘッドを嫌って構造体へのポインターをかわりによく使う)。コンパ
イラの中には構造体へのポインターしか渡さないものもいる。ただし、
そういうコンパイラでも引数渡しであるという意味を残しておくため
に自分用のコピーを持っているかもしれない。
構造体はコンパイラが用意する領域に置かれて関数から返ってくるの
が一般的である。コンパイラは、この領域のアドレスを、特別な"隠
れた"引数として、呼ばれる側の関数に渡す。古いコンパイラの中に
は、構造体を返すのに特別な静的な領域を使うものもあった。これは
関数を再入(リエントラント)不能にするので、ANSI Cは禁止している。
References:
ANSI Sec. 2.2.3; ISO Sec. 5.2.3.
2.10:
構造体を引数として取る関数に定数値をどうやって渡せばよいか。
A:
C言語には名前のない構造体の値を作り出す方法は存在しない。一時
的に構造体の変数を使うか、構造体を作り出すちょっとした関数を用
意しなければいけない。(gccは構造体の定数を拡張機能として用意し
ている。こういう仕組みはC規格の将来の拡張でたぶん追加されるだ
ろう。) 質問4.10も参照のこと。
2.11:
構造体をデータファイルから読む、あるいはデータファイルに書き込
むのはどうすればよいか。
A:
fwrite()を使って構造体をデータファイルに書き込むのはそんなに難
しくない。
fwrite(&somestruct, sizeof somestruct, 1, fp);
これに対応するfread()を使えば読み返すことができる。(ANSI Cが決
まる前のCでは、最初の引数に(char *)のキャストが必要である。大
事なのは、fwrite()はバイトへのポインターを受け取るのであって、
構造体へのポインターを受け取るのではないということである。) し
かし、こうやって書き込んだデータファイルは、たいして移植性が高
いわけではない(質問2.12と20.5を参照のこと)。構造体がポインター
を含んでいたとしてもポインターの値が書かれるだけで、その値は読
み返したときに有効であるとは考えにくいということに注意すること。
最後に、高い移植性のためにはfopen()でファイルを開くときに「b」
フラグが必要なことにも注意すること。質問12.38を参照。
もっと移植性の高い解決策は、段取りに少し手間がかかるけれど、移
植性の高い方法で(たぶん人間にも読みやすい方法で)フィールド単位
で構造体を読む関数と書く関数を1組を用意することである。
References:
H&S Sec. 15.13 p. 381.
2.12:
私が使っているコンパイラは構造体の内部に穴を開けるので、領域は
無駄になるし、データを保存した外部のファイルに対して"バイナリー
"で入出力することができない。この埋め草を止めたり、構造体の整
列を制御することはできないのか。
A:
君が使っているコンパイラは、この制御をおこなう拡張機能を(たぶ
ん#pragmaで、質問11.20参照)用意しているかもしれない。ただし
標準化された方法はない。
質問20.5も参照のこと。
References:
K&R2 Sec. 6.4 p. 138; H&S Sec. 5.6.4 p. 135.
2.13:
構造体にsizeof演算子を使ったら、私が思っていたよりも大きな大き
さを返してきた。まるで、おしりに詰め物がしてあるようだ。
A:
構造体は、構造体を連続的に配置したときに各構造体の先頭が整列す
るように、このような詰め物を持つ場合がある(内部に詰め物をする
こともある)。たとえ構造体が配列の一部でないとしても、sizeofが
一貫した値を返すように、おしりの詰め物は付いたままとなる。上の
質問2.12を参照のこと。
References:
H&S Sec. 5.6.7 pp. 139-40.
2.14:
構造体内のフィールドのバイトオフセットを知る方法は。
A:
ANSI Cは、offsetofマクロを用意しているので、用意されている場合
は使うこと。<stddef.h>を参照。もし手に入れることができなければ、
実装の一つは以下のようになる。
- #define offsetof(type, mem) ((size_t) \
- ((char *)&((type *)0)->mem - (char *)(type *)0))
この実装も100%の移植性を持つわけではない。コンパイラの中には、
はねつけるものがあるかもしれないが、それはそれで文法的に正しい。
次の質問2.15への解答を、使い方の参考にすること。
References:
ANSI Sec. 4.1.5; ISO Sec. 7.1.6; Rationale
Sec. 3.5.4.2; H&S Sec. 11.1 pp. 292-3.
2.15:
どうやれば構造体のフィールドを、実行時に名前でアクセスできるか。
A:
まずoffsetof()マクロを使って名前とオフセットの対応表を用意する。
構造体aのフィールドbのオフセットは、
offsetb = offsetof(struct a, b)
で与えられる。もし以下の式でstructpが、構造体の実体へのポイン
ターで、bが上で計算したオフセットを持つintのフィールドとすると、
bの値は間接的に
*(int *)((char *)structp + offsetb) = value;
として得られる。
2.18:
以下の関数は正しい結果を出力するけれど終了した時点でcoreを吐く。
なぜか。
- struct list {
- char *item;
- struct list *next;
- }
- /* ここからmainプログラムが始まる */
- main(argc, argv)
- { ... }
A:
セミコロンが抜けたことで、関数mainは構造体を返すとコンパイラに
思いこませてしまった(間に入ったコメントが、構造体とmainが結び
付いていることをわかりにくくしている)。構造体を戻り値として持
つ関数は、たいてい隠れた戻り値のポインター(質問2.9を参照)を持
つように実装されるので、上のmain()に対して生成されたコードは3
つの引数を取ろうとする。このうち2つが渡される(この場合は、C言
語のスタートアップのコードによって)。質問10.9と16.4も参照のこ
と。
References:
CT&P Sec. 2.3 pp. 21-2.
2.20:
共用体を初期化することはできるか。
A:
共用体の最初のメンバーを初期化に使うことをANSI C規格は許してい
る。他のメンバーを初期化する標準的な方法はない(そもそもANSI規
格成立より前のたいていのコンパイラで、どのメンバーを使っても初
期化することはできなかった)。
References:
K&R2 Sec. 6.8 pp. 148-9; ANSI Sec. 3.5.7; ISO
Sec. 6.5.7; H&S Sec. 4.6.7 p. 100.
2.22:
プリプロセッサーで#defineを複数使うこととコンパイラで列挙型を
使うことの違いは。
A:
現状ではほとんど違いはない。多くの人が望んだ方向とは反対に、C
規格は、列挙型とそのほかの整数型を混合して使っても問題ないと述
べている。 (もしそのような混合が明示的なキャストなしには使えな
いとしたら、よく考えて使われた列挙型により、ある種のプログラミ
ングの誤りを捉えることができるのであるが。)
列挙型の利点としては値が自動的に与えられることと、デバッガーを
使って列挙型の値を調べる時にデバッガーがシンボリックな値を表示
してくれるかもしれないということ、また列挙型の有効範囲がブロッ
クであることが挙げられる(コンパイラは、列挙型と整数が見境なく
混合して使われたときに、致命的ではない警告を出すかもしれない。
そのように混ぜて使うことは、厳密にいえば文法違反ではないけれど、
よくない作法と考えられるからである)。欠点としてはプログラマー
がデータの大きさを(さっきの致命的でない警告についても)ほとんど
制御できないことが挙げられる。
References:
K&R2 Sec. 2.3 p. 39, Sec. A4.2 p. 196; ANSI
Sec. 3.1.2.5, Sec. 3.5.2, Sec. 3.5.2.2, Appendix E; ISO
Sec. 6.1.2.5, Sec. 6.5.2, Sec. 6.5.2.2, Annex F; H&S Sec. 5.5
pp. 127-9, Sec. 5.11.2 p. 153.
2.24:
列挙型の値を(値ではなく)シンボルで表示する楽な方法はないのか。
A:
ない。列挙型の定数を文字列に対応させるちょっとした関数を書けば
いい。(デバッグのことしか気にしてないのなら、よくできたデバッ
ガーを使えば勝手に列挙型の定数をシンボルで表示してくれるので心
配ない。)