第82回 プログラミングについて『いまさらですがC言語の門を叩こう その5 ~ スコープと寿命 ~』
前回までの話で、明示的に初期化しない変数の初期値が0になるか不定のどちらかなどという曖昧な説明をしましたが、その理由に変数のスコープと寿命が関係しているのです。C言語のおおよその構造は次のような感じです。
main()
{
:
{
:
}
:
}
sub()
{
:
{
:
}
:
}
:
この構造の main の部分だけを見てみると、
-A
main()
-B
{
: -C
{
: -D
}
: -C
}
-A
となり、{ } の間をブロックと呼びます。
A 関数の外側
B 関数とブロックの間
C ブロックの中
D ブロックCの中のブロックの中
と言えます。C言語ではこのA~Dのどの部分でも変数を定義することができます。ただし、Bの部分では関数の引数の定義を行う部分です。C言語ではブロックの外側の変数に対してはいつでも参照することができます。ですので、A部で定義された変数は1つのファイル内であれば全ての関数やブロック内で参照することができ、プログラムの起動時から終了時までメモリ上に存在することになります。B部で定義された引数はその関数のブロック内の全てで参照することができ、関数が呼び出されたときに生成され、呼び出し側に戻るときに抹消されます。C部で定義された変数もB部のものと同様です。D部で定義されたものはそのブロックが開始されたときに生成され、ブロックの終了時に抹消されます。ですので次の図のようになります。
────────┐
│
main() │
──────┐ │
{ │ │
────┐│ │
: ││ │
││ │
{ ││ │
─┐ ││ │
│ ││ │
: │ ││ │
│ ││ │
←┘ ││ │
} ││ │
││ │
: ││ │
←───┘│ │
│ │
←────┘ │
} │
↓
要するに、外側のブロックで定義した変数は内側のブロックで参照できるということが言えます。これが基本です。
だからと言って常にこの基本だけではプログラムが書けないときがあります。内側のブロック内で定義した変数とその内容が抹消されたくないときもあるはずです。このようなときに、static という属性を変数に付けるのです。
int i1;
static int i2;
main()
{
int i3;
static int i4;
}
このようにしたときには、
i1 プログラムの起動時から終了時まで存在し、他のソースプログラムファイルからも、全ての部分で参照できる。
i2 プログラムの起動時から終了時まで存在するが、他のソースプログラムファイルからは参照できないが、同一ファイル内の全ての部分で参照できる。
i3 関数が呼び出されたときに生成され、呼び出し側に戻るときに抹消される。ブロック内でのみ参照できる。
i4 プログラムの起動時から終了時まで存在し、ブロック内でのみ参照できる。
のように参照できる範囲と寿命が違います。
プログラムの起動時から終了時まで存在する変数は静的変数とかスタティック変数とか呼び、ブロックの開始時に生成されブロックの終了時に消滅する変数を自動変数と通常呼んでいます。
明示的に初期化しない場合に、プログラムの起動時から終了時まで存在する変数は0に初期化され、そうでないものの初期値が不定になるのです。どうしてこのように初期化するものとしないものがあるのかというと、プログラムの実行速度を高めることとコードサイズを小さくすることが大きな目的なのです。静的な変数はプログラムの起動時に一度だけ初期化すればいいのですが、自動変数は生成されたときに暗黙に初期化していたのでは無駄な実行時間がかかってしまうからなのです。
自動変数を明示的に初期化するのを忘れたためにバグが発生することがかなり頻繁にあり、またこのバグは意外と見つけづらいので注意が必要です。
次にブロック内で外側と同じ変数名が定義された場合は、
int i; ーA
int j;
main()
{
int j; ーB
int k;
i は A部のもの。
j は B部のもの。
k は B部のもの。
{
int k; ーC
int l;
i は A部のもの。
j は B部のもの。
k は C部のもの。
l は C部のもの。
}
}
ということになります。
C言語は単純な考え方ではありますが、このようなスコープや寿命を細かく設定できるのでC言語の自由度が非常に大きくなっています。
説明の補足ですが、実際にコンピュータ内でどうやってスタティック変数と自動変数を実現しているのかをちょっとだけ簡単に説明しましょう。まず次のようなプログラムがあったとします。
int data;
main()
{
int i;
sub1();
sub2();
}
sub1()
{
int i;
{
int j;
}
}
sub2()
{
int i;
{
int j;
}
}
いま上のプログラムを実行しようとしたときに、メモリ内の空き領域が500番地以降だったとします。まず関数の外側で定義されている変数 data 用のメモリが確保されます。
500番地
|
□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
| data |
この時点で空き領域は504番地以降になります。次に関数 main のブロック内の変数iのためのメモリが確保されます。
500番地
| 504番地
| |
□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
| data |main-i|
次に関数 sub1 を呼び出します。sub1 が呼び出されたときには508番地以降が空き領域になっていますので、sub1 の i の領域を確保します。
500番地
| 504番地
| | 508番地
| | |
□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
| data |main-i|sub1-i|
次に sub1 のブロックの中のブロックを開始するときにそのブロックの j の領域を確保します。このときの空き領域は512番地以降です。
500番地
| 504番地
| | 508番地
| | | 512番地
| | | |
□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
| data |main-i|sub1-i|sub1-j|
ブロックが終了したときに sub1 のブロックの中のブロックの j を消滅させるので、516番地以降が空き領域だったのを、512番地以降を空き領域とします。
500番地
| 504番地
| | 508番地
| | |
□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
| data |main-i|sub1-i|
更に関数 sub1 が終了して呼び出し側に戻るときに、この関数内の i を消滅させるので、512番地以降が空き領域だったのを508番地以降を空き領域とします。
500番地
| 504番地
| |
□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
| data |main-i|
次に関数 sub2 を呼び出します。sub2 が呼び出されたときには508番地以降が空き領域になっていますので、sub2 の i の領域を確保します。
500番地
| 504番地
| | 508番地
| | |
□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
| data |main-i|sub2-i|
次に sub2 のブロックの中のブロックを開始するときにそのブロックの j の領域を確保します。このときの空き領域は512番地以降です。
500番地
| 504番地
| | 508番地
| | | 512番地
| | | |
□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
| data |main-i|sub2-i|sub2-j|
ブロックが終了したときに sub2 のブロックの中のブロックの j を消滅させるので、516番地以降が空き領域だったのを、512番地以降を空き領域とします。
500番地
| 504番地
| | 508番地
| | |
□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
| data |main-i|sub2-i|
更に関数 sub2 が終了して呼び出し側に戻るときに、この関数内の i を消滅させるので、512番地以降が空き領域だったのを508番地以降を空き領域とします。
500番地
| 504番地
| |
□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
| data |main-i|
という動作になります。実際のコンピュータのなかではこれほど単純ではないのですが、基本的にはこのように自動変数を実現しています。
FORTRANは全ての変数が静的に存在するので、大量なメモリを消費しますが、C言語は自動変数というものが使えますので、メモリの消費量が格段に少なくなります。このような利点もC言語で書いたプログラムがDOSなどの小さいメモリ上でも元気に動作していた理由の1つなのです。
それではまた次回。