閉じる

【C言語】関数内のstatic変数とは?値が残る仕組みと初期化のタイミング

関数内でstaticを付けた変数は、関数を抜けても値が消えずに次回呼び出しまで残ります。

ローカル(自動)変数と同じスコープを持ちながら、寿命はプログラム全体にわたるという独特の性質があり、呼び出し回数のカウントや一度だけの初期化ガードに役立ちます。

本記事では仕組み、初期化のタイミング、基本的な使い方、注意点を初心者向けに丁寧に解説します。

C言語のstatic変数とは

関数の内部でstaticを付けて宣言した変数は、ブロック内のスコープを持ちつつ、プログラム実行中ずっと生き続ける(静的記憶域期間)という特徴があります。

通常のローカル変数と同じく関数外からは見えませんが、関数を抜けても値が保持されます。

値が残る仕組み

静的ローカル変数は、スタックではなく静的領域(データ領域やBSS領域)に配置されます。

したがって、関数の呼び出しと終了によって割り当てや解放が行われることはありません。

割り当てはプログラム開始時(もしくは初回アクセスまでに)一度だけ行われ、以降は同じメモリアドレスを使い続けます

補足として、C言語規格上は「静的記憶域のゼロ初期化」はプログラム開始前に行われます。

ブロックスコープのstatic変数に定数式で初期値を与えた場合も、実装は初回アクセス前までに一度だけその初期化を完了します。

ローカル変数との違い

違いは主に「寿命」と「初期化の回数」にあります

スコープはいずれもブロック内に限られますが、静的ローカル変数は次回呼び出しまで値が残ります。

以下に比較表を示します。

項目自動(ローカル)変数静的ローカル変数(static付き)
スコープ宣言したブロック内宣言したブロック内
寿命関数呼び出し中のみプログラム終了まで
初期化呼び出しごとに実行されがち一度だけ(初回前まで)
メモリ領域スタック静的領域(データ/BSS)
既定初期値不定(未初期化は未定義値)0で初期化
再帰・並行呼び出し呼び出しごとに独立すべての呼び出しで共有
典型用途一時的な計算用カウンタ、前回値保持、初期化ガード

特に再帰やマルチスレッド環境では「共有される」点が差を生みます

静的ローカルは呼び出し間で共有されるため、意図しない干渉が起きる可能性があります。

初期化のタイミングと書き方

静的ローカル変数の初期化は一度きりで、関数を何度呼んでも繰り返されません

書き方は通常の宣言と同じですが、初期値としては「定数式」を用いるのが原則です。

初期化は1回だけ

以下のプログラムは、初期化が1度きりであることを実演します。

C言語
#include <stdio.h>

// 静的ローカル変数の初期化は1回だけ
void demo(void) {
    static int x = 100; // ここは最初の1回だけ評価・代入される
    printf("x=%d\n", x);
    x++;                // 値は次回呼び出しまで保持される
}

int main(void) {
    demo(); // x=100
    demo(); // x=101
    demo(); // x=102
    return 0;
}
実行結果
x=100
x=101
x=102

2回目以降の呼び出しでも<x>=100</x>には戻らず、前回の続きから始まっている点がポイントです

初期値を省略したとき

静的ローカル変数は初期値を省略すると0で初期化されます

これは配列やポインタでも同様で、要素やメンバーはゼロクリアされます。

C言語
#include <stdio.h>

// 初期値省略時は0クリアされる
void demo_zero(void) {
    static int s;   // 0で初期化される
    printf("s=%d\n", s);
    s += 5;         // 値は保持される
}

int main(void) {
    demo_zero(); // s=0
    demo_zero(); // s=5
    return 0;
}
実行結果
s=0
s=5

未初期化の自動変数が「不定値」になるのとは対照的です。

初期化と代入の違い

「初期化(一度だけ)」と「代入(毎回実行)」は明確に区別されます

次の例で違いを確認しましょう。

