識別子の結合

こんにちは、めのんです!

前回予告したように今回は結合(linkage)について解説することにします。
結合の概念を理解するには、前回解説した分割コンパイルや翻訳単位の理解が不可欠なので、まだ怪しい方は復習しておいてくださいね。

そもそも結合って?

細かな話をする前に、そもそも「結合(linkage)」とは何なのかからお話しないといけませんね。

前回の分割コンパイルの解説のところや「コンパイルの流れ」の回にも簡単に解説しましたが、Cのソースファイルをコンパイルして実行できる形式のファイルを生成する最後の段階が「リンク」です。

リンク時には、リンカが関数やオブジェクトのアドレスを最終的に決定します。
オブジェクトファイルの段階では、関数やオブジェクトを参照している箇所には仮のアドレスしか入っていません。
それをリンカが正式なアドレスに直して実行ファイルに埋め込むのです。

関数やオブジェクトの参照元と参照先を結びつける際には識別子がキーになります。
このとき、合致する識別子をどの範囲で探すかの種別が、今回解説する「結合(linkage)」です。

結合は有効範囲とよく似ていますが違います。
有効範囲はあくまでも

  • 関数
  • ファイル
  • ブロック
  • 関数原型

の4種類です。
結合はそれらとはまったく別の概念です。

結合の種類

Cには次の3種類の結合(linkage)があります。

  • 外部結合
  • 内部結合
  • 無結合

順に解説していきますね。

外部結合

外部結合では、翻訳単位にまたがって識別子の参照を解決することができます。
グローバルな結合だという言い方をしてもいいでしょう。

前回、fmp.cで宣言したfmp_setupなどの関数をmain.cで定義したmain関数から呼び出せたのは、各関数が外部結合だったからです。

ファイル有効範囲で定義した関数やオブジェクトはデフォルトで外部結合になります。
明示的に外部結合であることを指定するには「extern指定子」を使います。

int a;         // デフォルトで外部結合
int func(void) // デフォルトで外部結合
{
  ...
}

extern int b;           // 明示的に外部結合であることを指定
extern void test(void); // 明示的に外部結合であることを指定

内部結合

内部結合では、翻訳単位の中だけで識別子の参照を解決することができます。
ファイル有効範囲で宣言した関数やオブジェクトのうち、「static指定子」を付けたものは内部結合になります。

static int a;         // 内部結合
static int func(void) // 内部結合
{
  ...
}

内部結合の識別子は同じ翻訳単位の中だけで有効ですので、翻訳単位が異なれば同じ名前の識別子を別の用途に使ってもかまいません。

無結合

関数やオブジェクト以外の識別子や、ファイル有効範囲以外でextern指定子を付けずに宣言した識別子は無結合になります。
ちょっとわかりにくいので具体例を挙げますね。

void func(int arg)  // funcは外部結合、仮引数argは無結合
{
  static int a;   // aは無結合
  extern int b;   // bは外部結合
  int c;          // cは無結合
  ...
}

関数の中で宣言するとstatic指定子を付けても無結合になることに注意してください。
static指定子は、静的記憶域間を表す使い方と、内部結合を表す使い方の2種類があります。

宣言と仮定義

さきほど、ファイル有効範囲で宣言するとデフォルトで外部結合になると書きました。
実際にはもう少し複雑なルールがあります。

extern指定子もstatic指定子も付けない宣言をした場合、同じ翻訳単位でその宣言よりあとに同じ識別子をextern指定子やstatic指定子を付けて宣言しなおすると、をextern指定子やstatic指定子の有無によって結合の種類が変わります。

要素数の無い配列やタグ名だけの不完全型による宣言のあとに完全型として宣言し直すこともできます。

たとえばこんな感じです。

int a;
extern int a;  // ここで外部結合に決定

int b[];   // 不完全型の配列
int b[10]; // ここで要素数が決定されて完全型になる。

ただし、矛盾する宣言、たとえばextern指定子を付けて宣言したあとでstatic指定子を付けて宣言し直すとか、その逆とかです。

int a;
extern int a;  // ここで外部結合に決定
static int a;  // 矛盾する宣言なのでコンパイルエラー!

ところで、ファイル有効範囲で宣言するオブジェクトの場合、初期化子が無くて、extern指定子もstatic指定子も付けないかstatic指定子を付けて宣言すると、それは「仮定義(tentative definition)」になります。

int a;  // 仮定義
int b = 123;  // 初期化子があるので仮定義ではない
static int c; // 仮定義
extern int d; // extern指定子が付いているので仮定義ではない

仮定義したオブジェクトが、翻訳単位の最後まで初期化子を付けて宣言し直されない場合はゼロで初期化されます。

int a;  // 仮定義
...
// 翻訳単位の最後までaの初期化子が無いのでゼロで初期化される。

(static指定子を付けた)内部結合の仮定義が不完全型だった場合、翻訳単位の最後まで完全型にならなければコンパイルエラーになります。

int a[];  // 仮定義
...
// 翻訳単位の最後までaは不完全型のままなのでコンパイルエラー!

ところで、明示的にextern指定子を付けた外部結合のオブジェクトが翻訳単位の最後まで初期化子を与えられなかった場合、そのオブジェクトはどこか別の翻訳単位で定義されていることを期待することになります。

そのようなオブジェクトを参照しているのに、どの翻訳単位でも定義されていなければリンクエラーになります。

extern int a;  // ほかの翻訳単位で定義されていることを期待

int func(int arg)
{
  return arg + a;  // を参照
}

// ほかのどの翻訳単位でもaを外部結合として定義されていなければリンクエラー!

Cでは、関数にしてもオブジェクトにしてもそうですが、宣言と定義は別物になります。
もう少し正確にいうと、定義は宣言の一種で、定義した場合だけ関数やオブジェクトの実体が作られます。

複数の翻訳単位がある場合、あっちの翻訳単位でもこっちの翻訳単位でも同じ関数やオブジェクトの定義があるというのはおかしいですよね。
同じ翻訳単位の中であれば何度でも仮定義できますが、最終的な定義はあくまでも1カ所でないといけません。

ちょっと難しい概念でしたが、いかがだったでしょうか?
あまりうまく説明できなったので、何らかの方法で補足説明できればと考えています。


今回の解説は以上となります。
次回はヘッダファイルの書き方を解説する予定です。
どうぞご期待ください!