【C言語】変数にstaticをつけるとどうなるのか解説

C言語のプログラミングを学ぶ中で、変数の使い方は非常に重要です。

特に、staticというキーワードを使った変数は、他の変数とは異なる特性を持っています。

この記事では、static変数の基本的な概念から、スコープやライフタイム、使用例、利点と欠点、他の変数との違い、注意点までをわかりやすく解説します。

これを読むことで、static変数の使い方やそのメリット・デメリットを理解し、プログラムをより効果的に作成できるようになるでしょう。

目次から探す

static変数の基本概念

C言語におけるstaticは、変数のストレージクラスを指定するキーワードです。

staticを使うことで、変数のスコープやライフタイムを制御することができます。

これにより、プログラムの動作をより柔軟に管理することが可能になります。

staticの意味

staticは、変数のスコープ(有効範囲)とライフタイム(生存期間)を変更するために使用されます。

通常、C言語の変数は、関数が呼び出されるたびに新たに生成され、関数が終了すると消失します。

しかし、staticを付けることで、変数は関数の呼び出しが終わってもその値を保持し続けることができます。

具体的には、static変数は以下の特性を持ちます:

  • スコープ: static変数は、定義されたブロック内でのみアクセス可能です。

つまり、関数内で定義された場合、その関数内でのみ有効です。

  • ライフタイム: プログラムの実行中、static変数はプログラムが終了するまで存在し続けます。

初期化は一度だけ行われ、以降はその値が保持されます。

static変数の定義

static変数は、関数内またはファイル内で定義することができます。

以下に、static変数の定義方法を示します。

  1. 関数内のstatic変数: 関数内でstaticを使って変数を定義すると、その変数は関数が呼び出されるたびに初期化されず、前回の値を保持します。
#include <stdio.h>
void counter() {
    static int count = 0; // static変数の定義
    count++;
    printf("Count: %d\n", count);
}
int main() {
    counter(); // Count: 1
    counter(); // Count: 2
    counter(); // Count: 3
    return 0;
}

この例では、countというstatic変数が関数counter内で定義されています。

関数が呼び出されるたびにcountの値が増加し、前回の値が保持されていることがわかります。

  1. グローバルstatic変数: ファイル内でstaticを使って変数を定義すると、その変数はそのファイル内でのみ有効になります。

他のファイルからはアクセスできません。

#include <stdio.h>
static int globalCount = 0; // グローバルstatic変数の定義
void increment() {
    globalCount++;
}
void display() {
    printf("Global Count: %d\n", globalCount);
}
int main() {
    increment();
    display(); // Global Count: 1
    return 0;
}

この例では、globalCountというstatic変数がファイル内で定義されています。

この変数は、他のファイルからはアクセスできず、同じファイル内の関数からのみ操作できます。

このように、static変数を使うことで、変数のスコープやライフタイムを制御し、プログラムの動作をより効率的に管理することができます。

static変数のスコープとライフタイム

スコープとは

スコープとは、プログラム内で変数が有効な範囲を指します。

C言語では、変数のスコープは主に以下の3つに分類されます。

  1. ブロックスコープ: 変数が定義されたブロック内でのみ有効です。

例えば、関数内の中括弧 {} で囲まれた部分です。

  1. 関数スコープ: 関数内で定義された変数は、その関数内でのみ有効です。
  2. ファイルスコープ: グローバル変数は、定義されたファイル内で有効です。

ライフタイムとは

ライフタイムは、変数がメモリに存在する期間を指します。

C言語では、変数のライフタイムは以下のように分類されます。

  1. 自動変数: 関数が呼び出されるとメモリが割り当てられ、関数が終了するとメモリが解放されます。

通常のローカル変数がこれに該当します。

  1. 静的変数: プログラムの実行中、メモリに常駐し続ける変数です。

関数が終了してもメモリは解放されず、次回関数が呼び出されたときに前回の値を保持します。

  1. グローバル変数: プログラム全体で有効で、プログラムの実行が終了するまでメモリに存在します。

static変数のスコープ

static修飾子を使った変数は、そのスコープによって異なります。

関数内で定義されたstatic変数は、その関数内でのみ有効ですが、通常のローカル変数とは異なり、関数が終了してもメモリに残ります。

一方、ファイルスコープのstatic変数は、そのファイル内でのみ有効で、他のファイルからはアクセスできません。

以下は、関数内のstatic変数の例です。

#include <stdio.h>
void counter() {
    static int count = 0; // static変数の定義
    count++;
    printf("Count: %d\n", count);
}
int main() {
    counter(); // Count: 1
    counter(); // Count: 2
    counter(); // Count: 3
    return 0;
}

