先頭へ戻る

スコープ | Programming Place Plus C言語編 第22章

Programming Place Plus トップページC言語編

先頭へ戻る

この章の概要

この章の概要です。


スコープ(有効範囲)

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

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

ここでは変数を例にとっていますが、スコープの概念が適用されるのは、各種の識別子です。第3章でわずかに触れたように、識別子とは名前のことですから、関数にも当てはまります。スコープの範囲内にない関数は呼び出せないというわけです。

関数の宣言や定義が、呼び出し位置よりも後ろにあると、その関数を呼び出せないことを、第10章で確認済みです。第10章では、「関数の定義を見つけられない」という表現がされていましたが、言い換えると、「スコープの範囲内にないので、可視でない(使用可能でない)」ということです。

なお、同じ名前の異なる変数や関数を宣言することが許されないのも、スコープに関係します。つまり、同一のスコープの範囲内に、同じ名前の異なる変数や関数を宣言できません。

スコープはいくつかの種類に分類できます。この章ではそれらを順番に確認していきます。


ブロックスコープ

ブロックの内側で宣言した変数のスコープを、ブロックスコープと呼びます。ブロックスコープは、宣言位置からブロックの末尾までです。

関数の仮引数についても、ブロックスコープを持つものとみなされます。この場合は、関数全体がスコープです。

ブロックスコープを持つ変数のことをよく、ローカル変数(局所変数)と呼びます。また、ブロックスコープのことを、ローカルスコープ(局所スコープ)と呼ぶことがあります。

ブロックスコープの挙動を確認してみましょう。

#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;  // この位置から、main関数の終わりまで使える

    if( 1 ){
        int b = 20;  // if文の真の { } の中で使える
    }
    else{
        int c = 30;  // if文の偽の { } の中で使える
    }

    int d = 40;  // この位置から、main関数の終わりまで使える

    for( ; a >= 0; --a ){
        int e = 50;  // for文の終わりまで使える
    }

    while( 0 ){
        int f = 60;  // while文の終わりまで使える
    }

    do{
        int g = 70;  // do-while文の終わりまで使える
    } while( 0 );

    return 0;
}

【上級】goto文を使って、初期値を与えている変数宣言を飛ばしてしまうようなコードを書くと、その変数は宣言されているものの、初期化はされていないという状態を作り出せてしまいます。これは非常に奇妙なプログラムであり、避けるべきです。変数が初期化されないのは、実行中に、初期化している部分のコードを通っていないからです。

【上級】switch文の { の直後の位置でも変数宣言することができますが、これはまず使うことはありません。この位置で変数を宣言すると、それが静的ローカル変数第24章でない限りは、初期化されません。なぜなら、プログラムの実行中、その位置は通過しないからです。

ブロックは、構文上必ず必要な場面以外でも、{ } を使えば任意に作れますから、自分で限定的なブロックスコープを作れます。

#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文などを入れ子にするだけで起こることなので、完全に避けるのは難しいでしょう。それでも、ある名前が、どの宣言によるものを指しているのかが、簡単に把握できなくなるようなコードは避けるべきです。単に名前を変えるのでも良いですし、新たに作ろうとしたスコープの部分を関数化してしまうのも良いでしょう。

また、第15章で説明したとおり、for文の初期設定式のところでも、変数を宣言できます。その変数のスコープは、その for文全体に限定されます。

ファイルスコープ

変数は、関数定義の外側でも宣言できます。関数定義の外側で宣言された変数は、特定の関数専用ではなく、複数の関数で共用できます。このような変数はよく、グローバル変数(大域変数)と呼ばれています。

#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 やヌルポインタが与えられます。

また、静的記憶域期間を持つ変数に明示的に初期値を与える場合は、定数でなければなりません

なお、これまでに登場したようなローカル変数は、自動記憶域期間を持ちます。自動記憶域期間を持つ変数は、その変数の宣言が含まれているブロックに処理が入ってきたときに作られ、抜け出した時点で消失します。

第24章で取り上げる静的ローカル変数は、ブロックスコープを持ちながら、静的記憶域期間を持つ変数です。

ブロックスコープのところでも触れましたが、一般的に、スコープは極力狭く保つべきです。その意味では、グローバル変数は完全に外れた発想であり、実際、グローバル変数の使用もできるだけ避けるべきだと言われています

たとえば、先ほどのサンプルプログラムは、引数のしくみを使って、次のように書き換えられます。

#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文は手前側にも、後ろ側にもジャンプできます。

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

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

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


練習問題

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

#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 をそれぞれ指定子と表記するように修正。

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



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

次の章へ (第23章 プリプロセス)

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
Twitter でツイート Twitter をフォロー LINE で送る
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー