閉じる

C言語のグローバル変数とは?定義の基本と安全な書き方

C言語のプログラムでは、どこからでも参照できる変数(グローバル変数)は簡単に値を共有できる反面、設計やテストを難しくすることがあります。

本記事では、グローバル変数の基本的な定義方法、宣言との違い、複数ファイルでの扱い方、そして安全に使うための考え方と注意点を、初心者向けにていねいに解説します。

便利さとリスクのバランスを理解し、最小限かつ安全に使うことを目指します。

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

グローバル変数とは、ファイル外(関数の外)で定義され、プログラムの実行期間全体にわたって存在し続ける変数です。

プログラムのどこからでも参照できる(外部リンケージ)か、ファイル内だけで見える(内部リンケージ)かは宣言のしかたで変わります

ローカル変数との違い

ローカル変数は関数やブロックの中で宣言され、そのブロックの実行中だけ存在します。

一方グローバル変数(静的記憶期間)は、プログラム開始時に確保され、終了まで生き続けます。

次の点が特に重要です。

  • ローカル変数は毎回初期化が必要ですが、グローバル変数はゼロ初期化が自動で行われます(明示的な初期化子があればそれが適用されます)。
  • ローカル変数はスコープが狭いので影響範囲が限定されます。グローバル変数は広く共有されるため、影響が大きくなります。

スコープとライフタイムが根本的に異なるため、設計上の使い分けが大切です。

サンプル(グローバルとローカルの違い)

C言語
// demo_global_vs_local.c
#include <stdio.h>

// グローバル変数はプログラム全体で共有され、実行中ずっと値を保持します
int g_total = 0;

void use_global(void) {
    g_total += 10;               // 呼び出すたびに累積される
    printf("[global] g_total = %d\n", g_total);
}

void use_local(void) {
    int local = 0;               // ローカル変数は関数に入るたびに再初期化される
    local += 10;
    printf("[local ] local   = %d\n", local);
}

int main(void) {
    use_global(); // 10
    use_global(); // 20
    use_local();  // 10
    use_local();  // 10 (毎回0から)
    return 0;
}
実行結果
[global] g_total = 10
[global] g_total = 20
[local ] local   = 10
[local ] local   = 10

どこに書くか

グローバル変数の定義は、必ず関数の外(ファイル先頭付近)に書きます。

ヘッダファイル(.h)には定義ではなく宣言(extern)を書くのが原則です。

基本ルールは次のとおりです。

  • 1つの変数につき、定義はプロジェクト全体でちょうど1回だけ
  • その変数を使う複数のファイルでは、ヘッダに書いたextern宣言を#includeする

表にまとめます。

置き場所書き方の例可視範囲備考
.cファイル(関数の外)int g_count = 0;全翻訳単位(外部リンケージ)これが「定義」。1プロジェクトで1回のみ
.hファイルextern int g_count;そのヘッダを#includeした場所これは「宣言」。ストレージは確保しない
.cファイル(関数の外)static int s_count = 0;同一ファイル内のみ(内部リンケージ)モジュール内で隠すのに有用

ヘッダに「定義」を書かないのが鉄則です。

ヘッダにint g_count;と書くと、複数の.cから重複定義になりやすいため要注意です。

使ってよい場面の例

グローバル変数は便利ですが、むやみに増やすと管理が難しくなります。

次のようなケースに絞るのが現実的です。

  • アプリ全体で共有する読み取り専用の定数(例: ビルド時に決まる設定値)。ただし#defineまたはstatic constでモジュール内に閉じるのがより安全です。
  • ログや統計のカウンタなど、単純で副作用を把握しやすい値。アクセス関数で包むとより安全です。
  • 組み込みやデバイス制御で、ハードウェアレジスタに対応する領域(この場合はvolatileを正しく使う)。ただしスレッド安全性はvolatileでは得られません

迷ったら、まずは関数の引数で渡す設計を検討すると良いです。

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

定義の書き方と初期化

定義はストレージ(メモリ)を確保します。

Cではファイルスコープで次のように書きます。

  • 明示的な初期化あり: int g_count = 0;
  • 初期化省略: int g_count; (Cでは暗黙に0で初期化されます。これは「暫定定義」とみなされ、最終的に= 0が付いた定義と同等になります)

