C言語編 第22章 スコープ

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

ローカル変数

これまで、変数を宣言する場所についてはあまり気にしませんでした。 今までのルールは、「関数の先頭で宣言します」という1点だけでした。 しかし、もしかすると、異なる関数で宣言した変数を使うことができないことは意識することがあったかも知れません。

ある変数を使用できる(アクセスできる)のが、ソースコード上のどこからどこまでなのか、 ということをスコープという言葉で表現します。 変数に限らずスコープといえばそういう意味になります。

例えば、main関数の先頭で宣言した num という名前の変数は、その宣言位置から、main関数の終わりまでの範囲からしかアクセスできません。 このように、特定の関数内だけで使用できる変数を、ローカル変数(局所変数)と呼びます。 次のプログラムで確認しておきましょう。

#include <stdio.h>

void func(void);

int main(void)
{
	int num;

	num = 100;
	printf( "%d\n", num );

	func();
	printf( "%d\n", num );

	return 0;
}

void func(void)
{
	int num;

	num = 500;
	printf( "%d\n", num );
}

実行結果:

100
500
100

main関数にも、func関数にも、同じ名前の変数num が存在しますが、お互いのことはまったく認知していないことが分かります。 func関数内の変数num に 500 を代入しても、main関数側の変数num には一切影響を与えていません。

今度は、少し変えてみます。

#include <stdio.h>

void func(int num);

int main(void)
{
	int num;

	num = 100;
	printf( "%d\n", num );

	func( num );
	printf( "%d\n", num );

	return 0;
}

void func(int num)
{
	num = 500;
	printf( "%d\n", num );
}

実行結果:

100
500
100

今度は、仮引数に変数num を登場させました。 この場合でもやはり、仮引数num と main関数の変数num には関係性はありません。 実は、仮引数もローカル変数とみなすことができます

ブロックスコープ

ローカル変数は、関数の先頭で宣言しなければならない…という訳ではありません。 実は、ローカル変数は、ブロックの始まるところで宣言しなければならないというのが正解です。

ブロックというのは、{ と } で囲まれた範囲ですが、よく見ると、関数全体は { と } で囲まれていますね。 ですから、関数の先頭も、ブロックの始まるところと考えることができます。

ブロック内で宣言された変数は、そのブロックか、それよりも内側のブロックからしかアクセスできません。 このようなスコープの考え方を、ブロックスコープと呼ぶことがあります。

ブロックは至るところで登場します。 今までに登場したのは、if、else、switch、for、while、do、関数全体といったところです。

#include <stdio.h>

int main(void)
{
	int a = 10;

	if( 1 ){
		int b = 20;
	}
	else{
		int c = 30;
	}

	switch( a ){
		int d = 40;
		case 0:
			break;
		case 10:
			break;
		default:
			break;
	}

	for( ; a >= 0; --a ){
		int e = 50;
	}

	while( 0 ){
		int f = 60;
	}

	do{
		int g = 70;
	}
	while( 0 );
	
	return 0;
}

プログラム自体に意味はありませんが、このように、ブロックの始まりでさえあれば、どこでも変数を宣言できます。 この中で、switch文のブロックに関しては、使う機会はないと思います。 実際、この位置の変数宣言は非常に特異なもので、実は、宣言もアクセスもできますが、初期化されません。 (あとで登場する静的変数の場合は初期化されます)。

こういった特定のキーワードに付属するブロック以外にも、自分でブロックを作ることもできます。

#include <stdio.h>

int main(void)
{
	int num = 10;
	
	{
		int num = 20;  /* ここもブロックの先頭である */
		printf( "%d\n", num );
	}
	
	printf( "%d\n", num );  /* これは main関数の先頭で宣言された変数num の値を出力する */
	
	return 0;
}

実行結果:

20
10

このように、自分で { と } を作れば、それも立派なブロックです。 そのため、この内側の最初の部分で、変数を宣言することができます。

C99 (変数宣言位置の自由化)

C99 になって、ローカル変数の宣言を、ブロックの先頭以外の場所でも行えるようになりました。

VisualC++ 2013/2015/2017、clang 3.7 のいずれも対応しています

#include <stdio.h>