この例では、countstatic変数であり、関数counterが呼び出されるたびにその値が保持されます。

static変数のライフタイム

static変数のライフタイムは、プログラムの実行中ずっと続きます。

つまり、static変数はプログラムが終了するまでメモリに存在し続け、初期化はプログラムの開始時に一度だけ行われます。

これにより、関数が何度呼び出されても、static変数は前回の値を保持し続けることができます。

以下は、グローバルstatic変数の例です。

#include <stdio.h>
static int globalCount = 0; // グローバルstatic変数の定義
void increment() {
    globalCount++;
    printf("Global Count: %d\n", globalCount);
}
int main() {
    increment(); // Global Count: 1
    increment(); // Global Count: 2
    return 0;
}

この例では、globalCountはグローバルスコープのstatic変数であり、increment関数が呼び出されるたびにその値が保持されます。

他のファイルからはアクセスできないため、名前の衝突を避けることができます。

static変数の使用例

C言語におけるstatic変数は、特定の用途において非常に便利です。

ここでは、関数内のstatic変数、グローバルstatic変数、そしてstatic変数の初期化について詳しく解説します。

関数内のstatic変数

関数内でstatic変数を定義すると、その変数は関数が呼び出されるたびに新たに作成されるのではなく、最初の呼び出し時に一度だけ初期化され、その後は関数が終了しても値が保持されます。

これにより、関数が再度呼び出されたときに前回の値を利用することができます。

以下は、関数内のstatic変数の例です。

#include <stdio.h>
void countCalls() {
    static int callCount = 0; // static変数の定義
    callCount++; // 呼び出し回数をカウント
    printf("関数が呼ばれた回数: %d\n", callCount);
}
int main() {
    countCalls(); // 1回目の呼び出し
    countCalls(); // 2回目の呼び出し
    countCalls(); // 3回目の呼び出し
    return 0;
}

このプログラムを実行すると、以下のような出力が得られます。

関数が呼ばれた回数: 1
関数が呼ばれた回数: 2
関数が呼ばれた回数: 3

このように、static変数は関数の呼び出し回数を記録するのに役立ちます。

グローバルstatic変数

グローバルstatic変数は、ファイル内でのみアクセス可能な変数です。

これにより、他のファイルからのアクセスを防ぎ、名前の衝突を避けることができます。

グローバルstatic変数は、プログラム全体の実行中にその値を保持します。

以下は、グローバルstatic変数の例です。

#include <stdio.h>
static int globalCount = 0; // グローバルstatic変数の定義
void incrementCount() {
    globalCount++; // グローバル変数をインクリメント
}
void printCount() {
    printf("グローバルカウント: %d\n", globalCount);
}
int main() {
    incrementCount();
    incrementCount();
    printCount(); // グローバルカウントを表示
    return 0;
}

このプログラムを実行すると、以下のような出力が得られます。

グローバルカウント: 2

このように、グローバルstatic変数は、関数間でデータを共有するのに便利です。

static変数の初期化

static変数は、初期化を行わない場合、自動的にゼロで初期化されます。

これは、関数内のstatic変数でも、グローバルstatic変数でも同様です。

初期化を明示的に行うこともできますが、初期化を省略した場合でも、C言語が自動的にゼロで初期化してくれます。

以下は、static変数の初期化の例です。

#include <stdio.h>
void displayValue() {
    static int value; // 初期化を省略
    printf("値: %d\n", value); // 初期化されているため、0が表示される
    value++; // 値をインクリメント
}
int main() {
    displayValue(); // 1回目の呼び出し
    displayValue(); // 2回目の呼び出し
    return 0;
}

このプログラムを実行すると、以下のような出力が得られます。

値: 0
値: 1

このように、static変数は初期化を省略しても自動的にゼロで初期化されるため、初期値を意識せずに使用することができます。

static変数の利点と欠点

C言語におけるstatic変数は、特定の利点と欠点を持っています。

これらを理解することで、プログラムの設計や実装において適切に利用することができます。

利点

データの保持

static変数の最大の利点は、関数が呼び出されるたびにその値が保持されることです。

通常のローカル変数は、関数の呼び出しが終了するとメモリから消去されますが、static変数はプログラムの実行中ずっとその値を保持します。

これにより、関数が再度呼び出されたときに、前回の状態を簡単に再利用することができます。

例えば、以下のコードを見てみましょう。

