記憶クラスと変数の初期設定

記憶クラス

記憶クラスとは変数をメモリにどのように確保・管理するか、という方法に関する名称です。 メモリに確保する方法によって変数の生存期間、有効範囲などが定まります。 その指示は記憶クラス指定子によって行いますが、 本章ではこのうちautoとstaticについて説明します。

文法上はそのほかにregisterとtypedef, externがありますが registerは効率や細かな制約を除けばautoと同様であり、 typedefは新しい型を指定するという異なる目的のために用いるのでここでは説明を省きます。 extern は複数ファイルに分割してプログラム開発をする手法で使われるので、やはりここでは説明しません(興味のある人は分割コンパイルとリンケージを参照)。

変数の利用とメモリの確保

変数を用意する、ということは「いつ・どのようにして」メモリ上にその変数の領域を確保するか、ということと同義です。 大まかに考えて、変数をメモリ上に確保する方法は以下の二種に分けられます。


動的: 必要になった時に用意する
関数が呼び出されたとき(または実行中にその変数のブロックに入ったとき)にはじめてメモリ上に変数領域が確保され、関数から(またはブロックから)出たときにメモリを解放する。

静的: 常に用意されっぱなし
プログラムの実行開始時から変数の領域は確保されており、プログラム実行中ずっとメモリ中に存在し、実行終了時に解放される。

それぞれ「変数領域をメモリ上に動的に確保する」「静的に確保する」などと言います。 今まで学んできた、C 言語の局所変数と大域変数がきれいにその両方に対応していることがわかるでしょうか。

局所変数と動的確保

(特に今まで学んできた)局所変数の振る舞いと、動的確保がうまく適合するのがわかるでしょうか。

大域変数と静的確保

逆に大域変数は静的確保とうまく合います。

この節ではメモリ確保の方法などを非常に簡単に考えて「これしかない」といった表現で説明していますが、厳密には多様な方法があり得ます。 ここでは説明の簡単さのために多くを端折っていることに注意してください。

記憶クラス指定子

つまり今までは記憶クラスについてはほとんど意識せず、「局所変数は動的」「大域変数は静的」なものとして学んできたことになります。 実際、C 言語では局所変数と大域変数は、その変数定義の位置によって区別されているため、記憶クラスを明示的に指定する必要はありません。

しかし C 言語ではそれ以外にも「局所変数だが静的な変数」というものが使え、明示的に記憶クラスを指定する方法、記憶クラス指定子が用意されています。 記憶クラス指定子は次のように変数の定義の先頭に置きます。 auto が動的確保、static が静的確保に相当します。

auto int a;
static double x;

こうして 「a という名前で、整数型変数を動的にメモリ上に確保しなさい」 「x という名前で、実数型変数を静的にメモリ上に確保しなさい」 という指示をコンパイラに与えるのです。

記憶クラス指示子は局所変数と大域変数では幾らか意味が異なるので、これ以降は両者を分けて説明します。

局所変数における記憶クラス

auto 局所変数(自動変数)

autoをつけて定義された局所変数は自動変数あるいは auto変数と呼ばれます。 ただしこの変数はもっとも頻繁に用いられるため次のようなルールがあります。 それはブロック内で記憶クラス指定子を 何も付けずに定義された変数は自動変数とするというものです。 よって、これまでブロック内で次のように定義してきた 局所変数はすべて自動変数になります。

int a;
double x;

明示的に自動変数であることを示したい場合は 次のようにauto記憶クラス指定子をつけることになります。

auto int a;
auto double x;

しかし、上のルールがあるのでまず用いる必要はありません。

関数の呼び出しと変数の生存期間(生成と消滅)

自動変数は実行中にその変数のブロックに入ったときに メモリ上に領域が確保されます。そして そのブロックから出るときに 領域が解放されます。 すなわちそのメモリ領域は別の用途に再利用できるようになります。

関数ブロックの局所変数であれば、関数が呼び出されたときに メモリ上に値を収める場所が確保されて、関数が終了するときに その場所が解放されます。すなわち、 同じ関数が複数回呼び出されるような場合には 呼び出されるたびにメモリの確保と解放を繰り返します。

#include <stdio.h>

int twice(int a)
{
  int x;          // 自動変数x

  x = a * 2;
  return x;
}

int main(void)
{
  int y;

  y=5;
  y=twice(y);        // 1回目のxの確保と解放
  y=twice(y);        // 2回目のxの確保と解放
  printf("answer=%d\n", y);
  return 0;
}

再帰的な呼び出しと生存期間

また関数の実行中に同じ関数が再帰的に呼び出される場合には、 呼び出されるたびにそれぞれ別のメモリ領域が確保されます。

#include <stdio.h>

int fact(int a)
{
  int x;          // 自動変数x

  if (a > 1) {
    x = fact(a-1);
    return a * x;
  } else {
    return 1;
  }
}

int main(void)
{
  int y;

  y=5;
  printf("%d! = %d\n", y, fact(y)); // xの確保が5回、解放が5回起こる。
  return 0;
}

プログラムが実行されるときのメモリ領域の使われ方には いくつかの種類があり、自動変数が確保されるメモリ領域では、 上のようにプログラムの実行が始まってから必要に応じて確保され (これを動的なメモリ管理と呼びます)、 後から確保した領域が先に解放されるように使われます。 このような使われ方をされるメモリ領域は スタック領域と呼ばれます。
メモリ領域の使われ方には他に、 大域変数や次節のstatic記憶クラス指定子で用いられる 静的領域と mallocやfreeなどの標準ライブラリを通じて使うことのできる ヒープ領域があります。 これらについてはそれぞれの章や節で詳しく説明することにしましょう。

