スコープと記憶域期間 | Programming Place Plus C言語編 第22章

トップページC言語編

このページの概要

以下は目次です。


スコープ(有効範囲)

ある関数の中で宣言した変数は、ほかの関数からは使用できません。つまり、関数の中で宣言した変数は、その関数専用の変数になっています。このようなルールを決めているのは、スコープ(有効範囲) (scope) と呼ばれる考え方です。スコープは、ある識別子を使えるソースコード上の範囲を表しています。

スコープ内にある識別子は、可視 (visible) であると表現します。また、識別子は「スコープを持つ」と表現されます。

スコープは識別子に対する概念です。第4章で説明したように、識別子とは名前のことですから、たとえば関数も含まれます。スコープ内にない関数(可視でない関数)は呼び出せないというわけです。関数の宣言や定義が呼び出し位置よりも後ろにあると、その関数を呼び出せないことを、第9章で説明しましたが、これはスコープ内にないからです。

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

スコープには次の4種類があります。

  1. ブロックスコープ
  2. ファイルスコープ
  3. 関数スコープ
  4. 関数プロトタイプスコープ

ブロックスコープ

ブロックの内側で宣言された識別子や、関数の定義に記述する仮引数は、ブロックスコープ (block scope) を持ちます。

ブロックスコープは、その識別子が宣言された位置から、その宣言が含まれているブロックの末尾までです。関数定義の仮引数については、その関数の終わりまでとなります。

同じ仮引数でも、関数の宣言のほうに記述したものは、関数プロトタイプスコープです。

規格上は正式な表現ではないものの、ブロックスコープを持つ変数のことをローカル変数(局所変数) (local variable) と呼ぶことが非常に多いです。

ブロックスコープを持つ変数を動作を確認してみましょう。

#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 とは別の存在
}

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 には影響を与えていません。異なるスコープで宣言された識別子は、同じ名前であっても別の存在です。

for、while、do や if、switch の構文の中にも {} が登場しますが、これもブロックなので、この内側で宣言した識別子は、そのブロックについてのブロックスコープを持つことになります。

int main(void)
{
    int a = 10;  // この位置から、main関数の終わりまで使える

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

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

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

    for (int i = 0; i < 5; ++i) {  // i は for文全体で使える
    }

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

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

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

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

#include <stdio.h>

int main(void)
{
    int v1 = 10;

    {  // 新しいブロックの開始
        int v2 = 20;  // このブロックの終わりまで使える
        printf("%d\n", v2);
    }  // 新しいブロックの終わり。ここより下で v2 は使えない

    printf("%d\n", v1);
}

実行結果:

20
10

一般に、どこから使われているかと、何を使っているのかを分かりやすくするために、スコープを極力せまく保つことが良いとされています。このサンプルプログラムのように、いつでも新たなスコープを追加でき、そうすれば狭いスコープを作り出せますが、かえって分かりにくくなる可能性もあります。というのは、スコープは入れ子になるため、同じ識別子が別の実体を指す状態も作れてしまうためです。

ある識別子を使うときには、その識別子がどの実体のことなのかを、近くのスコープにある宣言から順番に探して判断します。そのため、同じ識別子の宣言が、内側のスコープと外側のスコープの両方にあったら、外側にある方は発見されません不可視 (invisible) になる、隠蔽 (hiding) されるなどと表現します)。

#include <stdio.h>

int main(void)
{
    int v = 10;

    {
        int v = 20;  // このブロックの終わりまで使える
        printf("%d\n", v);  // ここでの v は 20 で初期化したほうの v
    }

    printf("%d\n", v);  // ここでの v は 10 で初期化したほうの v
}

実行結果:

20
10

ファイルスコープ

どのブロック内でも、関数宣言内でもない場所で宣言された識別子は、ファイルスコープ (file scope) を持ちます。

規格上は正式な表現ではないものの、このような変数はよくローカル変数との対比で、グローバル変数(大域変数) (global variable) と呼ばれます。

#include <stdio.h>

int g_num = 10;  // ファイルスコープを持つ変数(グローバル変数)

void myprint(void);  // ファイルスコープを持つ関数の宣言

int main(void)
{
    printf("%d\n", g_num);
    myprint();

    g_num = 20;
    myprint();
}

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

実行結果:

10
10
20

