C言語のきほん|externで変数を共有する

extern を使えるようになると、分かれたソースファイル同士をきれいにつなげられる

分割コンパイルを使うと、C言語のプログラムを役割ごとに複数のソースファイルへ分けて書けるようになります。
これはとても便利ですが、そこで新しく出てくるのが「別のソースファイルにある変数をどう使うのか」という問題です。

たとえば、

  • main.c で状態を表示したい
  • counter.c でその状態を更新したい
  • display.c でも同じ変数を参照したい

といった場面では、複数のソースファイルから同じグローバル変数を共有したくなります。

でも、グローバル変数はただ関数の外に書いただけでは、別ファイルから自動的に自由に見えるわけではありません。
そこで使うのが extern です。

extern は、
この場所には変数の実体はないけれど、別のソースファイルに定義された変数を使います
とコンパイラに伝えるための指定子です。

この考え方が分かると、

  • 定義と宣言の違い
  • どこに実体を書くべきか
  • ヘッダファイルに何を書くべきか
  • なぜ extern 宣言では初期化できないのか

といった点がすっきり整理できます。

ここでは、extern を使って複数のソースファイル間で変数を共有する基本を、シンプルな例に置き換えながら、やさしく丁寧に見ていきましょう。

extern とは何か

extern は、別の場所で定義されている変数を参照するための指定子です。

たとえば、

extern int g_count;

と書くと、これは

  • ここで g_count という名前を使う
  • ただし、この場所には実体はない
  • 実体は別のソースファイルのどこかにある

という意味になります。

ここで大事なのは、extern は変数の宣言であって、定義ではないという点です。

定義と宣言の違い

extern を理解するには、まず 定義宣言 の違いをはっきりさせることが大切です。

用語意味
定義実際に変数の実体を用意すること
宣言その名前や型が存在することを知らせること

たとえば、次のコードは定義です。

int g_count = 0;

これは、g_count という変数の実体を本当に作っています。

一方、次のコードは宣言です。

extern int g_count;

これは、g_count という変数が別の場所にあることを知らせているだけです。

つまり、

  • 実体を作るのは 1回だけ
  • 他のファイルでは extern で参照する

というのが基本ルールです。

なぜ extern が必要なのか

分割コンパイルでは、ソースファイルごとにコンパイルが行われます。
そのため、main.c をコンパイルしているとき、counter.c の中身はまだ直接は見えていません。

たとえば、main.c の中で g_count を使っていても、その変数が別ファイルにあるなら、コンパイラは

  • g_count という名前は何か
  • どんな型か

を知る必要があります。

そこで extern 宣言を書いておくと、
「g_count は int 型の変数で、別の場所に実体があります」
と分かるので、安心してコンパイルできます。

基本の構成をシンプルな例で見てみよう

ここでは、score というグローバル変数を複数ファイルで共有する例で見ていきます。

score.c

int score = 0;

score.h

#ifndef SCORE_H
#define SCORE_H

extern int score;

#endif

main.c

#include <stdio.h>
#include "score.h"

int main(void)
{
    score = 10;
    printf("score = %d\n", score);
    return 0;
}

この構成では、

  • score.c で score の実体を定義
  • score.h で extern 宣言
  • main.c でその宣言を読み込んで利用

という流れになっています。

この構成の意味

この3ファイルの役割を表にすると、次のようになります。

ファイル役割
score.cグローバル変数の実体を定義する
score.hextern 宣言を書いて他ファイルから見えるようにする
main.cextern 宣言を読み込んで変数を使う

このように、実体はソースファイルに1つだけ置き、宣言はヘッダファイルに書いて共有する というのが基本の形です。

extern 宣言はヘッダファイルに書くのが一般的

extern 宣言は、複数のソースファイルで共有して使うことが多いので、普通はヘッダファイルに書きます。

たとえば、

extern int score;

を毎回 main.c や display.c や update.c に手で書くこともできますが、それでは管理がしにくくなります。

そこで、score.h のような共通ヘッダに書いて、

#include "score.h"

で読み込む形にしておけば、宣言を1か所で管理できます。

これは関数プロトタイプ宣言をヘッダファイルにまとめるのと同じ考え方です。

extern 宣言では初期化できない

ここは大切な注意点です。

