【C言語】変数のスコープと生存期間

C言語における変数のスコープと生存期間の解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、変数の「スコープ」と「生存期間 (ライフタイム)」について解説していきます。

後で詳細は説明しますが、スコープと生存期間の意味合いは下記となります。

  • 変数の見える範囲
  • 変数のメモリーが確保されている範囲

上記では、あえて似ていないように記述していますが、実はこれらの概念は混同しやすいです。なので、このページでしっかりと違いを理解していきましょう!

また、特に生存期間についてしっかり理解しておかないとポインターやアドレスを適切に利用することができません。ポインターやアドレスを適切に利用できるようになるためにも、是非このページでスコープと生存期間について理解していってください!

前知識:変数の種類

ここから「スコープ」と「生存期間」について解説を進めていきますが、これらの解説が理解しやすくなるよう、このページでは主に変数を下記の4種類に分けて解説を進めていきます。

  • ローカル変数
  • グローバル変数
  • static ローカル変数
  • static グローバル変数

ローカル変数とは、関数内で宣言された変数のことになります。もう少し正確に言えば、ブロック内で宣言された変数です。ブロックとは、{} で囲まれた区間のことです。

例えば各関数は {} で囲まれていますし、ループ文に関しても、繰り返し実行する処理は {} で囲まれています。これらの {} で囲まれた区間がブロックであり、ブロック内で宣言された変数はローカル変数となります。

ローカル変数
int func(int a) {
    int x; // ローカル変数

    /* 略 */
    {
        int y; // ローカル変数
        /* 略 */
    }
}

逆に、ブロックの外側で宣言された変数をグローバル変数と言います。もう少し分かりやすく言えば、ソースコードファイルの先頭部分の関数外で宣言された変数がグローバル変数です。

グローバル変数
int g_x; // グローバル変数

int func1(int a) {
    /* 略 */
}

int func2(int b) {
    /* 略 */
}

さらに、これらのローカル変数やグローバル変数には、修飾子 static を付けることができます。そして、これによってスコープや生存期間が変化することになります。以降では、static を付けたローカル変数を static ローカル変数、static を付けたグローバル変数を static グローバル変数と呼びます。

static ローカル変数
int func(int a) {
    static int x; // staticローカル変数

    /* 略 */
    {
        static int y; // staticローカル変数
        /* 略 */
    }
}
static グローバル変数
static int g_x; // staticグローバル変数

int func1(int a) {
    /* 略 */
}

int func2(int b) {
    /* 略 */
}

ここからは、スコープと生存期間の概要とともに、これらの、”ローカル変数”・”グローバル変数”・”static ローカル変数”・”static グローバル変数” の4種類でのスコープ・生存期間の違いについて解説していきます。また、例えば関数の仮引数など、これらの4種類に当てはまらない例外的なものに関しては随時補足を行う形で解説していきます。

スコープ

では、スコープについて解説していきます。

スコープとは「変数が見える範囲」のことを言います。もう少し分かりやすく言えば、その変数が参照可能(使用可能)な区間のことをスコープと言います。

スコープの説明図

例えば関数内で宣言された変数は、その関数以外からは参照不可(見えない状態)となります。つまり、特定の関数内で宣言された変数は、その関数内のみがスコープであり、他の関数からはスコープ外ということになります。そして、スコープ外の変数を参照した場合はコンパイル時(or リンク時)にエラーが発生することになります。

このスコープは、変数の種類によって異なります。

スポンサーリンク

ローカル変数のスコープ

まず、ローカル変数のスコープは「その変数を宣言した位置 〜 その変数を宣言したブロックの終端」となります。そして、この範囲内でのみ、そのローカル変数を参照することが可能です。

ローカル変数のスコープの説明図

ローカル変数はブロックの先頭で宣言することが多く、この場合は「ブロック内 = スコープ」と考えることができます。

例えば、下記のようなコードを考えてみましょう!

まず  では同じブロック内で宣言された変数 a を参照しており、これはスコープ内の変数ですので参照可能となります。それに対し、 では異なるブロック内で宣言された変数 a を参照しようとしています。ですが、これらのブロックからは変数 a はスコープ外ですので、コンパイル時にエラーが発生することになります。

ローカル変数のスコープ1
#include <stdio.h>

int main(void) {

    {
        int a;
        a = 100; // ①
    }

    {
        a = 200; // ②
    }

    printf("a = %d\n", a); // ③
}

ブロックを入れ子にした場合のローカル変数のスコープ

