06 月 24 日(金)1-2h

記憶クラスとスコープ

今回はまず,これまでにほとんど説明もなく使ってきたCの基本的な事項 ({ }; など)について,正しく理解しよう.

C言語の「正しいプログラミング」への準備.

また,変数のさまざま性質について理解しよう.


ソースコードの構成要素

Cのソースコードは次のような単語・記号を要素として構成されている:

データや演算子を組み合わせたものは, 予約語や式を組み合わせたものはと呼ばれている. 文の末尾には必ずセミコロン「;」が置かれる.

複数の文を中括弧「{ }」でまとめたものは, ブロックと呼ばれ, ひとつの文と同じように取り扱われる. これまでに何度も,for 文や関数とともに利用してきた.

ブロックの中に,さらにブロックを作ることも可能だ. たとえば,for ブロックの中に, さらに for ブロックとか if ブロックを入れてよい. ただし,一番外側のブロックは関数(名前付きのブロック)でなければならない. また,関数の中に関数を定義することはできない.

これらのブロック構造の規則については, 教科書 p.67,102 にも説明がある. ちなみに,List 1 のように,名無しのブロックも作れる.

なお,ブロックの記号 { } は, 文だけでなく複数のデータをひとまとめにするためにも利用される. たとえば,配列の初期化 にも使っていた.

違いに注意:


データのスコープ

Cでは,異なる場所(ブロック)で定義された変数は, たとえ名前が同じであっても異なる変数として区別される. (名前は同じでも,異なるメモリ領域を使い,実体は異なる. 要するに,同姓同名の他人だ.)

さらには,前回の再帰関数では, 同じブロック内の変数でさえ, 呼び出すたびに異なる変数になっていた. 異なるブロック間なら,なおさら異なる.

たとえば,次のようなソースコードでは, 関数 main( ) 内の変数 a, x と 関数 sub( ) 内の変数 a, x とは, 名前は同じだが,メモリ上の実体はまったくの別物になる:

sub()
{
	int	a;
	int	x;
	...
}

main()
{
	int	a;	// 上の a と同型同名だが,別物
	double	x;	// 上の x と異型だが,別物なので無問題
	...
}

変数 x の型の不一致については, そもそも別物なので,気にする必要はない.

名前と実体の区別については,変数だけでなく関数でも同様. たとえば再帰呼び出しの場合, 前回の説明の通り, 呼び出し側と呼び出され側は同じ名前の関数だが, 実体は異なる物(分身,コピー)だった.

一方,複数の関数の間で同一の変数を共有したいという場合もある. そこでCでは,変数の スコープ(scope;通用範囲)を 使い分けられるようになっている:

なお,ローカルとグローバルに同じ変数名が使われている場合, そのブロック内ではローカル変数だけが有効になり, グローバル変数は一時的に無効になる.

しかし,この能能を利用してはいけない. ローカルとグローバルに同じ名前をつけてしまうと, どちらの変数を使っているのか?混乱の元だ.

オススメの命名方法:

List 1 のプログラムを実行し, グローバル変数とローカル変数の違いを考えてみよう. 同じ名前の変数がいくつも定義されているが, それぞれ異なる変数であることを確認しよう.

List 1. スコープの実験 scope.c
// グローバル変数
char  *s = "global";
int   x = 100;

/* 関数 sub */
sub()
{
	printf("sub : s = %s , x = %d\n",s, x);
}

/* 関数 main */
main()
{
	// main( ) のローカル変数
	char *s = "local";
	int  x = 10;

	sub();

	printf("main1 : s = %s , x = %d\n",s, x);

	// 名無しブロック
	{
		// 名無しブロックのローカル変数
		char *s = "block";
		int  x = 20;

		printf("block : s = %s , x = %d\n", s, x);
	}

	printf("main2 : s = %s , x = %d\n", s, x);
}

変数の宣言(int x 等)については, 必ず,ブロック内の先頭部分に記述すること. 他の部分に記述しても許してくれるコンパイラもあるが, これはあくまでもC言語の方言であり,標準語ではない:

...
main()
{
	int  a;		// ブロック内の先頭での変数宣言...正しい

	a = 1 + 2;	// 何らかの処理
	printf(...);	// ...

	int  b;		// ブロック内の途中での変数宣言...誤り(動くが...正しくない)
	...
}

なお,グローバル変数を使えば関数間でのデータの受け渡しが楽になるので, 「何でもグローバルにすればよい」と考えてしまう人がいる. しかし,よく理解しないまま利用してしまうと,思わぬトラブルの原因にもなる. うまく使い分けること.

本当に必要なとき以外,グローバル変数は使用禁止. 大抵の場合,ローカル変数で済むハズだ.

グローバル変数の罠:

この授業では,グローバル変数の利用は必要最低限にとどめる. 今回の課題以外で, 必要もなくグローバル変数を使ったレポートは減点対象. (将来,複雑なプログラムを作る段階になったら, グローバル変数とローカル変数とを適切に使い分けよう.)

