C言語編 第22章 スコープ

先頭へ戻る

この章の概要

この章の概要です。

スコープ(有効範囲)

これまであまり意識する機会は無かったかも知れませんが、 ある関数の中で宣言した変数は、ほかの関数から使用することができません。 つまり、関数の中で宣言した変数は、その関数専用の変数になります。

このように、ある変数を使用することができる範囲というものがあります。 この範囲のことを、スコープ(有効範囲)といいます。 スコープの範囲内にあるときにだけ、その変数が可視である(つまり使用可能である)と表現します。

ここでは変数を例にとっていますが、スコープの概念が適用されるのは、各種の識別子です。 第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 (変数宣言位置の自由化)

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

このように、引数や戻り値を使って情報を受け渡す方が、グローバル変数を使うよりも良いスタイルです

このような作りにしておくと、関数を部品として使いやすくなります。グローバル変数を使っていると、関数の外側にあるグローバル変数までセットになりますが、初期化や代入などの各種のアクセスは、関数の外側でも行われている可能性があるため、容易には切り離せません。有用な関数を他のプログラムで再利用しようとしても、大変な労力をかけなくてはならなくなります。

関数スコープ

goto文で使用するラベル名は、その関数定義全体にスコープを持ちます。 これは、関数スコープと呼ばれます。

ブロックスコープと違って、ラベル名が登場するところより手前からも可視です。 そのため、goto文は手前側にも、後ろ側にもジャンプできます。

関数プロトタイプスコープ

関数プロトタイプに記述する仮引数は、その宣言の中にだけスコープを持ちます。 これは、関数プロトタイプスコープと呼ばれます。

これは他の宣言や、関数定義内にまで、仮引数の名前が露出して邪魔をしないようにするための処置です。


時代遅れのキーワード

現在では役目を終えたと考えても差し支えない、2つの古いキーワードが残っています。

auto

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

auto int num;

auto は、自動記憶域期間の「自動」から来た名前ですが、ローカル変数はそもそも自動記憶域期間を持つので、付けても何ら効果を及ぼしません。これは古い時代からの名残で残っているだけの機能です。

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

register

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

register int num;

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/5/12 静的記憶域期間を持つ変数に明示的に与える初期値が、定数でなければならないことについて追記。

'2018/2/25 全面的に文章を見直し、修正を行った。
項の分類も大幅に変更した。以前は「ローカル変数」「グローバル変数」のようなタイトルで項を分けていたが、 章のテーマに合わせて、「〇〇スコープ」という分類に直した。
「静的ローカル変数」の項は、第23章へ移動。

'2018/1/5 コンパイラの対応状況について、対応している場合は明記しない方針にした。

'2017/7/30 clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。

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

≪更に古い更新履歴を展開する≫



前の章へ(第21章 型変換)

次の章へ(第23章 複数ファイルによるプログラム)

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

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS