| 局所変数とその有効範囲 | ブロックの入れ子による有効範囲の限定 | 大域変数とその有効範囲 | 関数の仮引数の有効範囲 | 演習問題 |
そもそもプログラミング言語で変数を使うためには, 値を格納する場所をメモリ上に確保し, その変数名でそこにアクセスできるようにしなければなりません. C言語では変数の定義でそれを行います.
これについて説明するために, プログラミングAで出てきた階乗を計算するプログラムを 再びここで挙げてみましょう(ただし関数プロトタイプなどを追加してあります).
#include <stdio.h> int factorial(int); int main(void) { int n, fac; printf("n="); scanf("%d", &n); fac=factorial(n); printf("factorial = %d\n", fac); return 0; } int factorial(int n) { int i,f; f=n; for(i=n-1; i>1; i--) { f*=i; } return f; }
このプログラムに現れている変数の定義は,関数mainの中の
int n, fac;
および,関数factorialの中の
int i,f;
です.プログラミングAで学んだように,これにより n, fac, i, fという名前の変数にint型の値を格納することが できるようになります.しかし実はこれらの変数はプログラムの中で いつでもどこでも使えるというわけではありません. ではこれらはいつそしてどこで 使えるのでしょうか.
ここではまず,これらの変数が定義されている場所に注目してみましょう. 定義が行われているのは{}で囲まれた部分の先頭です.
C言語では{}を用いて複数の定義や文などを一つにまとめて 単一の文と同等に扱うことができます. これをブロックもしくは複合文と呼びます. といっても特に目新しいものではなく, 関数の定義に現れる{}がその典型ですし, if文やfor文の例でも現れているので既にご存じでしょう.
ここで現れた変数のようにブロック内部の先頭で定義されている変数は 局所変数もしくはローカル変数と呼ばれます. その名前が示す通り,局所変数は使える場所がブロックにより限定されています. その範囲はプログラム上で変数が定義された地点から ブロックが閉じる地点までの間です. その範囲の外からはその変数を参照することは出来ません. これを局所変数の有効範囲 もしくはスコープと呼びます.
さてこれで局所変数がどこで使えるか(有効範囲,スコープ)はわかりましたが, いつ使えるか(生存期間,エクステント) については自動変数とstatic局所変数という二種類の変数の場合で異なります. その詳しい説明は次章で行いますが, とりあえずこのプログラムに現れた変数はすべて自動変数であり, それらがメモリ上にあって使用可能なのは, その変数が定義されたブロックを含む関数(mainもしくはfactorial)が 呼び出された時点からその関数が終了するまでの期間のみである, ということだけ述べておきましょう.
上で述べたようにブロックは複数の文をまとめて 単一の文と同等に扱うものですから, さらにそれを文として含むもう一つのブロックを考えることもできます. こうしてブロックは何段でも入れ子にすることができます.
といっても無制限というわけではなく,JIS規格で保証されているのは15段までです.
{ { { .... } } }
このことから想像がつくように, これまでは局所変数の有効範囲は関数のブロックでのみ考えていましたが, 入れ子になったブロックの内部で局所変数を定義することにより, 有効範囲をさらに細かく限定することができます. このような手法は,次の例のように,本当にその場所でしか使わない変数などに有効です.
ある関数 fun() はその内容が数十行あったとします. ある時,得られた値 x1 と x2 の二つの値のうち、小さい方が x1 に入っているようにしたかった,とします. つまり x1 > x2 の場合は x1 と x2 の中身を入れ替える事になりますが,その処理は以下のように書かれるでしょう.
int func() { int i, j, temp; ..... ..... ..... (いろいろな処理が数十行) ..... ..... ..... if( x1 > x2 ) { temp=x2; x2=x1; x1=temp; // 一時的に変数 temp を利用して x1, x2 の中身を入れ替える } ..... }
上のサンプルにある // はコメントのもう一つの書き方であり, コンパイラは // から行末までをコメントとして無視します.
プログラムとしては正しいとしても,この記述には一つ問題があります. 一時的にしか使わない変数 temp の宣言が余りにも遠く(何十行も前)にあり,プログラムの全体構造を見渡しにくくなっています. 以下のように if 文のブロック { } の中に変数宣言を含めてみるとどうでしょう.
int func() { int i, j; ..... ..... ..... (いろいろな処理が数十行) ..... ..... ..... if( x1 > x2 ) { int temp; temp=x2; x2=x1; x1=temp; } ..... }
入れ子になったif文のブロックの内部で局所的に定義しています. これでこの変数はここでしか使わない,ということが明確にわかり,変数 temp の定義も遠く離れたところを見ずに確認できます.
なお,ブロックの内部と外部で同じ名前の変数が定義されているときには, その変数名を使用する地点から見て一番内側の有効範囲を持つ変数が 参照されます.外側の変数は参照することができません. (ただしこのように同じ名前をつかうのは混乱を招くので実際には避けるべきです).
さらにC言語では関数のブロックを超えた有効範囲を持つ変数を使うこともできます. 次の例を見てみましょう.
#include <stdio.h> int x; // 大域変数 void twice(void); // 関数プロトタイプ int main(void) { x=5; twice(); printf("result=%d\n", x); return 0; } void twice(void) { x = x * 2; }
このプログラムにおいて変数xは関数の外部で定義されており, このような変数は関数mainからも関数twiceからも参照することができます. そのためmainでxに5が代入された後, twiceが実行されるときには同じxが参照されてその値5を2倍した値10が xに代入されます.そして最後にmainで再びその変数xが参照されて 値10がprintfで出力されます.
このように関数の外部で定義された 変数を大域変数もしくはグローバル変数と呼びます. 大域変数の定義の有効範囲はその定義から そのソースファイルの最後までの間です.すなわち その範囲で定義される関数はすべてその大域変数を参照することができます.
上の例からわかるように, このような大域変数は関数同士でデータを共有するには便利ですが, プログラムの規模が大きくなるにつれて目が行き届かなくなり 予期せぬことが起こる可能性が高くなるのと, 次章で述べるように局所変数に比べてその動作がかなり複雑なので, 大域変数の使用はなるべく避けるようにしましょう.
なお,大域変数と局所変数で同じ名前を用いた場合は, ブロックの入れ子の場合と同様に その局所変数の有効範囲内では局所変数が有効であり, 大域変数は参照できません. (局所変数 x が存在するところでは変数 x と指定すると局所変数を参照してしまうため,そこでは大域変数 x を参照することはできない.)
ここでfはmainのブロックの局所変数xの有効範囲で実行されていますが, fが定義されているのは大域変数xの有効範囲なので, fの実行で5が代入されるのは大域変数xになります. このように関数が定義されたときの有効範囲に基づいて変数を参照する動作を レキシカルスコーピングと言います. すなわちC言語はレキシカルスコーピングで動作する言語です. それに対して関数が実行されるときの有効範囲に基づいて変数を参照する プログラミング言語(例えばEmacs Lisp)の動作はダイナミックスコーピングと言います.
また,大域変数は局所変数と異なり プログラムの実行中常にメモリ上に存在します. よってその変数名の有効範囲内にあって参照できる限りは いつでも使うことができます.
C言語では関数に値が渡されるときには,その値を入れる変数を新たに用意して そこに渡された値をコピーしてそれを関数の内部で使用します (これを値渡しと呼びます). このときに用意される変数を関数の仮引数 もしくはパラメータと呼びます. 既に学んだように,仮引数を参照するための名前は関数の定義の時に与えられます. 従って関数の内部では仮引数もその名前によって局所変数と 同じように参照することができます. このような仮引数名の有効範囲は,プログラム上で 関数定義における仮引数名の宣言から その関数定義のブロックが閉じる地点までの間となります.
なお前章で述べた関数プロトタイプや関数宣言の仮引数名の有効範囲は
そのプロトタイプ宣言の中だけであり,
プログラムの他の部分に影響を与えることはありません.
もう一つ。
ここで「定義」と「宣言」を使い分けていますが、その違いについては今は説明しません。
前者がメモリ領域を割り当てるのに対し、後者は割り当てない(名前だけの存在として扱う)のが違いですが、その差が明確に現れるのは分割コンパイル時の大域変数の(定義ではない)宣言などですので、今は追求しません。
A. 以下の二つのプログラムの実行結果を予想し,なぜそうなるか説明しなさい. そして実際に実行してその予想と合っているか確認しなさい.
#include <stdio.h> int f(void) { int x; x=0; x++; return x; } int main(void) { printf("answer=%d\n", f()); printf("answer=%d\n", f()); printf("answer=%d\n", f()); return 0; }
#include <stdio.h> int x; int f(void) { x++; return x; } int main(void) { x=0; printf("answer=%d\n", f()); printf("answer=%d\n", f()); printf("answer=%d\n", f()); return 0; }
B. 次のプログラムを変更して,関数factが呼び出されるたびに それが何回目の呼び出しかを表示するようにしなさい. ただし回数を数えるのに大域変数を用いること.
#include <stdio.h> #define NUMBER 5 int fact(int a) { if (a > 1) return a * fact(a-1); else return 1; } int main(void) { int x; x=NUMBER; printf("%d! = %d\n", x, fact(x)); return 0; }
表示は例えば以下のようになる.
count = 1 count = 2 count = 3 count = 4 count = 5 5! = 120