閉じる

【C言語】static変数の使い方と落とし穴|ローカル/グローバル完全ガイド

C言語のstatic変数は、ローカルにもグローバルにも使える強力な仕組みですが、その寿命やスコープの違いを正しく理解していないと、大きなバグや設計の劣化を招きます。

本記事では、C言語におけるstatic変数の仕組みを、ローカル/グローバルの両面から丁寧に解説し、具体的な使い方と典型的な落とし穴までを一気に整理していきます。

C言語のstatic変数とは

static変数の基本概念と特徴

C言語のstatic変数は、「スコープ(どこから見えるか)」と「寿命(いつまで存在するか)」をコントロールするためのキーワードです。

大きく分けて、次の2通りの使い方があります。

1つ目は、関数内で使うローカルstatic変数です。

これは「その関数の中からしか見えないが、プログラムの実行中ずっと値が保持される」変数です。

2つ目は、ファイルスコープで使うグローバルstatic変数で、「そのソースファイル内からしか参照できないが、プログラム全体の実行期間ずっと存在する」変数です。

特徴を簡単にまとめると、次のようになります。

  • 寿命: プログラム開始から終了まで(静的記憶域期間)
  • スコープ: 宣言位置によって異なる
    • 関数内で宣言した場合: その関数の中だけ
    • ファイルスコープで宣言した場合: そのファイルの中だけ
  • 初期化: 明示しない場合は0で初期化される(静的領域のルール)

このようにstaticは、「一度だけ作られて最後まで残る変数」でありつつ、「どこからでも見える」わけではないという点が重要です。

自動変数(auto)との違い

通常のローカル変数はauto(自動変数)と呼ばれます。

C言語ではautoキーワードを省略するのが一般的なため、単に「ローカル変数」と呼ぶことが多いです。

自動変数とローカルstatic変数は、次の点で異なります。

  • 寿命
    • 自動変数: 関数に入ったときに作られ、関数から出ると破棄される
    • static変数: プログラム開始時に1度だけ作られ、終了まで残る
  • 初期化のタイミング
    • 自動変数: 関数に入るたびに初期化式が実行される
    • static変数: プログラムの起動時に1度だけ初期化され、その後は保持された値を使う
  • メモリ領域のイメージ
    • 自動変数: スタック領域
    • static変数: データ領域(.dataや.bss)

この違いにより、関数をまたいで値を保持したいときにローカルstatic変数が役に立ちます

グローバル変数との違い

グローバル変数(ファイルスコープで宣言された非static変数)と、グローバルstatic変数は、寿命は同じですがスコープ(可視性)が異なります

  • グローバル変数
    • 宣言場所: 関数の外(ファイルの先頭など)
    • 寿命: プログラム開始から終了まで
    • 可視性: 他のソースファイルからexternで参照可能(外部リンケージ)
  • グローバルstatic変数
    • 宣言場所: 同じく関数の外
    • 寿命: 同じく開始から終了まで
    • 可視性: そのファイル内からだけ参照可能(内部リンケージ)

つまり、「グローバルなのに外から見えない」のがグローバルstatic変数です。

この性質は、モジュールの内部状態を隠蔽するカプセル化に役立ちます。

ローカルstatic変数の使い方

ローカルstatic変数の宣言と初期化

ローカルstatic変数は、通常のローカル変数と同じように関数の中で宣言しますが、staticキーワードを付ける点が異なります。

C言語
#include <stdio.h>

// ローカルstatic変数の宣言と初期化例
void func(void) {
    // staticを付けることで、この変数はプログラム終了まで生存する
    static int counter = 0;  // 初回呼び出し時に一度だけ0で初期化される

    counter++;  // 関数が呼ばれるたびに1ずつ増える
    printf("counter = %d\n", counter);
}

int main(void) {
    func();
    func();
    func();
    return 0;
}
実行結果
counter = 1
counter = 2
counter = 3

このコードではcounter関数呼び出し間で値を保持していることがわかります。

通常のローカル変数であれば、毎回0に初期化されて1しか表示されません。

