閉じる

【C言語】グローバル変数の危険性と注意点10選|バグを防ぐ実践ルール

C言語では、グローバル変数はとても便利に見えますが、安易に使うとコードが複雑になり、バグの温床になりやすい存在です。

本記事では、グローバル変数がなぜ危険とされるのかを具体例とともに解説し、実務で役立つ「注意点10選」と安全な使い方のデザインパターンまでを丁寧に説明します。

既存コードの改善にも役立つ内容です。

目次 [ close ]
  1. グローバル変数とは何か
    1. グローバル変数とローカル変数の違い
    2. C言語におけるグローバル変数の寿命とスコープ
    3. ヘッダファイルとextern宣言の基本
  2. C言語のグローバル変数が危険と言われる理由
    1. 可読性低下とバグ発見の困難さ
    2. 依存関係の増加と保守性の低下
    3. マルチスレッドでのデータ競合と未定義動作
    4. 初期化順序問題と予期しない動作
    5. 名前衝突とリンカエラーのリスク
  3. グローバル変数の危険性を減らす実践ルール10選
    1. ルール1: 状態共有を最小限にする
    2. ルール2: constグローバル変数で読み取り専用にする
    3. ルール3: staticで内部リンケージに限定する
    4. ルール4: 構造体にまとめて「疑似名前空間化」する
    5. ルール5: アクセサ関数(getter/setter)経由で操作する
    6. ルール6: 初期化関数(init)と終了処理関数を用意する
    7. ルール7: グローバル変数の命名規則を統一する
    8. ルール8: テストしやすいよう依存を注入(injection)する
    9. ルール9: マルチスレッドではロックやatomicを必ず使う
    10. ルール10: 静的解析ツールでグローバル使用箇所を検出する
  4. グローバル変数を安全に使うデザインパターン
    1. コンフィグ(config)や定数はグローバルで一元管理
    2. ログ出力など「副作用サービス」の扱い方
    3. シングルトン風モジュール設計でスコープ管理
    4. 既存C言語コードのグローバル変数リファクタリング方針
  5. まとめ

グローバル変数とは何か

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

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

両者の違いは、主に定義場所・スコープ(有効範囲)・寿命です。

グローバル変数

グローバル変数は、すべての関数の外側で定義された変数で、同じ翻訳単位(同じ.cファイル)のあらゆる場所から参照できます。

適切に外部リンケージを設定すると、別のファイルからも参照ができます。

ローカル変数

ローカル変数は関数やブロックの内部で定義された変数で、そのブロックの内側でしか参照できません。

関数が呼び出されているあいだだけ生きており、通常はスタック領域に確保されます。

簡単なコードで両者の違いを見てみます。

C言語
#include <stdio.h>

// グローバル変数(ファイル全体から見える)
int g_counter = 0;

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

int main(void) {
    // ローカル変数(main関数の中だけ有効)
    int local = 10;

    printf("初期 g_counter = %d\n", g_counter); // 0
    increment();
    increment();
    printf("2回 increment 後 g_counter = %d\n", g_counter); // 2

    // printf("local in main = %d\n", local); // 有効
    // printf("local in increment = %d\n", local); // これはコンパイルエラーになる

    return 0;
}

void increment(void) {
    g_counter++;        // グローバル変数にアクセス可能

    // int local = 5;   // これは increment 関数のローカル変数
    // local++;         // main の local とは別物
}
実行結果
初期 g_counter = 0
2回 increment 後 g_counter = 2

この例からも分かるように、グローバル変数はどの関数からでも簡単に参照・変更できる一方で、ローカル変数は関数ごとに独立した状態を持つという違いがあります。

C言語におけるグローバル変数の寿命とスコープ

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

これは、プログラムの開始から終了までずっとメモリに存在し続けるという意味です。

C言語では、変数は主に次の組み合わせで特徴づけられます。

  • スコープ(scope): どこから見えるか
  • リンケージ(linkage): 別ファイルから見えるか
  • 記憶域期間(storage duration): いつからいつまで存在するか

グローバル変数の典型的な性質は次の通りです。

