C言語基礎|ファイル有効範囲とextern

関数の外に置くと、みんなで共有できる

前回は ブロック有効範囲({ } の中だけ名前が通用)でしたね。
今回はスケールを一段上げて、関数の外に置いた名前がどう扱われるかを見ていきます。

C言語では、関数の外に変数や配列を置くと、その名前は ソースファイル全体で通用します。
これが ファイル有効範囲(file scope)
さらに、別の場所で作られた変数を「使います」と宣言するのが extern です。

サンプルプログラム

1週間(7日)の歩数を入力して最大値を求めるプログラムを例に解説をします。

プロジェクト名:chap6-11-1 ソースファイル名:chap6-11-1.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

#include <stdio.h>

#define DAYS 7

int steps[DAYS];          // 配列の定義(実体を作る)
int max_steps(void);      // 関数の関数原型宣言

int main(void)
{
    puts("1週間の歩数を入力してください。");
    for (int i = 0; i < DAYS; i++) {
        printf("%d日目:", i + 1);
        scanf("%d", &steps[i]);
    }

    printf("最大の歩数は%dです。\n", max_steps());
    return 0;
}

//--- 配列stepsの最大値を返す ---//
int max_steps(void)
{
    int max = steps[0];

    for (int i = 1; i < DAYS; i++) {
        if (steps[i] > max) {
            max = steps[i];
        }
    }
    return max;
}

実行例

1週間の歩数を入力してください。
1日目:5421
2日目:6000
3日目:4320
4日目:7123
5日目:3890
6日目:6500
7日目:7001
最大の歩数は7123です。

このプログラムのポイントは、steps が main の外にあること。
main でも max_steps でも、同じ配列 steps を共有して使えます。

ファイル有効範囲とブロック有効範囲の違い

名前が通用する範囲(イメージ)

識別子宣言場所有効範囲
steps関数の外(ファイル先頭付近)宣言位置〜ファイル終端まで(ファイル有効範囲)
max(関数内変数)max_steps の { } の中宣言位置〜その } まで(ブロック有効範囲)

この表は「同じC言語でも、置く場所で scope が変わる」ことをはっきり見せるための図です。

extern は何をする?(結論:実体は作らず“借りる宣言”)

まず「定義」と「宣言」を分けよう

C言語では、変数について

  • 定義(definition):実体(メモリの置き場)を作る。
  • 宣言(declaration):名前と型を知らせる。

という2つの役割があります。

定義と宣言の違い

書き方役割実体を作る?
int steps[DAYS];定義でもある宣言作る配列の本体が確保される。
extern int steps[];定義ではない宣言作らないどこかにある steps を使うと宣言

つまり extern は「ここで作る」のではなく、
どこか別の場所にある変数を使いますという“紹介状”みたいなものです。

今回の例で extern を入れるとどうなる?

さっきのサンプルは、同じファイル内に steps の定義があるので、main や max_steps の中で extern を書く必要はありません。

ただし、学習として「こう書ける」を見せるならこうなります(※省略可の宣言)。

int main(void)
{
    extern int steps[];   // 省略可:外に定義があるstepsを使う宣言
    /* ... */
}

int max_steps(void)
{
    extern int steps[];   // 省略可
    /* ... */
}

なぜ省略できる?

steps はファイル有効範囲を持つので、同一ファイル内なら
宣言位置より後ろのコードから普通に見えるからです。

「extern が本当に必要になる」場面(ファイル分割)

extern の本領は、ソースファイルが2つ以上に分かれたときです。

ファイルを分けたときの役割

ファイル中身のイメージ
data.cint steps[DAYS]; のように実体を1回だけ作る(定義)
logic.cextern int steps[]; として借りて使う(宣言)

ここで超重要なのが 実体を作る定義は1回だけ というルールです。
あちこちで int steps[DAYS]; を書くと、同名の実体が複数できてリンクエラーになります。

登場する命令の書式と「何をする命令か」

extern の書式

  • extern 型 変数名;
  • extern 型 配列名[];

何をする命令か:
この変数(配列)はここでは作らない。どこかにあるものを使うと宣言します。

グローバル変数(関数の外の変数)の書式

  • 型 変数名;
  • 型 配列名[要素数];

何をする命令か:
ファイル全体で共有できる変数/配列を作る(定義)
宣言位置からファイル終端まで名前が通用します。

関数原型宣言の書式

  • 返却値型 関数名(仮引数型並び);

例:int max_steps(void);

何をする命令か:
この関数はこういう形で後で出てきますとコンパイラに知らせます。
main より後ろに関数定義があるときに特に重要です。

ここまでのまとめ(要点だけギュッと)

ファイル有効範囲とexternのポイント

項目要点
ファイル有効範囲関数の外で宣言した名前は、宣言位置〜ファイル終端まで通用
定義実体(メモリ)を作る宣言。例:int steps[DAYS];
extern 宣言実体を作らず「どこかの実体を使う」と宣言
同一ファイル内外側に定義があれば、関数内で extern は基本いらない
ファイル分割実体は1ファイルで定義、他ファイルは extern で利用