extern 宣言では、基本的に初期化を書いてはいけません。

正しい例はこれです。

extern int score;

一方、次のように書くと、それは宣言ではなく定義に近い意味を持ってしまいます。

extern int score = 0;

学習段階では、extern 宣言には初期化を書かない と覚えておくのが安全です。

なぜなら、extern の目的は「ここには実体はない」と伝えることだからです。
初期化を書くと、実体を作る意味が混ざってしまい、分かりにくくなります。

extern を使うときのルール

extern を使って変数を共有するときは、次のルールを守ることが大切です。

ルール内容
実体の定義は1か所だけ複数回定義すると重複定義エラーの原因になる
他ファイルでは extern で参照する実体を増やさずに使える
extern 宣言はヘッダに書くのが基本複数ファイルで共有しやすい
extern 宣言では初期化しない定義と宣言を混同しないため

図で流れを整理しよう

extern の仕組みは、図で見るとかなり分かりやすくなります。

この図では、score.c に score の実体があり、score.h がその存在を他ファイルへ知らせている様子を表しています。
main.c は score.h を読み込むことで score を使えるようになりますが、実体そのものは score.c にあるままです。

分割コンパイルでの注意点

extern を使うと、複数ファイルで変数を共有できるようになります。
ただし、使い方を間違えるとエラーや混乱の原因になります。

実体を複数書いてしまう

たとえば main.c にも counter.c にも

int score = 0;

と書いてしまうと、同じ名前の変数の実体が複数できてしまいます。
これは基本的に避けるべきです。

extern を書かずに使う

別ファイルの変数を使うのに宣言が見えていないと、コンパイラはその名前を正しく理解できません。
だから、共有したい変数は extern 宣言を見えるようにしておく必要があります。

実践問題

次の仕様に従って、プログラムを複数ファイルに分割してください。

ファイル構成

  • status.h
    マクロ、プロトタイプ宣言、グローバル変数の extern 宣言を書くヘッダファイル
  • main.c
    main 関数を書き、状態を表示する
  • status.c
    increase_status 関数と decrease_status 関数を書く

仕様

  • グローバル変数 level の初期値は 5 とする
  • increase_status は level を 2 増やす
  • decrease_status は level を 1 減らす
  • main 関数から順番に呼び出して結果を表示する

実行結果例

最初の level は 5 です。
2 増加したので 7 です。
1 減少したので 6 です。

解答例

status.h

#ifndef STATUS_H
#define STATUS_H

extern int level;

void increase_status(void);
void decrease_status(void);

#endif

status.c

#include "status.h"

int level = 5;

void increase_status(void)
{
    level += 2;
}

void decrease_status(void)
{
    level -= 1;
}

main.c

#include <stdio.h>
#include "status.h"

int main(void)
{
    printf("最初の level は %d です。\n", level);

    increase_status();
    printf("2 増加したので %d です。\n", level);

    decrease_status();
    printf("1 減少したので %d です。\n", level);

    return 0;
}

解答例の解説

この問題では、level の実体は status.c にあります。

int level = 5;

これが唯一の定義です。

そして、他のファイルで使うために、status.h に

extern int level;

と書いています。

main.c は status.h をインクルードすることで、level を使えるようになっています。
このように、

  • 実体は1か所
  • extern 宣言で共有
  • ヘッダ経由で読み込む

というのが extern の基本形です。

実践問題

テーマは じゃんけんの手の状態を複数ファイルで共有するプログラム を作成します。

以下の仕様を満たすプログラムを、複数のソースファイルに分割して作成してください。

仕様

  • グローバル変数として次の2つを用意する
    int player_hand;
    int cpu_hand;
  • player_hand と cpu_hand は extern を使って共有する
  • player_hand と cpu_hand の値は 0:グー 1:チョキ 2:パー とする
  • display_hands 関数で現在の手を表示する
  • judge_game 関数で勝敗を判定する
  • main 関数で値を設定し、表示と判定を行う

ファイル構成例

  • game.h
  • main.c
  • hands.c
  • judge.c

実行結果例

プレイヤー: グー
コンピュータ: チョキ
結果: プレイヤーの勝ちです。

解答例

game.h

#ifndef GAME_H
#define GAME_H

extern int player_hand;
extern int cpu_hand;

void display_hands(void);
void judge_game(void);

