【C言語】extern宣言について解説(ファイル間で変数を共有)

extern宣言の解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、C言語における extern 宣言について解説していきます。

extern 宣言とは

C言語における extern 宣言とは下記のようなものになります。

extern宣言とは…

“他のファイルで変数宣言された” 変数を使用することの宣言

この extern 宣言を行うことで、他のファイルで宣言されている変数の中身を読み取ったり、変数の中身を変更したりと、複数のファイル間で変数を共有することができるようになります。

MEMO

実は、同じファイルで変数宣言された変数を extern 宣言するようなことも可能です

が、意味のない宣言ですし、まず行うこともありませんので、extern 宣言は基本的に “他のファイルで宣言された” 変数を使用するための宣言であると考えて良いです

例えば、グローバル変数はファイル内のどの関数からも使用可能ですよね?

グローバル変数にファイル内の全関数からアクセスできる様子

ただし、グローバル変数であっても、そのままでは他のファイルの関数からは使用することができません。

他のファイルのグローバル変数にアクセスできない様子

このようなグローバル変数を、他のファイルから使用するために行うのが extern 宣言です。

この extern 宣言を行うことで、他のファイルで宣言されたグローバル変数を使用することができるようになります。

extern宣言により他のファイルのグローバル変数を共有してもらう様子

単に同じ変数名の変数というわけでは無いです。全く同じ実体の変数(同じアドレスの変数)を使用することができるようになります。

つまり、extern 宣言を行うことで、変数を複数のファイル間で共有することができます。

もし、グローバル変数がファイル間で共有できないのであれば、そのグローバル変数を直接使用している関数は全て同じソースコード内に定義しなければなりません。

ですが、グローバル変数が複数のファイル間で共有できるのであれば、そのグローバル変数を使用している関数を別のファイルに定義することができるようになります。

 

つまり、グローバル変数を使用しているソースコードでも分割しやすくなるということになります。

extern 宣言には、このような、”ソースコードの分割が簡単に行える” というメリットがあります。

extern 宣言の仕方

この extern 宣言は、下記のように extern 指定子の後に、他のファイルで変数宣言された変数の “変数の型名” と “変数名” を指定することで行います。

extern宣言
extern 変数の型名 変数名;

例えば、他のファイルで下記のように変数宣言されていたとします。

externしたい変数
int g_data;

この変数を extern 宣言したい場合は、下記のように記述します。

extern宣言の例
extern int g_data;

要は、extern の後ろに “使用したい変数の変数宣言” をそのまま記述してやれば良いです。

このように extern 宣言を行うことで、そのファイルで変数 g_data を使用することができるようになります。

ただし、この extern 宣言を行う変数(上記の例では g_data)は、必ずプログラム全体のいずれかのファイルで変数宣言されている必要があります。

externする変数がプログラム全体のいずれかのファイルで定義されている必要があることを示す説明図

もし変数宣言されていない場合、コンパイル時ではなく、リンク時にエラーが発生します。

実際に、私の環境では下記のようなエラーが発生しました。

$ gcc source1.c source2.c -o main.exe -Wall
Undefined symbols for architecture x86_64:
  "_g_data", referenced from:
      _func in source2-cf2df7.o
ld: symbol(s) not found for architecture x86_64

また、後述で解説しますが、extern 宣言時には初期化は不要です。ですので、下記のように変数宣言時に初期化が行われている場合は、

初期化ありの変数宣言
int g_data = 100;

この初期化部分を取り除いて extern 宣言を行う必要があります。

初期化されている変数のextern宣言
extern int g_data;

スポンサーリンク

extern 宣言の具体例

では、具体例を用いて extern 宣言の使い方や効果を確認していきたいと思います。

例えば下記のようなファイル source1.c とファイル source2.c を分割コンパイルすることを考えてみましょう。

source1.c
#include <stdio.h>

/* source2.cで定義された関数 */
void func(void);

/* グローバル変数を変数宣言 */
int g_data = 100;

int main(void) {

    g_data = 200;

    func();

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

}
source2.c
#include <stdio.h>

void func(void) {

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

    g_data = 500;

}

上記2つのファイルの分割コンパイルは、gcc を用いてコマンドラインから下記コマンドにより実行するこができます。

gcc source1.c source2.c -o main.exe

ただし、上記のままだと分割コンパイルを行なっても source2.c のコンパイル時にエラーが発生してしまいます。私の環境では上記コマンドを実行すると次のようなエラーが発生しました。

source2.c:5:20: error: use of undeclared identifier 'g_data'
    printf("%d\n", g_data);
                   ^
