Cにおける識別子の有効範囲と変数の生存期間:目指せ! Cプログラマ(9)(2/2 ページ)
変数や関数をプログラム中のどこから使うか、変数に保存した値をいつまで使いたいか、といった要求によって、これらの定義の仕方が変わります。今回は、要求に応じて変数や関数を定義できるようになるために必要な、型修飾子、識別子の有効範囲、変数の生存期間について学びます。
オブジェクト(変数)の記憶域期間
識別子の有効範囲について理解したところで次は変数の生存期間について説明します。 識別子の有効範囲と同じように、オブジェクトにも有効となっている期間があります。
ここで言う「期間」とは、プログラムの実行のある位置からある位置まで、という意味の期間です。識別子はソースコードをコンパイルするまでに使われる静的なものですが、オブジェクトはプログラムの実行時に作られる動的なものです。
そのためオブジェクトが有効になっている期間と識別子の有効範囲とは、別々に考える必要があります。
オブジェクトが有効になると、プログラムは実際にメモリを割り当て、値を保存できる状態になります。オブジェクトが無効になると、プログラムはメモリを解放し、値も一緒に消えてしまいます。
オブジェクトが生成されてから消滅するまでの有効な期間を“記憶域期間”、あるいは“生存期間”と呼びます。記憶域期間には“自動記憶域期間”、“静的記憶域期間”、“割り付け記憶域期間”の3種類があります。割り付け記憶域期間は他の2つよりも若干複雑なので別の機会に説明することとし、ここでは初めの2つについて説明します。
(1) 自動記憶域期間
キーワード「auto」を使用して宣言されたオブジェクトは、自動記憶域期間を持ちます。このオブジェクトの生存期間は、オブジェクトが宣言されたブロックに入ったときから、そのブロックが終了するまでです(C99から追加された可変長配列型のオブジェクトは記憶域期間が異なります。これについては今後の記事で説明します)。
void func(void) { /* 変数aの生存期間開始 */ // ... auto int a = 0; // ... /* 変数aの生存期間終了 */ }
ただし、キーワード「auto」は省略することができます。この例では単に「int a = 0;」と書いても、同じように変数aは自動記憶域期間を持ちます。つまりこれまで関数の中で宣言されていた変数はすべて自動記憶域期間を持つ変数です。実際のプログラムで「auto」と明示する人はいませんので、省略されているということを知った上で書かない方が良いでしょう。
識別子aの有効範囲と終了位置は同じですが、開始位置はもっと前にあります。しかし、識別子aが有効になるまでは識別子aを使って変数aを参照することはできませんので、通常の方法では参照することはできません。
なお、自動記憶域期間を持つオブジェクトは、生成されたときに自動的に初期化されません。「int a;」と書いた場合には、変数aの値はどんな値になっているか分かりません。明示的に初期化したいときは「int a = 0;」のように書きましょう。
(2) 静的記憶域期間
静的記憶域期間を持つオブジェクトの生存期間はプログラムの開始から終了までです。main関数が開始されたときにはすでにオブジェクトは用意されていて、しかも生成と同時に初期化も行われています。そして、プログラムが終了するときまでずっと存在しています。
自動記憶域期間を持つオブジェクトでは「int a;」と宣言すると値は不定になりますが、静的記憶域期間を持つオブジェクトでは同じように書いても初期化されます。その初期値は0です。
静的記憶域期間を持つオブジェクトを宣言する方法は2つあります。
- 識別子がファイル有効範囲を持っているオブジェクトを宣言する
- キーワード「static」を使用してオブジェクトを宣言する
// 識別子aはファイル有効範囲を持っているので、変数aは静的記憶域期間を持つ。 int a = 0; void func(void) { // キーワード「static」を用いて宣言されているので、変数bは静的記憶域期間を持つ。 static int b = 0; }
この2つの変数aとbは違う場所で宣言されていますが、同じ静的記憶域期間を持ちますので、どちらもプログラム開始前に領域が確保された上で初期化され、プログラムの終了後に開放されます。
つまりこの2つの変数は識別子の有効範囲が違うだけで、記憶域期間も、初期化されるタイミングも同じです。従って、関数funcが何度呼ばれても、変数bはプログラムの開始前にたった1回だけ初期化されるだけになります。もちろん識別子bはブロック有効範囲を持っていますので、宣言されたブロックの外から識別子bを使って変数を参照することはできません。
#include <stdio.h> #include <stdlib.h> void func(void) { static int b = 0; b++; printf("b = %d\n", b); } // 関数mainが呼び出される前に、すでに変数bは初期化されている。 int main(void) { // 関数funcを2回呼び出してみる。 func(); // b = 1 func(); // b = 2 return EXIT_SUCCESS; }
まず関数mainが呼び出される前に、変数bはすでに初期化されています。関数funcを呼び出したときには変数bは初期化されず、「b++」でbの値がインクリメント(+1)されます。変数bが初期化されたときには値は0ですから、新しい値は1になり、コンソールに表示されます。2回目の呼び出でもまったく同じで、「b++」で値は2になりコンソールに表示されます。
なお、自動記憶域期間を持つ変数を“自動変数”、静的記憶域期間を持つ変数を“静的変数”と呼ぶこともあります。
コラム registerキーワード
記憶域を指定するキーワードには「auto」「static」の他に、「register」があります。registerで修飾されたオブジェクトは、なるべくアクセスが高速化されるようにコンパイルされます。
ただし、具体的にどのように処理されるのかは規定されていないため、高速化される場合もありますが、まったく変化がない場合もあります。
registerという名前は、そのオブジェクトがCPUのレジスターに割り当てられることを期待したものです。確かに自動変数や静的変数が割り当てられるメインメモリと比べると、レジスターへのアクセスはかなり高速です。
しかし、だからといって単純に変数をレジスターに割り当ててもプログラムが速く動くとは限らないのが面白いところです。レジスターの数は限られていますので、他の重要な変数へのアクセスが遅くなってしまうかもしれません。
最近はregister修飾子を使うことはありません。もしCPUのレジスターを意識するくらいの高速化が必要になったなら、CPUやコンパイラーのマニュアルをくまなく読んでみると良いでしょう。きっと、もっと良い方法が見つかるはずです。
今回学んだこと
- constで修飾された型のオブジェクトはプログラムの中で値を変更できなくなります。
- 変数にvolatile修飾子を付けると、勝手に値が変わったり、値を変えようとすると値が変わる以外の動作をする可能性があることをコンパイラに知らせます。
- 識別子の有効範囲には「ブロック有効範囲」「ファイル有効範囲」「プロトタイプ有効範囲」があります。
- ブロック有効範囲では、宣言された位置からブロックの終了まで有効です。
- ファイル有効範囲では、宣言された位置からソースファイルの終了まで有効です。
- 同じ有効範囲を持つ複数の識別子を宣言することはできません。
- 別の有効範囲を持つ複数の識別子を宣言することはできますが、同じ識別子を持つ複数のオブジェクトの有効範囲が重なることがあり、その場合は内側の有効範囲を持つ識別子のみ参照できます。
- オブジェクトの記憶域期間には「自動記憶域期間」「静的記憶域期間」「割り付け記憶域期間」があります。
- 自動記憶域期間では、宣言された位置からブロックの終了まで有効です。
- 静的記憶域期間では、プログラムの開始から終了まで有効で、プログラム開始前に1回だけ初期化されます。
Copyright © ITmedia, Inc. All Rights Reserved.
関連記事
- いまさらアルゴリズムを学ぶ意味
コーディングに役立つ! アルゴリズムの基本(1) コンピュータに「3の倍数と3の付く数字」を判断させるにはどうしたらいいか。発想力を鍛えよう - Zope 3の魅力に迫る
Zope 3とは何ぞや?(1) Pythonで書かれたWebアプリケーションフレームワーク「Zope 3」。ほかのソフトウェアとは一体何が違っているのか? - 貧弱環境プログラミングのススメ
柴田 淳のコーディング天国 高性能なIT機器に囲まれた環境でコンピュータの動作原理に触れることは可能だろうか。貧弱なPC上にビットマップの直線をどうやって引く? - Haskellプログラミングの楽しみ方
のんびりHaskell(1) 関数型言語に分類されるHaskell。C言語などの手続き型言語とまったく異なるプログラミングの世界に踏み出してみよう - ちょっと変わったLisp入門
Gaucheでメタプログラミング(1) Lispの一種であるScheme。いくつかある処理系の中でも気軽にスクリプトを書けるGaucheでLispの世界を体験してみよう