閉じる

【C言語】バグの元になりやすいグローバル変数を安全に使うための3つのコツ

C言語のプログラムを書いていると、どこからでも使えるグローバル変数はとても便利に感じます。

しかし、便利さの裏にはバグの温床になる危険も潜んでいます。

本記事では、C言語初心者の方に向けて、グローバル変数の基本から、メリット・デメリット、安全に使うための具体的な3つのコツまで、サンプルコードを交えながら丁寧に解説します。

C言語のグローバル変数とは

グローバル変数とローカル変数の違い

C言語では、変数は大きくグローバル変数ローカル変数に分けられます。

ここでは両者の違いを、初心者の方にも分かりやすいように整理します。

グローバル変数とは

グローバル変数は、関数の外で宣言された変数です。

プログラム全体(同じソースファイル、もしくは適切に宣言された他のファイル)からアクセスできます。

C言語
#include <stdio.h>

// グローバル変数の定義(どの関数からも使える)
int g_count = 0;

void increment(void) {
    // グローバル変数を更新
    g_count++;
}

int main(void) {
    increment();
    increment();
    printf("g_count = %d\n", g_count);  // 2 と表示される
    return 0;
}
実行結果
g_count = 2

このように、グローバル変数は複数の関数で共有したい情報を保持するのに使われます。

ローカル変数とは

一方でローカル変数は、関数の中やブロックの中(中括弧で囲まれた範囲)で宣言される変数です。

そのブロックの外からはアクセスできません。

C言語
#include <stdio.h>

void func(void) {
    int local_value = 10;   // ローカル変数
    printf("local_value = %d\n", local_value);
}

int main(void) {
    func();
    // printf("%d\n", local_value); // エラー: local_value はここから見えない
    return 0;
}

ローカル変数は、その関数だけで使う一時的な値を扱うのに向いています。

比較表

グローバル変数とローカル変数の違いを表で整理します。

種類宣言場所有効範囲(スコープ)生存期間
グローバル変数関数の外ファイル全体(+externで他ファイル)プログラム開始~終了まで
ローカル変数関数内やブロック内そのブロックの中だけブロック開始~終了まで

この表から分かるように、グローバル変数は見える範囲も生きている時間も長いという特徴があります。

これが便利さと危険さの両方の原因になります。

宣言場所と有効範囲(スコープ)の考え方

変数のスコープ(scope)とは、「その変数が見える範囲」のことです。

C言語では、スコープは主に宣言された場所によって決まります。

ファイルスコープ(グローバル変数)

グローバル変数は、ファイルスコープを持ちます。

ソースファイルの先頭や、いずれかの関数の外で宣言された変数は、そのファイルの中ならどこからでもアクセスできます。

C言語
#include <stdio.h>

int g_value = 100;  // ファイルスコープ(グローバル変数)

void print_value(void) {
    printf("g_value = %d\n", g_value);  // 参照できる
}

int main(void) {
    g_value = 200;                      // 更新できる
    print_value();
    return 0;
}
実行結果
g_value = 200

ブロックスコープ(ローカル変数)

ローカル変数はブロックスコープを持ちます。

{ }で囲まれたブロックの中だけ有効です。

C言語
#include <stdio.h>

int main(void) {
    int x = 10;  // ここから main 関数の終わりまで有効

    if (x > 0) {
        int y = 20;  // if 文の中でだけ有効
        printf("x = %d, y = %d\n", x, y);
    }

    // printf("%d\n", y); // エラー: y は if ブロックの外から見えない
    return 0;
}

スコープを意識して変数を宣言することは、グローバル変数を安全に使う上で非常に重要です。

後ほどの「3つのコツ」の中でも、この考え方が何度も登場します。

メモリ上の扱い(静的記憶域期間)の概要

C言語では、変数の「どこに置かれるか」「いつ作られて、いつ消えるか」を記憶域期間(storage duration)と呼びます。

グローバル変数は静的記憶域期間(static storage duration)を持ちます。

これは次のような意味があります。

  • プログラムの開始時にメモリ上に確保される
  • プログラムの終了時まで残り続ける
  • 初期値を指定しなかった場合、自動的に0で初期化される
C言語
#include <stdio.h>

int g_a;          // 0 で自動初期化される
int g_b = 5;      // 5 で初期化