#include <stdio.h>
void countCalls() {
    static int callCount = 0; // static変数の定義
    callCount++; // 呼び出し回数をカウント
    printf("関数が呼ばれた回数: %d\n", callCount);
}
int main() {
    countCalls(); // 1回目の呼び出し
    countCalls(); // 2回目の呼び出し
    countCalls(); // 3回目の呼び出し
    return 0;
}

このプログラムを実行すると、関数countCallsが呼ばれるたびに呼び出し回数が増加し、最終的に3回呼ばれたことが表示されます。

static変数callCountは、関数のスコープ内でのみアクセス可能ですが、その値は関数の呼び出し間で保持されます。

名前の衝突を避ける

static変数は、スコープが限られているため、他のファイルや関数で同じ名前の変数を定義しても衝突しません。

これにより、プログラムの可読性が向上し、意図しないバグを防ぐことができます。

例えば、以下のように異なるファイルで同じ名前のstatic変数を定義しても、互いに影響を与えることはありません。

// file1.c
static int value = 10;
// file2.c
static int value = 20;

この場合、file1.cfile2.cvalueはそれぞれ独立しており、他のファイルからアクセスすることはできません。

欠点

メモリ使用量

static変数はプログラムの実行中ずっとメモリに保持されるため、必要以上にメモリを消費する可能性があります。

特に、static変数が多くの関数で定義されている場合、メモリの使用量が増加し、リソースの無駄遣いにつながることがあります。

例えば、以下のように多くの関数でstatic変数を使用すると、メモリの消費が増加します。

void functionA() {
    static int a = 0;
    // 何らかの処理
}
void functionB() {
    static int b = 0;
    // 何らかの処理
}
// さらに多くの関数でstatic変数を使用する場合

このように、static変数を多用することは、特にメモリが限られている環境では注意が必要です。

テストの難しさ

static変数は、関数の外部からアクセスできないため、ユニットテストを行う際に問題が生じることがあります。

特に、関数の状態をリセットすることが難しく、テストの結果が他のテストに影響を与える可能性があります。

例えば、static変数を使用している関数をテストする場合、前回のテストの影響を受けることがあるため、テストの独立性が損なわれることがあります。

このため、static変数を使用する際は、テストの設計に注意が必要です。

以上のように、static変数には利点と欠点が存在します。

これらを理解し、適切に利用することで、より効果的なプログラムを作成することができます。

staticと他のストレージクラスの比較

C言語には、変数のストレージクラスを指定するためのいくつかのキーワードがあります。

ここでは、static変数と他のストレージクラスであるautoexternregisterとの違いについて詳しく解説します。

auto変数との違い

autoは、C言語におけるデフォルトのストレージクラスです。

関数内で宣言された変数は、特に指定しなくてもautoとして扱われます。

auto変数の特徴は以下の通りです。

  • スコープ: auto変数は、宣言されたブロック内でのみ有効です。

ブロックを抜けると、その変数は破棄されます。

  • ライフタイム: auto変数は、関数が呼び出されるたびに新たに生成され、関数が終了すると破棄されます。

つまり、関数の呼び出しごとに初期化されます。

一方、static変数は、関数内で宣言されていても、関数が終了してもその値を保持し続けます。

次回関数が呼び出されたときには、前回の値がそのまま使用されます。

#include <stdio.h>
void autoVariableExample() {
    auto int count = 0; // auto変数
    count++;
    printf("auto変数の値: %d\n", count);
}
void staticVariableExample() {
    static int count = 0; // static変数
    count++;
    printf("static変数の値: %d\n", count);
}
int main() {
    for (int i = 0; i < 3; i++) {
        autoVariableExample(); // auto変数の例
    }
    
    for (int i = 0; i < 3; i++) {
        staticVariableExample(); // static変数の例
    }
    
    return 0;
}

このプログラムを実行すると、auto変数は毎回0から始まるのに対し、static変数は前回の値を保持していることがわかります。

extern変数との違い

externは、他のファイルやスコープで定義された変数を参照するために使用されます。

extern変数の特徴は以下の通りです。

  • スコープ: extern変数は、プログラム全体でアクセス可能です。

異なるファイルからも参照できます。

  • ライフタイム: extern変数は、プログラムの実行中ずっと存在します。

プログラムが終了するまでその値を保持します。

static変数は、同じファイル内でのみ有効であり、他のファイルからはアクセスできません。

これにより、名前の衝突を避けることができます。

// file1.c
#include <stdio.h>
static int staticVar = 10; // file1.c内でのみ有効
void printStaticVar() {
    printf("file1.cのstatic変数: %d\n", staticVar);
}
// file2.c
#include <stdio.h>
extern int externVar; // 他のファイルで定義されたextern変数
void printExternVar() {
    printf("file2.cのextern変数: %d\n", externVar);
}