#endif

hands.c

#include <stdio.h>
#include "game.h"

int player_hand = 0;
int cpu_hand = 1;

void display_hands(void)
{
    const char *names[] = {"グー", "チョキ", "パー"};

    printf("プレイヤー: %s\n", names[player_hand]);
    printf("コンピュータ: %s\n", names[cpu_hand]);
}

judge.c

#include <stdio.h>
#include "game.h"

void judge_game(void)
{
    if (player_hand == cpu_hand) {
        printf("結果: あいこです。\n");
    }
    else if ((player_hand == 0 && cpu_hand == 1) ||
             (player_hand == 1 && cpu_hand == 2) ||
             (player_hand == 2 && cpu_hand == 0)) {
        printf("結果: プレイヤーの勝ちです。\n");
    }
    else {
        printf("結果: コンピュータの勝ちです。\n");
    }
}

main.c

#include "game.h"

int main(void)
{
    display_hands();
    judge_game();

    return 0;
}

解答例の解説

この問題では、player_hand と cpu_hand の実体は hands.c にあります。
judge.c と main.c では、それらを extern 宣言経由で共有しています。

これにより、

  • hands.c で状態を持つ
  • judge.c で状態を判定する
  • main.c で全体を実行する

という役割分担ができます。

extern は、こうした「複数ファイルで同じ状態を共有する」場面でとても役立ちます。

13章の確認問題

次の項目について、正しいものには ○、間違っているものには × をつけてください。

①関数プロトタイプ宣言は、関数の返却値型や引数型をコンパイラに知らせるために使う。
➁関数呼び出し側の引数を仮引数、関数定義側の引数を実引数と呼ぶ。
➂返却値がない関数では、返却値型に void を使う。
④値渡しでは、仮引数の値を変更すると実引数の変数も必ず変更される。
➄配列を関数に渡すとき、通常は先頭要素のアドレスが渡される。
➅arr[2] と *(arr + 2) は等価である。
⑦多次元配列を関数に渡すとき、2次元目以降の要素数は省略できる。
⑧ローカル変数とグローバル変数で同じ名前が重複した場合、内側のローカル変数が優先される。
⑨static 付きローカル変数は、関数を抜けても値を保持する。
⑩extern は、別のソースファイルに定義された変数を参照するために使う。
⑪extern 宣言では初期化を書いてもよい。
⑫自作ヘッダファイルは、通常 #include "file.h" のように書く。

解答と解説

① ○
関数プロトタイプ宣言は、関数名、返却値型、引数型をコンパイラに知らせるために使います。

② ×
逆です。呼び出し側は実引数、定義側は仮引数です。

③ ○
返却値がない関数では、返却値型に void を使います。

④ ×
値渡しでは、実引数の値がコピーされて仮引数に渡されるので、仮引数を変えても元の変数は変わりません。

⑤ ○
配列を関数に渡すとき、通常は配列名が先頭要素へのポインタとして扱われます。

⑥ ○
配列アクセス arr[2] は、ポインタ演算を使った *(arr + 2) と等価です。

⑦ ×
多次元配列では、最初の次元は省略できても、2次元目以降の要素数は必要です。

⑧ ○
同じ名前が重なった場合は、より内側のスコープのローカル変数が優先されます。

⑨ ○
static 付きローカル変数は、スコープは関数内ですが、寿命はプログラム終了までなので値を保持します。

⑩ ○
extern は、別の場所にある変数の実体を参照するための宣言です。

⑪ ×
extern 宣言では初期化を書かないのが基本です。実体の定義と混同しないことが大切です。

⑫ ○
自作ヘッダファイルは通常、ダブルクォーテーションで囲んでインクルードします。

extern を学ぶときのコツ

extern を理解するときは、次の3点をセットで考えると整理しやすいです。

ポイント意識したいこと
定義実体を1か所だけ作る
宣言extern で存在を知らせる
共有ヘッダファイル経由で複数ファイルに見せる

特に大切なのは、変数の実体は1つ、宣言は必要な場所で共有する という考え方です。

また、extern は便利ですが、グローバル変数を複数ファイルから自由に触れるようになるぶん、設計が雑だと見通しが悪くなることもあります。
そのため、共有する変数は本当に必要なものだけに絞る意識も大切です。