いまさらC言語のexternで悩む

とある製品のソースコードを眺めていたときに疑問に思って調べたことをメモします。

C言語で通常、大域変数(グローバル変数)を複数のソースファイルで共有する場合、共通でincludeするヘッダファイルにextern int abc;みたいに書いて、ソースファイルのいずれか1つに実体をint abc;みたいに書くと(私は)思っていたのですが、その製品のソースではヘッダファイルの方にint abc;とあり、各ソースファイルにextern int abc;という風に書かれています。

一瞬、それぞれのモジュール(ソースファイル)に別々の実体が存在するのかな、と思ったのですが、実際のその製品の動きを見てみるとちゃんと変数を共有して使っているようです。
私のいままでの理解が間違っているかもしれないので調べようと思ったのですが、C言語の規約を調べるのも億劫だし、環境依存かもしれないので、とりあえずサンプルを書いて手元のUbuntuのgcc(4.2.4)で挙動を調べてみました。

次のような2つのソースファイルを用意し、コンパイルして実行してみました。
(ヘッダファイルは作らずにそれぞれのソースに同じ変数宣言を書きました。)

ソースその1(a.c)

#include <stdio.h>

int value;

extern int value;

extern void b(void);

int main()
{
        value = 1;

        b();

        printf("a:value=%d\n",value);
        return 0;
}

ソースその2(b.c)

#include <stdio.h>

int value;

extern int value;

void b(void)
{
        printf("b:value=%d\n",value++);
}

実行結果

ubuntu:~/test/extern$ ./a.out
b:value=1
a:value=2

ちゃんと共有されています。

int value;

extern int value;

のところを、externのみにしたり、externの方を外したりしてみて共有されるか確認した結果は以下のとおり。

ソースファイル 挙動
a.c b.c
externなし externなし 共有される
externのみ externのみ コンパイル(リンク)エラー
externなし externのみ 共有される
externのみ externなし 共有される

大域変数は暗黙でstaticにはならないので、やはり、コンパイルが通れば共有されますね。(よく考えれば当たり前ですが。)
ちなみに、a.cとb.cの両方で、int value=0;のように初期化するとコンパイル(リンク)エラーとなります。

ubuntu:~/test/extern$ gcc a.c b.c
/tmp/ccuqLUhV.o:(.bss+0x0): multiple definition of `value'
/tmp/cce2atrh.o:(.data+0x0): first defined here

片方(1つ)のみ初期化する場合はエラーにはなりません。

これらの結果からコンパイラとかリンカの挙動を想像すると、
externのついた大域変数は必ずいずれかのモジュールに実体がないとダメで、externのつかない大域変数は基本的にexternありと同じだが、いずれのモジュールにも実体がなければリンカが実体を作る、
という感じになるでしょうか。
(あと、初期化をしたら必ずそのモジュールに実体が作られ、複数のモジュールに実体があればエラーとなる。)

で、ここで疑問に思ったのですが、意図的に共有する場合はexternなしでもいいと思うのですが、それではたまたま他のモジュールにあるものと同じ(大域)変数名を使っていたら、意図せずに共有されてしまう、ということになります。
そういう状況になったら、かなり見つけにくいバグになりそうですね。
(externとかstaticとか明示する習慣をつけておけば起こりにくい問題ですが。)

というわけで、そうした問題を未然に防ぐコンパイル(リンカ)オプションを調べておきました。
(このオプション、デフォルトでは無効のようです。)

ubuntu:~/test/extern$ gcc a.c b.c -Xlinker --warn-common
/tmp/ccu1WiL1.o: warning: multiple common of `value'
/tmp/ccsffaYm.o: warning: previous common is here

externのつかない同じ名前の大域変数を複数のモジュールで宣言するとこのようにwarningが出ます。

もう1パターンあります。

ubuntu:~/test/extern$ gcc a.c b.c -fno-common
/tmp/cc1Pz9uE.o:(.bss+0x0): multiple definition of `value'
/tmp/cckai0mp.o:(.bss+0x0): first defined here

こちらはコンパイル(リンク)エラーとなります。

以上、知っているようで知らない(のは私だけ?)、C言語にまつわる小ネタでした。

#今検索したら、同じことに悩んだ方が他にもいたようです。

4 Comments

  1. K&Rの第2版、A10.2に以下のような記述があります。

    初期値式をもたず、またextern指示子を含まない外部的なオブジェクト宣言は、仮の宣言(※)と呼ばれる。オブジェクトの定義が翻訳単位に現れると、仮の定義は単に冗長な定義として扱われる。そのオブジェクトに対する定義が翻訳単位に現れないときには、仮の定義はすべて、初期値0をもつ単一の定義になる。

    ※は誤訳で、正しくは「仮の定義」。

    翻訳単位(コンパイル&リンクされる一連のソースファイル)内に表れる大域的に書かれた複数の「int value;」は、初期値式を持つ定義が無ければ暗黙で一つの定義「int value=0;」としてまとめられ、初期値式を持つ定義(int value=1; etc.)があれば、それが唯一の定義として扱われる(当然、複数の定義は許されない)というのが言語仕様通りの動作のようです。

    私も全然知りませんでした。ただ、明示的に宣言しておかないと、バグの素になりそうですね…。

  2. なるほど!
    言語仕様にちゃんと書いてあるんですね。
    フォローありがとうございます。

    確かに、ちゃんとextern等をつけないとバグの素になりそうなので、一般的な入門書にはこういう仕様は書かれていないのでしょうね。

  3. このような言語仕様になった(externの省略が許されるよう緩和された)理由は、もともとUNIX他のオペレーティングシステムのリンカーがFortran言語を前提に作られている現状を追認するためなのではないかと思います。Fortranは上記の「初期値式を持つ定義」は許されない代わりに、extern宣言は不要ですから。

  4. > fsさん

    コメントありがとうございます。
    なるほど、Fortranの影響を受けているのですね。
    私はFortranについては「昔の言語」という程度の知識しかなかったのですが、勉強になりました。(調べたら、今でも進化しているようですね。)
    比較的新しい言語も、こういう歴史の古い言語の影響を受けていたりするので、古い言語を知っているのと知らないのでは理解の深さが違うだろうな、と思いました。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*