グローバル変数はプログラム開始前にゼロ初期化され、明示的初期化子があればそれが適用されます。

宣言と定義の違い

宣言は「ここにその名前の変数が存在する」とコンパイラに伝えるだけで、ストレージは確保しません。

Cではexternを付けます。

種別書き方ストレージ確保目的
宣言extern int g_count;しない他ファイルにある定義を参照するため
定義int g_count = 0;する実体を作る(1回だけ)

ヘッダには宣言(extern)、.cには定義という住み分けを徹底しましょう。

複数ファイルで共有する方法

最も安全な方法は、ヘッダに宣言、1つの.cに定義、他の.cからはヘッダを#includeする形です。

サンプル(ヘッダ宣言 + 1箇所定義)

C言語
/* counter.h */
#ifndef COUNTER_H
#define COUNTER_H

// グローバル変数の宣言(定義ではないためexternを付ける)
extern int g_count;

// 変数を直接触らせないための操作関数(推奨)
void counter_increment(void);
void counter_reset(void);
int  counter_get(void);

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

// グローバル変数の「定義」はここだけ
int g_count = 0;

void counter_increment(void) {
    ++g_count;
}

void counter_reset(void) {
    g_count = 0;
}

int counter_get(void) {
    return g_count;
}
C言語
/* main.c */
#include <stdio.h>
#include "counter.h"

int main(void) {
    counter_increment();
    counter_increment();
    printf("count = %d\n", counter_get());  // 2
    counter_reset();
    printf("after reset = %d\n", counter_get()); // 0
    return 0;
}
Shell
# コンパイルと実行例(GCCの場合)
gcc -std=c11 -Wall -Wextra -o app main.c counter.c
./app
実行結果
count = 2
after reset = 0

補足として、変数を外部公開したくない場合は、.c内でstatic int s_count;として内部リンケージにし、ヘッダでは変数を公開せず関数だけ公開します。

これはモジュール内に状態を閉じ込める良い方法です。

よくあるエラー

初心者がつまずきやすい点を原因と対処と合わせてまとめます。

リンクエラー: multiple definition of g_count

ヘッダファイルに int g_count; と書いてしまうと、複数の .c ファイルから定義されてしまいリンクエラーになります。

対処として、ヘッダには extern int g_count; を記述し、実際の定義は必ず 1 つの .c にのみ置く必要があります。

リンクエラー: undefined reference to g_count

宣言はあるものの、どの .c にも定義が存在しない、あるいはオブジェクトファイルをリンクしていない場合に発生します。

解決策は、どこか 1 つの .cint g_count = 0; を追加し、必ずリンク対象に含めることです。

コンパイルエラー: conflicting types for g_count

ファイル間で型が一致していないと発生します(例: intlong で宣言している)。

ヘッダの宣言を唯一の参照元にし、すべてのファイルで同じ型を使うように統一します。

実行時バグ: 予期せず値が変わる

どこかで意図せず書き換えている場合や、同名のローカル変数でグローバルが隠れている場合に起こります。

命名規約(例: g_ 接頭辞)を導入し、アクセス用関数を介することで保護するのが有効です。

ヘッダに定義を書かない、型を1箇所に集約するだけで、多くのトラブルを避けられます。

安全な使い方とベストプラクティス

まずは引数で渡すことを優先

グローバルに頼る前に、関数の引数で必要な情報を渡す設計を検討します。

状態を1か所にまとめた「コンテキスト構造体」を渡すと、テストもしやすくなります。

サンプル(コンテキストを引数で渡す)

C言語
// demo_context.c
#include <stdio.h>

// アプリの状態を1つの構造体にまとめる
typedef struct {
    int counter;
} AppState;

void inc(AppState *s) {
    // ポインタ経由で更新する
    s->counter += 1;
}

int main(void) {
    AppState app = {0};  // 局所的に状態を作る
    inc(&app);
    inc(&app);
    printf("app.counter = %d\n", app.counter); // 2
    return 0;
}
実行結果
app.counter = 2