void show(void) {
    static int s_counter;  // これも静的記憶域期間を持つ
    s_counter++;
    printf("g_a=%d, g_b=%d, s_counter=%d\n", g_a, g_b, s_counter);
}

int main(void) {
    show();
    show();
    return 0;
}
実行結果
g_a=0, g_b=5, s_counter=1
g_a=0, g_b=5, s_counter=2

この例では、関数内のstatic int s_counter;も静的記憶域期間を持ち、呼び出しのたびに値が保持されます。

グローバル変数も static 変数も、メモリ上では似たような扱いを受けます。

一方、ローカル変数は通常自動記憶域期間を持ち、関数の呼び出しごとに作られ、関数から戻ると破棄されます。

C言語のグローバル変数のメリット・デメリット

メリット

グローバル変数には、使い方を間違えなければ便利な側面もあります。

複数の関数から共有しやすい

複数の関数で同じ情報を共有したい場合、グローバル変数を使うと、引数で何度も渡す必要がありません。

C言語
#include <stdio.h>

int g_sum = 0;  // 合計値をためるグローバル変数

void add(int x) {
    g_sum += x;  // どの関数からでも g_sum を更新できる
}

int main(void) {
    add(10);
    add(20);
    printf("合計 = %d\n", g_sum);
    return 0;
}
実行結果
合計 = 30

初心者にとって直感的に理解しやすい

プログラム全体で1つだけ持ちたい情報(設定値や状態など)を表現したい場合に、「どこからでも見える変数が1つある」という考え方は分かりやすく、最初のうちは実装も簡単です。

メモリ割り当てを意識しなくてよい

グローバル変数は静的に確保されるため、malloc/free などの動的メモリ確保を使わずに済みます。

初心者のうちは、メモリリークや解放忘れの心配がない点はメリットと言えます。

デメリット

一方、グローバル変数には多くの落とし穴があります。

ここが本記事の重要なポイントです。

どこからでも書き換えられるため、バグの原因になりやすい

グローバル変数を使う最大のデメリットは、プログラムのどこからでも値を書き換えられてしまうことです。

これにより、次のような問題が起きます。

  • 値がいつ、どこで変わったのか追いにくい
  • 思わぬ関数が値を変更してしまい、バグの原因になる
  • 関数同士がグローバル変数を通じて強く結びつき、再利用しにくい
C言語
#include <stdio.h>

int g_mode = 0;  // 0: 通常モード, 1: デバッグモード

void set_mode_debug(void) {
    g_mode = 1;
}

void print_message(void) {
    if (g_mode == 1) {
        printf("Debug Message\n");
    } else {
        printf("Normal Message\n");
    }
}

int main(void) {
    print_message();      // Normal Message

    set_mode_debug();     // ここでモードが変わる

    print_message();      // Debug Message
    return 0;
}
実行結果
Normal Message
Debug Message

この程度ならまだ追えますが、関数が多くなりファイルも増えると、どこで g_mode が変わったのか分からなくなることがよくあります。

関数の「入力」と「出力」が分かりにくくなる

本来、関数は引数を入力として受け取り、戻り値を出力として返す形が望ましいです。

しかし、グローバル変数を多用すると、関数がグローバル変数を勝手に読んだり書き換えたりするようになり、次のような問題が起きます。

  • 関数の仕様が分かりにくい
  • テストしにくい(グローバル変数の状態を毎回整える必要がある)
  • 関数の再利用性が下がる(他のプログラムにコピペしても、そのまま動かない)

名前の衝突(バグ)が起きやすい

グローバル変数は一度宣言すればどこからでも見えるため、同じ名前を別の意味で使ってしまう危険があります。

特に大きなプロジェクトでは、名前の管理が難しくなります。

いつグローバル変数を使うべきかの判断基準

デメリットが多いとはいえ、グローバル変数を完全に禁止するのは現実的ではありません。

重要なのは「いつ使ってよいか」を自分なりの基準で判断することです。

初心者の方が基準にしやすい考え方をいくつか挙げます。

  • プログラム全体で1つしか存在しない情報かどうか
    例: アプリケーション全体の設定、ビルド時に決まる定数など。
  • 値を変更する必要があるか、読み取り専用でよいか
    読み取り専用ならconstを付ける、といった工夫で安全性を高められます。
  • グローバル変数を使わずに、引数や戻り値で表現できないか
    後述する「3つのコツ」の中で詳しく説明しますが、まずはローカル変数で実装してみて、本当に必要な場面だけグローバル変数にすると良いです。