int main(void)
{
	puts( "整数を入力して下さい。" );

	char buf[40];
	int num;
	fgets( buf, sizeof(buf), stdin );
	sscanf( buf, "%d", &num );

	printf( "%d\n", num );

	return 0;
}

実行結果:

整数を入力して下さい。
100
100

変数buf、num の宣言よりも手前に、puts関数の呼び出しがあるので、関数の先頭で宣言していませんが、C99 では許されます。

また、for文の初期設定式のところでも、変数を宣言できるようになりました。 その変数のスコープは、for文の内側に限定されます。

#include <stdio.h>

int main(void)
{
	for( int i = 0; i < 5; ++i ){
		puts( "OK" );
	}

	return 0;
}

実行結果:

OK
OK
OK
OK
OK

この例のように、ループ制御変数を関数の先頭で宣言する必要がなくなります。 この使い方はかなり一般的なものです。


関数の途中で宣言できるということは、例えば、ループの内側で宣言した場合等、何度も変数宣言のコードを通過する可能性があります。 変数に初期値を与えている場合、その定義を行っている箇所を通過するたびに初期値が与えられます。 逆に、その定義箇所を通過しない限り、初期値を与えられることはなく、不定な値なままになります。 ですから、(そういうスタイルの良し悪しは別として)goto文を使って、変数の定義を飛び越えてしまうと、 初期値が不定なままで、変数を参照してしまう可能性があります。

#include <stdio.h>

int main(void)
{
	goto A;
	
	int num = 123;  // goto文によってここを飛ばしている
	
A:
	printf( "%d\n", num );  /* 変数num の存在は認識されているが、初期値は入っていない */

	return 0;
}

実行結果:

2130567168

先ほどのサンプルプログラムでは、ブロックの外側と内側とに、同じ名前の変数num が宣言されています。 ブロックが異なれば、両者は異なる変数であるとみなされます。 今回の場合、ブロックの内側にある変数num は、外側にある変数num を覆い隠すような結果になるため、 これを隠蔽(いんぺい)と呼びます。

一般に、隠蔽は、プログラムを読みづらくしてしまいます。 隠蔽していることに気付かないと、思っていた変数とは違う変数にアクセスしてしまうかも知れません。
ですから、隠蔽自体、避けることが無難ですが、意図せずにうっかり隠蔽してしまうこともあるかも知れないので、注意が必要です。 どうしても同じ名前の変数を使うことが望ましいような場合、ブロックの部分を関数化できないか考えてみるべきでしょう。

静的ローカル変数

関数の中で宣言した変数は、関数を呼び出すたびに作られていました。 例えば、次のプログラムを試してみましょう。

#include <stdio.h>

void myprint(void);

int main(void)
{
	int i;
	
	for( i = 0; i < 5; ++i ){
		myprint();
	}
	
	return 0;
}

void myprint(void)
{
	int num = 0;      /* 関数を呼び出すたびに作り直される */
	
	num += 10;        /* 何回呼び出しても、num の値は 10 にしかならない */
	printf( "%d\n", num );
}

実行結果:

10
10
10
10
10

myprint関数のローカル変数num は、myprint関数を呼び出すたびに作られ、そのたびに 0 で初期化されます。 そのため、myprint関数を何回呼び出しても、printf関数が出力する値は 10 にしかなりません。

これに対して、一度作ったローカル変数の値をそのままずっと記憶しておく方法があります。 そのためには、変数宣言の際に、static というキーワードを付け加えます。

static int num;

このように宣言された変数を、静的変数と呼びます。 ローカル変数であれば、静的ローカル変数と呼ばれることもあります。 静的変数は、どこに宣言していようとも関係なく、プログラムの実行開始時にメモリ上に作られ、初期値も与えられます。 そして、プログラムの実行が終了する直前まで、消えることなくメモリ上に存在し続けます

C++ の場合、ローカルな静的変数は、その宣言箇所が実行されるときに初めてメモリ上に作られ、初期値が与えられることになっています。 このように、生成されるタイミングがC言語とは異なることに注意が必要です。

先ほどのプログラムを、静的変数を使ったものに変えてみます。

#include <stdio.h>

void myprint(void);

int main(void)
{
	int i;
	
	for( i = 0; i < 5; ++i ){
		myprint();
	}
	
	return 0;
}

