局所変数,大域変数,有効範囲

| 局所変数とその有効範囲 | ブロックの入れ子による有効範囲の限定 | 大域変数とその有効範囲 | 関数の仮引数の有効範囲 | 演習問題 |

局所変数とその有効範囲

そもそもプログラミング言語で変数を使うためには, 値を格納する場所をメモリ上に確保し, その変数名でそこにアクセスできるようにしなければなりません. 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段までです.

{
  {
    {
      ....
    }
  }
}

このことから想像がつくように, これまでは局所変数の有効範囲は関数のブロックでのみ考えていましたが, 入れ子になったブロックの内部で局所変数を定義することにより, 有効範囲をさらに細かく限定することができます. このような手法は,例えば本当にその場所でしか使わない変数などに有効です. そのような変数はなるべく他の部分に影響が及ばないようにしておきたいですし, 変数の定義の位置が使う位置から遠くなると その場所でしか使わないということがわかりにくくなるからです.

なお,ブロックの内部と外部で同じ名前の変数が定義されているときには, その変数名を使用する地点から見て一番内側の有効範囲を持つ変数が 参照されます.外側の変数は参照することができません. (ただしこのように同じ名前をつかうのは混乱を招くので実際には避けるべきです).

このようなテクニックは例えば次のように用いることが考えられます.

void bubble_sort(int a[], int n)
{
  int i,j;

  for (i=0; i<n-1; i++)
    for (j=n-1; j>i; j--)
      if (a[j-1]>a[j])
      {
        int t;                           // 一時的な変数
        t = a[j]; a[j]=a[j-1]; a[j-1]=t; // a[j]とa[j-1]を交換
      }
}

ただし//から行末まではコメントの もう一つの書き方であり,コンパイラはこの部分を無視します.

これはバブルソートと呼ばれるデータを順番に並べ替える アルゴリズムを実装した関数ですが, a[j]a[j-1] の内容を入れ替えるときに一時的に使う変数tを 入れ子になったif文のブロックの内部で局所的に定義しています.

ページ先頭へ戻る

大域変数とその有効範囲

さらに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で出力されます.

このように関数の外部で定義された 変数を大域変数もしくはグローバル変数と呼びます. 大域変数の定義の有効範囲はその定義から そのソースファイルの最後までの間です.すなわち その範囲で定義される関数はすべてその大域変数を参照することができます.

上の例からわかるように, このような大域変数は関数同士でデータを共有するには便利ですが, プログラムの規模が大きくなるにつれて目が行き届かなくなり 予期せぬことが起こる可能性が高くなるのと, 次章で述べるように局所変数に比べてその動作がかなり複雑なので, 大域変数の使用はなるべく避けるようにしましょう

特にBASICのプログラミングなどで大域変数を使う癖が付いている人は 今のうちに直しておきましょう.

なお,大域変数と局所変数で同じ名前を用いた場合は, ブロックの入れ子の場合と同様に その局所変数の有効範囲内では局所変数が有効であり, 大域変数は参照できません.

ここで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

ページ先頭へ戻る