C言語
#include <stdio.h>

void f_static_init(void) {
    static int a = 10; // 初期化: これが評価されるのは1回だけ
    printf("a=%d\n", a);
    a++;               // 値は保持される
}

void f_assignment(void) {
    int b;             // 自動変数
    b = 10;            // 代入: 呼び出しのたびに10に戻す
    printf("b=%d\n", b);
    b++;
}

int main(void) {
    puts("--- static 初期化 ---");
    f_static_init(); // a=10
    f_static_init(); // a=11

    puts("--- 代入 ---");
    f_assignment();  // b=10
    f_assignment();  // b=10 (毎回10に戻る)
    return 0;
}
実行結果
--- static 初期化 ---
a=10
a=11
--- 代入 ---
b=10
b=10

また、静的ローカル変数の初期化子は原則として「定数式」に限られます

例えばstatic int t = time(NULL);のように実行時にしか得られない値は初期化に使えません(多くの処理系でコンパイルエラー)。

動的な初期化が必要なら「一度だけ実行する」パターンで代替してください。

使い方の基本パターン

静的ローカル変数は強力ですが、意図が明確な用途に絞って使うと理解しやすく安全です。

ここでは初学者がよく使う3パターンを紹介します。

呼び出し回数を数える

関数が呼ばれた回数を累積する定番パターンです。

C言語
#include <stdio.h>

// 呼び出されるたびにカウントアップして返す
int hit_counter(void) {
    static int count = 0; // 値を保持
    count++;
    return count;
}

int main(void) {
    for (int i = 0; i < 3; i++) {
        printf("hit %d\n", hit_counter());
    }
    return 0;
}
実行結果
hit 1
hit 2
hit 3

外部にグローバル変数を露出せずに、関数内部だけで状態を完結できる点がメリットです。

一度だけ実行する

重い初期化や設定の読み込みなどを、最初の1回だけ実行し、それ以降はスキップするガードです。

C言語
#include <stdio.h>

// 一度だけ初期化したい処理をガードする
void init_once(void) {
    static int initialized = 0; // 0:未実行, 1:実行済み
    if (!initialized) {
        // ここに重い初期化処理を置く
        puts("heavy init ... done");
        initialized = 1; // マークする
    } else {
        puts("already initialized");
    }
}

int main(void) {
    init_once(); // heavy init ... done
    init_once(); // already initialized
    init_once(); // already initialized
    return 0;
}
実行結果
heavy init ... done
already initialized
already initialized

実行コストの高い処理を必要最小限に抑えるのに有効です。

並行実行がある場合は後述の排他制御も検討します。

直前の値を保持する

ストリーム処理や前回値との差分計算に便利です。

C言語
#include <stdio.h>

// 直前の入力値を覚えて差分を計算する
int diff_from_prev(int current) {
    static int prev = 0;     // 初回は0
    int diff = current - prev;
    prev = current;          // 次回のために保存
    return diff;
}

int main(void) {
    int data[] = {10, 15, 12};
    for (int i = 0; i < 3; i++) {
        printf("current=%d, diff=%d\n", data[i], diff_from_prev(data[i]));
    }
    return 0;
}
実行結果
current=10, diff=10
current=15, diff=5
current=12, diff=-3

関数の外に状態を出さずに「前回との関係」を扱えるため、APIをシンプルに保てます。

注意点とベストプラクティス

静的ローカル変数は便利な半面、隠れた状態を生みやすく、テストや拡張で思わぬ副作用の原因になります。

以下の点に注意してください。

使いすぎない

「値が残るから」と安易に使わず、本当に関数内部の状態として閉じる必要があるかを検討します。

設計上は以下の代替も有効です。

  • 状態を引数や戻り値で受け渡しする
  • 状態を構造体にまとめ、呼び出し側でインスタンスを管理する
  • 必要に応じてモジュールスコープ(別名:ファイルスコープ)に移し、インターフェースを明確化する