static局所変数

上で説明したように自動変数は関数やブロックに出入りするごとに メモリ領域の確保と解放を繰り返します。 これは関数やブロックを超えて変数の値を保持することが出来ないことを意味します。

つまり「同じ関数やブロックを実行するときに、前回実行した時の値を(覚えておいて)再び使う」ことができません。

これに対してstatic記憶クラス指定子をつけて定義された局所変数 (これをstatic局所変数と呼ぶことにしましょう)は プログラムの実行開始時から変数の領域が確保されており、 プログラムの終了時に初めて解放されます。 すなわち局所変数であるにもかかわらず、 ブロックの出入りに関係なくプログラムの最初から最後までずっと同じ領域を使い続けることが出来ます。

つまり「前回実行した時の値を再び使う」ことができるようになります。

以下はそのようにして作られた「過去に渡された値を全て繰り込み続ける関数」の例です。

#include <stdio.h>

int total(int a)
{
  static int t = 0;     // static局所変数

  t = t + a;
  printf("total=%d\n", t);
}

int main(void)
{
  total(1);             // total=1と出力
  total(2);             // total=3と出力
  total(3);             // total=6と出力
  return 0;
}

このようなstatic局所変数が置かれるメモリ領域は 先のスタック領域とは異なったメモリ管理が行われます。すなわち、 プログラムそれ自体がプログラムの開始時にメモリ上のある領域に置かれ 終了時にその領域が解放されるのと同じようなタイミングで、 static局所変数の領域の確保と解放が行われます (これは静的なメモリ管理と呼ばれます)。 このような使われ方をするメモリ領域は静的領域と呼ばれます。 ちなみにプログラム自体が置かれるメモリ領域は プログラム領域と呼ばれます。

大域変数における記憶クラス

大域変数は常に静的に確保されます(動的には確保できません)。 そのため、auto 記憶クラス指示子は大域変数には指定できません。

対して static 指示子はつけるべき場合とそうでない場合の両方がありますが、 この授業ではその理由などについては追いません。 単に大域変数には static 記憶クラス指示子はつけないで良いとだけ理解しておけばいいでしょう。
(いつか必要になるまで、大域変数には static と付けないように癖づけておけばよいでしょう。)

static 記憶指示子の有無

この授業では追いませんと言われても好奇心が湧いて仕方がない人のために大まかに書いておきます。

局所変数における static 記憶クラス指示子の省略が、自動変数における auto の省略と同様に単なる省略であれば良いのですが、実際にはそうではありません。 困ったことに書いた場合と書かなかった場合では意味が異なってきます。

大域変数にstatic記憶クラス指定子が付いている場合と付いていない場合の違いは 単一のファイルでプログラムを作成している場合には現れず、 複数のファイルからなるプログラムを作成する場合に初めて現れます。

従って例えば、複数のファイルで定義された 同じ名前のstatic付き大域変数は別の変数として扱われます。
なお、上のどちらの大域変数も、メモリ上ではstatic局所変数と同様に静的領域に確保されて静的なメモリ管理が行われます。

初期設定

局所変数でも大域変数でも、また記憶クラスにかかわらず変数を定義するときには初期設定を行うことが出来ます。 例えば次のように記述します。

int x=100;
int y=x+100;
static int i=0;

すなわち初期設定の構文は次のようになります。

データ型 変数名 = 初期値式;
記憶クラス指定子 データ型 変数名 = 初期値式;

従って記憶クラスの節の説明では以下のように二行に分けて書いていましたが、

int y;
y=5;

これは次のようにまとめて書けます。

int y=5;

次に初期設定が行われるタイミングですが、 これは変数の種類によって2通りあります。

  1. 自動変数は定義されているブロックに入るごとに初期設定されます。
  2. 大域変数とstatic局所変数はプログラムの実行開始時に一度だけ行われます。

実際には実行効率を上げるために、ほとんどの場合 2の初期設定はコンパイル時にすでに行われています。 このために細かいことをいえば2の初期設定で初期値式に使える式には多少制限があります。 2の場合はコンパイル時にその値があらかじめ計算して定まるような式でないといけません。 一方、1の場合は実行時にブロックに入るたびに値が異なるような式でも構いません。

さらに、変数の初期設定が行われていない場合の初期値については、 これも変数の種類によって2通りに分かれます。

  1. 自動変数では初期設定されないのでその値は不定です。
  2. 大域変数とstatic局所変数では暗黙に 0(すべてのビットが0の値) に初期設定されます。

従ってstatic局所変数の節で挙げたプログラムでは、 実際にはstatic局所変数tの初期設定をしなくても正しく動作します。

演習問題

A. 以下の二つのプログラムの実行結果を予想し、なぜそうなるか説明しなさい。 そして実際に実行してその予想と合っているか確認しなさい。

#include <stdio.h>

int f(void)
{
  int 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 f(void)
{
  static int 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;
}

B. 次のプログラムを変更して、関数factが呼び出されるたびに それが何回目の呼び出しかを表示するようにしなさい。 ただし回数を数えるのにstatic局所変数を用いること。

#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=NUMBER;

  printf("%d! = %d\n", x, fact(x));
  return 0;
}

表示は例えば以下のようになる。

count = 1
count = 2
count = 3
count = 4
count = 5
5! = 120