このページでは、C言語における extern
宣言について解説していきます。
Contents
extern
宣言とは
C言語における extern
宣言とは下記のようなものになります。
“他のファイルで変数宣言された” 変数を使用することの宣言
この extern
宣言を行うことで、他のファイルで宣言されている変数の中身を読み取ったり、変数の中身を変更したりと、複数のファイル間で変数を共有することができるようになります。
実は、同じファイルで変数宣言された変数を extern
宣言するようなことも可能です
が、意味のない宣言ですし、まず行うこともありませんので、extern
宣言は基本的に “他のファイルで宣言された” 変数を使用するための宣言であると考えて良いです
例えば、グローバル変数はファイル内のどの関数からも使用可能ですよね?
ただし、グローバル変数であっても、そのままでは他のファイルの関数からは使用することができません。
このようなグローバル変数を、他のファイルから使用するために行うのが extern
宣言です。
この extern
宣言を行うことで、他のファイルで宣言されたグローバル変数を使用することができるようになります。
単に同じ変数名の変数というわけでは無いです。全く同じ実体の変数(同じアドレスの変数)を使用することができるようになります。
つまり、extern
宣言を行うことで、変数を複数のファイル間で共有することができます。
もし、グローバル変数がファイル間で共有できないのであれば、そのグローバル変数を直接使用している関数は全て同じソースコード内に定義しなければなりません。
ですが、グローバル変数が複数のファイル間で共有できるのであれば、そのグローバル変数を使用している関数を別のファイルに定義することができるようになります。
つまり、グローバル変数を使用しているソースコードでも分割しやすくなるということになります。
extern
宣言には、このような、”ソースコードの分割が簡単に行える” というメリットがあります。
extern
宣言の仕方
この extern
宣言は、下記のように extern
指定子の後に、他のファイルで変数宣言された変数の “変数の型名” と “変数名” を指定することで行います。
extern 変数の型名 変数名;
例えば、他のファイルで下記のように変数宣言されていたとします。
int g_data;
この変数を extern
宣言したい場合は、下記のように記述します。
extern int g_data;
要は、extern
の後ろに “使用したい変数の変数宣言” をそのまま記述してやれば良いです。
このように extern
宣言を行うことで、そのファイルで変数 g_data
を使用することができるようになります。
ただし、この extern
宣言を行う変数(上記の例では g_data
)は、必ずプログラム全体のいずれかのファイルで変数宣言されている必要があります。
もし変数宣言されていない場合、コンパイル時ではなく、リンク時にエラーが発生します。
実際に、私の環境では下記のようなエラーが発生しました。
$ 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 int g_data;
スポンサーリンク
extern
宣言の具体例
では、具体例を用いて extern
宣言の使い方や効果を確認していきたいと思います。
例えば下記のようなファイル source1.c
とファイル source2.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);
}
#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_data
は source1.c
で変数宣言されている変数になります。そして、source1.c
と source2.c
は分割コンパイルされて最終的にリンクされるので、source2.c
で extern
宣言を行うことで source1.c
で変数宣言されている変数を使用することができるようになります。
これを確認するために、source2.c
の先頭部分で extern
宣言を行うようにしてみましょう!source1.c
では g_data
の変数宣言は下記のように行われています。つまり、g_data
は int
型の変数です。
int g_data = 100;
同様に source2.c
でも g_data
を int
型の変数として使用するためには、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
がメモリ上に配置されます。
int g_data = 100;
要は、g_data
用のメモリがメモリ空間上のどこかに確保されます。このメモリのサイズは int
型のサイズである 4
バイトになります(環境によってサイズは異なる可能性はあります)。さらに、source1.c
からは g_data
という変数名でこの確保されたメモリにアクセスできるようになります。
このように、変数宣言によって、その変数用のメモリがメモリ空間上に確保されることを、”変数の実体を作成する” などと言ったりします。
特に extern
宣言を理解する上では、この変数の実体が作成される or 作成されないを意識することが重要です。
また、上記の変数宣言時には g_data
が 100
で初期化されています。ですので、先程確保されたメモリには 100
が格納されることになります。
一方で、source2.c
では変数宣言ではなく extern
宣言を行なっています。
extern int g_data;
この extern
宣言では変数の実体が作成されません。この extern
宣言の効果は、source2.c
から g_data
という変数名で他のファイルによって確保された g_data
用のメモリにアクセスできるようになることのみです。
つまり、上記により source2.c
から source1.c
で確保されたメモリに g_data
という変数名でアクセスできるようになります。
なので、source1.c
と source2.c
とでメモリ(変数用のメモリ)を共有して動作することができるようになります。
より具体的にいうと、source1.c
の main
関数の下記部分が実行されると、
g_data = 200;
g_data
用のメモリに 200
が格納されることになります。
そして、main
関数から func
関数が実行されて source2.c
の下記部分が実行されると、
printf("%d\n", g_data);
g_data
用のメモリの内容が取得され、それが表示されることになります。つまり、source1.c
で先ほど格納された 200
が表示されます。
さらに、source2.c
の func
関数の下記が実行されると、
g_data = 500;
g_data
用のメモリに 500
が格納されます。
なので、source1.c
の main
関数の下記が実行されると、
printf("%d\n", g_data);
g_data
用のメモリに格納されている 500
が表示されるようになります。
こんな感じで、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.c
と source2.c
になります。
#include <stdio.h>
/* 変数宣言 */
int data;
void func(void);
int main(void) {
data = 100;
func();
/* source2.cで代入した結果が表示される */
printf("%d\n", data);
return 0;
}
/* 変数宣言 */
int data;
void func(void) {
/* dataはsource1.cと共有されてしまっている */
data = 200;
}
これらをコンパイル・リンクした場合、data
という変数の実体が作成され、その変数が source1.c
と source2.c
とで共有されることになります。つまり、extern
宣言していないのにも関わらず extern
宣言したものとして data
が扱われます。
これ、かなり厄介だと思うんですよね…。上記の source1.c
と source2.c
それぞれでは extern
宣言を行なっていないので、あくまでもファイル内で専用の変数として data
を変数宣言したものだと考えられます。ただし、実際には g_data
は2つのファイル間で共有されることになります。
なので、各ファイルそれぞれで、想定していないタイミングで想定していない値が変数に格納されるようなことがあり得ます。そして、これによりプログラムの動作が不正になる可能性があります。
“通常の変数宣言を行なっているだけなのに、extern
宣言したものとして扱われることがある” ということを知らないと、プログラムの動作が不正になる原因を解明するだけでも苦労すると思います….。
原因
上記の source1.c
と source2.c
で変数宣言されている data
がファイル間で共有される原因は、変数宣言の仕方にあります。
/* 変数宣言 */
int data;
/* 変数宣言 */
int data;
この変数宣言では変数の “初期化” を行なっていません。ここが変数が extern
宣言したものとして扱われることがある原因です。
変数宣言時に “初期化が行われていないグローバル変数” は、下記のように扱われるようです。
- その変数の実体が “まだ作成されていない” 場合:
- 変数の実体が作成される
- その変数の実体が “すでに作成されている” 場合:
extern
宣言をした場合と同様の動作- 変数の実体は作成されず、すでに作成された変数の実体への参照のみ(変数の共有のみ)行われる
なので、source1.c
と source2.c
のどちらかの変数宣言では変数の実体が作成されるのですが、他方では変数の実体は作成されず、extern
宣言と同様に扱われることになります。
したがって、変数宣言時に “初期化が行われていないグローバル変数” は、意図しない extern
宣言が行われ、意図せずにファイル間で共有される可能性があるので注意が必要です。
対処法
このような現象は、”グローバル変数の変数宣言時に必ず初期化する” ようにすることで防ぐことが可能です(そもそもどんな変数でも初期化しておいた方が無難です)。
/* 変数宣言 */
int data = 100;
変数宣言時に初期化されているグローバル変数は、プログラム起動時に “必ず” 実体が作成されます。もし、同じ名前のグローバル変数を別のファイルで変数宣言した場合は、同じ名前の変数の実体が複数作成されることになります。
ですが、同じ名前の変数の実体が複数作成された場合、リンク時にエラーが発生するようになっています。
なので、グローバル変数の変数宣言時に初期化を行うようにしておけば、意図しない変数の共有が行われてしまう可能性がある場合には、そのことを未然にリンクエラーで検知することができるようになります。
また、そもそも他のファイルから extern
宣言されたくないようなグローバル変数については積極的に static
を付加するようにしましょう。これにより、他のファイルから extern
宣言できないようにすることができます。
extern
宣言時には初期化しない
また、extern
宣言時には初期化は行わない方が良いです。
extern int g_data = 100;
もしかしたら使用するコンパイラ等の環境によって異なるかもしれませんが、extern
宣言時に初期化を行った場合、どうも extern
指定子が無視されるようでした。
つまり、初期化ありのグローバル変数の変数宣言として扱われます。そしてこの場合、extern を付けなくても extern 宣言として扱われることもありで解説した通り、必ずその変数の実体が作成されるようになります。
ですので、通常の変数宣言と extern
宣言との両方で同じ名前の変数の実体が作成されることになり、リンク時にエラーが発生してしまいます。
このような意図しない動作が行われないようにするため、extern
宣言時には初期化は行わない方が良いです。
グローバル変数同様のデメリットあり
ここまで解説してきたように、extern
はグローバル変数が使用できる範囲を広げる宣言になります。
なので、extern
宣言によるメリット・デメリットもグローバル変数同様であると考えて良いです。
ご存知の通り、グローバル変数はどの関数からも使用できるので非常に便利です。
ただしその一方で、変数が使用できる箇所が増えるため、プログラム実行時にその変数に格納されている値が不正な場合は、その変数がどこで・どのタイミングで変更されたのが原因で不正になっているのかを追うのが難しくなる等のデメリットもあります。
extern
宣言を行うと、このグローバル変数が使用できる箇所がさらに増えることになるため、より原因を追うのが大変になります。
ですので、extern
宣言はむやみに行わず、本当に必要なときにだけ使用するようにした方が良いです。
例えば、下記のような source1.c
と source2.c
であれば、グローバル変数 data
を両方のファイルから直接変更しています。
#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;
}
/* 変数宣言 */
int data = 0;
void calc(void) {
data = data * 1.1;
}
void set(int num) {
data = num;
}
int get(void) {
return data;
}
ですが、この場合であれば別に source1.c
で data
を変更しなくても、下記のように source2.c
から data
を変更するための関数を提供してやればやりたいことは達成できます。
#include <stdio.h>
/* extern宣言 */
extern int data;
void calc(void);
int main(void) {
/* 直接dataを変更 */
data = 100;
calc();
/* 直接dataを参照 */
printf("%d\n", data);
return 0;
}
#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/
[…] ※【C言語】extern宣言について解説(ファイル間で変数を共有) […]