こんにちは、ナナです。
「extern宣言」というものを見たこと・聞いたことはあるけど、その正体がいったい何なのかを理解していない方って結構います。
C言語において「extern宣言」は複数ファイルによるシステム構成において、グローバル変数を共有するための仕組みです。
本記事では次の疑問点を解消する内容となっています。
では、「extern宣言」とは何かを学んでいきましょう。
extern宣言を知る前にグローバル変数を考察しよう!
「extern宣言」はグローバル変数と強い関連性を持った機能です。そのため、まずは「グローバル変数」について理解しましょう。
「グローバル変数」と「ローカル変数」の定義方法の違い
C言語の変数には大きく分けて「グローバル変数」と「ローカル変数」の2つが存在します。
#include <stdio.h>
int gNumber = 100; // グローバル変数
void func(void)
{
gNumber += 100;
}
int main(void)
{
int tmp = 100; // ローカル変数
func();
printf("tmp : %d\n", tmp);
printf("gNumber : %d\n", gNumber);
return 0;
}
tmp : 100
gNumber : 200
このように変数を定義する場所によって、「グローバル変数」と「ローカル変数」が変化します。
グローバル変数とは広域参照可能な変数
それぞれの変数の特徴は次のものです。
変数の種類 | 特徴 |
---|---|
ローカル変数 | 定義した関数内でしかアクセスできない局所的な変数。 |
グローバル変数 | 関数に所属せず、どの関数からもアクセスできる広域的な変数。 |
「グローバル」と「ローカル」の名が表す通り、この違いは参照可能な範囲に違いがあります。
関数に所属せず、束縛されない自由な変数が「グローバル変数」なんです。
グローバル変数はファイルを跨いで参照可能な変数
広域的な「グローバル」である変数とは、いったいどこまで広域なのでしょうか?
次のように複数のソースファイルで構成されるシステムがあったとしましょう。
#include <stdio.h>
int main(void)
{
func();
printf("gNumber : %d\n", gNumber);
return 0;
}
int gNumber = 100; // グローバル変数
void func(void)
{
gNumber += 100;
}
C言語におけるこのようなシステムは、主に3つの参照範囲(スコープ)で構成されることになります。
No | スコープ範囲 | 参照範囲 |
---|---|---|
① | 関数スコープ | 関数の中でのみ有効な参照範囲 |
② | ファイルスコープ | ファイルの中でのみ有効な参照範囲 |
③ | システムスコープ | システム全体で有効な参照範囲 |
グローバル変数とは、③のシステムスコープを標準で備えている変数です。つまり、異なるファイルからでも参照することが可能です。
「extern宣言」とは、この③の「システムスコープ」に関するグローバル変数の参照を可能にするための機能なのです。
②の「ファイルスコープ」の変数に関しては『C言語 staticを変数と関数に付ける価値【保護の仕組みを解説】』で解説しています。
興味のある方は是非参照ください。
extern宣言の必要性を知る
それではそろそろ本題の「extern宣言」の話に入っていきましょう。
他のファイルから参照できないグローバル変数
先ほど紹介した次のプログラムは、実はビルドエラーが発生します。
#include <stdio.h>
int main(void)
{
func();
printf("gNumber : %d\n", gNumber);
return 0;
}
int gNumber = 100; // グローバル変数
void func(void)
{
gNumber += 100;
}
main.c側において次のように「定義されていない識別子」というビルドエラー(正確にはコンパイルエラー)が発生します。
main.c(6): error C2065: 'gNumber': 定義されていない識別子です。
コンパイラは「main.c」を翻訳する際に、「gNumber」というシンボルが何者なのかを把握しておく必要があります。
しかし、コンパイラは唐突に登場した「gNumber」シンボルに対して「定義されていません!」とエラーを発行したのです。
別ファイルである「sub.c」に定義された「gNumber」をコンパイラは知ることができないのです。
コンパイラについて詳しく知りたい方は『C言語 コンパイラの役割【エラーの取り除き方の鉄則教えます】』の記事を見ておきましょう。
コンパイラやリンカに関する知識は、プログラミングと関係ないように思うかもしれませんが、すごく大事な知識なんです。「コンパイラ」や「リンカ」を知ることは、C言語の仕組みを知ることなんですよ。
extern宣言:他ファイルからのアクセスを可能とするための宣言
「extern」とは「外部」という意味です。つまり、変数に対する「extern宣言」とは
外部のファイルに変数が定義されていますよ宣言
なのです。
extern宣言は次のように書きます。
書き方
extern データ型 変数名;
プログラム例
extern int gNumber;
この時に注意しなければならないのは、決して初期化してはならないことです。つまり、次のようなプログラムは書いてはいけません。ビルドエラーが発生します。
extern int gNumber = 50; // 初期化を伴うextern宣言はNG
「extern」とは宣言なのです。初期化とは「定義」に対して行うものであり、実体を持たない宣言に対しては実施できません。
extern宣言を使ったプログラム例
具体的にプログラムで示しましょう。先ほどのプログラムは次のように変更することでビルドが通るようになります。
#include <stdio.h>
extern int gNumber; // extern宣言
int main(void)
{
func();
printf("gNumber : %d\n", gNumber);
return 0;
}
int gNumber = 100; // グローバル変数
void func(void)
{
gNumber += 100;
}
main.cの中に「extern」を付与した変数定義を行いました。これにより、コンパイラに対して「別のファイルで定義された変数です」と伝えることができます。
「extern」を付けた変数定義は「定義」ではなく「宣言」という扱いとなり、変数がメモリに実体化されません。
「定義」とは実体が生成されるもの、「宣言」とは実体が生成されないものです。
extern宣言の実践的な使用例
extern宣言を実践的に使う場合の書き方について触れておきましょう。
extern宣言はヘッダファイルに定義
externを伴う「グローバル変数の外部参照宣言」と「グローバル変数の定義」はソースファイルとヘッダファイルに分けて定義します。
記述項目 | 定義場所 |
---|---|
グローバル変数の「extern宣言」 | ヘッダファイルに記述 |
グローバル変数の定義 | ソースファイルに記述 |
このあたりのルールに関して把握していない方は『C言語 ファイル分割の考え方【何を基準に分けるのかを解説】』を見ておくとよいでしょう。
つまり、次のような構成としてプログラムします。
#include <stdio.h>
#include "sub.h"
int main(void)
{
func();
printf("gNumber : %d\n", gNumber);
return 0;
}
#ifndef SUB_H
#define SUB_H
extern int gNumber; // extern宣言
#endif
int gNumber = 100; // グローバル変数
void func(void)
{
gNumber += 100;
}
sub.cの管轄となっている「gNumber」グローバル変数は、sub.hに「extern宣言」を行うのが正しい構成です。
ヘッダファイルを経由して参照したい側のファイルは「extern宣言」を取り込むのです。
このような部品をファイルのどこに配置するかは暗黙のルールで決まっています。しっかりと覚えておきましょう!
extern宣言が不要なシステム設計を目指せ!
ここまでさんざん「extern宣言」を解説しておいてはなんですが、20年近く開発した経歴の中でわたしは「extern宣言」を実際の開発の中で使用したことがほとんどありません。
グローバル変数をファイル間で共有する設計は危険
変数とは「データ」を管理するための箱であり、「データ」とはプログラムの世界において厳重に保護されるべき対象なのです。
グローバル変数にするということは、無防備にデータへのアクセスを許すということになります。
意図するか意図せずかは関係なく、誰でも変数の情報を読み書きできる状態となるということです。それはとても危険なことなのです。
大規模開発では、様々な人たちが各担当のソースファイルを管理しています。最終的にはそれらが1つのシステムとして結合されて動くのです。
皆さんが管理するグローバル変数を、どこかの誰かが誤って書き換えてしまうかもしれませんよ!
カプセル化によるグローバル変数への直接アクセスの禁止の設計
グローバル変数をファイル間で共有することが良くないとするのであれば、いったいどのように共有すればよいのでしょう。
オブジェクト指向言語に触れたことがある方は「カプセル化」という概念を聞いたことがあるかもしれません。
C言語はオブジェクト指向言語ではありませんが、疑似的に「カプセル化」を作り出すことができます。
例えば「gNumber変数」には0~500の値しか設定できない情報だったとしましょう。「カプセル化」を行ったプログラムは次のようになります。
#include <stdio.h>
int main(void)
{
int num;
num = getNumber();
printf("%d\n", num);
setNumber(4649);
printf("%d\n", num);
return 0;
}
// staticなグローバル変数
static int gNumber = 100;
int getNumber(void)
{
return gNumber;
}
int setNumber(int num)
{
if (num >= 0 && num <= 500)
{
gNumber = num;
return 0;
}
return -1;
}
グローバル変数にstatic修飾子を付けることにより、「main.c」からはグローバル変数「gNumber」に直接アクセスできないようにしています。
※static修飾子を知らない方は『C言語 staticを変数と関数に付ける価値【保護の仕組みを解説】』参照。
その代わりに「関数」という代理人を経由してアクセスできる形になっています。
setNumber関数では引数の値をチェックし、問題なければgNumber変数へ設定しています。不正な引数であれば設定できないようにしています。
このようにデータを保護する仕組みを関数として定義し、関数を外部へ公開するのです。
システムをモジュールという形でうまく分割すると「extern宣言」という機能は使うことはほとんどなくなるはずです。わたし自身、「extern宣言」が絶対必要というシーンに出会ったことがありませんから。
本格的なモジュール分割のやり方について知りたい方は『マイコン入門編』を体験するとよいでしょう。
関数に対して指定する「extern宣言」とは
ここまではグローバル変数に対しての「extern宣言」について語ってきました。
実は「extern宣言」は関数に対しても宣言を行うことが可能です。
#include <stdio.h>
#include "sub.h"
int main(void)
{
func();
printf("gNumber : %d\n", gNumber);
return 0;
}
#ifndef SUB_H
#define SUB_H
extern int gNumber; // グローバル変数のextern宣言
extern void func(void); // 関数のextern宣言
#endif
int gNumber = 100; // グローバル変数
void func(void)
{
gNumber += 100;
}
これを見てパッと気づく方はよくC言語を勉強されている方ですね。
extern void func(void); // 関数のextern宣言
これは関数の「プロトタイプ宣言」と呼ばれるものです。「関数のプロトタイプ宣言にextern宣言なんて付けたことないよ~」という方も大勢いることでしょう。
そうなんです。関数のプロトタイプ宣言の「extern宣言」は省略することが可能であり、実践的には「extern宣言」をしている方はほとんどいないのです。
// プロトタイプ宣言の2つの書き方(どっちも同じ意味)
extern void func(void);
void func(void);
プロトタイプ宣言について詳しく知りたい方は『C言語 プロトタイプ宣言の効果【関数を安全に呼び出す仕組み】』を見ておくとよいでしょう。
わたしもプロトタイプ宣言には「extern宣言」を付けることはありません。付ける手間が増えるだけです。
extern宣言のまとめ
「extern宣言」を使うことでファイルを跨いだグローバル変数の共有は可能です。
しかし、ファイルを跨いで共有したい状況を生み出している考え方自体を改善する力を蓄えましょう。