本文へスキップ
BecomeCoder

C言語コース · 第14章 複数ファイルと堅牢化 · レッスン51

ストレージクラスと修飾子 ― static・extern・const・volatile

ブラウザで完結

導入

関数を呼ぶたびに「前回どこまで進んだか」を覚えていてほしい、という場面があります。普通のローカル変数は呼び出しのたびに作り直され、関数を抜けると値が消えてしまいます。static を付けるだけで、ローカル変数に「値を覚えたままにする」という特別な性質を持たせられます。

説明

関数内の static ― 値を覚えているローカル変数

#include <stdio.h>

void increment(void) {
    static int count = 0;   /* 初期化は最初の呼び出し時に1回だけ */
    count++;
    printf("%d\n", count);
}

int main(void) {
    increment();
    increment();
    increment();
    return 0;
}

普通のローカル変数なら increment を呼ぶたびに count は毎回 0 に戻るはずですが、static を付けた変数は関数を抜けても値が消えず、次に呼ばれたときに前回の値から続きます。だから出力は 123 の3行になります。static なローカル変数の実体は、関数を呼ぶたびに積まれて消えるスタックではなく、プログラムが動いている間ずっと存在する領域に置かれます。

ファイルスコープの static と extern ― 公開する・しないの対比

static は、関数の外(ファイルの先頭など)に書いた変数・関数にも付けられます。この場合の意味は「値を覚える」ではなく、「このファイルの中だけで使える(他のファイルからは見えない)」という内部リンケージです。反対に、前のレッスンで見た extern は「他のファイルにも公開する」という外部リンケージの宣言でした。2つのファイルがある場面を想定すると、対比がはっきりします。

/* util.c */
static int internalCounter = 0;   /* このファイルの中だけで使える。他の.cからは見えない */
int publicTotal = 0;              /* static を付けなければ、既定で他のファイルにも公開される */

void bump(void) {
    internalCounter++;
    publicTotal += internalCounter;
}
/* main.c */
extern int publicTotal;   /* util.c にある publicTotal を「借りてくる」宣言 */
/* extern int internalCounter; と書いても、util.c 側が static なのでリンクできずエラーになる */

同じ「ファイルの外側で static」でも、関数に付けたときは「このファイルの中だけで呼べる関数」という意味になります。プロジェクト内の他ファイルから呼ばれたくない補助関数に static を付けておくと、うっかり誤って外から使われる事故を防げます。

const ― 書き換え禁止の意図を示す

const を付けた変数は、初期化したあとに値を書き換えることができません。書き換えようとするコードは、実行前のコンパイルの時点でエラーになります。

#include <stdio.h>

int main(void) {
    const double TAX_RATE = 0.1;   /* この後、TAX_RATE は書き換えられない */
    int price = 1000;
    printf("%.0f\n", price * (1 + TAX_RATE));
    return 0;
}

値そのものは普通の変数と同じですが、「この値はこの先変わらない」という意図をコンパイラと読む人の両方に伝えられるのが const の役割です。関数の引数を const char *name のように書けば、「この関数は name の中身を書き換えません」という約束にもなります。

volatile ― 最適化で消させない

volatile は、これまでの3つとは少し毛色が違います。コンパイラは通常、プログラムの見た目の動きが変わらない範囲で、無駄な読み書きを省略する最適化を行います。しかし、ハードウェアのレジスタや割り込みハンドラが書き換える変数など、「コンパイラの知らないところで値が変わりうる」変数に対してこの最適化をされると、値の変化を見逃してしまうことがあります。volatile int flag; のように付けると、「この変数は毎回メモリから読み直せ、最適化で省略するな」とコンパイラに伝えられます。組み込み開発やハードウェア制御でよく使われる修飾子で、通常のアプリケーションのコードではあまり出てきません。

やってみよう

increment の呼び出し回数を4回、5回と増やして出力がどう続くか確かめましょう。static int count = 0; から static を外すとどうなるか(毎回 1 だけが表示されるようになる)も試してみてください。

演習

static なローカル変数を使って、呼ぶたびに10ずつ増える通し番号を返す関数 nextId(void) を書いてください。1回目の呼び出しで 100、2回目で 110、3回目で 120 を返すようにし、main から3回呼んで、それぞれの戻り値を1行ずつ printf("%d\n", ...) で表示してください。

ヒント1を見る

static int id = 90; のように、最初に返したい値より10小さい値で初期化しておき、関数の中で先に id += 10; してから return id; すると、1回目の呼び出しで 100 になります。

ヒント2を見る

main の中で printf("%d\n", nextId()); を3回書きます。

実際に動かしてみよう

下のエディタにCを書いて「コンパイル & 実行」を押すと、ブラウザ内で本物のCコンパイラ(clang + dlmalloc)がその場でコンパイルして実行し、出力が表示されます。構造体・共用体・malloc/free・関数ポインタ・ファイル入出力などがそのまま動きます。本文の例を書き換えて試しながら進めましょう(初回だけコンパイラの読み込みに時間がかかります。以降はキャッシュされます。この回は標準入力(scanf)は使わず、値はコードに直接書きます)。

C/C++ — ブラウザ内で本物のコンパイラで実行

ブラウザ内で本物のC/C++コンパイラ(clang + libc++ / WebAssembly)を動かします。構造体・malloc/free・std::string・vector・クラス・テンプレート・STL などがそのまま実行できます(初回のみコンパイラの読み込みに時間がかかります。以降はブラウザにキャッシュされます)。
スクロールして表示された時点でも自動で読み込まれます。