void myprint(void)
{
	static int num = 0;   /* プログラム開始時に作られて、ずっとそのまま */
	
	num += 10;            /* num はずっと記憶されるので、呼び出すたびに値は増える */
	printf( "%d\n", num );
}

実行結果:

10
20
30
40
50

今度は、myprint関数の変数num は静的変数なので、myprint関数から抜け出した後も、値が保持されたままになります。 このおかげで、実行結果にあるように、出力される値は増加していきます。

静的変数は、具体的な初期値を与えなくても、必ず 0 で初期化されることが保証されています (静的変数でないローカル変数は、初期値を与えないと不定値のままです)。 ですが、明示的に初期値を与えた方が無難でしょう。

なお、静的変数のように、プログラムの実行の開始から終了まで存在し続ける場合、この存在期間を、静的記憶域期間と呼びます。 一方、静的変数でないローカル変数は、宣言されている関数内でだけ存在でき、これを自動記憶域期間と呼びます。

グローバル変数

実は変数を、関数の外に宣言することも可能です。これを、グローバル変数(大域変数)と呼びます。

#include <stdio.h>

int gNum = 10;  /* グローバル変数 */

void myprint(void);

int main(void)
{
	printf( "%d\n", gNum );  /* グローバル変数gNum を表示 */
	myprint();

	gNum = 20;
	myprint();
	
	return 0;
}

void myprint(void)
{
	printf( "%d\n", gNum );  /* グローバル変数gNum を表示 */
}

実行結果:

10
10
20

グローバル変数は、関数の外にあるので、特定の関数専用ということにはなりません。 複数の関数からアクセスできます(つまり、共有しています)。 なお、グローバル変数の名前に、「gNum」や「g_num」のように、先頭に「Global」であることを表す目印を付けるのは、一般的によく行われていることです。 また、グローバル変数には、簡単すぎる名前を付けるべきではありません

グローバル変数は、staticキーワードを付けたローカル変数同様、静的記憶域期間を持ちます。 従って、プログラムの実行が開始されたときに作られ、実行が終了するときまで、ずっと存在し続けています。 明示的に初期値を与えなくても、0 で初期化されるという点も同様です
また、グローバル変数のスコープは、グローバルスコープ(大域スコープ)と呼ばれます。 これは、プログラム全体に及ぶスコープです。

今のところ、ソースファイルは1つだけなので「プログラム全体」という意味が分かりにくいかも知れませんが、 ソースファイルの数が増えると意味が出てきます。複数のソースファイルを使うプログラムについては、第23章で説明します。

どの関数からでもアクセスできるという特徴を、便利と感じるでしょうか、それとも分かりにくそうだと感じるでしょうか。 一般的なガイドラインとして、スコープは可能な限り狭く保つべきだと言われています。
スコープが狭いというのは、要するに、ある変数にアクセスすることができる範囲が狭いということです。 そうすることで、「この変数は、どこから書きかえられ、どこから参照されているのか」が把握しやすくなります。

スコープを狭くするという点においては、グローバル変数は最悪だと言えます。 可能であれば、ローカル変数に置き換えることを考えるべきです。

#include <stdio.h>

void myprint(int num);

int main(void)
{
	int num = 10;

	printf( "%d\n", num );
	myprint( num );

	num = 20;
	myprint( num );
	
	return 0;
}

void myprint(int num)
{
	printf( "%d\n", num );
}

実行結果:

10
10
20

このように、引数や戻り値を使って情報を受け渡す方が、グローバル変数を使うよりも望まれます。 こうすることで、関数を部品とみなすことが出来ます。 グローバル変数を使っていると、関数の外側にまで関係性が広がってしまい、部品化が損なわれます。 (部品化が損なわれると、有用な関数を他のプログラムで再利用しようとしても、大変な労力を使わなくてはならなくなります)


グローバル変数は、ローカル変数によって隠蔽されてしまう点にも注意しておきましょう。 何となく、グローバル変数の方が強そうな感じがするかも知れませんが、常に、スコープの狭い方が、広い方を隠蔽しますから、 グローバル変数は簡単に隠蔽されてしまいます。
グローバル変数の名前に「g」や「g_」を付けるというルールを守っていれば、うっかり隠蔽してしまうミスを防ぐ効果があります。

ローカル変数に static を付けるように、グローバル変数にも static を付けられますが、 これについては、次章で説明します。

