Programming Place Plus トップページ -- C言語編
この章の概要です。
これまであまり意識する機会は無かったかもしれませんが、ある関数の中で宣言した変数は、ほかの関数から使用できません。つまり、関数の中で宣言した変数は、その関数専用の変数です。
このように、ある変数を使用できる範囲というものがあります。この範囲のことを、スコープ(有効範囲)といいます。スコープの範囲内にあるときにだけ、その変数が可視である(つまり使用可能である)と表現します。
ここでは変数を例にとっていますが、スコープの概念が適用されるのは、各種の識別子です。第3章でわずかに触れたように、識別子とは名前のことですから、関数にも当てはまります。スコープの範囲内にない関数は呼び出せないという訳です。
関数の宣言や定義が、呼び出し位置よりも後ろにあると、その関数を呼び出せないことを、第10章で確認済みです。第10章では、「関数の定義を見つけられない」という表現がされていましたが、言い換えると、「スコープの範囲内にないので、可視でない(使用可能でない)」ということです。
なお、同じ名前の異なる変数や関数を宣言することが許されないのも、スコープに関係します。つまり、同一のスコープの範囲内に、同じ名前の異なる変数や関数を宣言できません。
スコープはいくつかの種類に分類できます。この章ではそれらを順番に確認していきます。
これまでの章では、関数内の変数の宣言はいつも、関数の先頭に書くようにしてきました。しかし、これは実は正確なルールではありません。正しいルールは(C95規格までは)「関数内で宣言する変数は、ブロックの先頭で宣言する」というものです。
あとで取り上げていますが、C99 ではブロックの "先頭" で宣言する必要はなくなっています。古い慣習は無意味なので、従うべきではありません。
ブロック内で宣言した変数のスコープを、ブロックスコープと呼びます。ブロックスコープは、宣言位置からブロックの末尾までです。
関数の先頭で宣言した場合も同様に、ブロックスコープを持つものとみなされます。この場合は、宣言位置から関数の末尾までがスコープです。
また、関数の仮引数についても、ブロックスコープを持つものとみなされます。これらに関しては、関数全体がスコープです。
ブロックスコープを持つ変数のことをよく、ローカル変数(局所変数)と呼びます。また、ブロックスコープのことを、ローカルスコープ(局所スコープ)と呼ぶことがあります。
ブロックスコープの挙動を確認してみましょう。
#include <stdio.h>
void func(void);
int main(void)
{
int num;
num = 100;
printf( "%d\n", num );
func();
printf( "%d\n", num ); /* この num は func関数内の num とは無関係 */
return 0;
}
void func(void)
{
int num;
/* 以下の num は main関数内の num とは無関係 */
num = 500;
printf( "%d\n", num );
}
実行結果:
100
500
100
main関数にも、func関数にも、同じ名前の変数num がありますが、お互いのことはまったく認知していないことが分かります。func関数内の変数num に 500 を代入しても、main関数側の変数num には影響を与えていません。
次のプログラムが示しているように、ブロックスコープは至るところにあります。
#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;
}
このプログラムが示しているように、ブロックの始まりでさえあれば、どこにでも変数を宣言できます(繰り返しますが、C95以前の場合です。C99 での事情は後で取り上げます)。
switch文のブロック内での宣言に関しては、使う機会はないと思います。実際、この位置の変数宣言は非常に特異なもので、宣言もアクセスもできますが、初期化されません。
ただし、次の章で取り上げる、静的ローカル変数の場合は初期化されます。
この位置の変数が初期化されないのは、実行時にこの位置を通過しないからです。同じ理由で、変数の初期化を行っている箇所を goto文で飛ばすようなコードを書くと、変数は宣言されているものの、初期値は与えられていないという状態が作り出せてしまいます。当然、このような妙なプログラムは避けるようにすべきです。
{ } を使ってブロックを作ることが可能であるため、自分で限定的なブロックスコープを作れます。
#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
一般に、どこから使われているかと、何を使っているのかを分かりやすくするために、スコープを極力狭く保つことが良いとされています。
しかし、このサンプルプログラムのように、新たなスコープを導入することは、かえって分かりにくさにつながってしまうこともあります。これは、スコープが入れ子構造になり、同じ名前だが、別の変数を指すという状態を作れてしまうためです。
ある名前を使うときには、その名前が何を指しているのかを、近くのスコープにある宣言から順番に探して判断します。そのため、同じ名前の宣言が、内側のスコープと外側のスコープの両方にあったら、外側にある方は隠されます(不可視になる、隠蔽されるなどと表現します)。
スコープの入れ子自体は、自力でスコープを作らずとも、if文や for文などを入れ子にするだけで起こることなので、完全に避けるのは難しいでしょう。それでも、ある名前が、どの宣言によるものを指しているのかが、簡単に把握できなくなるようなコードは避けるべきです。単に名前を変えるのでも良いですし、新たに作ろうとしたスコープの部分を関数化してしまうのも良いでしょう。
C99 になって、関数内で行う変数の宣言を、ブロックの先頭以外の箇所でも行えるようになりました。
#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
この例のように、ループ制御変数を関数の先頭で宣言する必要がなくなります。
変数は、関数定義の外側でも宣言できます。関数定義の外側で宣言された変数は、特定の関数専用ではなく、複数の関数で共用できます。このような変数はよく、グローバル変数(大域変数)と呼ばれています。
#include <stdio.h>
int gNum = 10; /* グローバル変数 */
void myprint(void);
int main(void)
{
printf( "%d\n", gNum );
myprint();
gNum = 20;
myprint();
return 0;
}
void myprint(void)
{
printf( "%d\n", gNum );
}
実行結果:
10
10
20
グローバル変数の宣言の構文は、ローカル変数の場合と変わりありません。
グローバル変数の名前の先頭に「g」や「g_」を付けて、グローバル変数であることを示す方法は、一般的によく使われます。これは、ブロックスコープのような、ファイルスコープよりも狭いスコープに、同じ名前の変数が宣言されると、隠蔽が起きて、混乱を招くことを防止するためです。このような命名方法自体は必須事項ではありませんが、少なくとも、グローバル変数には、簡単すぎる名前を付けるべきではありません。
グローバル変数は、プログラムの実行が始まったときから、実行が終了するときまで、ずっと存在しています。このような変数は、静的記憶域期間を持つと表現します。
グローバル変数のように、静的記憶域期間を持つ変数は、明示的に初期値を与えなくても初期化されます。この場合の初期値は、大ざっぱにいうと 0 です。
ここで与えられる初期値は、その変数の型が算術型の場合は 0、ポインタ型の場合はヌルポインタです(第31章)。また、集成体(第26章)の場合は各メンバの型に応じて 0 やヌルポインタが、共用体(第55章)の場合は最初のメンバの型に応じて 0 やヌルポインタが与えられます。
また、静的記憶域期間を持つ変数に明示的に初期値を与える場合は、定数でなければなりません。
なお、これまでに登場したローカル変数は、自動記憶域期間を持ちます。自動記憶域期間を持つ変数は、その変数の宣言が含まれているブロックに処理が入ってきたときに作られ、抜け出すときに消えます。
第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
このように、引数や戻り値を使って情報を受け渡す方が、グローバル変数を使うよりも良いスタイルです。
このような作りにしておくと、関数を部品として使いやすくなります。グローバル変数を使っていると、そのグローバル変数まで関数の1セットに含まれているような状態になってしまいます。初期化や代入といった処理を、ほかの関数でも行っている可能性があるため、容易には切り離せません。有用な関数を他のプログラムで再利用しようとしても、大変な労力をかけなくてはならなくなります。
goto文で使用するラベル名は、その関数定義全体にスコープを持ちます。これは、関数スコープと呼ばれます。
ブロックスコープと違って、ラベル名が登場するところより手前からも可視です。そのため、goto文は手前側にも、後ろ側にもジャンプできます。
関数プロトタイプに記述する仮引数は、その宣言の中にだけスコープを持ちます。これは、関数プロトタイプスコープと呼ばれます。
これは他の宣言や、関数定義内にまで、仮引数の名前が露出して邪魔をしないようにするための処置です。
現在では役目を終えたと考えても差し支えない、2つの古いキーワードが残っています。
まず、autoキーワードです。これは、ローカル変数の宣言時に auto指定子として使用します。
auto int 変数名;
auto は、自動記憶域期間の「自動」から来た名前ですが、ローカル変数はそもそも自動記憶域期間を持つので、付けても何ら効果を及ぼしません。これは古い時代からの名残で残っているだけの機能です。
C++ では、C++11規格以降、auto に新しい意味が与えられており、C言語とは意味が異なっているので注意してください(C++編【言語解説]第2章参照)。
もう1つは、registerキーワードです。これもローカル変数宣言時に register指定子として使います。
register int 変数名;
register は、コンパイラに、「可能な限り高速にアクセスできるように最適化せよ」という指示を与えます。ただし、register を無視することも、具体的にどのような効力を持つか決めることもコンパイラ次第となっています。
現代のコンパイラは非常に優秀なので、register に頼る必要性がほとんどありません。register を付けても無視されるかもしれないし、付けなくても最適化を施してくれるかもしれません。そのため、基本的に使う必要はありません。使うとしてもコンパイラのマニュアルを読むなどして、本当に効果があるのかを確かめるべきです。
第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 );
}
}
問題② 呼び出すたびに 0、1、2 … という具合に増加する整数を出力する myprint関数を作ってください。
問題③ 次のようなプログラムがあります。
#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 の値を出力できるように、 プログラムを書き換えてください。
------------------------------------------------------------------------
'2018/7/21 auto、register をそれぞれ指定子と表記するように修正。
'2018/5/12 静的記憶域期間を持つ変数に明示的に与える初期値が、定数でなければならないことについて追記。
'2018/2/25 全面的に文章を見直し、修正を行った。
項の分類も大幅に変更した。以前は「ローカル変数」「グローバル変数」のようなタイトルで項を分けていたが、 章のテーマに合わせて、「〇〇スコープ」という分類に直した。
「静的ローカル変数」の項は、第23章へ移動。
'2018/1/5 コンパイラの対応状況について、対応している場合は明記しない方針にした。
'2017/7/30 clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。
≪さらに古い更新履歴を展開する≫
Programming Place Plus のトップページへ
はてなブックマーク に保存 | Pocket に保存 | Facebook でシェア |
Twitter でツイート | Twitter をフォロー | LINE で送る |
![]() |
管理者情報 | プライバシーポリシー |