ややこしいのがブロックを入れ子にした場合のローカル変数のスコープです。前述の通り、ローカル変数のスコープは、その変数を宣言した位置から、その変数を宣言したブロックの終端までとなります。そのため、特定のブロックで宣言されたローカル変数は、そのブロックの内側のブロックからも参照可能です。

入れ子にしたブロックとスコープ

具体的に言えば、下記のようなコードの場合、外側のブロックで宣言された変数 a は、その内側のブロックから参照してもコンパイルエラーは発生せず、正常に動作することになります。例えば下記においては、最後に 200 が出力されることになります。

ローカル変数のスコープ2
#include <stdio.h>

int main(void) {

    int a;

    {
        a = 200;
    }

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

さらに、C言語においては、入れ子にしたブロック内で同じ名前の変数を宣言することが許されています。そして、この場合、それらの変数は、同じ名前にもかかわらず異なる変数として扱われることになります。そして、変数参照時には、その参照箇所から見て「スコープ内」かつ「一番内側のブロックで宣言された変数」が参照されることになります。

例えば下記のコードはコンパイルしてもエラーは発生しません。このように、入れ子にしたブロック内で同じ名前の変数を宣言することは許可されています。

ローカル変数のスコープ3
#include <stdio.h>

int main(void) {

    int a = 100; // ①
    {
        int a = 200; // ②
        {
            int a = 300; // ③
            {
                int a = 400; // ④
                {
                    a = 500; // ⑤
                }
            }
            printf("a = %d\n", a); // ⑥
        }
    }
}

ただし、入れ子にしたブロック内で同じ名前の変数を宣言すると非常にややこしいコードになってしまいます。実際に、このコードのプログラムがどのように動作するのか、ポイントとなる箇所を確認していきたいと思います。

まず、 の printf で出力される変数 a の値は何になるでしょうか?

単純に考えると、 の直前に の位置で変数 a に 500 が代入されているので、 では 500 が出力されるようにも思えます。ですが、入れ子にしたブロック内で同じ名前の変数を宣言した場合は、それらは同じ名前であるものの、異なる変数として扱われます。したがって、 で宣言された変数 a は、それぞれ別の変数ということになります。

入れ子にしたブロック内で宣言された同じ名前のローカル変数が異なる変数として扱われる様子

また、特定の位置で参照可能な同じ名前の変数が複数ある場合、その位置から見て一番近い位置、もう少し詳しく言えば、その位置から見て一番内側、かつ、宣言済みの変数が参照されることになります。そのため、 の位置では、 で宣言された変数 a500 が代入されることになります。

入れ子にしたブロック内で同じ名前のローカル変数が宣言された時の参照の説明図1

それに対し、 の位置では、同様の考え方で  で宣言された変数 a の値が出力されることになります。 で変数 a500 が代入されますが、この代入先の変数 a で宣言されたものであり、 で宣言された変数 a とは別物です。そのため、 の位置では  で代入された 300 がそのまま printf で出力されることになります。

入れ子にしたブロック内で同じ名前のローカル変数が宣言された時の参照の説明図2

このように、ローカル変数は入れ子にしたブロック内で同じ名前で宣言することが可能ですが、このように宣言を行うとどの変数が参照されるかを意識しながらコーディングする必要があって非常にややこしいです…。

ということで、入れ子にした場合、ローカル変数は同じ名前で宣言することは可能ではあるのですが、基本的には同じ名前の変数が同一スコープ内に存在しないようにした方が良いと思います。これにより、上記のようなややこしいルールを理解する必要もなくなります。もちろん、上記のように複数のブロックを入れ子にすること自体少ないとは思いますが、スコープ内に同じ変数名の変数は宣言しないようにしましょう。

for 文で宣言した変数や関数の仮引数のスコープ

また、C言語においては、皆さんご存知の通り for 文でブロック内の処理を繰り返すことが可能です。また、for 文の初期化式部分で変数宣言することも可能です。

forループ
for (int a = 0; a < 10; a++) {
    printf("%d\n", a);
}

この初期化式部分で宣言した変数(上記の場合は変数 a)に関しても、ローカル変数と同様のスコープを持つことになります。もう少し具体的に言えば、繰り返し実行するブロックの先頭で宣言された変数と同じスコープを持ちます。したがって、繰り返し実行するブロック内でのみ参照可能な変数となり、その外側で参照することは不可となります。ループの内側でも外側でも参照したい変数は、for 文の外側で宣言する必要があります。

また、関数では仮引数を宣言することが可能で、これに関してもローカル変数と同じスコープを持つことになります。もう少し具体的に言えば、関数の先頭で宣言した変数と同じスコープを持つことになります。したがって、仮引数は関数内全体から参照可能ですが、関数外からは参照不可ということになります。

forの初期化式で宣言した変数と関数の引数のスコープ

static ローカル変数のスコープ

static ローカル変数のスコープは、単なるローカル変数のスコープと全く同じとなります。そのため、static ローカル変数のスコープについて知りたい方は、ローカル変数のスコープ を参照してください。

スコープは同じですが、static ローカル変数と単なるローカル変数とは生存期間が異なります。これに関しては、後述の 生存期間 の章で解説していきます。

グローバル変数

続いてグローバル変数のスコープについて解説していきます。

グローバル変数のスコープは、プログラムのソースコード内全体になります。要は、プログラム内であれば、どの位置からも、どのタイミングでも参照することのできる変数となります。

グローバル変数のスコープ

ただし、他のソースコードで宣言されたグローバル変数を参照する場合は、そのグローバル変数を参照することを、参照する側のソースコードで extern 宣言する必要があります。

例えば、下記のように宣言したグローバル変数 g は、ソースコード内の、どの関数からも参照可能です(関数 sub については次に紹介するソースコードで定義します)。

グローバル変数を宣言するコード
#include <stdio.h>

int g = 100; // グローバル変数

void add(int x) {
    g = g + x; // グローバル変数gを使用
}

int main(void) {
    printf("g = %d\n", g); // グローバル変数gを使用
    
    add(50);
    printf("g = %d\n", g); // グローバル変数gを使用

    sub(30);
    printf("g = %d\n", g); // グローバル変数gを使用
}

また、C言語では、複数のソースコードのコンパイル結果をリンクして1つのプログラムを作成することが可能です。そして、特定のソースコードで宣言されたグローバル変数は、extern 宣言することでリンクし合う他のソースコードからも参照可能となります。

externで他のソースコードファイルのグローバル変数が参照可能になる様子

例えば下記のように extern 宣言すれば、先ほど紹介したソースコードで宣言したグローバル変数 g を、下記のソースコードからも参照することが可能となります。異なるソースコードから参照されることになりますが、両者のソースコードから参照される変数 g は全く同じ実体のものとなります。そのため、一方のソースコードから変数 g の値を変更すれば、他方側で使用する変数 g の値も変化することになります。

グローバル変数をextern宣言するコード
extern int g; // 他のファイルのグローバル変数gを使用することを宣言

void sub(int x) {
    g = g - x; // グローバル変数gを使用
}

ということで、上記の2つのソースコードをコンパイル・リンクして1つのプログラムを作成して実行すれば、最初の printf では 100 が、2回目の printf では 150 が、3回目の printf では 120 が出力されることになります。特に3回目の出力結果より、2つのソースコードで同じ実体の変数が参照されていることを理解していただけると思います。

このように、グローバル変数のスコープはリンク対象の全てのソースコード(プログラム内全体)であり、どこからでも参照可能な変数となります。ただし、他のソースコードから参照する場合は extern 宣言が必要なので、その点には注意してください。extern についての詳細は下記で解説していますので、extern について詳しく知りたい方は是非下記ページも読んでみてください。

extern宣言の解説ページアイキャッチ 【C言語】extern宣言について解説(ファイル間で変数を共有)

スポンサーリンク

static グローバル変数

単なるグローバル変数のスコープがコンパイル対象の全てのソースコードであるのに対し、static グローバル変数のスコープは、その変数を宣言したソースコードファイル内のみとなります。つまり、グローバル変数に static 修飾子を付けることで、他のソースコードファイルからの参照を禁止することができます。

staticグローバル変数のスコープ

単なるグローバル変数の場合も、元々は、その変数を宣言したソースコード内でのみ参照可能でしたが、extern 宣言することで他のソースコードファイルからも参照可能となります。ですが、static グローバル変数の場合は extern 宣言をするとリンク時にエラーが発生することになります。そのため、static グローバル変数は他のソースコードファイルからは参照することができません。

変数のスコープまとめ

ここで一旦、各種変数のスコープについてまとめておきます。

ここまで説明してきた内容をまとめた表が下記になります。

変数の種類 スコープ
ローカル変数 ブロック内
static ローカル変数 ブロック内
グローバル変数 プログラム全体※
static グローバル変数 ファイル内

※他のファイルから参照する場合は extern 宣言が必要

また、各変数のスコープの広さには下記の関係が成り立つことになります。

ローカル変数 = staticローカル変数 < staticグローバル変数 < グローバル変数

ここまでの説明を聞いて、どこからでも参照可能なグローバル変数が一番便利なように感じる人もいると思いますし、わざわざスコープの狭いローカル変数を利用する必要もないように感じる人もおられるのではないかと思います。

ただ、逆に言えば、グローバル変数はどこからでも変更が可能であるため、グローバル変数を利用する場合は、常にどのタイミングで変数の値が変更されるかを意識しながらプログラミングする必要があって実は大変です。そして、これを意識しないとバグが発生する可能性があります。

この辺りのグローバル変数を扱う難しさについては下記ページでまとめていますので、興味があれば読んでみてください。

グローバル変数を使わないほうが良い理由の解説ページアイキャッチ 【C言語】なぜグローバル変数は使わない方が良いのか?

実は、スコープの狭い変数の方が品質向上という観点では扱いやすいです。もちろん、グローバル変数を使うこと自体が禁止であるというわけではないですが、不必要な場面ではグローバル変数をむやみに利用せず、スコープの狭いローカル変数を積極的に利用することをオススメします。

生存期間

続いて、変数の生存期間について解説していきます。

変数の生存期間とは「変数のメモリーが確保されている範囲」のことになります。

少し難しい内容となるため、まずは変数とメモリーの関係について説明し、その後に生存期間について解説していきたいと思います。

スポンサーリンク

変数とメモリーと生存期間

プログラムでは、データを記憶するために変数を利用することになります。この変数の実体はメモリーとなります。

変数を宣言すれば、変数の型に応じたサイズのメモリーが、その変数専用のメモリーとして確保されることになります。そして、このメモリーにデータを格納することで、プログラムでデータを記憶することが可能となります。また、このメモリーからデータを取得することも可能です。さらに、このメモリーは、その変数専用に確保されていますので、確保されている限りは他の用途で使用されることはありません。

変数とメモリーの関係を示す図

ただし、このようなメモリーは、永遠に確保され続けるというわけではありません。確保される期間には限りがあります。さらに、この期間を過ぎると、そのメモリー解放されて別の用途で利用されることになります。例えば、他の変数専用のメモリー用に確保され、その変数のデータを格納するために利用されることもあります。

生存期間内のみメモリーが確保されることを示す図

このような期間、すなわち「変数のメモリーが確保されている範囲」のことを生存期間と言います。

変数の生存期間内であれば、その変数専用のメモリーには、その変数への代入やコピーによって格納されたデータが保持され続けることになります。ですが、生存期間を過ぎれば、その変数のメモリーが他の用途で利用されることになって別のデータで上書きされてしまう可能性があります。そして、これが何のデータで上書きされるかは分からないため、生存期間を過ぎた変数のメモリーを参照するとプログラムが意図しない動作となる可能性が高いです。そのため、生存期間を過ぎた変数(変数のメモリー)を参照してはいけません

生存期間を過ぎた変数のメモリーのデータを参照してはいけないことを示す図

ということで、プログラムを意図した通りに動作させるためには生存期間をしっかり理解してプログラミングする必要があります。

MEMO

基本的には生存期間を過ぎた変数を参照するとプログラムが意図しない動作となりますが、偶然意図通りに動作してしまうこともあるので注意してください

この偶然性があるため、生存期間を過ぎた変数の参照によるバグを見つけるのが難しいです

この変数の生存期間に関しても、前述で紹介した4種類の変数で異なることになりますので、ここからは各変数の生存期間について解説していきます。

ローカル変数の生存期間

ローカル変数の生存期間はブロック内となります。要は、ローカル変数の生存期間はスコープと同じです。

ローカル変数の生存期間の説明図

したがって、ブロック外に処理が進んだタイミングで、ローカル変数用に確保されていたメモリーは解放され、他のデータで上書きされてしまう可能性があります。

後述で説明するように、生存期間が「プログラム全体」でない変数はローカル変数のみとなります。そのため、ローカル変数の生存期間については特に注意が必要となります。これに関しては後述で解説していきます。

static ローカル変数の生存期間

単なるローカル変数の生存期間がブロック内であるのに対し、static ローカル変数の生存期間は「プログラム全体」となります。つまり、プログラムの起動直後からプログラムが終了するまでが static ローカル変数の生存期間となります。

staticローカル変数の生存期間

この生存期間が、単なるローカル変数と static ローカル変数との決定的な違いとなります。

static ローカル変数の生存期間が「プログラム全体」であるため、変数のメモリーはプログラム終了時まで確保され続け、関数内で宣言した変数の値は関数終了後も残り続けることになります。そのため、static ローカル変数を利用することで、下記のような処理で関数が呼び出しされた回数をプログラム終了までカウントし続けることも可能となります。

staticローカル変数の生存期間
int func(int x) {
    static unsigned int count = 0;

    /* 何らかの処理 */

    count++;
}

上記の変数 countstatic ローカル変数であるため、プログラム起動時に 0 に初期化されます。

MEMO

static ローカル変数を含めて、プログラム全体が生存期間となる変数は、プログラム起動時に初期化が実行されることになります

そして、最初に func が呼び出されると count++ が実行されて値が 1 に変化します。さらに、countstatic ローカル変数であるため、関数が終了しても(ブロックが終了しても)値が保持され続けることになり、次回 func が呼び出された時には count++ で値が 2 に変化します。このように、count の値が関数終了後も保持され続けることになるため、関数が呼び出された回数をカウントすることができるようになっています。

もし、変数 count が単なるローカル変数であれば、関数終了時の変数 count の値が消え去る(他のデータで上書きされる)ことになるため、上手くカウントを行うことができません。

このように、単なるローカル変数と static ローカル変数はスコープは同じではあるものの、生存期間が異なるため、実現可能なことが異なります。そのため、実現したいことに合わせて適切に使い分ける必要があります。

スポンサーリンク

グローバル変数の生存期間

グローバル変数の生存期間は、static ローカル変数と同じく「プログラム全体」となります。なので、グローバル変数の場合は、スコープと生存期間が同等ということになります。

グローバル変数の生存期間

static グローバル変数の生存期間

また、static グローバル変数の生存期間も「プログラム全体」です。つまり、ローカル変数以外は全て生存期間は同じということになります。

staticグローバル変数の生存期間

malloc 関数で確保したメモリーの生存期間

変数の話からは少し離れるのですが、生存期間を考える上で重要になる「malloc 関数で確保したメモリーの生存期間」についてもここで説明しておきたいと思います。

malloc とは、メモリーを確保するC言語の標準関数です。前述の通り、変数宣言に関しても、型や配列のサイズ等に応じたメモリーを確保するためのコードとなります。C言語では、変数宣言以外でもメモリーを確保することが可能で、それが malloc 関数によるメモリー確保になります。

malloc 関数で確保したメモリーの生存期間は「malloc 関数を実行したタイミングから free 関数で解放するまで」となります。これらの関数は、プログラム内の任意のタイミングで実行することができるため、malloc 関数で確保したメモリーの生存期間は開発者が自由に決めることが可能ということになります。好きなタイミングで好きなサイズのメモリーを利用できるようになるため、malloc はかなり便利な関数であると言えます。

malloc関数で確保したメモリーの生存期間

ただし、変数宣言して確保したメモリー同様に、malloc で確保したメモリーも生存期間を過ぎると他のデータで上書きされる可能性があります。そのため、free 関数で解放した後は、そのメモリーは使用してはいけません。また、free 関数での解放を忘れると、確保したメモリーがプログラム終了まで残り続けることになります。この free 関数での解放を忘れた状態で malloc 関数を繰り返し実行すると、いずれメモリーが足りなくなってプログラムが意図通りに動かなくなる可能性もあるので注意が必要です。

ということで、使い方は難しいものの、便利ではあるため、この malloc 関数や malloc 関数の生存期間についても是非覚えておきましょう!malloc 関数の詳細は下記ページで解説していますので、興味があれば是非読んでみてください。

malloc解説ページのアイキャッチ 【C言語】malloc関数(メモリの動的確保)について分かりやすく解説

スポンサーリンク

生存期間のまとめ

ここまで説明してきた生存期間の内容を表にまとめたものが下記となります。

ポイントの1つは、ローカル変数では static 修飾子の有無によって生存期間が変化する点になります。実現したいことに応じて単なるローカル変数と static ローカル変数を適切に使い分けましょう。

変数の種類 生存期間
ローカル変数 ブロック内
static ローカル変数 プログラム全体
グローバル変数 プログラム全体
static グローバル変数 プログラム全体

また、上記のように、基本的に変数の生存期間はC言語の規格として特定の範囲に決められていますが、malloc 関数で確保したメモリーに関しては生存期間を自由に決めることもできます。便利な関数なので、malloc 関数についても是非覚えておきましょう!

スコープと生存期間のまとめ

ここまで説明してきたスコープと生存期間を変数の種類毎にまとめた表が下記となります。

変数の種類 スコープ 生存期間
ローカル変数 ブロック内 ブロック内
static ローカル変数 ブロック内 プログラム全体
グローバル変数 プログラム全体 プログラム全体
static グローバル変数 ソースコード内 プログラム全体

ポイントは、全ての種類の変数において下記が成立していることになります。

生存期間 ≦ スコープ

生存期間 の章で解説したように、生存期間を過ぎた変数は参照してはいけません。ですが、上記が成立しているため、”基本的には” 生存期間を過ぎた変数は参照できないようになっています。スコープ の章で解説したように、スコープ外の変数を参照するとコンパイルやリンク時にエラーが発生するようになっており、そのような参照を行うプログラムはそもそも生成できないようになっています。そのため、C言語では、”基本的には” 変数の生存期間を意識しなくても、自然と生存期間を過ぎた変数が参照できないようになっています。

じゃあ、ここまでせっかく解説を読んできたのに意味ないじゃん!と思った方もおられるかもしれませんが、それは違います。ここまでの説明で “基本的には” という言葉を強調してきたように、実は生存期間を過ぎた変数を参照することができてしまう場合があります。C言語では、この点に注意しながらプログラミングする必要があります。

ということで、最後にスコープと生存期間に関する注意点を説明しておきたいと思います。

生存期間の注意点

では、このページの最後として、C言語での変数利用時の注意点について、スコープと生存期間の観点で解説していきます。

この注意点とは、ここまでも何回も言ってきたように、「生存期間を過ぎた変数を参照してはいけない」という点になります。

ただ、先ほど説明したように、基本的に変数の生存期間はスコープよりも狭いです。したがって、変数を使ってのみ変数を参照する場合、生存期間の過ぎた変数を参照することはコンパイラーやリンカーによって禁止されることになります。

ですが、C言語では、変数を使わずに変数を参照することも可能です。その手段が、アドレスを利用した間接参照になります。この間接参照を実施した場合に、生存期間を過ぎた変数への参照が発生する可能性があります。

スポンサーリンク

直接参照と間接参照

この間接参照による「生存期間を過ぎた変数への参照」を解説する前に、まずは「間接参照」について解説しておきます。

生存期間 の章の冒頭で説明したように、変数を宣言すると、その変数専用のメモリーが確保されることになります。そして、コードに変数名を記述すれば、直接的にそのメモリーを参照することが可能です。

さらに、C言語ではアドレスを扱うことができ、変数のアドレス、すなわち、その変数専用のメモリーのアドレスを &変数名 で取得することが可能です。このアドレスはポインター変数に格納することができ、さらに *ポインター変数 によって、そのポインター変数に格納されたアドレスの変数(メモリー)を参照することが可能です。このアドレスから変数を参照することを間接参照といいます。

つまり、変数への参照には、変数名による「直接参照」アドレス(ポインター変数)による「間接参照」の2種類が存在します。

直接参照と間接参照の違いを説明する図

下記は、直接参照と間接参照の2つの参照を用いて変数 x を参照する例(データの格納やデータの取得を行う例)となります。 および では、参照の仕方は異なるものの、両方とも同じ変数 x の値を出力しているため、それぞれで同じ値が出力されることになります。

直接参照と間接参照
#include <stdio.h>

int main(void) {
    int x;
    int *p; // ポインター変数

    p = &x; // 変数xのアドレスを格納

    x = 1234; // 直接参照

    printf("%d\n", x); // ①直接参照
    printf("%d\n", *p); // ②間接参照

    *p = 5678; // 間接参照

    printf("%d\n", x); // ③直接参照
    printf("%d\n", *p); // ④間接参照
}

直接参照と生存期間

そして、直接参照においては、前述でも説明したように全種類の変数において下記が成立します。なので、直接参照する場合は、生存期間を過ぎた変数を参照しようとするとコンパイルやリンク時にエラーが発生することになります。つまり、生存期間が過ぎた変数の参照はコンパイルやリンクによって防止されることになります。

生存期間 ≦ スコープ

間接参照と生存期間

その一方で、間接参照の場合は上記が成立するとは限りません。したがって、生存期間を過ぎた変数を参照するコードを書いたとしてもコンパイルやリンクに成功し、そのプログラムを実行することができてしまいます。つまり、生存期間を過ぎた変数が参照できてしまいます。

この理由は「変数と、その変数のアドレスを格納するポインター変数のスコープが同じとは限らないから」になります。より具体的には、変数の生存期間よりも、その変数のアドレスを格納するポインター変数の方がスコープが広い場合に生存期間を過ぎた変数を参照できてしまうことになります。ポインター変数では、変数の生存期間に関わらず、その変数のアドレスを格納することが可能です。なので、生存期間の短い変数のアドレスも格納可能です。

よりスコープの広い変数から、生存期間を過ぎた変数を間接参照できてしまうことを説明する図

例えば下記のコードは、生存期間を過ぎた変数を参照する例として分かりやすいのではないかと思います。

生存期間を過ぎた変数の参照1
#include <stdio.h>

int func(void) {
    int *p;

    {
        int a = 100;
        p = &a;
        // 変数aの生存期間終了
    }
    
    printf("*p = %d\n", *p);
    // 変数pの生存期間終了
}

上記では、ポインター変数 p に変数 a のアドレスを格納しており、最後の printf 部分で変数 p、すなわちアドレスから変数 a を間接参照しています。変数 a の生存期間は内側のブロック内となるため、この printf 部分での間接参照は生存期間を過ぎた変数の参照となります。そのため、意図しない動作となる可能性があります。

このように、スコープの広いポインター変数から、生存期間の短い変数を間接参照すると、生存期間の過ぎた変数の参照が発生する可能性があります。そして、C言語においては間接参照はアドレスを利用して実施されることになるため、アドレスやポインター変数を利用する場合に注意が必要となります。

ただし、生存期間 の章で説明したように、ローカル変数以外は生存期間が「プログラム全体」となりますので、ローカル変数以外は生存期間を過ぎた変数への参照が発生することは無いと考えてよいです。この理由は、プログラム終了後に変数が参照されることは無いからになります。つまり、生存期間を過ぎた変数への参照が発生しうるのは、ローカル変数のアドレスを利用した間接参照を実施する場合のみとなります。

スポンサーリンク

生存期間を過ぎた変数の参照例

ということで、ローカル変数のアドレスを扱う場合は、生存期間を過ぎた変数への参照が発生しないように注意を払いながらプログラミングする必要があります。例えば、下記のことを行うと生存期間の過ぎた変数への参照が発生してしまう可能性があるため、特に注意が必要となります。

  • ①:ローカル変数のアドレスを関数の返却値にする
  • ②:非同期で動作する関数の引数にローカル変数のアドレスを指定する

どちらも、特定の関数からローカル変数のアドレスを他の関数に渡す場合のことを示しています。つまり、ローカル変数のアドレスを他の関数に渡す際には注意が必要です。

関数からローカル変数のアドレスを返却してはダメ

例えば、次のソースコードは上記の①に当てはまる例となります。func1 で宣言されるローカル変数 x は func1 内のみが生存期間となります。にも関わらず、x のアドレスを func1 から返却しているため、func1 終了後にも変数 x を間接参照することができる作りとなってしまっています。そのため、このプログラムは意図しない動作となる可能性があります。具体的には、printf で出力される値が意図しないものになってしまう可能性があります。

生存期間を過ぎた変数の参照2
#include <stdio.h>

int *func1(void) {
    int x = 1234;
    int *p_x = &x;

    return p_x;
}


int main(void) {
    int *p = func1();

    /* 略 */
    
    printf("*p = %d\n", *p);
}

これを修正するのであれば、1つの案としては func1 で xstatic ローカル変数として宣言することが考えられます。これにより、func1 終了後も x が生存し続けることになります。

修正案1
#include <stdio.h>

int *func1(void) {
    static int x = 1234;
    int *p_x = &x;

    return p_x;
}


int main(void) {
    int *p = func1();

    /* 略 */
    
    printf("*p = %d\n", *p);
}

また、返却値がアドレスである必要性がないのであれば、func1 でアドレスを返却するのではなく、x の値を返却するように変更する案も考えられます。

修正案2
#include <stdio.h>

int func1(void) {
    int x = 1234;

    return x;
}


int main(void) {
    int p = func1();

    /* 略 */
    
    printf("p = %d\n", p);
}

この場合、func1 終了時に x の生存期間は終了しますが、x の値が関数呼び出し側に渡されて他の変数に格納されることになるため、その変数の生存期間が続く限り、func1 の返却値を利用することが可能となります。

値そのものを返却することで生存期間の過ぎた変数への参照を防ぐ様子

とにかく、関数からローカル変数のアドレスを返却すると「生存期間の過ぎた変数への参照」が発生してしまうことになるため、関数からのローカル変数のアドレスは返却しないように注意してください。

非同期で実行される関数にローカル変数のアドレスを渡すのはダメ

また、前述の②に関しては詳細を解説すると内容が難しくため、簡略的に説明させていただきますが、非同期で処理が実行される関数にローカル変数のアドレスを渡すときも注意が必要となります。

基本的に、関数が実行される期間は、呼び出される側の関数よりも呼び出す側の関数の方が長くなります。なぜなら、呼び出す側の関数(下図の関数 A)は、呼び出した関数(下図の関数 B)が終了するまで待たされることになるためです。そのため、”基本的には” 関数の引数にローカル変数のアドレスを渡しても、生存期間を過ぎた変数への参照が発生することはありません。

同期関数にローカル変数のアドレスを渡しても問題ないことを示す図

ですが、プログラムでは、複数の関数(処理)を同時に非同期に実行することが可能です。これらの非同期に実行される関数間でローカル変数のアドレスを渡すと、両方の関数から、そのローカル変数を参照することができるようになります。そして、この時に、生存期間を過ぎた変数への参照が発生してしまう可能性があります。

この理由は、そのローカル変数を宣言した関数(下図の関数 A)の方が、そのアドレスを受け取った関数(下図の関数 B)よりも長く実行され続けるとは限らないからです。もし、そのローカル変数を宣言した関数の方が先に終了し、その後、そのアドレスを受け取った関数が、間接参照を実施すると、生存期間を過ぎた変数への参照が発生してしまうことになります

同期関数にローカル変数のアドレスを渡してはいけないことを示す図

ということで、非同期処理を導入した場合には、ローカル変数のアドレスを他の関数に渡す際に注意が必要となります。

ただし、非同期処理を行った場合に、ローカル変数のアドレスを他の関数に渡すと必ず生存期間を過ぎた変数への参照が発生するというわけではありません。例えば上の図の例であれば、関数 A の終了が関数 B の終了よりも必ず後になるように上手く同期しながら動作するようにすれば、関数 A で宣言したローカル変数は関数 B が実行されている期間は必ず生存している(メモリーが確保されている)ことになるため、問題ないことになります。結局は、生存期間を過ぎた変数への参照が発生しないように設計・プログラミングされていることが重要になります。

ちなみに、この非同期処理は、C言語ではマルチスレッドという仕組みを利用して実現することが可能です。マルチスレッドについては下記ページで解説していますので、詳しく知りたい方は下記ページを参照してください。

入門者向け!C言語でのマルチスレッドをわかりやすく解説

ここまで説明してきたように、特定の関数からローカル変数のアドレスを他の関数に渡すことで生存期間の過ぎた変数への参照が発生する可能性があるため注意が必要となります。より具体的には、下記の場合に注意が必要です。①はやってはダメで、②は注意が必要になります。

  • ①:ローカル変数のアドレスを関数の返却値にする
  • ②:非同期で動作する関数の引数にローカル変数のアドレスを指定する

ただし、ローカル変数のアドレスを他の関数に渡すこと自体が問題というわけではありません。関数の引数にローカル変数のアドレスを渡すようなことは、実際の開発現場でも普通に行われます。重要なことは、あくまでも生存期間の過ぎた変数への参照を行わないことです。これが行われないのであれば、ローカル変数のアドレスを他の関数に渡しても問題ありません。

特に初心者の方の方であれば、何も考えずにプログラミングしていても生存期間を過ぎた変数へのアクセスは自然と防げているはずです。これは、スコープの仕組みによって生存期間の過ぎた変数への参照が行われないように制限されるからです。

ただ、アドレス・ポインターを利用したり、非同期処理を導入したりしだすと、上記のような参照が行われてしまう可能性が出てくるので、その際には生存期間をしっかり意識しながらプログラミングするようにしましょう!

まとめ

このページでは、C言語における変数のスコープと生存期間について解説しました!

スコープとは「変数が見える範囲」のことを言います。それに対し、生存期間とは「変数のメモリーが確保され、そのメモリーのデータが保持される範囲」のことを言います。

これらのスコープと生存期間は、変数の種類(ローカル変数 or グローバル変数、さらには static の有無)によって変わります。各変数のスコープと生存期間についてはしっかり理解しておきましょう!

また、スコープ外の変数への参照を行うとコンパイル時やリンク時にエラーが発生することになりますが、生存期間外の変数への参照に関してはコンパイル時やリンク時にエラーが発生しません。なので、生存期間外の変数の参照が発生していることには気付きにくいです。そのため、特にアドレス・ポインター・非同期処理を利用する場合は、生存期間については常に注意を払いながらプログラミングすることが必要となります。

品質の高いプログラムを開発するためにも、生存期間(&スコープ)の知識は必ず役に立ちますので、是非このページで学んだ内容は覚えておいてください!

オススメの参考書(PR)

C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!

まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。

  • 参考書によって、解説の仕方は異なる
  • 読み手によって、理解しやすい解説の仕方は異なる

ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?

それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。

なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。

特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。

もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!

入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です