1章. 宣言と初期化
1.1:
どの整数型を使えばよいか、どうやって決めればよいか。
A:
大きな値(32767より大きな値か、-32767より小さい値)が必要なら
longを使う。もしメモリの効率を気にするなら(例:大きな配列がある
とか構造体がたくさんあるなど)shortを使う。どうでもいいならint
を使う。オーバーフローの時の性質の明確なことが大事で負の値が不
要なら、対応する符号なしの整数を使え(ただし符号付と符号なしの
整数の混在には注意すること)。
文字データ型を(特にunsigned charを)"小さな"整数として使うこと
はできるが、そうすることによる予想不能な符号拡張の問題とコード
サイズの増加を考えると、その価値よりも伴う面倒のほうが大きくな
る場合がある。(unsigned charを使うことは助けになる。関連する問
題については質問12.1を参照のこと)
同じような空間と処理時間を天秤にかける話が、floatを使うべきか
doubleを使うべきかの議論にもあてはまる。変数のアドレスが必要で、
そのアドレスの型が特定の型でなければならない場合は、上記の規則
は明らかにあてはまらない。
なんらかの理由で決まった大きさに何かを宣言する必要があるときも、
自分が選択した方法を適切なtypedefで隠すこと(こんなことをしなけ
ればいけない理由は、たいてい外から押し付けられた保存領域の形に
合わせるためである。このような場合については質問20.5を参照のこと)。どのデータ型を選んだとしても適切なtypedefで隠すこと。
References:
K&R1 Sec. 2.2 p. 34; K&R2 Sec. 2.2 p. 36, Sec. A4.2
pp. 195-6, Sec. B11 p. 257; ANSI Sec. 2.2.4.2.1, Sec. 3.1.2.5;
ISO Sec. 5.2.4.2.1, Sec. 6.1.2.5; H&S Secs. 5.1,5.2 pp. 110-114.
1.4:
最近登場した64ビットマシンの、64ビット整数のデータ型は何であるべきか。
A:
64ビットマシン向けのC言語のベンダーの一部は、64ビット長の整数型を提供している。既存のコードのあまりにも多くがintとlongの大
きさが同じであるとか、どちらかがきっちり32ビットであると思いこ
んで書かれていることを懸念して、標準ではない新しい64ビット長の
データ型long long(あるいは__longlong)をかわりに用意したところ
もある。
移植性の高いコードを書くことに関心のあるプログラマーは、64ビッ
トの大きさのデータ型を適切なtypedefで隠さなければならない。新
しい、もっと大きな整数型を導入せざるをえないと考えているベンダー
は、新しい整数型を「少なくとも64ビットある」(それは本当に新し
い型である。従来のCにはない)と宣伝すべきで、「ぴったり64ビット」
とは宣伝すべきではない。
References:
ANSI Sec. F.5.6; ISO Sec. G.5.6.
1.7:
外部変数を宣言、定義する一番よい方法は。
A:
まず、一つのグローバル(厳密にいえば外部(external))変数や関数に、
たくさんの"宣言"が(たくさんの変換単位に)存在してもいいが、"定
義"はただ一つしか存在してはならない。(定義とは実際に場所を確保
する宣言で、初期値があるときは初期値を与える。) 一番よい取り決
めは、各定義を関連する.cファイルに置き、外部宣言をヘッダーファ
イル(".h")に置くことである。そしてヘッダーファイルを宣言が必要
になったら必ず#includeする。定義をふくんだ.cファイルも同じヘッ
ダーファイルを#includeして、コンパイラーが定義と宣言を照らし合
わせることができるようにする。
この規則は移植可能性の度合を高める。ANSI C規格が要求するところ
と一致し、ANSI C規格ができる前のコンパイラやリンカーの多くの動
作とも一致する。(Unixのコンパイラやリンカーは「共通モデル
(common model)」を使う、これは2回以上初期化されないかぎり複数
の定義を許すものである。この動作は「共通の拡張(common
extension)」としてANSI C規格に出てくる。両方とも「共通」がつく
けれど共通点はない。いくつかの本当に奇妙なシステムは定義と外部
宣言を区別するのに明示的な初期化子(initializer)を必要とするか
もしれない。)
プリプロセッサーを巧みに使って以下のような行を、
DEFINE(int, i);
ただ1つのヘッダーファイルに一度だけ書いてマクロの設定に応じて
定義にしたり宣言にしたりすることは可能である。しかし、こうまで
する必要があるかどうかは疑問である。
ヘッダーファイルにグローバル宣言を置くことは、宣言の矛盾をコン
パイラーに捕まえさせるつもりなら大切である。絶対に外部関数のプ
ロトタイプを. cファイルに置いてはならない。一般に、.cに置かれ
たプロトタイプは実際の宣言との一貫性をチェックされることはない
し、実際の定義と矛盾したプロトタイプは有害無益である。
質問10.6と18.8も参照のこと。
References:
K&R1 Sec. 4.5 pp. 76-7; K&R2 Sec. 4.4 pp. 80-1; ANSI
Sec. 3.1.2.2, Sec. 3.7, Sec. 3.7.2, Sec. F.5.11; ISO
Sec. 6.1.2.2, Sec. 6.7, Sec. 6.7.2, Sec. G.5.11; Rationale
Sec. 3.1.2.2; H&S Sec. 4.8 pp. 101-104, Sec. 9.2.3 p. 267; CT&P
Sec. 4.2 pp. 54-56.
1.11:
関数宣言についたexternは何を意味するのか。
A:
こういう書き方をすることで、関数の定義がたぶん別のソースファイ
ルにあるということを、ほのめかすことができる。しかし以下の2つ
に違いはない。
extern int f();
int f();
References:
ANSI Sec. 3.1.2.2, Sec. 3.5.1; ISO Sec. 6.1.2.2,
Sec. 6.5.1; Rationale Sec. 3.1.2.2; H&S Secs. 4.3,4.3.1 pp. 75-
6.
1.12:
autoというキーワードは何の役に立つのか。
A:
何もない。それは時代遅れだ。質問20.37も参照のこと。
References:
K&R1 Sec. A8.1 p. 193; ANSI Sec. 3.1.2.4,
Sec. 3.5.1; ISO Sec. 6.1.2.4, Sec. 6.5.1; H&S Sec. 4.3 p. 75,
Sec. 4.3.1 p. 76.
1.14:
リンク付リストをうまく定義することができない。
- typedef struct {
- char *item;
- NODEPTR next;
- } *NODEPTR;
上のコードを試したがコンパイラーはエラーを返す。Cの構造体は自
身へのポインターを含むことができないのか。
A:
Cの構造体は、自身へのポインターを要素として持つことができる。
K&Rの6.5章の議論と例が、この点を明らかにしてくれる。上記のコー
ドの問題はNODEPTRのtypedefが「next」フィールドを宣言した時点で
は完成していないことである。修正するには最初に構造体にタグを付
け(「struct node」)、次にnextフィールドを「struct node *」とし
て宣言するか、typedefの宣言と構造体の定義を分離して、typedefの
宣言を構造体の定義のまるっきり前か、まるっきり後ろに持ってくる。
上の修正を両方おこなってもよい。修整版の一つは以下のようになる。
- struct node {
- char *item;
- struct node *next;
- };
typedef struct node *NODEPTR;
少なくとも他に3種類の修整方法がある。
互いに参照しあう構造体の組をtypedefしようとすると、同じような
問題が持ち上がるので、同じような解決策を取ればいい。
質問2.1も参照のこと。
References:
K&R1 Sec. 6.5 p. 101; K&R2 Sec. 6.5 p. 139; ANSI
Sec. 3.5.2, Sec. 3.5.2.3, esp. examples; ISO Sec. 6.5.2,
Sec. 6.5.2.3; H&S Sec. 5.6.1 pp. 132-3.
1.21:
charへのポインターを返す関数へのポインターを返す関数へのポインターN個からなる配列をどうやって宣言すればよいか。
A:
この質問は少なくとも3つの方法で答えることができる。
- char *(*(*a[N])())();
- 宣言をtypedefを使って段階的に作り出す。
typedef char *pc; /* charへのポインター */
typedef pc fpc(); /* charへのポインターを返す関数 */
typedef fpc *pfpc; /* 上記へのポインター */
typedef pfpc fpfpc(); /* …を返す関数 */
typedef fpfpc *pfpfpc; /* …を指すポインター */
pfpfpc a[N]; /* …の配列 */
- プログラムcdeclを使う。cdeclは英語で書いた宣言をCの変数宣言
に変換し、またその逆変換を行う。
cdecl> declare a as array of pointer to function returning pointer to function returning pointer to char
char *(*(*a[])())()
cdeclは、複雑な宣言をキャストを使って説明してくれるし、引数が
どの括弧の組に入るか示してくれる(上記のような複雑な関数の定義
では)。いくつかのバージョンのcdeclがcomp.sources.unixのボリュー
ム14に保存されているし(質問18.16参照)K&R第二版にも存在する。
Cに関するどんなよい教科書も、これらの複雑なCの宣言を「内から外
へ」読む方法について教えてくれる(宣言はその使われかたを真似る)。
上の例の関数へのポインターの宣言は引数の型の情報を含んでいない。
引数が複雑な型を含むときは、宣言は本当に混乱したものとなる(こ
こでも新しい版のcdeclは役に立つ)。
References:
K&R2 Sec. 5.12 p. 122; ANSI Sec. 3.5ff (esp.
Sec. 3.5.4); ISO Sec. 6.5ff (esp. Sec. 6.5.4); H&S Sec. 4.5 pp.
85-92, Sec. 5.10.1 pp. 149-50.
1.22:
同じ型を持つ関数へのポインターを返すことのできる関数をどうやっ
て宣言すればよいか。今、状態マシンを作っている。この状態マシン
の状態ごとに関数を用意する。そして各関数が次の状態の関数へのポ
インターを返す。けれど、こういう関数を宣言する方法が見つからな
い。
A:
直接に定義することはできない。汎用の関数ポインターを返す関数を
用意して、ポインターを渡すたびに関数の型にあわせて慎重にキャス
トを行う。または関数が構造体を返すようにする。その構造体は、そ
の関数へのポインターだけをメンバーに持つようにする。
1.25:
私が使っているコンパイラは関数の無効な再宣言だと文句をつける。 一度定義して、一回呼んでるだけなのに。
A:
関数を呼び出すところの有効範囲内に関数の宣言がないときは(おそ
らく最初の関数呼び出しが関数の定義より前にあるからだろう)その
関数はintを返す(かつ引数のデータ型の情報なし)と宣言されている
と考える。そのため、後で関数がint以外を返すと宣言されると不一
致が生じる。intを返さない関数は、呼び出す前に宣言しなければな
らない。
その外に考えられる問題の種は、その関数がヘッダーファイルで宣言
されている別の関数と同じ名前を持っていることである。
質問11.3や15.1も参照のこと。
References:
K&R1 Sec. 4.2 p. 70; K&R2 Sec. 4.2 p. 72; ANSI
Sec. 3.3.2.2; ISO Sec. 6.3.2.2; H&S Sec. 4.7 p. 101.
1.30:
明示的には初期化されていない変数の初期値について、どこまで安心
して仮定することができるか。グローバル変数の初期値が"0"で初期
化されるのなら、ヌルポインターや浮動小数についても0であること
が保証されるのか。
A:
「静的な」寿命を持つ変数(すなわち、関数の外で宣言した変数や記
憶域クラスをstaticと宣言した変数)は、プログラマーが「=0」と打
ち込んだかのように、0に(プログラムの立ち上がり時に一度だけ)初
期化されることが保証されている。すなわちポインターは(正しい型
の:5章参照)ヌルポインターに、浮動小数は0.0に初期化される。
「自動の(automatic)」寿命を持つ変数(すなわち、記憶域クラスを
staticと宣言していないローカル変数)の中身は、明示的に初期化し
ない限り、ゴミである(ゴミが役に立つかどうかは、予測できない)。
malloc()やrealloc()を使って動的に確保した領域も、中身はゴミで
あると考えたほうがいい。だから呼び出した側のプログラムで適切に
初期化しなければならない。calloc()によって確保した領域はすべて
のビットが0である。しかし、この初期化も、ポインター変数や浮動
小数点表示の変数に対しては必ずしも役にたたない(5章と質問7.31を
参照)。
References:
K&R1 Sec. 4.9 pp. 82-4; K&R2 Sec. 4.9 pp. 85-86;
ANSI Sec. 3.5.7, Sec. 4.10.3.1, Sec. 4.10.5.3; ISO Sec. 6.5.7,
Sec. 7.10.3.1, Sec. 7.10.5.3; H&S Sec. 4.2.8 pp. 72-3, Sec. 4.6
pp. 92-3, Sec. 4.6.2 pp. 94-5, Sec. 4.6.3 p. 96, Sec. 16.1 p.
386.
1.31:
下のコード、本からそのまま写したのに、コンパイルできない。
- f()
- {
- char a[] = "Hello, world!";
- }
A:
たぶん、君の使っているコンパイラはANSI規格ができる前のもので
「autoの集成体型(automatic aggregates)」(例:staticでないローカ
ルな配列や構造体や共用体)の初期化を許してない。回避策としては、
配列をstatic(もし、その後の呼び出しで真っ新なコピーが必要でな
いなら)かグローバルにする、またはポインターで代用する(もし行列
に書き込むことがないのなら)。(文字列リテラルを指すローカルの
char *変数を初期化することはいつでも可能である。ただし以下の質
問1.32を参照のこと)。上のどの条件もあてはまらないなければ、f()
を呼ぶところでstrcpy()を使って配列を自分の手を煩わして初期化す
る必要がある。質問11.29も参照のこと。
1.32:
以下の二つの初期化の違いは。
char a[] = "string literal";
char *p = "string literal";
p[i]に新しい値を代入しようとするとプログラムがクラッシュする。
A:
文字列リテラルには2種類の少し違った使いみちがある。配列の初期
化指定子(char a[]の宣言で使うような)に使うときは、その配列の各
文字の初期値を指定する。その他の場所で使うときは、文字の名無し
のstaticの配列となる。このときは書き込み禁止のメモリーに保存さ
れるかもしれない。だから無事に値を変更できない可能性がある。式
が必要な場所では、いつもと同じく(6章を参照のこと)配列はその場
でポインターに変換される。だから2番目の宣言はpを名無しの配列の
最初の要素を指すように初期化する。
(古いコードをコンパイルするために、文字列を書き込み可能にする
かどうかを制御するスイッチを持っているコンパイラーもある。)
質問1.31,6.1,6.2,6.8も参照のこと。
References:
K&R2 Sec. 5.5 p. 104; ANSI Sec. 3.1.4, Sec. 3.5.7;
ISO Sec. 6.1.4, Sec. 6.5.7; Rationale Sec. 3.1.4; H&S Sec. 2.7.4
pp. 31-2.
1.34:
やっとのことで関数へのポインターを宣言する構文を理解した。さて
どうやってこれを初期化すればよいか。
A:
以下のような方法を取る。
extern int func();
int (*fp)() = func;
関数名が、式に現われたが実行されないときは配列名と同じようにポ
インターに成り下がる(つまり、そのアドレスが暗黙のうちに取られ
る)。
一般に関数の明示的な宣言が必要である。なぜならこの場合、暗黙の
外部宣言は起こらないから(初期化子に現れる関数の名前は関数呼び
出しの一部ではないから)。
質問4.12も参照のこと。