連載
» 2014年04月11日 18時00分 公開

C初心者が知っておきたいヘッダーファイルとリンクの基礎知識目指せ! Cプログラマ(終)(2/4 ページ)

[長沼立巳, 小山博史,SSS(G)/ガリレオ]

オブジェクトにおける「リンク」(結合)

 あるソースファイルと別のソースファイルで宣言された識別子は、別の有効範囲を持ちます。これをファイル有効範囲と呼びました。別の有効範囲で宣言された識別子は基本的に参照できません。もし別々の有効範囲で同じ識別子を持つオブジェクトがあったとしても、それらは全くの別物として扱われます。

 もちろん、参照する方法も用意されています。そうでなければ、ソースファイルを分けた意味がありませんね。ただ、定義されたものが全て参照できてしまったら、それはそれで混乱してしまいます。参照するものとしないものは、プログラマーが正確に制御できます。

 参照できるようにする処理を「リンク」(以下「結合」)と呼びます。

 「結合」の基本として、「同じ識別子でしか結合されない」というルールがあります。「a」という識別子のオブジェクトと「b」という識別子のオブジェクトが結合されることはありません。しかし、同じ名前ならば全て結合されるというわけではなく、以下で説明するルールにのっとって結合されます。

定義と仮定義

 実際に結合を確認するコードを書いてみましょう。ファイル「test.c」が次のようになっていたとします。

int a = 1; // 外部定義
test.c

 このとき「main.c」では、「test.c」のオブジェクト「a」を参照するために次のようにします。

