どの変数にもある2つの問い
C言語のどの変数にも別々の2つの性質があり、これを混同することがこのページの驚きの大半の源です。
- スコープ - その名前をどこで使えるか。コンパイル時の問いです。
- 生存期間 - その記憶領域がどれだけ長く存在するか。実行時の問いです。
普通この2つは一緒に動きます(ふつうのローカル変数は自分のブロックで見え、ちょうどその間だけ生きます)が、static はこれを切り離します。だからこそここで理解する価値のあるキーワードなのです。
ブロックスコープ
波かっこの中で宣言された変数は、その宣言から閉じ波かっこまで見えており、それ以外のどこからも見えません。
内側のブロックは outer を見られます。その中に入れ子になっているからです。外側のブロックは inner を見られません。これはあらゆる波かっこの対に当てはまります - 関数の本体、if の本体、ループの本体、あるいは上のような裸のブロックです。
ループがそれを具体的にしてくれます。for のヘッダーで宣言されたカウンタは、そのループのものです。
for (int i = 0; i < 5; i++) {
/* i はここに生きている */
}
/* ここに i は存在しない */
これこそ望ましい挙動です。同じ関数の中の2つのループがそれぞれ干渉なく i を使えますし、ループのあとで誤ってカウンタを読むこともできません。あとでその値が必要なら - 探索が止まった添字など - ループの前で宣言しましょう。
関数の引数は関数本体のスコープを持つので、本体の先頭で宣言されたローカル変数のように振る舞います。
C99 では、ブロックの先頭だけでなくどこででも宣言できます。変数は最初に必要になる場所で宣言しましょう。スコープの短い変数は、間違えようのある道筋が少ない変数です。
生存期間:自動記憶域
ふつうのローカル変数は自動記憶域を持ちます。制御がそのブロックに入ると存在を始め、出ると破棄されます。そのメモリはスタックにあります。
これは 1 を3回表示します。呼び出しごとに真新しい count が 0 に初期化され、戻るときに捨てられます。再帰関数のフレームがそれぞれすべてのローカル変数の自分のコピーを持つのも、これが理由です。
はっきり述べておく価値のある帰結が2つあります。初期化されていないローカル変数にはゼロではなくゴミが入っています - スタック上にたまたまあったバイト列です。そしてローカル変数へのポインタを返すのは未定義動作です。関数が戻った瞬間にその記憶領域は消えるからです。
/* BUG: buf は return のあと存在しない */
char *broken(void) {
char buf[64] = "hello";
return buf;
}
static なローカル変数:呼び出しの間の記憶
ローカル変数に static を付けると、その生存期間はプログラム全体になり、スコープはまったく変わりません。
今度はカウンタが 1、2、3 を表示します。= 0 は main が始まる前に一度だけ実行され、呼び出しごとではありません。
static なローカル変数の3つの規則です。
- 初期化子は定数式でなければなりません。実行時ではなくプログラム開始時に適用されるからです。
- 初期化子がなければ
staticはゼロ初期化されます(ゴミを保持する自動ローカル変数とは違います)。 - 変数は依然として関数に閉じています。他のどのコードもその名前を呼べません。
この最後の点が魅力のすべてです。何でも変更しうるグローバル変数を晒すことなく、関数が呼び出しの間に何かを覚えておけるのです。呼び出し回数のカウンタ、初回使用時に構築されるキャッシュ用の表、一度きりの初期化フラグに使いましょう。
代償は、その関数がもはや入力から出力への純粋な対応でなくなることです - 同じ引数が違う答えを返しうる - ので、テストが難しくなり、保護なしに複数スレッドから同時に呼ぶのは危険になります。static なローカル変数は便利だからではなく、意図して使いましょう。
ファイルスコープ:グローバル変数
すべての関数の外で宣言された変数はファイルスコープを持ちます。その宣言からファイルの終わりまで、その下のすべての関数から見え、生存期間はプログラム全体です。
グローバル変数は既定でゼロ初期化されるので、= 0 がなくても totalOperations は 0 から始まります。
既定ではグローバル変数は外部リンケージも持ちます。同じプログラム内の他の .c ファイルが extern と宣言することで到達できるのです。
/* stats.c の中 */
int totalOperations = 0; /* 定義 - プログラム内にちょうど1つ */
/* main.c の中 */
extern int totalOperations; /* 宣言: 「どこかに存在する」 */
extern の宣言はヘッダーファイルに置き、すべての利用者が同じものを見るようにしましょう。非対称性に注意してください。extern の宣言は変数が存在すると言うだけで、ちょうど1つの .c ファイルが実際にそれを定義しなければなりません。
グローバル変数が噛みつく理由
グローバル変数はデータを共有する最も簡単な方法であり、局所化できないバグを作る最も簡単な方法でもあります。具体的な問題を4つ挙げます。
何からでも変更できる。 totalOperations が間違った値を持っているとき、犯人はどのファイルのどの行でもあり得ます。引数なら、容疑者は目に見える呼び出し箇所だけです。
関数がテスト不能になる。 グローバル変数を読む関数は単独では呼べません。まずグローバルな状態を用意し、あとで戻すことも忘れないようにしなければなりません。
名前がプログラム全体のもの。 count や buffer という名前のグローバル変数は、いずれ誰かのものと衝突します。
初期化子はコンパイル時定数でなければならない。 C言語では、グローバル変数は定数式でしか初期化できません - int limit = readConfig(); はコンパイルできませんし、ファイルスコープの int b = a * 2; も同じです。計算した値はどれも、何らかの初期化関数が実行時に代入するしかなく、その呼び出しより前にそのグローバル変数に触れるコードは、黙って 0 を見ることになります。
代替手段はほとんど常にあります。値を引数として渡し、結果を返し、関連する状態は構造体にまとめて明示的に渡し回しましょう。本物の例外もあります - プログラム全体の設定オブジェクトやログのハンドルなど - が、それらもアクセサ関数を伴うファイルスコープの static にするほうが優れており、それが次の節です。
ファイルスコープの static:非公開化
グローバル変数や関数に付いた static は、ローカル変数に付いたときとまったく違う意味を持ちます。内部リンケージです。その名前は自分の .c ファイルに閉じ、他のどこからも到達できなくなります。
/* counter.c */
#include "counter.h"
static int count = 0; /* このファイルに閉じている - 他のファイルは触れない */
static void validate(void) { /* インターフェースに含まれない非公開ヘルパー */
if (count < 0) count = 0;
}
void increment(void) { /* 公開: counter.h で宣言されている */
count++;
validate();
}
int getCount(void) { /* 公開 */
return count;
}
他のファイルは increment と getCount を呼べますが、count は見えず、validate も呼べません - リンカがそれらの名前を解決してくれないのです。これがC言語のモジュールシステムのすべてです。公開関数を宣言するヘッダーと、それ以外すべてに付ける static です。
整頓以外の実用的な利点が2つあります。2つのファイルがそれぞれ static void validate(void) を持っても衝突しません - 関数が公開だったらこうはいきません。そしてコンパイラは static な関数にファイル外からの呼び出し元がないと分かるので、より積極的にインライン化したり、使われていないと警告したりできます。
つまりこのキーワードには、どこに現れるかで決まる2つの異なる意味があります。
ローカル変数の static -> 生存期間が恒久になる(スコープは不変)
ファイルスコープの static -> リンケージが内部になる(生存期間は不変)
シャドーイング
外側のスコープにすでに存在する名前を内側のスコープで宣言すると、外側の名前を覆い隠します(シャドーイング)。内側のスコープの中では、その名前は新しい変数を指し、外側のものには手が届きません。
これは合法であり、ときには意図的でもあります。しかしもっと多くの場合は事故で、しかも高くつく事故です。グローバル変数を更新するつもりの関数が、代わりに同じ名前のローカル変数を更新し、グローバルは決して変わりません。ローカル変数が引数を覆い隠したときにも同じことが起き、入力に対して意図した代入がどこにも届きません。
gcc -Wshadow がすべての場合を報告します。-Wall には含まれていないので、明示的に有効にしましょう。
gcc -Wall -Wextra -Wshadow program.c -o program
この問題を完全に避ける習慣は、型ではなく役割で名前を付けることです。totalScore と itemScore は互いを覆い隠せませんが、どちらも n という名前の2つの変数はいずれそうなります。
早見表
ブロックの中で宣言 ブロックスコープ、自動生存期間、未初期化ならゴミ
関数の引数 関数本体のブロックスコープ、引数のコピー
関数の中の static ブロックスコープ、プログラム生存期間、ゼロ初期化、呼び出し間で保持
すべての関数の外で宣言 ファイルスコープ、プログラム生存期間、ゼロ初期化、他ファイルから見える
すべての関数の外の static ファイルスコープ、プログラム生存期間、この .c ファイルに閉じる
extern 宣言 他のファイルで定義された変数を名指しする
よくある質問
C言語のスコープとは何ですか?
名前が見えているコードの領域のことです。ブロック(波かっこの対)の中で宣言された変数は、そのブロックの中でだけ見えます - これがブロックスコープです。すべての関数の外で宣言されたものはファイルスコープを持ち、その宣言からファイルの終わりまで見えます。
C言語のローカル変数に static を付けると何が起きますか?
スコープを変えずに、変数の生存期間を変えます。static なローカル変数は一度だけ作られ一度だけ初期化され、呼び出しの間も値を保ちますが、見えるのは依然としてその関数の中だけです。グローバル変数を使わずに関数に記憶を持たせる方法です。
C言語の static とグローバル変数の違いは?
素のグローバル変数はプログラム中のすべてのファイルから見え、他のファイルは extern で到達できます。ファイルスコープの static な変数や関数は、その1つの .c ファイルに閉じており、他のファイルからリンクできません。ファイルスコープの static は隠すこと、ローカル変数の static は生存期間の話です。
なぜC言語でグローバル変数は良くないのですか?
どの関数からも変更できるのでバグの原因がプログラムのどこにでもあり得ますし、グローバル変数を読む関数は単独でテストできず、名前がプログラム全体を占めるので衝突を招きます。代わりに値を引数として渡し、結果を返しましょう。共有状態が本当に必要な場合は、1つのファイルだけが触れるようファイルスコープの static にします。