グローバル変数の基本的な使い方とルール

ここからは、C言語でのグローバル変数の正しい書き方や、複数ファイルで扱う場合のルールを整理します。

グローバル変数の宣言と定義の書き方

C言語では「定義(definition)」と「宣言(declaration)」を区別します。

グローバル変数ではこの違いを理解しておくことがとても重要です。

定義とは

変数を実体付きで1箇所だけ作ることを「定義」と呼びます。

メモリ上に領域が確保されます。

C言語
// sample.c
int g_value = 10;  // これは定義(実体を持つ)

宣言とは

「ここにこういう変数がありますよ」とコンパイラに知らせるだけのものを「宣言」と呼びます。

実体は別の場所にあります。

C言語
// sample.c のどこか別の場所、または別ファイル
extern int g_value;  // これは宣言(実体は他にある)

初心者のうちは、同じ変数を複数のファイルで「定義」してしまうミスをよくします。

このときはリンクエラーが発生します。

複数ファイルで共有する場合のexternの使い方

C言語のプログラムは、通常複数の.cファイルに分かれます。

グローバル変数を別ファイルから使いたい場合は、1つのファイルでだけ「定義」し、他のファイルからはexternで「宣言」します。

例: 2つのソースファイルでグローバル変数を共有する

config.cmain.c に分けた例を示します。

C言語
/* config.c - 設定値を定義するファイル */
#include <stdio.h>

// グローバル変数の「定義」(実体はここだけ)
int g_config_mode = 1;

void print_config(void) {
    printf("g_config_mode = %d\n", g_config_mode);
}
C言語
/* main.c - メイン処理のファイル */
#include <stdio.h>

// 別ファイルにある g_config_mode を「宣言」する
extern int g_config_mode;

// 別ファイルの関数もプロトタイプ宣言しておく
void print_config(void);

int main(void) {
    print_config();  // 初期値を表示

    // ここでグローバル変数を変更する
    g_config_mode = 2;

    print_config();  // 変更後の値を表示
    return 0;
}

コンパイルとリンクは、環境にもよりますが次のように行います。

Shell
gcc main.c config.c -o sample
実行結果
g_config_mode = 1
g_config_mode = 2

このように、「定義は1箇所」「他のファイルからは extern で宣言」というルールを守ることが大切です。

main関数以外から値を参照・更新する方法

グローバル変数は同じファイル内であれば extern なしで直接使えるため、main関数以外からの参照や更新も容易です。

C言語
#include <stdio.h>

int g_counter = 0;  // グローバル変数

void increment(void) {
    g_counter++;  // 更新
}

void print_counter(void) {
    printf("g_counter = %d\n", g_counter);  // 参照
}

int main(void) {
    increment();
    increment();
    print_counter();
    return 0;
}
実行結果
g_counter = 2

ただし、このようにどこからでも更新できると、プログラムが大きくなったときに制御が難しくなるという問題があります。

後ほど説明する「変更箇所を限定する」テクニックにつながる話です。

constを使った読み取り専用のグローバル変数

グローバル変数を誤って書き換えてしまうバグを防ぐために役立つのがconst修飾子です。

constを付けることで、その変数を読み取り専用として扱えます。

C言語
#include <stdio.h>

// 読み取り専用のグローバル変数
const int g_max_value = 100;

int main(void) {
    printf("max = %d\n", g_max_value);

    // g_max_value = 200; // エラー: const なので代入できない
    return 0;
}
実行結果
max = 100

プログラム全体で共有する定数には#defineではなくconstを使うと、型がはっきりし、デバッガで値も確認しやすくなります。

ヘッダファイルに書いてよいこと・書いてはいけないこと

C言語では、ヘッダファイル(.h)を使って宣言を共有します。

ここでグローバル変数をヘッダにどう書くかは、とても重要です。

ヘッダに書いてよいもの

  • 関数のプロトタイプ宣言
  • 構造体、列挙体、共用体の定義
  • extern付きのグローバル変数の宣言
  • #definetypedef など
C言語
/* config.h */
#ifndef CONFIG_H
#define CONFIG_H

// 関数のプロトタイプ宣言
void print_config(void);

// グローバル変数の「宣言」(extern を付ける)
extern int g_config_mode;

#endif  // CONFIG_H

ヘッダに書いてはいけないもの

グローバル変数の「定義」をヘッダに書いてはいけません