source2.c:7:5: error: use of undeclared identifier 'g_data'
    g_data = 500;

なぜエラーが発生するのでしょうか?

C言語では、変数を使用するためには事前に変数宣言(もしくは extern 宣言)を行う必要があります。もし変数宣言をしていない変数を使用しようとすると上記のようなコンパイルエラーが発生します。

つまり、source2.c では変数 g_data を使用しているものの、source2.c では変数 g_data の変数宣言が行われていないことが、このコンパイルエラーの原因になります。

で、この g_datasource1.c で変数宣言されている変数になります。そして、source1.csource2.c は分割コンパイルされて最終的にリンクされるので、source2.cextern 宣言を行うことで source1.c で変数宣言されている変数を使用することができるようになります。

これを確認するために、source2.c の先頭部分で extern 宣言を行うようにしてみましょう!source1.c では g_data の変数宣言は下記のように行われています。つまり、g_dataint 型の変数です。

g_dataの変数宣言
int g_data = 100;

同様に source2.c でも g_dataint 型の変数として使用するためには、source2.c を下記のように変更すれば良いです。

修正後のsource2.c
#include <stdio.h>

/* 使用したい変数をextern宣言 */
extern int g_data;

void func(void) {

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

    g_data = 500;

}

これにより、コンパイルとリンクに成功し、プログラム(実行可能ファイル)が生成されるようになります。

次はこのプログラムの動作を確認してみましょう。

まずプログラム起動時に、source1.c の下記の変数宣言により変数 g_data がメモリ上に配置されます。

g_dataの配置
int g_data = 100;

要は、g_data 用のメモリがメモリ空間上のどこかに確保されます。このメモリのサイズは int 型のサイズである 4 バイトになります(環境によってサイズは異なる可能性はあります)。さらに、source1.c からは g_data という変数名でこの確保されたメモリにアクセスできるようになります。

グローバル変数の実体が作成される様子

このように、変数宣言によって、その変数用のメモリがメモリ空間上に確保されることを、”変数の実体を作成する” などと言ったりします。

特に extern 宣言を理解する上では、この変数の実体が作成される or 作成されないを意識することが重要です。

また、上記の変数宣言時には g_data100 で初期化されています。ですので、先程確保されたメモリには 100 が格納されることになります。

初期化によりグローバル変数に値が格納される様子

一方で、source2.c では変数宣言ではなく extern 宣言を行なっています。

g_dataのextern宣言
extern int g_data;

この extern 宣言では変数の実体が作成されません。この extern 宣言の効果は、source2.c から g_data という変数名で他のファイルによって確保された g_data 用のメモリにアクセスできるようになることのみです。

つまり、上記により source2.c から source1.c で確保されたメモリに g_data という変数名でアクセスできるようになります。

extern宣言で他のファイルの変数を使用することを宣言する様子

なので、source1.c と source2.c とでメモリ(変数用のメモリ)を共有して動作することができるようになります。

より具体的にいうと、source1.cmain 関数の下記部分が実行されると、

source1.cからのg_dataの使用1
g_data = 200;

g_data 用のメモリに 200 が格納されることになります。

2つのファイル間で変数を共有して使用する様子1

そして、main 関数から func 関数が実行されて source2.c の下記部分が実行されると、

source2.cからのg_dataの使用1
printf("%d\n", g_data);

g_data 用のメモリの内容が取得され、それが表示されることになります。つまり、source1.c で先ほど格納された 200 が表示されます。

2つのファイル間で変数を共有して使用する様子2

さらに、source2.cfunc 関数の下記が実行されると、

source2.cからのg_dataの使用2
g_data = 500;

g_data 用のメモリに 500 が格納されます。

2つのファイル間で変数を共有して使用する様子3

なので、source1.cmain 関数の下記が実行されると、

source1.cからのg_dataの使用2
printf("%d\n", g_data);

g_data 用のメモリに格納されている 500 が表示されるようになります。

2つのファイル間で変数を共有して使用する様子4

こんな感じで、extern 宣言を利用することで、同じ変数を複数のファイル間で共有しながら処理を実行することができるようになります。

extern 宣言時の注意点

続いて extern 利用時・extern 宣言時の注意点について解説していきます。

extern 宣言が有効なのは static なしのグローバル変数のみ

まず extern 宣言で使用できるようになる変数は、他のファイルで “static なしのグローバル変数” として変数宣言された変数のみになります。

extern 宣言は、どの位置でも行えるというわけではありません。変数が extern 宣言できるかどうかは、extern 宣言する位置で、その変数が有効範囲内である(スコープ内)であるかによって決まります。