ローカルstatic変数の初期化には次のルールがあります。

  • 初期化式を書かなかった場合は0で初期化される
  • 初期化はプログラム起動時に一度だけ行われる
  • 初期値には定数式(定数、配列表記など)を使うのが基本

関数内での値の保持と寿命

ローカルstatic変数は、関数の中に宣言されていますが寿命はプログラム全体です。

ただし、スコープはその関数内に限定されるため、外部から直接アクセスすることはできません。

次のように、複数の関数でローカルstatic変数を使うことで、それぞれが独立した状態を保持できます。

C言語
#include <stdio.h>

void inc_a(void) {
    static int a = 0;  // inc_a専用のカウンタ
    a++;
    printf("a = %d\n", a);
}

void inc_b(void) {
    static int b = 100;  // inc_b専用のカウンタ
    b += 10;
    printf("b = %d\n", b);
}

int main(void) {
    inc_a();  // a = 1
    inc_a();  // a = 2
    inc_b();  // b = 110
    inc_a();  // a = 3
    inc_b();  // b = 120
    return 0;
}
実行結果
a = 1
a = 2
b = 110
a = 3
b = 120

このように、関数ごとに独立した「長寿命なローカル状態」を持たせたいときに、ローカルstatic変数が有効です。

カウンタや状態管理への具体的な使い方

ローカルstatic変数は、「関数が何回呼ばれたか」や「前回の入力値」など、状態を関数の内部に閉じ込めたいときにとても便利です。

呼び出し回数を数えるログ関数

C言語
#include <stdio.h>

// ログ出力に通し番号を付ける例
void log_with_seq(const char *msg) {
    static unsigned int seq = 0;  // 呼び出し回数を記録する通し番号

    seq++;  // 呼び出されるたびにインクリメント
    printf("[%u] %s\n", seq, msg);
}

int main(void) {
    log_with_seq("start");
    log_with_seq("processing");
    log_with_seq("end");
    return 0;
}
実行結果
[1] start
[2] processing
[3] end

ここではseqがローカルstatic変数として、ログに連番を付与する役割を果たしています。

外部から書き換えられないため、意図しない変更が入りにくいという利点もあります。

前回の値との差分を計算する関数

C言語
#include <stdio.h>

// 前回の値との差分を返す関数
int diff_from_previous(int current) {
    static int prev = 0;  // 前回の値を保持
    int diff = current - prev;
    prev = current;
    return diff;
}

int main(void) {
    int values[] = {10, 15, 12, 20};
    int size = (int)(sizeof(values) / sizeof(values[0]));

    for (int i = 0; i < size; i++) {
        int d = diff_from_previous(values[i]);
        printf("current = %d, diff = %d\n", values[i], d);
    }
    return 0;
}
実行結果
current = 10, diff = 10
current = 15, diff = 5
current = 12, diff = -3
current = 20, diff = 8

この関数は前回の入力値を内部に覚えておくことで、引数が1つでも差分を計算できるようになっています。

ローカルstatic変数のメリット・デメリット

ローカルstatic変数には、次のようなメリットがあります。

  • 状態を関数内に閉じ込めることで、グローバル変数よりも影響範囲を小さくできる
  • 呼び出しのたびに初期化されないため、呼び出し回数や履歴を簡単に保持できる
  • インターフェース(引数・戻り値)を変えずに、関数に少しだけ状態を持たせられる

一方で、デメリットも明確です。

  • 関数が「状態に依存するブラックボックス」になりやすく、テストやデバッグが難しくなる
  • マルチスレッドや割り込み環境では、同時アクセスによる競合が起きやすくなる
  • 状態を内部に持つことで、その関数を再利用しにくくなる(再入不可になりやすい)

そのため、ローカルstatic変数は便利ですが乱用は避け、用途を絞って使用することが重要です。

グローバルstatic変数の使い方

ファイルスコープにおけるstatic変数

ファイルスコープ(static付きグローバル変数)は、次のように宣言します。

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

// このファイル内でのみ有効なグローバルstatic変数
static int g_counter = 0;

void inc_counter(void) {
    g_counter++;
    printf("g_counter = %d\n", g_counter);
}

void reset_counter(void) {
    g_counter = 0;
}