この設計だと、テストではAppStateを用意して関数を呼ぶだけでよく、他のテストに影響を与えません。

変更を最小化する

グローバル変数を使う場合でも、変更点を減らす工夫が重要です。

  • 読み取り専用ならconstを付ける(例: static const int kMaxSize = 256;)。
  • 直接代入を禁止し、アクセサ関数(get/set/increment)に限定する。
  • 複数スレッドから更新するなら排他制御(ミューテックスやC11の原子型stdatomic.h)を使う。volatileはスレッド安全性の確保には使えません

名前付けと配置

命名と配置の一貫性はバグ予防になります。

  • グローバル変数はg_接頭辞、モジュール内限定はs_などの規約で区別する。
  • 定義はファイル先頭の「グローバル変数」セクションにまとめ、用途のコメントを添える。
  • ヘッダには宣言のみを置き、インクルードガードで重複を防ぐ。

見通しのよい配置と規約が、意図しない上書きを大幅に減らします

初期化とリセットのルールを決める

グローバル状態には、初期化とリセットの責務を明確にします。

  • 初期化関数(例: module_init())を用意し、必要ならmodule_reset()も設ける。
  • 明示的に初期化する値はヘッダや定義近くにコメントで根拠を書く。
  • ブールフラグg_initializedを用いて二重初期化を防ぐ。

初期化の流れが一目で追えるようにしておくと、起動時の不具合を避けやすくなります。

グローバル変数の注意点

依存が増えてテストが難しくなる

グローバルに依存する関数は、隠れた入力(状態)と出力(副作用)を持ちます。

個別テストが難しく、テスト間の独立性も失われがちです。

先述のコンテキスト引数や依存性注入(必要な関数ポインタやデータを渡す)で解消できます。

意図しない上書きやバグの原因

同名のローカル変数による「名前のかぶり」は典型的なバグ原因です。

サンプル(名前のシャドーイングによる勘違い)

C言語
// shadowing_bug.c
#include <stdio.h>

int g_value = 100; // グローバル

void change_wrong(void) {
    int g_value = 0;  // ローカルが同名でグローバルを覆い隠す(シャドーイング)
    g_value = 200;    // ここで変えているのはローカル。グローバルは変わらない
}

int main(void) {
    change_wrong();
    printf("global g_value = %d\n", g_value); // 100 のまま
    return 0;
}
実行結果
global g_value = 100

命名規約で役割を区別し、グローバルを直接書き換えない設計にすることで防げます。

また、複数スレッドからの同時更新やポインタの別名(alias)による想定外の書き換えにも注意が必要です。

並行更新は必ず排他や原子操作で守ることを習慣にしてください。

初期化のタイミングに注意

Cでは静的記憶期間のオブジェクト(グローバル変数など)はプログラム開始前にゼロ初期化され、定義で指定した初期化子があればそれが適用されます。

ただし次の点に注意します。

  • 初期化が0に依存するロジック(例: if (g_flag) ...)は、明示的初期化やコメントで意図を示す。ブール型_Bool/stdbool.hboolを使うと読みやすい。
  • 遅延初期化(最初の使用時に初期化)をするなら、重複実行を防ぐガードを設ける。
  • 複数のグローバルに依存関係がある場合は、初期化順序を1つの関数に集約して明示的に呼び出すと安全です。

「いつ・どこで初期化されるか」をコードから即座にたどれる状態にしておくことが品質に直結します。

まとめ

本記事では、C言語におけるグローバル変数の定義・宣言の基本、複数ファイルでの共有方法、そして安全に使うための考え方を解説しました。

グローバル変数はプログラム全体に影響する強力な仕組みである一方、バグやテストの難しさを招きやすいため、次の原則を守ると良いです。

まずは引数で渡す設計を優先し、どうしても必要な場合のみ最小限に用いる。

ヘッダにはextern宣言、定義は.cに1つだけ。

名前や配置の規約、初期化とリセットの責務を明確にする。

さらに、並行更新時は必ず排他・原子操作で守ることを忘れないでください。

これらを徹底すれば、グローバル変数の利便性を活かしつつ、安全で読みやすいCコードを書けるようになります。

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

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

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

URLをコピーしました!