2026/08/21

C 言語の変数定義

C 言語の変数定義は宣言部に若干クセがあるのでまとめました。

ローカル変数

変数のスコープが宣言されたブロックの内側に限定される変数です。 ブロックとは { と } の内側などで、一番判りやすいのは関数の宣言部分です。

以前は変数宣言はブロックの先頭のみで可能でしたが ISO/IEC 9899:1999 (C99) から任意の場所の変数宣言が可能となっています。

  1int     foo(void)
  2{
  3
  4    int     i;
  5
  6    i = 0;                  /* ローカル変数 */
  7
  8    {
  9        int     j;          /* ローカル変数 */
 10            :
 11    }
 12
 13    /* ここでは j は参照できない */
 14
 15}
    
ローカル変数の特徴を以下に示します。
  • スコープ (有効範囲)は宣言されたブロック内に限られます。
  • 関数の呼び出しごとに新しく確保され関数から戻るときに破棄されます。
  • 実体は基本的に後述するスタック上に置かれます。
  • 初期化しないと値は不定で内容は保証されていません。

グローバル変数

関数の外で宣言する変数です。

  1int     status;                 /* グローバル変数 (初期値は 0) */
  2
  3int     foo(void)
  4{
  5
  6    if(status == 1){            /* ファイル内のどこからでも参照できる */
  7        :
    
グローバル変数の特徴を以下に示します。
  • スコープはファイル全体です (extern を宣言する事で他ファイルからもアクセス可能)。
  • プログラム開始時に確保され終了するまで存在し続けます。
  • 明示的に初期化しなくても自動的に 0 で初期化されます。
  • 実体は実行ファイル内の「データ領域」に配置されます。
グローバル変数のうち、初期値を持つものは .data セグメント、 初期値を持たないもの (自動で初期化されるもの) は .bss セグメントに割り当てられるのが一般的です。

メモリレイアウトとスタック

典型的なプロセスのメモリ空間はアドレスの低い方から高い方へおおよそ次のように並びます。

テキスト領域
データ領域 (.data)
BSS 領域 (.bss)
ヒープ
↓
↑
スタック
←機械語コード
←初期化済みグローバル / static 変数
←未初期化グローバル / static 変数
←malloc(3) で動的に確保される領域
←関数ごとに利用される領域

ヒープとスタックが互いに向かって伸びていくため、両方が使いすぎて衝突すると「スタックオーバーフロー」などの問題が起きます。

スタックの動作(スタックフレーム)

関数を呼び出すたびに、スタック上に「スタックフレーム」と呼ばれる領域が積まれます。中身はおおよそ次のようなものです。

  • 呼び出し元に戻るためのリターンアドレス
  • 呼び出し元のベースポインタの退避値(スタックフレーム同士を辿るため)
  • 引数(レジスタ渡しの場合もあります)
  • その関数のローカル変数

具体的なスタック利用の流れ

  1int     add(int a, int b)
  2{
  3
  4    int result = a + b;     /* ローカル変数はこの関数のスタックフレームに置かれる */
  5
  6    return result;
  7    
  8}
  9
 10int     main(void)
 11{
 12
 13    int x = 3,
 14          y = 4;
 15    int z = add(x, y);      /* add() 呼び出し時に新しいスタックフレームが積まれる */
 16
 17    printf("%d\n", z);
 18
 19    return 0;
 20    
 21}
            
  1. main() が呼ばれるとスタックに main() 用のフレームが積まれ(プッシュ)、 スタック上に変数 x、y、z が確保されます。
  2. 関数 add(x, y) が呼ばれるとスタックにリターンアドレスなどを積んだ上で さらに add() 用のフレームが積まれ、引数 a、b、変数 result が確保されます。
  3. add() が return するとそのフレームはスタックから取り除かれ(ポップ)、 制御は main() に戻ります。 add() のローカル変数 (a、b、result) はこの時点で消滅します。
  4. main() が return すると、main() のフレームも取り除かれます。
この様な構造のため関数 add() で定義された変数 result は add() の外では参照できませんし、 参照しても値は保証されていません。

ダングリングポインタによる BUG

関数 add() を少し変更して文字列を操作する関数に変えてみます (例示のためなのでチェック処理やエラー処理などは実施していません)。

  1char    *add(char *a, char *b)
  2{
  3
  4    char    result[1024];               /* ローカル変数はこの関数のスタックフレームに置かれる */
  5
  6    snprintf(result, sizeof(result), "%s%s", a, b);
  7
  8    return result;
  9
 10}
 11
 12int     main(void)
 13{
 14
 15    char    *x = "foo",
 16            *y = "bar";
 17    char    *z = add(x, y);         /* 関数 add() のローカル変数を参照 */
 18
 19    printf("%s\n", z);
 20
 21    return 0;
 22    
 23}
            
関数 add() はローカル変数 result のアドレスをリターンしていますが、 前述の通り result はスタック上に確保された変数のため add() のリターン後は値は保証されません。 リターンした値はスタックのアドレスを示していますが値は保証されていません。 そのため z の値は不定となってしまいます。 関数からローカル変数をリターンする場合は必ず値をリターンする必要があります。 C 言語の仕様として配列の名前を参照した場合は配列の先頭要素を指すアドレスとして扱われますので特に注意が必要です。 この様にローカル変数のアドレスをリターンする事をダングリングポインタと呼びます。

再帰とスタックオーバーフロー

このLIFO (Last In First Out) 構造のおかげで、 関数呼び出しが何段ネストしても各関数は自分専用の変数領域を持てますし、 再帰呼び出しでも呼び出し回数分のフレームが積み重なるだけで正しく動作します。 逆に言うと、再帰が深すぎたり、ローカルに巨大な配列を確保しすぎたりすると、 このスタック領域を使い切って「スタックオーバーフロー」を起こします。

  1void    recurse(int n)
  2{
  3
  4    char    buf[1024 * 1024];           /* 1関数呼び出しごとに 1MB 消費 */
  5
  6    recurse(n + 1);                     /* 再帰呼び出し (すぐにスタックが尽きる) */
  7
  8}
            
とは言っても再帰呼び出しする場合は staic 変数は BUG の原因となるので、 再帰呼び出しの可能性がある場合は static 変数は使わない方が良いです。

以下は static 変数を利用したために不正な値となってしまう階乗計算処理です。
  1int     count(int n)
  2{
  3
  4    static  int total = 0;              /* すべての再帰で共有される */
  5    
  6    total *= n;
  7    if(n > 0)
  8        count(n - 1);                   /* 再帰呼び出し */
  9
 10    return total;
 11
 12}
 13
 14int     main(int argc, char *argv[])
 15{
 16
 17    printf("%d\n", count(atoi(argv[1])));
 18
 19    return 0;
 20
 21}
            
この様な BUG の温床になってしまう事もあるので、 特に最近では再帰呼び出し自体を悪だとする風潮もある様ですが 再帰呼び出し便利なんですよね...ついつい使ってしまいます。

static 宣言

C 言語の static 修飾子は文脈によって複数の意味と効果を持っているので初心者の頃は混乱しがちです。 そもそも複数の効果を持つ修飾子を同じ名前にするな...と Dennis に言いたい。

関数内の static 変数

関数の内部での static 修飾子は記憶クラスを定義します。 static 指定の変数を定義すると変数は前述のデータ領域に置かれ関数が終了した後も値が保持されますが、 変数のスコープはローカル変数同様にブロックの内部のみとなっています。 以下の関数 foo() では最初に呼ばれた時のみ変数 i が 0 で初期化され、 呼び出し毎に i がインクリメントされます。 2 回目の呼び出しでは i は 2 に、10 回目の呼び出しでは i は 10 となります。 当然ですが関数 foo() の外側では i は参照できません。

  1int     foo(void)
  2{
  3
  4    static  int     i = 0;          /* 変数領域は .data に確保され初期値として 0 が設定される */
  5
  6    i++;
  7
  8}
        

ファイルのグローバル変数

グローバル変数への static 修飾子は変数のスコープを定義します。 static 指定の変数はスコープがファイル内に制限されます。 static 修飾子を指定しない変数は他のファイルで extern を指定する事で参照する事ができますが、 static 修飾子を指定された変数は extern 指定されても他のファイルからは参照できません。

  1/* ファイル foo.c */
  2static  int     result;         /* 他のファイルから参照不可 */
  3        int     status;         /* 他のファイルから参照可能 */
  4
  5    :
  6
  7/* ファイル bar.c */
  8extern  int     status;         /* foo.c の status を参照 */
  9extern  int     result;         /* foo.c の result は参照できない (通常はコンパイル時にリンクエラーとなる) */
 10
 11    :
        

関数の指定

関数の宣言に static 修飾子を指定した場合はファイルのグローバル変数同様関数のスコープを定義します。 static 修飾子を指定した関数はスコープがファイル内に制限されます。 static 修飾子を指定しない関数は extern 指定する事で他のファイルからも呼び出す事ができますが、 static 修飾子を指定された関数は extern 指定されても他のファイルからは呼び出せません。

まとめ

グローバル変数は値を保持しグローバルに参照できるので便利な変数です。 しかし、多用すると処理全体の見通しが悪くなったり関数の独立性が阻害されてしまうので、 どうしても必要な場合に限ったほうが良いでしょう。


ローカル変数 グローバル変数
宣言位置 関数 / ブロック内 関数外
スコープ そのブロック内のみ ファイル全体 (extern で他ファイルも可)
記憶域期間 自動 (関数終了で消滅) 静的 (プログラム終了まで存在)
確保場所 スタック (static 以外) データ / BSS 領域
初期値 不定 自動的に 0
主なリスク スタックオーバーフロー、ダングリングポインタ 名前衝突、意図しない状態共有によるバグ、マルチスレッドでの競合

トラックバック
https://blog.bsdhack.org/index.cgi/Computer/20260821.trackback
コメント














メッセージ: Ready to post a comment.


Copyright © 2008-2020 Mitzyuki IMAIZUMI. All rights reserved.