#include <stdio.h>
#include <stdlib.h>
int a; // 仮定義
int main(void) {
    printf("a = %d\n", a); // a = 1
    return EXIT_SUCCESS;
}
main.c

 「main.c」の「a」は、「test.c」の「a」と結合され、同じオブジェクトを表します。この時のポイントは次の3つです。

  1. 「test.c」の「a」と「main.c」の「a」はそれぞれファイル有効範囲を持つ
  2. 「test.c」の「a」は宣言と同時に初期化されている(外部定義
  3. 「main.c」の「a」は宣言だけで、初期化はされていない(仮定義

外部から参照できる識別子はファイル有効範囲を持つ

 まず、外部から参照できる識別子はファイル有効範囲を持つ必要があります。「ブロック有効範囲」などの有効範囲を持つ識別子は外部から参照できません。例では、「test.c」の「a」も「main.c」の「a」もファイル有効範囲を持っています。

外部定義(定義)

 次に、外部から参照できる識別子は宣言と同時に初期化されている必要があります。このようなオブジェクト識別子の宣言を「外部定義」あるいは単に「定義」と呼びます。「test.c」の「a」は外部定義です。外部定義されたオブジェクト識別子はオブジェクトの本体となって記憶領域が確保されます。

仮定義

 一方、「main.c」の「a」はファイル有効範囲を持ちますが、宣言の時に初期化されていません。この場合は、外部定義のオブジェクトを参照する仮の定義となります。このような宣言を「仮定義」と呼びます。実際に外部定義のオブジェクトを参照するかどうかは、結合時に決まります。

 仮定義は実体となるオブジェクトの本体を確保するわけではないので、複数のファイルで宣言をしても構いません。

同じ名前の定義が2つ以上あるとエラー

 一方、定義は実体を作りますので、同じ名前の定義が2つ以上あるとエラーになります。試しに「main.c」の「int a;」を「int a = 1;」として仮定義から定義に変更してみると、「a」が複数定義されているとして結合時にコンパイルエラーになります。

同じ名前の仮定義が複数あってもエラーにならない

 仮定義ばかりで定義がどこにも無かった場合はどうなるでしょうか。試しに「test.c」と「main.c」の両方を「int a;」として仮定義にしてみます。するとコンパイルが成功して、実行すると「a = 0」と表示されるはずです。

 結合時に変数「a」の仮定義に対する外部の定義が見つからない場合は、「int a;」は仮定義ではなく「0」で初期化されて宣言された変数「a」の定義としてみなされ、オブジェクトの本体が用意されます。従って、両方のファイルにおける「a」の宣言を仮定義にすることで、「test.c」の「a」と「main.c」の「a」は別のファイル有効範囲を持つオブジェクトとなったわけです。

定義と仮定義のまとめ

 まとめると、次のようになります。

  • 定義:識別子の宣言と記憶領域の確保が行われる(実体を持つ)
  • 仮定義(外部定義がある場合):外部定義を参照する(実体を持たない=参照になる)
  • 仮定義(外部定義が無い場合):0で初期化された定義とみなされ、識別子の宣言と記憶領域の確保が行われる(実体を持つ)

外部結合と内部結合

 ファイル有効範囲を持つオブジェクトを定義すると、別のファイルから参照できることが分かりました。

「static」を付ける「内部結合」で別のファイルからは参照できないようにする

 ここで、ファイル有効範囲を持つオブジェクトを、別のファイルからは参照できないようにしたいときがあります。そのためには、宣言時に「static」を指定して定義をします。

 先ほどの「test.c」の例で、オブジェクト「a」を別のファイルから参照できないように定義してみましょう。

static int a = 1;
test.c

 これで、「test.c」の「a」は外部から参照できなくなり、「main.c」における「a」の仮定義「int a;」は外部定義が無いものとして扱われます。

 これは、ファイル有効範囲を持つ識別子に「static」を付けると内部結合になるからです。「static」が無い場合は外部結合になります。

 結合された識別子を持つオブジェクトは、同じ識別子のオブジェクトを参照するようになります。内部結合とは、この結合が翻訳単位の中で行われる結合で、外部結合は翻訳単位を超えて行われる結合です。

「extern」を付けて定義すると「外部結合」になる

 結合する識別子を宣言するときに使うキーワードとして、もう1つ「extern」があります。「extern」を指定すると次のようになります。

  • 前方に宣言があって、そこで外部結合か内部結合かが指定されていればそれに従う。
  • そうでなければ外部結合になる。

 コードで見てみましょう。ここではどの識別子もファイル有効範囲とします。

int a = 1;        // 定義、外部結合
static int b = 2; // 定義、内部結合
extern int a;     //(1)外部結合
extern int b;     //(2)内部結合
extern int c = 3; //(3)定義、外部結合
extern int d;     //(4)外部結合

 (1)と(2)は前方に定義の宣言がありますので、それぞれが宣言された時の結合と同じ結合になります。(3)の「c」は前方に宣言はなく、ここで初めて定義されているので外部結合となります。(4)の「d」も「c」と同じで、ここで初めて宣言されているので、外部結合となります。

 「extern」はブロック内に宣言するオブジェクトにも適用でき、そのブロック内では外部定義されたオブジェクトを参照できます。

「ブロック有効範囲」を持つ場合

 次の例では、変数「a」はブロック内で宣言されているので、「ブロック有効範囲」を持ちますが、外部結合をしているため、外部定義された変数「a」を参照することになります。

int a = 1; // 定義、外部結合
test.c
#include <stdio.h>
#include <stdlib.h>
int main(void) {
    extern int a; // 外部結合
    printf("a = %d\n", a); // a = 1
    return EXIT_SUCCESS;
}
main.c

 ここでもルールは同じです。前方に宣言が無いので外部結合になります。こうしておけば、「main」関数でしか外部定義された変数「a」は使わないようになっているので、「main」関数以外の関数で変数「a」をローカルオブジェクトとして使えるようになります。

「extern」を付ける際の4つの注意点

 こういった宣言をする場合は、次の点に注意してください。

  • 「extern」を付けた変数は外部定義の変数を参照するので初期化はできない
  • 「main.c」にファイル有効範囲を持つ「a」があり、それが内部結合のとき、「extern int a」も内部結合になる
  • 「extern int a」の「extern」を外すとローカルオブジェクトの宣言になり、それらは結合されない
  • 「extern int a」の「extern」を「static」に変更しても内部結合は行われない

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。