データの記憶クラス

これまで使ってきたローカル変数は, その関数(ブロック)を実行する時に生成され, その関数(ブロック)を終了する時に消滅する.

生成・消滅といっても, 物理的にメモリ容量が増えたり減ったりするという意味ではない. そのメモリ領域を確保したり放棄したりするだけだ. また,そのメモリ領域の場所は,確保する時に決定され, 関数を呼び出すたびに変化する.

ただし,関数終了時に,メモリがクリアされるわけではないので, ローカル変数の値は,メモリ上にゴミとして残ることになる. しかし,それを当てにしてはならない. その領域は,他の関数によっても再使用されるので, その残されたゴミの値は書き換えられてしまう可能性が高い. つまり,次にその関数を呼び出した時には, 以前の変数の値を忘れてしまっていることに注意しよう.

一方,以前の値を憶えておいて欲しい場合もある. そこでCでは,変数の記憶クラス(storage class)を使い分けられる:

「関数の実行時」と「プログラムの実行時」との違いに注意.

List 2 のプログラムを実行し, 記憶クラスによる違いを確認してみよう.

List 2. 記憶クラスの実験 class.c
sub()
{
	auto   int  a = 0;		// 自動変数
	static int  s = 0;		// 静的変数

	printf ("a = %d , s = %d\n",a,s);	// テスト出力

	a = 10;
	s = 10;
}

main()
{
	printf("1st call :\n");
	sub();

	printf("2nd call :\n");
	sub();
}

静的変数の濫用もトラブルの元だ. 自動変数で済む場合は,自動変数を使うこと.

この授業では,.... 今回の課題以外で,必要もなく静的変数を...同上.

変数の種類の選択順序:

  1. ローカルな自動変数
  2. ↑ これが基本.これでうまくできない場合だけ,仕方なく...↓
  3. ローカルな静的変数
  4. ↑ これでもうまくできない場合,仕方なく...↓
  5. グローバル変数
  6. ↑ これは最終手段.

練習問題

引数の整数値を累積し合計値を返す関数を次の2通りの方法で定義せよ:

基本ソースコード:

// グローバル変数の宣言
...

// グローバル変数版の累積関数
int accum1(int x)
{
	...
}

// 静的変数版の累積関数
int accum2(int x)
{
	...
}

// テスト用メイン関数(書き換え禁止)
main()
{
	int i;

	// 累積関数による総和 Sn = 1 + 2 + ... + n の計算
	for (i = 1; i <= 10; i++) {
		printf("S%02d = %2d , %2d\n", i, accum1(i), accum2(i));
	}
}

また,静的変数を自動変数に変えるとうまく行かないことも確かめよ.

余裕のある人は,メモリマップについても実験してみるとよい. 関数内で変数(グローバル変数,静的変数,自動変数)のアドレスを表示し, その変化を観察. (実は変化しないが,変数の種類によるアドレスの違いを観察.)


補足1

List 3 も試してみよう. 自動変数がどのようにメモリを使っているか? メモリマップの変化を想像しながら, 実行結果を確認しよう. 注目すべきは,sub1( ) のゴミの値の変化.

List 3. 自動変数の初期値の実験 auto.c
sub1()
{
	int a;		// 初期化していない(メモリを書き換えていない)

	printf("sub1: %d\n", a);	// ゴミを表示
}

sub2()
{
	int b = 2;	// 初期化している(メモリを書き換えている)

	printf("sub2: %d\n", b);	// 初期値を表示
}

main()
{
	sub1();		// a=ゴミ
	sub1();		// a=ゴミ

	sub2();		// b=2
	sub1();		// a=何? なぜ?

	printf("%f\n", 1.234);		// この関数でも実はメモリを...
	sub1();		// a=何? なぜ?
}

sub1() 内と sub2() 内で, 変数 ab のアドレスも表示してみれば, わかりやすくなるかもしれない.

このように,自動変数であっても, static のように記憶できる場合もある. しかし,それを当てにしたプログラムを作ってはいけない.


補足2

List 4 では,静的変数と自動変数のメモリマップの違いについて確認しよう.

List 4. クラスによるメモリマップの違いの確認 mmap.c
int sub1()
{
	auto   int  a;
	static int  s;
	printf("&a = 0x%08X  &s = 0x%08X\n", &a, &s);
}

int sub2()
{
	int  x[100];
	sub1();
}

int main()
{
	printf("sub1:\n");
	sub1();

	printf("sub2 -> sub1:\n");
	sub2();
}

sub1() だけ呼び出した場合と, sub2 経由で sub1() を呼び出した場合とで, 自動変数 a のアドレスは変化する. 一方,静的変数 s のアドレスは不変.


(c) 2016, yanagawa@kushiro-ct.ac.jp