このg_counterは、file1.c内のどの関数からもアクセスできますが、他のソースファイルからは見えません

staticによる内部リンク(ファイル内限定)の意味

C言語では、staticを付けることでシンボル(変数名や関数名)に内部リンケージ(internal linkage)を与えます。

内部リンケージを持つシンボルは、その翻訳単位(通常は1つの.cファイル)の中でしか参照できません。

対照的に、staticを付けないグローバル変数や関数は外部リンケージ(external linkage)を持ち、他のファイルからextern宣言で参照できます。

C言語
// file1.c
int global_var = 0;         // 外部リンケージ
static int internal_var = 0; // 内部リンケージ

void func1(void) {
    global_var++;
    internal_var++;
}
C言語
// file2.c
#include <stdio.h>

extern int global_var;  // file1.cのglobal_varを参照できる
// extern int internal_var;  // これはリンクエラーになる

void func2(void) {
    printf("global_var = %d\n", global_var);
    // internal_varにはアクセスできない
}

このように、staticによって「モジュールの外部に公開したくないシンボル」を隠すことができます。

グローバル変数とのスコープと可視性の比較

グローバル変数とグローバルstatic変数の違いを表にまとめます。

種類寿命スコープ(可視性)
グローバル変数プログラム開始〜終了プロジェクト全体(他ファイルからexternで参照可)
グローバルstatic変数プログラム開始〜終了宣言したファイル内のみ

寿命はどちらも同じであるため、「どのくらいの期間データを保持したいか」だけでは区別できません。

代わりに、「どこからアクセスさせたいか」「どこからはアクセスさせたくないか」という観点で、staticを付けるかどうかを判断します。

ヘッダファイルとstaticの扱い方の注意点

ヘッダファイルでのstaticの扱いには、特に注意が必要です。

ヘッダに次のように書いてしまうと危険です。

C言語
// config.h
static int config_value = 0;  // ヘッダにstatic変数を書いてしまった例

このヘッダを複数のソースファイルから#includeすると、各ソースファイルごとに別々のconfig_valueが生成されます

C言語
// file1.c
#include "config.h"
void set_config(int v) {
    config_value = v;  // file1.c専用のconfig_value
}

// file2.c
#include "config.h"
int get_config(void) {
    return config_value;  // file2.c専用のconfig_value (file1.cとは別物)
}

この場合、file1.cで設定した値はfile2.cから見えません

意図せず「同じ名前の別変数」ができてしまうため、ヘッダに定義(static付き変数)を書くのは原則として避けるべきです。

共有したいグローバル変数を使う場合は、次のようにします。

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

extern int g_config_value;  // 宣言だけ(定義ではない)

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

int g_config_value = 0;     // ここで1回だけ定義する

ヘッダに書くのはextern宣言、定義はどこか1つの.cファイルにまとめる、というのが基本ルールです。

static変数の落とし穴と注意点

マルチスレッド環境でのstatic変数の危険性

static変数は、多くの場合スレッドセーフではありません

複数のスレッドから同じstatic変数に同時アクセスすると、データ競合(race condition)が発生する可能性があります。

C言語
#include <stdio.h>

// 複数スレッドから呼ばれると危険な関数の例
int generate_id(void) {
    static int id = 0;  // 共有されるカウンタ
    id++;               // ここが競合する可能性あり
    return id;
}

この関数を2つのスレッドが同時に実行した場合、id++の処理が互いに干渉し、期待した値にインクリメントされないことがあります。

マルチスレッド環境では、static変数を共有する場合は必ずロック(ミューテックスなど)で保護する、もしくは_Thread_localやスレッドローカルストレージを利用するなどの配慮が必要です。

初期化タイミングと未定義動作のリスク

複数のソースファイルに静的変数(グローバルやstatic)の初期化式を分散させると、「どちらが先に初期化されるか」が処理系依存になることがあります。

極端な例ですが、次のようなコードは危険です。

C言語
// a.c
extern int g;

static int x = 1 + g;  // gがまだ初期化されていない可能性がある

int get_x(void) { return x; }
C言語
// b.c
int g = 10;