このように、static変数はファイル内でのスコープを持ち、extern変数はプログラム全体でのスコープを持つため、用途が異なります。

register変数との違い

registerは、変数をCPUのレジスタに格納することを示すストレージクラスです。

これにより、変数へのアクセスが高速化される可能性があります。

register変数の特徴は以下の通りです。

  • スコープ: register変数は、auto変数と同様に、宣言されたブロック内でのみ有効です。
  • ライフタイム: register変数も、auto変数と同様に、関数が呼び出されるたびに新たに生成され、関数が終了すると破棄されます。

ただし、register変数は、メモリに格納されるのではなく、CPUのレジスタに格納されるため、ポインタを持つことができません。

これに対して、static変数はメモリに格納され、プログラムの実行中ずっとその値を保持します。

#include <stdio.h>
void registerVariableExample() {
    register int count = 0; // register変数
    for (int i = 0; i < 5; i++) {
        count++;
        printf("register変数の値: %d\n", count);
    }
}
int main() {
    registerVariableExample(); // register変数の例
    return 0;
}

このプログラムでは、register変数が使用されていますが、static変数とは異なり、関数が終了するとその値は失われます。

以上のように、static変数はその特性から特定の用途に適しており、他のストレージクラスとの違いを理解することで、より効果的にC言語を活用することができます。

static変数の注意点

C言語におけるstatic変数は非常に便利ですが、使用する際にはいくつかの注意点があります。

ここでは、スレッドセーフ性、初期化のタイミング、再入可能性について詳しく解説します。

スレッドセーフ性

スレッドセーフ性とは、複数のスレッドが同時に同じデータにアクセスした場合に、データの整合性が保たれることを指します。

static変数は、プログラム全体で共有されるため、特にマルチスレッド環境では注意が必要です。

例えば、以下のようなコードを考えてみましょう。

#include <stdio.h>
#include <pthread.h>
static int counter = 0; // static変数
void* increment(void* arg) {
    for (int i = 0; i < 100000; i++) {
        counter++; // counterをインクリメント
    }
    return NULL;
}
int main() {
    pthread_t thread1, thread2;
    pthread_create(&thread1, NULL, increment, NULL);
    pthread_create(&thread2, NULL, increment, NULL);
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    printf("Counter: %d\n", counter); // 結果は予測できない
    return 0;
}

このプログラムでは、2つのスレッドが同時にcounterというstatic変数をインクリメントしています。

スレッドが同時にアクセスするため、最終的なcounterの値は予測できず、データの整合性が損なわれる可能性があります。

このような場合、ミューテックスやセマフォを使用して、スレッド間の排他制御を行う必要があります。

初期化のタイミング

static変数は、プログラムの実行時に一度だけ初期化されます。

初期化のタイミングは、プログラムの実行開始時であり、関数内で定義されたstatic変数は、その関数が最初に呼ばれたときに初期化されます。

以下の例を見てみましょう。

#include <stdio.h>
void function() {
    static int count = 0; // static変数
    count++;
    printf("Count: %d\n", count);
}
int main() {
    function(); // Count: 1
    function(); // Count: 2
    function(); // Count: 3
    return 0;
}

このプログラムでは、functionが呼ばれるたびにcountがインクリメントされます。

最初の呼び出し時にcountが初期化され、その後はその値が保持され続けます。

このように、static変数の初期化は一度だけ行われるため、意図しない初期化を避けることができます。

再入可能性

再入可能性とは、関数が同時に複数のスレッドから呼ばれた場合でも、正しく動作することを指します。

static変数を使用する関数は、再入可能性がないと見なされることが多いです。

なぜなら、static変数は関数の呼び出し間で状態を保持するため、同時に呼ばれた場合に状態が競合する可能性があるからです。

以下の例を考えてみましょう。

#include <stdio.h>
static int value = 0; // static変数
void updateValue() {
    value++;
    printf("Value: %d\n", value);
}
int main() {
    updateValue(); // Value: 1
    updateValue(); // Value: 2
    return 0;
}

このプログラムでは、updateValue関数が呼ばれるたびにvalueがインクリメントされます。

しかし、もしこの関数がマルチスレッド環境で同時に呼ばれた場合、valueの値は予測できない結果になる可能性があります。

再入可能性を確保するためには、static変数の使用を避けるか、適切な排他制御を行う必要があります。

以上のように、static変数を使用する際には、スレッドセーフ性、初期化のタイミング、再入可能性に注意を払うことが重要です。

これらの点を理解し、適切に対処することで、より安全で効率的なプログラムを作成することができます。

目次から探す