ヘッダは複数の.cファイルからインクルードされるため、定義を書くと「同じ変数が複数回定義された」ことになり、リンクエラーになります。

C言語
/* NG例: bad_config.h */
int g_config_mode = 1;  // ← ヘッダに定義を書くのはダメ

このヘッダを2つのファイルからインクルードすると、g_config_mode が2回定義されたことになり、ビルドに失敗します。

正しい分割は次のようになります。

C言語
/* config.h */
#ifndef CONFIG_H
#define CONFIG_H

extern int g_config_mode;
void print_config(void);

#endif
C言語
/* config.c */
#include <stdio.h>
#include "config.h"

// 実体の「定義」はここだけ
int g_config_mode = 1;

void print_config(void) {
    printf("g_config_mode = %d\n", g_config_mode);
}
C言語
/* main.c */
#include "config.h"

int main(void) {
    print_config();
    g_config_mode = 2;
    print_config();
    return 0;
}

このように、ヘッダファイルには extern 宣言だけを書くのが安全なルールです。

グローバル変数を安全に使うための3つのコツ

ここからが本記事のメインテーマです。

バグの元になりやすいグローバル変数を、できるだけ安全に使うための3つのコツを紹介します。

1. なるべくローカル変数・引数で済ませる

最初のコツは「そもそもグローバル変数にしない」ことです。

多くの場合、ローカル変数と関数の引数、戻り値を上手く使えば、グローバル変数は不要になります。

グローバル変数を使った例

C言語
#include <stdio.h>

int g_a;
int g_b;

void input_values(void) {
    scanf("%d%d", &g_a, &g_b);
}

int calc_sum(void) {
    return g_a + g_b;
}

int main(void) {
    input_values();
    int sum = calc_sum();
    printf("sum = %d\n", sum);
    return 0;
}

この例では、入力値g_ag_bをグローバル変数で共有していますが、これはグローバル変数を使う必要がありません。

ローカル変数と引数で書き直した例

C言語
#include <stdio.h>

// a と b を受け取り、和を返す
int calc_sum(int a, int b) {
    return a + b;
}

int main(void) {
    int a, b;

    // ローカル変数として値を持つ
    scanf("%d%d", &a, &b);

    int sum = calc_sum(a, b);
    printf("sum = %d\n", sum);
    return 0;
}

このように書き直すことで、関数 calc_sum は「2つの整数を受け取って、その和を返す」純粋な関数になります。

グローバル変数に依存していないため、テストや再利用がしやすくなります。

「まずはローカル変数と引数で書けないか考える」ことが、グローバル変数乱用を防ぐ第一歩です。

2. グローバル変数は用途ごとに最小限にする

どうしてもグローバル変数が必要な場合でも、数を最小限に抑えることが重要です。

関連するデータを構造体にまとめる

グローバル変数が増えやすい典型的な例として、「状態を表す変数」がバラバラに定義されているパターンがあります。

C言語
// NG例: ばらばらなグローバル変数
int g_player_x;
int g_player_y;
int g_player_hp;
int g_player_mp;

このような場合は、構造体にまとめて1つのグローバル変数にすると、管理しやすくなります。

C言語
#include <stdio.h>

// プレイヤーの状態をまとめた構造体
typedef struct {
    int x;
    int y;
    int hp;
    int mp;
} PlayerState;

// 構造体1つだけをグローバルにする
PlayerState g_player;

void init_player(void) {
    g_player.x = 0;
    g_player.y = 0;
    g_player.hp = 100;
    g_player.mp = 50;
}

void print_player(void) {
    printf("pos=(%d,%d) hp=%d mp=%d\n",
           g_player.x, g_player.y, g_player.hp, g_player.mp);
}

int main(void) {
    init_player();
    print_player();
    return 0;
}
実行結果
pos=(0,0) hp=100 mp=50

このように、意味的にまとまった情報は1つの構造体にまとめることで、グローバル変数の数を減らしつつ、コードの意図も分かりやすくなります。

3. 変更箇所を限定する

グローバル変数が問題を起こすのは、どこからでも自由に値を変更できてしまうからです。

そこで、設計として「書き換えられる場所」を意図的に限定することが重要になります。

アクセス用の関数を用意する

グローバル変数を直接書き換えるのではなく、専用の「getter」「setter」関数を通してアクセスするようにすると、制御しやすくなります。

C言語
#include <stdio.h>