Cの仕様上、異なる翻訳単位間の静的オブジェクトの初期化順序には制約が少なく、処理系に依存する部分があります。

結果として、gが0のままxが初期化されてしまうといった未定義動作や実装依存の挙動を招きかねません。

対策としては、次のような設計を心がけます。

  • 静的オブジェクトの初期化で、他の静的オブジェクトに依存しない
  • 必要であればinit()関数を用意し、明示的な初期化の順序を確保する

テストしにくいコードになる問題

static変数は、テストしにくいコードを生みやすいです。

理由は、関数の挙動が「引数」だけでなく「内部状態」にも依存するからです。

たとえば、先ほどのlog_with_seq()関数をテストするとき、テストケースAで3回呼び出し、続いてテストケースBでも同じ関数を使うと、テストケースAの呼び出し回数がBにも影響してしまいます。

テストの観点から見ると、理想的な関数は「同じ引数を与えれば常に同じ結果が返ってくる」純粋関数ですが、static変数を用いた関数はこれと逆の性質を持ちます。

そのため、ユニットテストをしやすくするには、

  • static変数の使用は最小限に抑える
  • 必要であれば「状態をリセットするための関数」を用意する
  • もしくは、状態を外部のコンテキスト構造体に持たせ、引数として渡す設計を優先する

といった工夫が有効です。

再入可能性(reentrancy)とstatic変数

再入可能性(reentrancy)とは、ある関数が実行中に、同じ関数が再度呼び出されても正しく動作できる性質のことです。

割り込みハンドラやコールバック、マルチスレッド環境では、この性質が重要になります。

static変数を使った関数は、多くの場合「再入不可能」になります。

C言語
#include <stdio.h>

// 再入不可能な関数の例
char *get_buf(void) {
    static char buf[256];  // 共有バッファ

    // ここでbufに何らかの文字列をセットする処理があると仮定
    // 例: snprintf(buf, sizeof(buf), "time: %d", get_time());

    return buf;
}

この関数が実行中にもう一度呼ばれると、同じbufが書き換えられてしまい、呼び出し元が期待した内容を保持できないという問題が起こります。

対策としては、

  • staticバッファを使わず、呼び出し側からバッファを渡してもらう
  • どうしてもstaticを使う場合、関数を再入しない設計に限定する(割り込み禁止区間内でしか呼ばないなど)

といった方法があります。

過度なstatic使用を避ける設計のポイント

static変数は便利ですが、「とりあえずグローバルよりマシだからstaticにしておこう」という使い方を続けると、コード全体が複雑になり保守性が低下します。

設計時のポイントとして、次のような方針が有効です。

  1. まずは関数を純粋に保つことを優先する
    可能な限り、状態を持たない関数(引数だけで結果が決まる)として設計します。
  2. 状態が本当に必要なら、「コンテキスト構造体」を作り、状態をそこにまとめる
    その構造体へのポインタを引数として渡すことで、staticに頼らずに状態管理ができます。
  3. モジュールの内部状態を隠蔽する目的で、最小限のグローバルstatic変数を使う
    例えば、デバイスドライバやライブラリ内部で、ハードウェア状態を表す変数を1つだけstaticで持つ、といった使い方です。
  4. テスト性と再入可能性が重要な部分では、static変数を避ける
    特にマルチスレッドや割り込みと関わるコードでは、static変数の使用に慎重になる必要があります。

このように、staticは「隠蔽」と「長寿命」を同時に提供する強力なツールですが、設計レベルでの意図を持って使うことが重要です。

まとめ

static変数は、C言語において変数の「寿命」と「スコープ」を細かく制御できる重要な仕組みです。

ローカルstatic変数は関数内に状態を閉じ込めるのに役立ち、グローバルstatic変数はモジュール内だけで共有したい情報のカプセル化に適しています。

一方で、マルチスレッドや再入性、テストの難しさなど、多くの落とし穴も存在します。

実務では、まずは状態を外部に持つ設計を優先し、それでも必要な場面でのみstaticを慎重に使うことで、読みやすく安全なCプログラムを実現しやすくなります。

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

URLをコピーしました!