種類定義場所スコープ記憶域期間代表例
グローバル変数ファイル先頭など翻訳単位全体(または全プログラム)静的記憶域期間int g;
ローカル自動変数関数・ブロック内そのブロックの中だけ自動記憶域期間int x;
staticローカル変数関数・ブロック内そのブロックの中だけ静的記憶域期間static int c;

グローバル変数の特徴は、寿命が長くスコープが広いことです。

これが便利さの源泉であると同時に、後述する危険性の原因にもなっています。

ヘッダファイルとextern宣言の基本

C言語でグローバル変数を複数ファイルから共有するには、定義宣言を正しく分ける必要があります。

定義と宣言の違い

  • 定義(definition): 実体を1つだけ作る
    • 例: int g_counter = 0;
  • 宣言(declaration): その変数がどこかにあると「知らせる」だけ
    • 例: extern int g_counter;

典型的な分割方法を見てみます。

C言語
/* module.h - ヘッダファイル */
#ifndef MODULE_H
#define MODULE_H

// 他ファイルから参照するための「宣言」
// 実体はどこかの .c ファイルに1つだけ置く
extern int g_counter;

void increment(void);

#endif /* MODULE_H */
C言語
/* module.c - 実装ファイル */
#include "module.h"

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

void increment(void) {
    g_counter++;
}
C言語
/* main.c */
#include <stdio.h>
#include "module.h"

int main(void) {
    printf("g_counter = %d\n", g_counter);
    increment();
    printf("g_counter = %d\n", g_counter);
    return 0;
}
実行結果
g_counter = 0
g_counter = 1

このように、グローバル変数の実体は必ず1つの.cファイルでだけ定義し、ヘッダファイルにはexternで宣言だけを書くことが基本ルールです。

これを守らないと、後述する名前衝突とリンカエラーの原因になります。

C言語のグローバル変数が危険と言われる理由

可読性低下とバグ発見の困難さ

グローバル変数はどこからでもアクセスできるため、少しずつ使い始めると、気付かないうちに依存先が雪だるま式に増えていくことがあります。

そうなると、あるバグが発生した時に「このグローバル変数を変更している関数はどれか」を追いかけるだけで大きな手間になります。

例えば、次のようなコードを考えます。

C言語
#include <stdio.h>

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

void foo(void) {
    if (g_mode == 1) {
        printf("foo: debug mode\n");
    }
}

void bar(void) {
    if (g_mode == 2) {
        printf("bar: safe mode\n");
    }
}

void switch_mode(int mode) {
    g_mode = mode;  // どの関数からでも書き換え可能
}

int main(void) {
    switch_mode(1);
    foo();
    bar();

    switch_mode(2);
    foo();
    bar();

    return 0;
}
実行結果
foo: debug mode
bar: safe mode

最初は単純に見えますが、このg_modeを使う関数が10個、20個と増えると、どの部分がどのモードを前提に動作しているのかが一気に分かりにくくなります。

さらに、誰かが別の場所でg_modeを変更した場合、その影響範囲を想像するのが難しくなります。

依存関係の増加と保守性の低下

グローバル変数を使うと、モジュール間の依存関係が暗黙的に増えるという問題があります。

たとえば、あるモジュールの挙動を変えたいだけなのに、そのモジュールと同じグローバル変数を共有している別モジュールにも影響が出てしまうことがあります。

このような状況では、コードの一部を差し替えたり、テストのためにモジュールだけ抜き出したりすることが難しくなります。

結果として、保守性や再利用性が大きく損なわれるのです。

マルチスレッドでのデータ競合と未定義動作

マルチスレッド環境でグローバル変数を共有すると、データ競合(data race)が発生する危険があります。

データ競合が起きた場合、その動作は未定義動作となり、バグの再現が困難になります。

例として、スレッドから共有カウンタを更新するコードを考えます。

(POSIX Threads を仮定)

C言語
#include <stdio.h>
#include <pthread.h>

int g_counter = 0; // 共有グローバル(ロックなし)