グローバル変数を宣言する構文は、ローカル変数の場合と変わりありません。このサンプルプログラムがしているように、変数名の先頭に gg_ といったプリフィックスを付けて、グローバル変数であることを示す方法はよく使われています。これは、ブロックスコープのように、ファイルスコープよりも狭いスコープに同じ名前の識別子が宣言されて隠蔽が起きることを防止する意味があります。このような命名方法自体は必須事項ではありませんが、少なくとも、グローバル変数には、簡単すぎる名前を付けるべきではありません

グローバル変数を宣言している位置は、プログラムの実行中に通過する場所ではないように思えますが、プログラムの実行開始直後から存在が保証され、初期化もされています。この話題はあとであらためて取り上げることにします

ブロックスコープのところでも触れましたが、一般に、スコープは極力せまく保つべきです。グローバル変数はそのような考え方は完全に外れたものであって、実際、グローバル変数の使用はできるだけ避けるべきだと言われることが多いです。さきほどのサンプルプログラムであれば、引数のしくみを使って、書き換えることで、グローバル変数をなくせます。

#include <stdio.h>

void myprint(int num);

int main(void)
{
    int num = 10;

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

    num = 20;
    myprint(num);
}

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

実行結果:

10
10
20

このように、引数や戻り値を使って情報を受け渡す方が、グローバル変数を使うよりも良いスタイルです。このような作りにしておくと、関数を部品として使いやすくなります。グローバル変数を使っていると、そのグローバル変数まで関数の1セットに含まれているような状態になってしまいます。初期化や代入といった処理を、ほかの関数でも行っている可能性があるため、容易には切り離せません。有用な関数を他のプログラムで再利用しようとしても、大変な労力をかけなくてはならなくなります。

関数スコープ

ラベル名は関数スコープ (function scope) を持ちます。

関数スコープは、そのラベルが宣言された関数内全体です。ブロックスコープと違って、関数スコープは、宣言位置と使用位置の前後関係は関係ありません。ラベルは goto文(第17章)のとび先として使用されますが、位置関係的に goto が先にあって、とび先のラベルが後で宣言されることがあります。それでも問題がないのは、関数スコープの仕様によるものです。

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

関数の宣言に記述する仮引数は、関数プロトタイプスコープ (function prototype scope) を持ちます。

関数プロトタイプスコープは、その宣言の中に限定されます。

記憶域期間

変数を定義すると、メモリにオブジェクト (object) が構築されます。オブジェクトとは、何かしらの値を表現しているメモリ上の部分のことです。その値を解釈できなければ無意味なので、通常は識別子を使ってそこにアクセスでき、何かしらの型があります。1

【上級】ここでいうオブジェクトはC言語の規格に登場するものであり、オブジェクト指向でいうオブジェクトとは関係ありません。

オブジェクトには生存期間(寿命) (lifetime) があります。生存期間は、そのオブジェクトがメモリ上に存在することが保証され、最後に記憶した値が維持される期間をあらわす用語です。そのオブジェクトのためのメモリ領域が確保された時点から始まり、そのメモリ領域を使わなくなったときに終わります。

オブジェクトの生存期間の開始と終了のタイミングは、記憶域期間 (storage duration) という考え方で決まります。

記憶域期間には以下の4種類あります。

  1. 自動記憶域期間
  2. 静的記憶域期間
  3. 動的記憶域期間(割付け記憶域期間)

あるオブジェクトがどの記憶域期間になるかは、オブジェクトをどのように構築したかによって決定されます。このページでは、自動記憶域期間と静的記憶域期間について取り上げます。動的記憶域期間については、第35章であらためて説明することにします。

記憶域期間の範囲外のタイミングでオブジェクトにアクセスすることは未定義の動作2なので、避けなければなりません。

記憶域期間の終わりを迎えたからといって、わざわざメモリ領域を消去することは時間の無駄なので、メモリ上に値が残ったままになっている可能性はあります。しかしそのような期待をして、アクセスしてはいけません。

スコープと記憶域期間を混同しがちですが、これらはまったく話なので、よく整理して理解してください。スコープはその識別子が使用できるかどうか(可視であるか)の話であり、記憶域期間はオブジェクトがメモリ上に生存している期間の話です。たとえば、記憶域期間は継続していても、スコープの範囲外なのでアクセスできないタイミングということがありえます。

自動記憶域期間

次のように構築されたオブジェクトは、自動記憶域期間 (automatic storage duration) を持ちます3

自動記憶域期間を持つオブジェクトの生存期間は、定義を行ったときに始まり、変数の定義を行ったブロックの終わりで終了します。ある関数内で定義した変数が自動記憶域期間を持つのなら、その関数から抜け出したときには確実に生存期間を終えることになります。