時代遅れのキーワード

2つのキーワードを軽く紹介しておきます。 この2つは、現在では役目を終えたと考えてもほぼ差し支えないと思われる古いキーワードです。

まず、autoキーワードですが、これはローカル変数の宣言時に、

auto int num;

このように記述します。 静的ローカル変数のところで触れたように、通常のローカル変数は、自動記憶期間を持ちます。 この「自動」の部分から、「auto」という名前が付きました。

しかし、autoキーワードを付加しても「一切何も起こりません」。 従って、使う必要性はまったくありません。
autoキーワードは、C言語よりも前に存在した非常に古い言語からの名残で残されているキーワードです。 C言語の世界にとっては無意味です。

C++ では、C++11規格以降、autoキーワードに新しい意味が与えられており、C言語とは意味が異なっているので注意して下さい(C++編【言語解説]第2章参照)。


もう1つは、registerキーワードです。 これもローカル変数宣言時に、

register int num;

このように記述します。 このキーワードは、コンパイラに、「可能な限り高速にアクセスできるようにせよ」という指示を与えます。 ただし、このキーワードを無視するのも、どの程度の効力を持つのかもコンパイラ次第であり、万能な訳ではありません。

また、現代のコンパイラのほとんどは非常に優秀なので、このキーワードに頼る必要性がほとんどありません。 基本的に使う必要はなく、使うとしてもコンパイラのマニュアルを読むなどして、本当に効果があるのか確かめるべきです。

第31章でポインタについて説明しますが、 registerキーワードが付加された変数に対しては使用できないことに注意して下さい。 registerキーワードは、変数をレジスタという特別な場所に割り当てるため、メモリアドレスが取得できないことが理由です。


練習問題

問題@ 次のプログラムの実行結果を答えて下さい。

#include <stdio.h>

int num = 100;

void func(void);

int main(void)
{
	{
		int num = 300;
		printf( "%d\n", num );
	}

	func();
	printf( "%d\n", num );
	
	return 0;
}

void func(void)
{
	int num = 200;

	printf( "%d\n", num );
	{
		printf( "%d\n", num );
	}
}

問題A 呼び出すたびに 0、1、2 … という具合に増加する整数を出力する myprint関数を作ってください。 このような関数を作る方法を最低でも 2通り考えてみて下さい。

問題B 次のようなプログラムがあります。

#include <stdio.h>

int num = 100;

int main(void)
{
	int num;

	num = 10;
	printf( "%d\n", num );
	printf( "%d\n", num );	/* グローバル変数の方を出力したい */
	
	return 0;
}

コメントの箇所にある printf関数で、グローバル変数の方の num の値を出力できるように、 プログラムを書き換えて下さい。


解答ページはこちら

参考リンク

更新履歴

'2017/3/25 VisualC++ 2017 に対応。

'2016/10/15 clang の対応バージョンを 3.7 に更新。

'2015/10/12 clang の対応バージョンを 3.4 に更新。

'2015/9/5 VisualC++ 2012 の対応終了。

'2015/8/23 「C99 (変数宣言位置の自由化)」を、ブロックスコープの項へ移動。 また、文面を「関数の先頭」から「ブロックの先頭」に修正。
autoキーワードに関するコラムを修正(C++11 に対応)

'2015/8/18 VisualC++ 2010 の対応終了。

'2015/8/15 VisualC++ 2015 に対応。

'2014/10/18 clang 3.2 に対応。

'2014/2/1 VisualC++ 2013 に対応。

'2014/1/14 VisualC++ 2008 の対応終了。

'2014/1/11 clang 3.0 に対応。
サンプルプログラムに含まれていた余計なコードを削除。

'2013/4/20 C99 の変数宣言位置の自由化に関する項を追加。

'2011/9/9 「ブロックスコープ」の項のサンプルプログラムを、より分かりやすい形に修正。

'2010/6/21 「グローバル変数」の項の2つ目のサンプルプログラムが、コンパイルエラーを起こしていたのを修正。

'2009/10/11 グローバルスコープとすべきところを、ファイルスコープと記述していたのを修正。

'2009/7/18 新規作成。



前の章へ

次の章へ

C言語編のトップページへ

Programming Place Plus のトップページへ