Gobble up pudding

プログラミングの記事がメインのブログです。

MENU

C言語のグローバル変数とexternについて

スポンサードリンク

f:id:fa11enprince:20200628230841j:plain
C言語では言語仕様上、グローバル変数は良く使うと思います。
できるだけ避けるのは言うまでもありませんが。
そこでよく混乱するのがexternではないでしょうか?
ヘッダなんかをインクルードすると
あれ?そういえばexternって……どうなんだっけ…ってことになります。

私なんかはなんかの参考書でプログラミングを学び始めたときに、
externつけてもつけなくても一緒というような怪しげな解説を見た記憶があり、
それによって、余計に混乱してしまいました。
ただし、関数に関しては一緒です。
ここより正確な記載のあるサイトを見つけましたのでURLを記載します。
C言語のexternキーワードについて(関数編) – cloudtofu
いまだに検索流入が多い(2019年8月時点)のでちょっとびっくりします。それだけC言語が息の長い言語であり、
年々使用者が減少しているのでしょうね。私自身もCはもう5年以上触っていません。
いまなら限られた環境でない限りはC++(もしくはGoかRustかもしれない)を使うでしょうね。

externは
他の場所に定義があって、宣言ですよ
って明示するためのものです。

宣言と定義について

厳密な説明ではないのですが、
C言語における宣言とは値や中身がかかれていないものです。
例えば、

int g_value;
extern int g_value2;
int foo(void);

は宣言です。
一方、定義は

int g_value = 0;
int foo(void) {
    return g_value;
}

などです。

グローバル変数を使うときどうすればいいか、
基本的にヘッダ側(.h)はextern付の宣言をして、.cファイルのどこかに
externなしの定義を書きます。その際に初期値を代入します。
これでほぼOKです。
もちろん、.h側にexternなしの変数宣言をしてはいけません。
ヘッダファイルにはいろいろお作法があるのですが、
きちんと書かれているものが少ないように思われます。
そういうわけで巷には間違って書かれている
ヘッダファイルがあふれているのではないでしょうか?

ただし、これでは定義がどこにあるのか、
しかも一つでなくてはならないので、管理が複雑になり、混乱します。
しかも仮定義という厄介な概念があり、もっと事情は複雑です。

こうしておけばよい

最初に最終版を書きます。
GLOBAL_VALUE_DEFINEDをmainのあるファイルにdefineし
マクロでextern有無しを制御します。
初期値を0以外に指定したいときはやはりマクロで制御します。

test.h

#ifndef TEST_H_INCLUDED_
#define TEST_H_INCLUDED_

#ifdef GLOBAL_VALUE_DEFINE
  #define GLOBAL
  #define GLOBAL_VAL(v) = (v)
#else
  #define GLOBAL extern
  #define GLOBAL_VAL(v)
#endif

GLOBAL int g_value;   // この場合は最初の定義で0で初期化
/* GLOBAL int g_value GLOBAL_VAL(1); */

void foo(void);

#endif /* TEST_H_INCLIDED_ */

test.c

#include "test.h"

void foo(void) {
    g_value++;
}

myapp.c

#define GLOBAL_VALUE_DEFINE
#include <stdio.h>
#include "test.h"

int main(void) {
    printf("%d\n", g_value);
    foo();
    printf("%d\n", g_value);
    g_value++;
    printf("%d\n", g_value);
    return 0;
}

Makefile

あとコンパイルがやや面倒なのでMakefileを書きます。
Makefileの解説をすると長くなるので割愛します。

CC           = gcc
CFLAGS       = -Wall
DEBUGFLAGS   = -O0 -D _DEBUG -g
PROG         = myapp
SOURCES      = myapp.c test.c
OBJS         = $(SOURCES:.c=.o)
INCDIR       =
LIBDIR       =
LIB          =

.PHONY: all
all: $(SOURCES) $(PROG)

# Primary Target
$(PROG): $(OBJS)
        $(CC) $(CFLAGS) $(DEBUGFLAGS) -o $@ $^ $(INCDIR) $(LIBDIR) $(LIB)

# Suffix Rule
.c.o:
        $(CC) $(CFLAGS) $(DEBUGFLAGS) -c $< $(INCDIR) $(LIBDIR) $(LIB)

.PHONY: clean
clean:
        $(RM) $(OBJS) $(PROG)

コンパイル&リンク

$ make
gcc -Wall  -O0 -D _DEBUG -g -c myapp.c
gcc -Wall  -O0 -D _DEBUG -g -c test.c
gcc -Wall  -O0 -D _DEBUG -g -o myapp myapp.o test.o

実行結果

$ ./myapp.exe
0
1
2

解説

結論はわかったとしてどうしてこうするのかというのを説明します。
話を単純化するために例えば次のような2つのファイルがあるとします。

test.c

extern int g_value;

void foo(void) {
    g_value++;
}

myapp.c

#include <stdio.h>

int g_value = 0;
/* extern */ void foo(void);

int main(void) {
    printf("%d\n", g_value);
    foo();
    printf("%d\n", g_value);
    g_value++;
    printf("%d\n", g_value);
    return 0;
}

もちろんこれは次のようにコンパイルすればちゃんと問題なく動きます。

コンパイル

gcc -Wall  -O0 -D _DEBUG -g -c myapp.c
gcc -Wall  -O0 -D _DEBUG -g -c test.c
gcc -Wall  -O0 -D _DEBUG -g -o myapp myapp.o test.o

補足ですが関数定義はexternがあってもなくても外部結合(=ファイルの外から見える)ので
あってもなくてもよいです。

ここで、test.c側のextern int g_value;
int g_value = 0;
に書き換えると、当然

test.o:test.c:(.bss+0x0): `g_value' が重複して定義されています

というようなエラーがでます。
つまり、.hファイルに単純にくくりだしてint g_value = 0;
として両者の.cでincludeすると同じことが起こります。
そのようなことを防ぐために最初のようにマクロで制御しています。

しかし、厄介なのがANSI Cの仮定義という概念で、
externをかかず、いろんなファイルでint g_value;とした場合は……

test.c

int g_value; /* 他に定義がないので int g_value = 0;の定義として扱う */
...

myapp.c

#include <stdio.h>

int g_value; /* 既にg_valueの定義があるので、extern int g_value;(宣言)として扱う */
...

これはなんとコンパイルが通ります。理由は上記コードのコメント部分です。

その他constのグローバル変数について

constにも同じことが言えます。
ただし、constでexternかどうか気にしたことがないぞ?っていう人もいるかもしれません。
……そもそも伝統的なCを使っている人は#defineを使っているかもしれませんが。
実はconstの場合、CとC++で微妙に振る舞いが違います。
externを使わない場合、
Cの場合は外部リンケージ
C++の場合は内部リンケージとなります。

参考リンク