// スレッドから呼ばれる関数
void *worker(void *arg) {
    for (int i = 0; i < 100000; i++) {
        g_counter++; // データ競合の可能性
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2;

    pthread_create(&t1, NULL, worker, NULL);
    pthread_create(&t2, NULL, worker, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("g_counter = %d\n", g_counter);
    return 0;
}
実行結果
g_counter = 157394   (例: 実行するたびに値が変わる)

本来期待する値は200000ですが、ロックをしていないため、インクリメント処理が途中で割り込まれ、中途半端な状態が書き戻されることがあります。

この種のバグは、再現性が低く、テストでも見逃されやすいため、非常に危険です。

初期化順序問題と予期しない動作

C言語では、異なる翻訳単位(.cファイル)間のグローバル変数の初期化順序は規定されていません

そのため、別ファイルにあるグローバル変数同士が互いの初期値に依存していると、環境によっては予期せぬ動作になる可能性があります。

例えば、次のようなケースです。

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

extern int g_default_port;
extern int g_current_port;

#endif
C言語
/* config_a.c */
#include "config.h"

int g_default_port = 8080;
// g_current_port を使って初期化しようとしている(危険)
int g_current_port = g_default_port;
C言語
/* config_b.c */
#include "config.h"

// こちら側でも g_current_port に別の初期値を与えようとする(さらに危険)
int g_current_port = 80;

これは定義の重複によるリンカエラーにもなりますが、仮に1つの翻訳単位内でも、定数式でない式による初期化や、別のグローバルへの依存は、設計として危険度が高いことを理解しておく必要があります。

安全のためには、グローバル変数の初期化は専用のinit関数などで明示的に行う方が望ましいです。

名前衝突とリンカエラーのリスク

グローバル変数は名前空間が広いため、名前の衝突が発生しやすくなります。

特に、extern宣言と定義の関係を正しく理解していないと、同じ名前のグローバル変数を複数のファイルで定義してしまい、リンカエラーになったり、意図しない変数が参照されたりする可能性があります。

例として、次のような状況を考えます。

C言語
/* file1.c */
int g_value = 10;
C言語
/* file2.c */
int g_value = 20; // うっかりもう一度定義してしまった

この場合、多くのコンパイラ・リンカでは次のようなエラーが出ます。

実行結果
multiple definition of `g_value'
first defined here ...

これを防ぐには、グローバル変数の定義は1つの.cファイルに限定し、他のファイルではexternで宣言だけを行うこと、また後述するstaticによる内部リンケージの活用が重要です。

グローバル変数の危険性を減らす実践ルール10選

ここからは、グローバル変数を完全に禁止するのではなく、現実的に安全性を高めるための「実践ルール10選」を紹介します。

ルール1: 状態共有を最小限にする

一番大事なルールは、そもそもグローバルで共有する状態を減らすことです。

何でもかんでもグローバルに置くのではなく、「本当に複数モジュールからアクセスする必要があるか」を一度立ち止まって考える習慣が重要です。

  • 単一関数だけで使うならローカル変数にする
  • 1つのモジュール内で完結するなら、後述のstatic内部変数にする
  • テストのために差し替えたいものは、後述の依存性注入を検討する

グローバル変数の数を減らせば減らすほど、バグの潜在的な発生源も減ると考えてよいです。

ルール2: constグローバル変数で読み取り専用にする

書き換える必要のない値は、constを付けて読み取り専用にすることで安全性が高まります。

たとえば、定数テーブルや設定値などはconstを付けたグローバルとして定義すると、コード上からも「不変である」という意図が明確になります。

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

extern const int g_max_clients;
extern const char * const g_app_name;

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

const int g_max_clients = 100;
const char * const g_app_name = "MyServerApp"; // ポインタも先も両方 const
C言語
/* main.c */
#include <stdio.h>
#include "config.h"

int main(void) {
    printf("App: %s, max clients: %d\n", g_app_name, g_max_clients);

    // g_max_clients = 200; // コンパイルエラー: 書き込み禁止
    return 0;
}
実行結果
App: MyServerApp, max clients: 100

「書き換える必要がないものには、必ずconstを付ける」というルールにするだけでも、多くのバグを防げます。

ルール3: staticで内部リンケージに限定する

グローバル変数を外部に公開する必要がない場合は、staticを付けて「そのファイル内限定」に閉じ込めるべきです。

これを内部リンケージ(internal linkage)と呼びます。

C言語
/* logger.c */

// この変数は logger.c の中からしか見えない
static int s_log_level = 0;

void logger_set_level(int level) {
    s_log_level = level;
}

void logger_log(const char *msg) {
    // s_log_level に応じて出力の仕方を変えるなど
}

ファイル外から直接アクセスされたくない状態はすべてstaticにすることで、名前空間の汚染と意図しない依存を防げます。

また、グローバル変数名の先頭にs_を付けるなど、内部用であることが分かる命名にするのも有効です。

ルール4: 構造体にまとめて「疑似名前空間化」する

複数のグローバル変数が関連している場合、構造体にまとめて1つのグローバルにすると、見通しがよくなります。

これにより、疑似的な「名前空間」のような効果も得られます。

C言語
/* app_state.h */
#ifndef APP_STATE_H
#define APP_STATE_H

struct AppState {
    int mode;
    int retry_count;
    int is_initialized;
};

extern struct AppState g_app_state;

#endif
C言語
/* app_state.c */
#include "app_state.h"

struct AppState g_app_state = {
    .mode = 0,
    .retry_count = 0,
    .is_initialized = 0
};
C言語
/* main.c */
#include "app_state.h"

int main(void) {
    g_app_state.mode = 1;
    g_app_state.is_initialized = 1;

    return 0;
}

このように関連する状態を1つの構造体にまとめることで、「何の状態なのか」が明確になり、フィールド名の衝突も避けやすくなります。

また、後でこの状態全体を関数の引数として渡したり、別モジュールに抽出したりもしやすくなります。

ルール5: アクセサ関数(getter/setter)経由で操作する

グローバル変数を直接公開せず、アクセサ関数を通してのみ読み書きさせる設計にすると、安全性が大きく高まります。

後で内部実装を変えても、外側のコードに影響を与えにくくなります。

C言語
/* settings.c */
#include "settings.h"

static int s_volume = 50; // 内部だけで管理

int settings_get_volume(void) {
    return s_volume;
}

void settings_set_volume(int value) {
    if (value < 0) value = 0;
    if (value > 100) value = 100;
    s_volume = value;
}
C言語
/* settings.h */
#ifndef SETTINGS_H
#define SETTINGS_H

int settings_get_volume(void);
void settings_set_volume(int value);

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

int main(void) {
    settings_set_volume(120); // 100 に丸められる
    printf("volume = %d\n", settings_get_volume());
    return 0;
}
実行結果
volume = 100

直接グローバル変数に触らせないことで、不正な値の代入や状態矛盾を防げるだけでなく、ログ取得やロック処理などを1カ所に集中させることもできます。

ルール6: 初期化関数(init)と終了処理関数を用意する

グローバル変数の初期化や後始末を、専用のinit関数・finalize関数にまとめると、ライフサイクルが明確になります。

C言語
/* resource.h */
#ifndef RESOURCE_H
#define RESOURCE_H

int resource_init(void);
void resource_finalize(void);
void resource_do_something(void);

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

static int s_initialized = 0;

int resource_init(void) {
    if (s_initialized) {
        return 0; // すでに初期化済み
    }
    // ここでグローバル状態を設定する
    printf("resource initialized\n");
    s_initialized = 1;
    return 0;
}

void resource_finalize(void) {
    if (!s_initialized) return;
    printf("resource finalized\n");
    s_initialized = 0;
}

void resource_do_something(void) {
    if (!s_initialized) {
        printf("resource not initialized!\n");
        return;
    }
    printf("resource working\n");
}
C言語
/* main.c */
#include "resource.h"

int main(void) {
    resource_do_something(); // 未初期化チェックが働く
    resource_init();
    resource_do_something();
    resource_finalize();
    return 0;
}
実行結果
resource not initialized!
resource initialized
resource working
resource finalized

「初期化されている前提」の隠れた依存をなくし、ライフサイクルの順番を明示することが、グローバル変数周りのトラブルを減らす鍵になります。

ルール7: グローバル変数の命名規則を統一する

グローバル変数には、一目で「グローバルだ」と分かる命名規則を適用すると、可読性が向上します。

例えば、次のようなルールがよく使われます。

用途プレフィックス例
外部公開グローバル変数g_g_app_state
モジュール内static変数s_s_initialized
グローバル定数(読み取り専用)k_k_default_port

プロジェクト内で命名ルールを統一しておくと、コードレビューやデバッグの際に、変数の性質を瞬時に判断できるため、グローバルに起因するミスを減らす効果があります。

ルール8: テストしやすいよう依存を注入(injection)する

テストしやすいコードを書くには、「必要なものは関数の引数として渡す」という発想が重要です。

これを依存性注入(dependency injection)と呼ぶこともあります。

C言語
/* Before: グローバルへの直接依存 */
int g_threshold = 10;

int is_valid(int value) {
    return value > g_threshold;
}
C言語
/* After: 依存する値を引数で受け取る */
int is_valid_with_threshold(int value, int threshold) {
    return value > threshold;
}

テストコードでは、好きなthresholdを渡して検証できるため、テストの独立性が高まり、グローバル状態に左右されにくくなります

実用上は、構造体や関数ポインタをまとめて渡すケースも多いです。

ルール9: マルチスレッドではロックやatomicを必ず使う

マルチスレッドでグローバル変数を共有する場合は、必ずロックやatomic操作で保護する必要があります。

POSIX Threads ならpthread_mutex_t、C11 の場合は_Atomic型などが利用できます。

C言語
#include <stdio.h>
#include <pthread.h>

int g_counter = 0;
pthread_mutex_t g_counter_mutex = PTHREAD_MUTEX_INITIALIZER;

void *worker(void *arg) {
    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&g_counter_mutex);
        g_counter++;
        pthread_mutex_unlock(&g_counter_mutex);
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2;

    pthread_create(&t1, NULL, worker, NULL);
    pthread_create(&t2, NULL, worker, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("g_counter = %d\n", g_counter);
    return 0;
}
実行結果
g_counter = 200000

グローバル変数 + マルチスレッド = 必ず同期処理が必要と覚えておくとよいです。

ロック漏れやデッドロックを防ぐためにも、後述するアクセサ関数内にロック処理を隠蔽する設計がおすすめです。

ルール10: 静的解析ツールでグローバル使用箇所を検出する

グローバル変数の使用箇所を人手だけで追いかけるのは大変です。

静的解析ツールやIDEの参照検索機能を活用して、グローバル変数の依存関係を定期的にチェックすると、安全性の向上につながります。

例えば、以下のようなツールが利用できます。

  • clang-tidy
  • cppcheck
  • commercialな組込み向け静的解析ツール各種

ツールの設定でglobal variableの使用を警告対象にし、新たなグローバル変数の追加にレビューの目が届くようにすることが重要です。

グローバル変数を安全に使うデザインパターン

ここからは、「グローバル変数をどうしても使う場合」に安全性を確保するためのデザインパターンをいくつか紹介します。

コンフィグ(config)や定数はグローバルで一元管理

複数のモジュールから参照される設定値や定数は、専用のConfigモジュールとして一元管理するのが有効です。

このとき、constグローバル変数やアクセサ関数を組み合わせて、安全に公開します。

C言語
/* app_config.h */
#ifndef APP_CONFIG_H
#define APP_CONFIG_H

extern const int k_max_clients;
extern const char * const k_log_file_path;

#endif
C言語
/* app_config.c */
#include "app_config.h"

const int k_max_clients = 100;
const char * const k_log_file_path = "/var/log/app.log";

各モジュールはこのヘッダをincludeするだけで、安全に同じ設定値を共有できます。

設定をビルド時に切り替えたい場合は、#ifdefや別の設定ファイルを生成する仕組みと組み合わせるとよいでしょう。

ログ出力など「副作用サービス」の扱い方

ログ出力やトレースといった「副作用サービス」も、グローバル変数と深く関わりやすい部分です。

典型的には、ログレベルやログ出力先のファイルハンドルなどをグローバルで持ち、関数経由で操作します。

C言語
/* logger.h */
#ifndef LOGGER_H
#define LOGGER_H

enum LogLevel {
    LOG_ERROR,
    LOG_WARN,
    LOG_INFO,
    LOG_DEBUG
};

int  logger_init(const char *filename);
void logger_set_level(enum LogLevel level);
void logger_log(enum LogLevel level, const char *fmt, ...);
void logger_finalize(void);

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

static FILE *s_log_fp = NULL;
static enum LogLevel s_log_level = LOG_INFO;

int logger_init(const char *filename) {
    s_log_fp = fopen(filename, "a");
    return (s_log_fp != NULL) ? 0 : -1;
}

void logger_set_level(enum LogLevel level) {
    s_log_level = level;
}

void logger_log(enum LogLevel level, const char *fmt, ...) {
    if (!s_log_fp || level > s_log_level) return;

    va_list args;
    va_start(args, fmt);
    vfprintf(s_log_fp, fmt, args);
    fprintf(s_log_fp, "\n");
    va_end(args);
}

void logger_finalize(void) {
    if (s_log_fp) {
        fclose(s_log_fp);
        s_log_fp = NULL;
    }
}

アプリ側はlogger_log()だけを使い、ログの状態自体はモジュール内部のstatic変数に隠蔽することで、グローバル乱用を防げます。

シングルトン風モジュール設計でスコープ管理

オブジェクト指向言語でいうシングルトンに近いパターンをCで模倣することもできます。

内部にstaticな状態を持ち、それへのポインタやハンドルだけを公開する方法です。

C言語
/* db.h */
#ifndef DB_H
#define DB_H

typedef struct DbHandle DbHandle;

DbHandle *db_get_instance(void);
int db_query(DbHandle *db, const char *sql);

#endif
C言語
/* db.c */
#include "db.h"

struct DbHandle {
    int connected;
    // 他の状態...
};

static struct DbHandle s_db = { 0 };

DbHandle *db_get_instance(void) {
    return &s_db;
}

int db_query(DbHandle *db, const char *sql) {
    if (!db->connected) {
        return -1;
    }
    // クエリ処理...
    return 0;
}

このパターンでは、内部の実装を後から差し替えたり、テスト時にモック実装を使ったりしやすくなるため、大規模なCプロジェクトでよく利用されます。

既存C言語コードのグローバル変数リファクタリング方針

既存のCコードには、すでに多くのグローバル変数が存在していることが珍しくありません。

その場合、一度にすべてを直そうとせず、段階的にリファクタリングするのが現実的です。

おおまかな方針は次の通りです。

  1. 静的解析ツールやgrepですべてのグローバル変数を一覧化する
  2. それぞれの変数について、役割・使用箇所・書き込み箇所を把握する
  3. 書き換え不要なものからconst化する
  4. 外部公開不要なものからstatic化する
  5. 関連する変数を構造体にまとめ、アクセサ関数を導入する
  6. マルチスレッドからアクセスされているものにロックを導入する

「影響範囲が小さいものから順番に」改善していくことで、リスクを抑えながらグローバル変数による問題を減らしていくことができます。

まとめ

グローバル変数は、C言語において非常に強力で便利な仕組みですが、その寿命の長さとスコープの広さゆえに、可読性の低下・バグ発見の難しさ・マルチスレッドでの未定義動作など、多くのリスクを内包しています。

本記事で紹介した10の実践ルールや、const化・static化・構造体による名前空間化・アクセサ関数・init/finalize設計などのパターンを取り入れることで、グローバル変数の危険性を大きく減らすことができます。

新規コードだけでなく既存コードのリファクタリングにも活用し、バグの少ない保守しやすいCプログラムを目指してください。

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

URLをコピーしました!