外部参照 |
前述したように,関数の内部で宣言・定義された型名・変数名・const定数名は,その関数の中だけで有効であり,
それらのスコープは“関数ローカル”であると言えます。
一方,関数の外で定義した型名・列挙定数名・変数名・const定数名そして関数名自体などは,大域(グローバル, global)スコープ
を持つ,と言います。コンパイル時には,これらの識別子は宣言された場所からその翻訳単位の末尾までのスコープを持ちます。
これをファイルスコープ(file scope)と呼びます。
これら大域の識別子のうち,“実体(コンパイルされると何らかの機械語コードやメモリスペースになるもの)”を持っている名前,
すなわち
関数と変数名,const定数名
は他のソースファイル(厳密には翻訳単位)から参照することが可能です。
これは,コンパイルされる際に,リンカが外部からこれらの名前を参照するための『リンク情報』(Fif. 0.38)も一緒に生成される為です。
このように,翻訳単位の外部から参照できる(見える)ことを,それらの名前は“外部リンケージ(external linkage)を持つ”と言います。
(識別子には他に型名などがありますが,『型』はコンパイルしても実体を持たないため,他の翻訳単位で定義された型名はそのままでは
使えないことになります。これを解決するのが後述する『ヘッダファイルのインクルード』という仕組みなのです)
実は,外部リンケージを明示するには extern という予約語を使って
extern 返却値型 関数名( 仮引数リスト ) { … }
extern 型名 大域変数名 = 初期値;
と定義・宣言する方法もあるのですが,外部リンケージを示すためのexternは省略可能なので,
グローバル(大域)な関数・変数・const定数
はexternつけて宣言・定義しなくとも自動的に外部リンケージを持つことになります。
しかし逆に,これら
グローバル(大域)な関数・変数・const定数
を,翻訳単位の外部から参照できないように隠しておきたいときもあります。その場合,予約語 static を明示的につけて公開したくない
名前を宣言する必要があります(例えばFig. 0.38の“参照されるモジュールA”の関数f()と大域変数a)。
さて,ここまでは参照される側の外部リンケージについての話しでした。
一方,参照する側は,ソースファイルで定義・宣言されていない外部の関数名や変数名を使うために,外部参照する対象の型を
明確にコンパイラに伝えておかなければいけません。
そのための方法が外部参照宣言です。外部参照宣言は
返却値型 関数名( 仮引数リスト );
extern 型名 大域変数名; // 外部参照宣言の場合は初期値指定を付けないこと。
という形になります。
前者は,関数の型を完全に規定する宣言で,関数プロトタイプ(function prototype)宣言と呼ばれます。
気をつけなければならないのは,後者の大域変数の外部参照宣言です。もし大域変数を宣言するときに初期化を忘れると,外部参照宣言
とまったく同じ形になってしまいます。そうなると,どこに実体が宣言されているのかが曖昧になってしまうのです。
ですから,大域変数はどこか一カ所で必ず初期化をともなった宣言をして実体を与えておかなければなりません。一般に初期化をとも
なった大域変数の宣言を,外部参照宣言と明確に区別するために“大域変数の定義”と呼びます。
では最後に外部参照が解決されるまでの行程を見てみましょう。まず,コンパイラはコンパイル中に宣言されていない変数名と定義され
ていない関数名を見つけると,それを外部参照の対象と見なします。
そのとき,外部参照宣言があればそれをもとに参照している部分をコンパイルし,実体との結合はリンカにまかせます(例えばFig. 0.38の“参照側モジュールB”)。
もし外部参照宣言がなければ,適当に型を推測してコンパイルしてしまいます。これが実際の型と一致していない場合は,リンク作業のエラー
やプログラムの動作不良の原因になります。そして,リンカは各モジュールの持つリンク情報を調べ,参照側と参照される実体を結びつける
というわけです。