// 外部に公開したくないので static を付ける
static int g_score = 0;

// スコアを取得する関数
int get_score(void) {
    return g_score;
}

// スコアを加算する関数
void add_score(int value) {
    if (value > 0) {  // 不正な値をチェックできる
        g_score += value;
    }
}

int main(void) {
    add_score(10);
    add_score(5);
    printf("score = %d\n", get_score());
    return 0;
}
実行結果
score = 15

この例では、g_scorestaticを付けてファイル内限定のグローバル変数にし、外部からは関数を通じてしか変更できないようにしています。

static を使ってファイル内に閉じ込める

staticをグローバル変数の前に付けると、その変数は同じファイル内からしか見えなくなります

これを内部結合(internal linkage)と呼びます。

C言語
// score.c
static int g_score = 0;  // このファイルの中からしか見えない

void add_score(int value) {
    g_score += value;
}
C言語
// main.c
#include <stdio.h>

void add_score(int value);  // 関数だけ宣言

int main(void) {
    add_score(10);

    // extern int g_score; // こう書いても見えない(リンクエラー)
    return 0;
}

このように、グローバル変数に static を付けることで、影響範囲を1ファイルに限定できます。

影響範囲を小さくするほど、バグは入れにくくなります。

名前の付け方とコメントで役割を明確にする

グローバル変数はプログラム全体に影響するため、名前の付け方とコメントの書き方がとても重要です。

名前の付け方のポイント

  • 用途がすぐに分かる名前にする
    例:g_mode より g_debug_modeg_config_mode など。
  • グローバル変数であることが分かる接頭辞を付ける
    プロジェクトによっては、g_g_ではなくg_以外の命名規則を使うこともあります。
    例:g_player_stateg_app_config など。
  • 単なる略語だけの名前は避ける
    g_ps のような略語は、後から見て意味が分かりにくくなります。

コメントで「誰がどう使うか」を書く

グローバル変数には、「誰が読み」「誰が書き換えるのか」をコメントで明示すると、後からコードを読む人(将来の自分も含めて)が理解しやすくなります。

C言語
// アプリケーション全体の設定を保持する
// 読み取り: 全モジュール
// 更新: config.c のみ
AppConfig g_app_config;

このようにルールをコメントで残しておくと、「勝手に書き換えてはいけない場所」を明確にできます

グローバル変数を減らす設計の考え方

最後に、もう一歩踏み込んでグローバル変数を減らすための設計の考え方を紹介します。

初心者の方でも、少しずつ意識していくことで、より堅牢なプログラムを書けるようになります。

「状態」と「処理」をセットで考える

グローバル変数は多くの場合、プログラムの「状態」を表します。

状態だけをばらまくのではなく、その状態を扱う処理(関数)とセットで考えると、安全な構造になります。

例として、ゲームのスコア管理を考えてみます。

  • 悪い例
    • int g_score; をグローバルにし、どこからでもg_score += 10;できてしまう
  • よい例
    • static int g_score; を score.c に閉じ込める
    • void add_score(int value);
    • int get_score(void);
      のような関数を通じてしかスコアを操作できないようにする

このように、「状態を持つモジュール」を意識して、状態と処理を1つのファイル(モジュール)にまとめると、自然とグローバル変数も整理されていきます。

コンテキスト構造体を使う

もう少し大きなプログラムでは、「コンテキスト(context)」や「環境(environment)」と呼ばれる構造体に、必要な状態をまとめて渡す設計がよく使われます。

C言語
typedef struct {
    int width;
    int height;
    int bg_color;
} RenderContext;

// 描画に必要な情報は context にまとめて渡す
void render_screen(const RenderContext *ctx);

このようにすると、関数はグローバル変数に依存せず、引数として渡された context だけを参照するようになります。

結果として、グローバル変数自体が減っていく効果があります。

まとめ

グローバル変数は、C言語初心者にとって分かりやすく便利な道具である一方で、バグの原因になりやすい両刃の剣です。

本記事では、グローバル変数のスコープや記憶域期間、extern や const の使い方、ヘッダファイルの正しい書き方を解説し、さらに「なるべくローカル変数・引数で済ませる」「用途ごとに最小限にする」「変更箇所を限定する」という3つのコツを紹介しました。

これらを意識することで、グローバル変数を完全に避けられない場面でも、より安全で読みやすいCプログラムを書くことができるようになります。

この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!