要は、変数を extern 宣言できる位置は、その変数の有効範囲内のみです。

一方で、変数宣言が行われたファイルの外まで有効範囲となる変数はC言語においては “static なしのグローバル変数のみ” となります。static なしのグローバル変数はプログラム全体がスコープとなる変数です。

なので、他のファイルで宣言された変数を extern 宣言しようと思うと、extern 宣言できるのは static なしのグローバル変数のみになります。

ローカル変数(関数内で変数宣言されている変数)に関してはスコープがその宣言が行われた関数内のみになるので、他のファイルはスコープ外になります。なので他のファイルで extern 宣言するとエラーが発生します。

また、static グローバル変数に関してはスコープがその宣言が行われたファイル内のみになるので、ローカル変数同様に他のファイルはスコープ外になります。なので、こちらも他のファイルで extern 宣言するとエラーが発生します。

各変数のスコープの説明図

一応補足しておくと、ローカル変数は、その変数が変数宣言された関数内で extern 宣言を行うことが可能ですし、static ありのグローバル変数宣言は同じファイル内で extern 宣言を行うことは可能です。

ただし、もともと extern 宣言しなくても使用できる変数に対して extern 宣言を行うことになるので宣言しても特に意味はないと思います。

スポンサーリンク

extern を付けなくても extern 宣言として扱われることもあり

すごく厄介なのですが、実は、通常の変数宣言を行なっているだけなのに、その宣言が extern 宣言として扱われることがあります。

これは、”別のファイル” で “同じ名前のグローバル変数” を変数宣言した場合に起こる可能性のある現象になります。

現象

この典型的な例は下記の source1.csource2.c になります。

source1.c
#include <stdio.h>

/* 変数宣言 */
int data;

void func(void);

int main(void) {

    data = 100;

    func();
    
    /* source2.cで代入した結果が表示される */
    printf("%d\n", data);
    
    return 0;
}
source2.c
/* 変数宣言 */
int data;

void func(void) {

    /* dataはsource1.cと共有されてしまっている */
    data = 200;
}

これらをコンパイル・リンクした場合、data という変数の実体が作成され、その変数が source1.csource2.c とで共有されることになります。つまり、extern 宣言していないのにも関わらず extern 宣言したものとして data が扱われます。

これ、かなり厄介だと思うんですよね…。上記の source1.csource2.c それぞれでは extern 宣言を行なっていないので、あくまでもファイル内で専用の変数として data を変数宣言したものだと考えられます。ただし、実際には g_data は2つのファイル間で共有されることになります。

なので、各ファイルそれぞれで、想定していないタイミングで想定していない値が変数に格納されるようなことがあり得ます。そして、これによりプログラムの動作が不正になる可能性があります。

“通常の変数宣言を行なっているだけなのに、extern 宣言したものとして扱われることがある” ということを知らないと、プログラムの動作が不正になる原因を解明するだけでも苦労すると思います….。

原因

上記の source1.csource2.c で変数宣言されている data がファイル間で共有される原因は、変数宣言の仕方にあります。

source1.cの変数宣言
/* 変数宣言 */
int data;
source2.cの変数宣言
/* 変数宣言 */
int data;

この変数宣言では変数の “初期化” を行なっていません。ここが変数が extern 宣言したものとして扱われることがある原因です。

変数宣言時に “初期化が行われていないグローバル変数” は、下記のように扱われるようです。

  • その変数の実体が “まだ作成されていない” 場合:
    • 変数の実体が作成される
  • その変数の実体が “すでに作成されている” 場合:
    • extern 宣言をした場合と同様の動作
    • 変数の実体は作成されず、すでに作成された変数の実体への参照のみ(変数の共有のみ)行われる

なので、source1.csource2.c のどちらかの変数宣言では変数の実体が作成されるのですが、他方では変数の実体は作成されず、extern 宣言と同様に扱われることになります。

したがって、変数宣言時に “初期化が行われていないグローバル変数” は、意図しない extern 宣言が行われ、意図せずにファイル間で共有される可能性があるので注意が必要です。

対処法

このような現象は、”グローバル変数の変数宣言時に必ず初期化する” ようにすることで防ぐことが可能です(そもそもどんな変数でも初期化しておいた方が無難です)。

安全な変数宣言
/* 変数宣言 */
int data = 100;

変数宣言時に初期化されているグローバル変数は、プログラム起動時に “必ず” 実体が作成されます。もし、同じ名前のグローバル変数を別のファイルで変数宣言した場合は、同じ名前の変数の実体が複数作成されることになります。

ですが、同じ名前の変数の実体が複数作成された場合、リンク時にエラーが発生するようになっています。

