ユーニックス総合研究所

  • home
  • archives
  • c-extern

C言語のexternの悪魔的で危ない誘惑【*ご利用は自己責任で】

  • 作成日: 2022-02-05
  • 更新日: 2023-12-25

C言語のexternの悪魔的で危ない誘惑

C言語には記憶クラス指定子という指定子があります。
これは「extern」、「static」などです。

このうちexternは利用方法を誤るとプログラムの設計が破綻してしまうこともある、非常に危ない機能です。
しかし、そのデメリットを把握してほどほどに使用すれば、安全に使うこともできます

この記事ではこのC言語のexternについて詳しく解説します。

関連記事

目が覚めるC言語のdo-while文の使い方【ループ処理、初心者向け】
明快!C言語のcontinue文の使い方
君はまだC言語のdefineのすべてを知らない【マクロ、プリプロセス】
プログラミングのポインタをわかりやすく解説【C言語】
コードで見るC言語とC++の7つの違い

externの役割

C言語のexternは↓のように書きます。

extern  

この記憶クラス指定子は、どういう役割を持つのか?
これはずばり「よそのファイルのグローバル変数を参照したい時に使う」です。

C言語ではローカル変数とグローバル変数があります。
ローカル変数は関数内などで定義する変数のことを言います。

#include <stdio.h>  

int main(void) {  
    int my_local = 0;  // <- my_localはローカル変数  

    printf("%d\n", my_local);  

    return 0;  
}  

いっぽうグローバル変数は、関数外で定義された変数のことを言います。

#include <stdio.h>  

int my_global = 0;  // <- my_globalはグローバル変数  

int main(void) {  

    printf("%d\n", my_global);  

    return 0;  
}  

関数の外で定義しているグローバル変数は、そのグローバル変数が「見える」ところからはどこからでも参照できます。
しかしこれは通常、その「グローバル変数が定義されたファイル内」のみの話です。
つまり、「main.c」というソースファイルから「sub.c」内で定義しているグローバル変数を参照することはできません。

複数のソースファイルのコンパイル

↓の2つのファイルを見てください。

// main.c  
#include <stdio.h>  

int main(void) {  
    printf("%d\n", my_global);  
    return 0;  
}  
// sub.c  
int my_global = 0;  

「main.c」からは「my_global」というグローバル変数を参照しています。
これは「sub.c」内で定義されている変数です。

これらのファイルをGCCなどのコンパイラでコンパイルすると↓のようなエラーになります

$ gcc -Wall main.c sub.c  
main.c: In function 'main':  
main.c:5:20: error: 'my_global' undeclared (first use in this function)  
     printf("%d\n", my_global);  
                    ^  
main.c:5:20: note: each undeclared identifier is reported only once for each function it appears in  

error: 'my_global' undeclared (first use in this function)」というエラーメッセージは日本語にすると
'my_global'は未宣言です(この関数ではじめて使われました)
になります。

つまり変数my_globalは宣言されていないので使うことができない、ということですね。
いやいや、my_globalは「sub.c」で定義されているじゃないか。
たしかにそうです。

しかし、C言語のコンパイラはコンパイルを「ファイル単位」で行います
つまりそのファイル内から見えていない変数はコンパイルできないということになります。

これをコンパイルが通るようにするには「my_globalという変数がどこかで定義されているよ」とコンパイラに教える必要があります
この教えてあげることを「宣言」と言います。

つまりmy_globalを「main.c」で使うには「main.c」内でmy_globalを宣言すればいいことになります。
このグローバル変数の宣言に使うのがつまりexternです。

extern宣言を使ってみる

修正する「main2.c」内でmy_globalを↓のように宣言します。

// main2.c  
#include <stdio.h>  

// sub.c内で定義されているmy_globalをexternで宣言する  
extern int my_global;  

int main(void) {  
    printf("%d\n", my_global);  
    return 0;  
}  
// sub.c  
int my_global = 0;  

↑のようにコードを修正するとコンパイルが通るようになります。

$ gcc -Wall main2.c sub.c  
$ ./a.exe  
0  

これはコンパイラの「main2.c」のコンパイルで、my_globalexternで宣言されているのがわかることになります。
ですのでコンパイラはその宣言を信用してコンパイルを行います。
結果的にプログラムが生成されて、コンパイルに成功します。

externの悪魔的な注意点

externを使えばよそのファイル内のグローバル変数を参照できる。
このことはわかりましたが、これには注意点があります。

この注意点はグローバル変数によるものです。
一般にグローバル変数を使った設計は「むずかしい」と言われています。

とくにオブジェクト指向などの考えでは「グローバル変数は悪」と見なされている場合もあります。

しかしC言語ではグローバル変数を使ったプログラムはけっこう一般的です。
代表的なのは標準ライブラリのerrnoなどがそうです。
これはエラー内容を表す定数を保存するグローバル変数です。

グローバル変数のむずかしい所は、グローバル変数は「色々なところから変更される可能性がある」という点です。
1人で行う開発では問題になることは少ないですが、多人数で行う開発ではこれが問題になることがあります。
たとえば開発者Aが自分の担当範囲でグローバル変数を使っていたとします。
そのグローバル変数を開発者Bが勝手に書き換えてしまいました。そのほうが自分に都合がいいからです。
そうすると開発者Aの担当範囲でバグが出るようになってしまいました。

これは一見すると開発者Bに問題があるように見えます。
しかし実際に問題があるのは設計のほうです。
開発者も人ですから、ミスをしたりしますし認識できる範囲には限界があります。
そのためこういったことが起こらないように工夫するのが設計の利点です。

グローバル変数を安易に使うとこのようなトラブルが簡単に起こる可能性が出ます。
これがグローバル変数が嫌われている理由です。

それからグローバル変数はマルチスレッドなプログラムでは問題になる場合があります。
グローバル変数をスレッドセーフにするには、その変数用の排他制御を行う必要があります。
しかしグローバル変数をたくさん使った後で、マルチスレッド化が必要になったとして、どこまで修正できるでしょうか?
おそらく複雑に入り組んだコードの場合は修正が困難になるでしょう。

このようにグローバル変数には悪魔的なリスクが含まれています。

用法容量を守って使おう

色々なファイル間でたくさんのグローバル変数をexternで共有して使うのは一般的に言ってデンジャーです。
ですので構造体と引数を使って工夫が必要になります。

C言語の玄人があえてグローバル変数を使う場合は、そのデメリットを把握している上です。
私たちもグローバル変数の悪魔的なリスクを踏まえたうえで使用する必要があります。

🦝 < プロジェクトが悪魔に壊される

🐭 < まさに悪夢だね

シングルスレッドなプログラム、あるいは書き捨てのプログラムである場合は、グローバル変数を使うのも良い選択と言えます。
じっさい、最近のC言語の開発者もグローバル変数を使っている人はたくさんいます。
しかしそれはグローバル変数のデメリットを承知しているからです。

おわりに

今回はC言語のexternについて解説しました。
C言語のexternはことなったファイル間でグローバル変数を共有するために使います。
しかし悪魔的なリスクもあるので用法容量を守った正しい使用が必要になります。

🦝 < externで悪魔を飼いならそう

🐭 < それとも悪魔を封じるか?

🦊 < それはあなた次第