テスト容易性、再利用性、並行実行の安全性を優先すると、静的ローカルを使わない設計が適することも多いです。

マルチスレッドは注意

静的ローカル変数は全スレッドで共有されます

同時に書き換えるとデータ競合が起き、結果が不定になります。

並行アクセスがあり得る場合は必ず排他制御を行いましょう

C言語
// POSIX Threadsを用いた簡易的な排他の例
// コンパイル例: gcc -pthread example.c
#include <pthread.h>
#include <stdio.h>

static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;

int next_id_safe(void) {
    static int id = 0; // 共有される
    pthread_mutex_lock(&g_lock);
    int v = ++id; // クリティカルセクション
    pthread_mutex_unlock(&g_lock);
    return v;
}

int main(void) {
    for (int i = 0; i < 10; i++) {
        int id = next_id_safe();
        printf("id = %d\n", id); // 出力は省略
    }
    return 0;
}
上記サンプルをWindowsで動かす場合

WindowsではPOSIX互換レイヤーを導入するか、Windows固有のAPIが必要です。

CygwinMSYS2 には pthread 互換ライブラリがあり、Linuxに近い環境でそのまま動かせますが、コンパイル時に gcc -pthread example.c のように、-pthreadを追加する必要があります。

Windows固有のAPIを使用する場合、最も手軽なのは、静的初期化できる SRWLOCK(排他ロック)を使う方法です(Windows Vista以降)。

C言語
// Win32 API を用いた簡易的な排他の例
// コンパイル例 (MSVC):  cl /W4 /O2 example.c
// コンパイル例 (MinGW): gcc -O2 example.c
#define WIN32_LEAN_AND_MEAN
#include <stdio.h>
#include <windows.h>

static SRWLOCK g_lock = SRWLOCK_INIT;

int next_id_safe(void) {
    static int id = 0; // 全スレッドで共有
    AcquireSRWLockExclusive(&g_lock);
    int v = ++id; // クリティカルセクション
    ReleaseSRWLockExclusive(&g_lock);
    return v;
}

int main(void) {
    for (int i = 0; i < 10; i++) {
        int id = next_id_safe();
        printf("id = %d\n", id); // 出力は省略
    }
    return 0;
}

再入可能(リエントラント)性が必要な関数では、静的ローカルの使用を避けるか、スレッドローカル記憶(_Thread_localや処理系拡張)を検討してください。

関数外のstaticとの違い

staticは「関数内」と「関数外(ファイルスコープ)」で意味が異なります。

関数内のstaticは寿命を延ばす(静的記憶域期間)効果関数外のstaticはリンケージを内部結合にする(翻訳単位外から参照できなくする)効果を持ちます。

C言語
// file1.c
static int hidden_value = 42;   // この翻訳単位(file1.c)の外からは見えない(内部結合)

int get_hidden(void) {
    return hidden_value;
}

// file2.c からは hidden_value にリンクできない。
// 代わりに get_hidden() のような公開関数を介してアクセスする設計にする。

「関数外のstatic」は公開範囲(リンク可能性)を絞るキーワードであり、「関数内のstatic」は寿命を延ばすキーワードです。

文脈により役割が違う点に注意してください。

まとめ

関数内のstatic変数は、スコープはローカルだが寿命はプログラム全体というユニークな性質を持ち、値が残るという特徴によりカウンタや初期化ガード、前回値保持などで威力を発揮します

一方で、再帰やマルチスレッドで共有されることによる副作用や、テストのしづらさも生じます。

初期化は一度だけで、初期値省略時は0になること、実行時の値では初期化できないこと(定数式が原則)を押さえておくと安全です。

用途が明確な場面に絞り、必要なら引数・構造体・モジュール分割や排他制御などの設計も併用してください。

「値をどこに置き、どれだけの範囲に見せるか」を意識することが、健全なCプログラミングの第一歩です。

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

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

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

URLをコピーしました!