【上級】そのため、自動記憶域期間を持つローカル変数を指すポインタを返すことは危険です。呼び出し元に戻ったとき、そのポインタが指す先にオブジェクトが生存している保証がなく、未定義動作になってしまいます。

また、関数に入るたびに作り直されているので、以前の値が引き継がれることはありません。

【上級】ほとんどの処理系で、自動記憶域期間を持つオブジェクトを、メモリ上のスタック領域に配置しますが、C言語の規格上はそのように強制しているわけではありません。

同じ位置で定義したとしても、staticキーワードを付加した場合は、静的記憶域期間を持ちます。

void f(void)
{
    int v1 = 100;         // 自動記憶域期間
    static int v2 = 100;  // 静的記憶域期間
}

externキーワードを付加した場合、その変数の定義が別のところで行われていることを示しており、つまり定義ではなく宣言になります。その宣言自体はオブジェクトを構築しません。

#include <stdio.h>

int v = 100;  // 定義

int main(void)
{
    extern int v;  // 宣言
    v = 200;
    printf("%d\n", v);
}

実行結果:

200

変数v の定義は関数の外側にあり、この場合、静的記憶域期間を持つことになります。

自動記憶域期間を持つ変数を自動変数 (automatic variable) と呼ぶことがあります。また、これをローカル変数と呼ぶことがありますが、ローカル変数という用語は、スコープを言い表したものであって、記憶域期間に対しては何もいっていません。たとえば、staticキーワードを付加して宣言された場合には静的記憶域期間を持つので、ローカル変数ではあっても自動変数ではないことに注意が必要です。

静的記憶域期間

次のように構築されたオブジェクトは、静的記憶域期間 (static storage duration) を持ちます4

【上級】2つ目のほうを正確に表現すると、内部結合または外部結合を持つ場合という意味です。

静的記憶域期間を持つオブジェクトの生存期間は、プログラムの実行開始直後に始まり、プログラムの実行が終わったときに終了します。つまりプログラムを実行しているあいだずっと生き続けています。

静的記憶域期間を持つ変数を静的変数 (static variable) と呼ぶことがあります。

前の項ですでに取り上げたとおり、staticキーワードを付加して定義されたローカル変数は、静的記憶域期間を持ち、よく静的ローカル変数 (static local variable) と呼ばれます。

静的変数は、プログラムの実行が開始した直後、main関数の処理が始まるより前の時点でオブジェクトが構築されています。明示的に初期化子を与えている場合はその値で初期化されますが、与えていない場合は型に応じたデフォルトの値で初期化されます。つまり、自動記憶域期間を持つ変数と違って、未初期化なままになることはありません。

デフォルトの初期化は、大ざっぱにいうと 0 で初期化するということです。

【上級】正確にいえば、その変数の型が算術型の場合は 0、ポインタ型の場合はヌルポインタです(第31章)。また、集成体(第26章)の場合は各メンバの型に応じて 0 やヌルポインタが、共用体第55章)の場合は最初のメンバの型に応じて 0 やヌルポインタが与えられます5

また、静的記憶域期間を持つ変数に明示的に初期値を与える場合は、定数式(第10章)や文字列リテラルでなければなりません6

静的ローカル変数の場合の挙動は少し違和感があるかもしれません。定義の記述位置は関数内ですが、そこで明示的に与えた初期化子による初期化は、プログラムの実行開始直後にすでに実行されています。

【C++プログラマー】C++ では、静的ローカル変数の明示的な初期化子による初期化は、定義位置を初めて通過するときに実行されます。そのため、定数でなければならないという制約はありません。


静的ローカル変数を使えば、関数から抜け出したあとも値が保持されるローカル変数を実現できます。

#include <stdio.h>

void f(void)
{
    static int s = 0;  // ずっと生存しているので、関数を抜けても値が保持されている
    printf("%d\n", s);
    s++;
}

int main(void)
{
    for (int i = 0; i < 10; ++i) {
        f();
    }
}

実行結果:

0
1
2
3
4
5
6
7
8
9

しかし、静的ローカル変数は取り扱いが難しくなるケースもあり、ほかの手段が取れるのならば基本的に避けておくのが無難です。


練習問題

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

#include <stdio.h>

int num = 100;

void func(void);

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

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

void func(void)
{
    int num = 200;

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

問題② 呼び出すたびに 0、1、2 … という具合に増加する整数を出力する countup_print関数を作ってください。

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

#include <stdio.h>

int num = 100;

int main(void)
{
    int num;

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


解答ページはこちら

参考リンク


更新履歴

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



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

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

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る