なので、グローバル変数の変数宣言時に初期化を行うようにしておけば、意図しない変数の共有が行われてしまう可能性がある場合には、そのことを未然にリンクエラーで検知することができるようになります。

また、そもそも他のファイルから extern 宣言されたくないようなグローバル変数については積極的に static を付加するようにしましょう。これにより、他のファイルから extern 宣言できないようにすることができます。

extern 宣言時には初期化しない

また、extern 宣言時には初期化は行わない方が良いです。

externの間違った使い方
extern int g_data = 100;

もしかしたら使用するコンパイラ等の環境によって異なるかもしれませんが、extern 宣言時に初期化を行った場合、どうも extern 指定子が無視されるようでした。

つまり、初期化ありのグローバル変数の変数宣言として扱われます。そしてこの場合、extern を付けなくても extern 宣言として扱われることもありで解説した通り、必ずその変数の実体が作成されるようになります。

ですので、通常の変数宣言と extern 宣言との両方で同じ名前の変数の実体が作成されることになり、リンク時にエラーが発生してしまいます。

このような意図しない動作が行われないようにするため、extern 宣言時には初期化は行わない方が良いです。

グローバル変数同様のデメリットあり

ここまで解説してきたように、extern はグローバル変数が使用できる範囲を広げる宣言になります。

なので、extern 宣言によるメリット・デメリットもグローバル変数同様であると考えて良いです。

ご存知の通り、グローバル変数はどの関数からも使用できるので非常に便利です。

ただしその一方で、変数が使用できる箇所が増えるため、プログラム実行時にその変数に格納されている値が不正な場合は、その変数がどこで・どのタイミングで変更されたのが原因で不正になっているのかを追うのが難しくなる等のデメリットもあります。

extern 宣言を行うと、このグローバル変数が使用できる箇所がさらに増えることになるため、より原因を追うのが大変になります。

ですので、extern 宣言はむやみに行わず、本当に必要なときにだけ使用するようにした方が良いです。

例えば、下記のような source1.csource2.c であれば、グローバル変数 data を両方のファイルから直接変更しています。

source1.c
#include <stdio.h>

void calc(void);
void set(int);
int get(void);

int main(void) {

    /* dataへの値格納はsource2.cで実行 */
    set(100);

    calc();
    
    /* dataからの値取得はsource2.cで実行 */
    printf("%d\n", get());
    
    return 0;
}
source2.c
/* 変数宣言 */
int data = 0;

void calc(void) {

    data = data * 1.1;
}

void set(int num) {
    data = num;
}

int get(void) {
    return data;
}

ですが、この場合であれば別に source1.cdata を変更しなくても、下記のように source2.c から data を変更するための関数を提供してやればやりたいことは達成できます。

変更後のsource1.c
#include <stdio.h>

/* extern宣言 */
extern int data;

void calc(void);

int main(void) {

    /* 直接dataを変更 */
    data = 100;

    calc();
    
    /* 直接dataを参照 */
    printf("%d\n", data);
    
    return 0;
}
変更後のsource2.c
#include <stdio.h>

/* 変数宣言 */
int data = 0;

void calc(void) {

    data = data * 1.1;
}

さらに、この場合は、source1.c から data を使用する必要がないので extern 宣言も不要になります。

extern 宣言がなくなったということは、変数 data を使用する箇所が減ったことになるため、もし data に格納されている値がおかしいような場合や、data に格納されている値のせいでプログラムがうまく動作しないような場合でも、使用する箇所が減った分、 data に対するどの変更が原因であるかが特定しやすくなります。

スポンサーリンク

まとめ

このページでは、C言語における extern について解説しました。

基本的に extern はファイル間で変数を共有するための指定子になります。

また、この extern によりソースコードの分割を楽に行うことが可能です。

が、実は私は使ったことないんですよねー。同様に、C言語使ってるけど extern を使ったことがない人も多いと思います。そもそもグローバル変数自体使いたくない人も多いですしね…。

ただ、規模の大きいソースコードだと結構この extern を使われているのを見かけます。

なので、もし extern は使わないにしても、extern の意味や効果は知っておいた方が良いと思います!

是非 “extern 宣言は他のファイルで変数宣言された変数を使用するために行われている” ということだけでも覚えておきましょう!

オススメの参考書(PR)

C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!

まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。

  • 参考書によって、解説の仕方は異なる
  • 読み手によって、理解しやすい解説の仕方は異なる

ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?

それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。

なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。

特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。

もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!

入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示

1 COMMENT

現在コメントは受け付けておりません。