C++の有効範囲(スコープ)と記憶域期間:変数の生存期間と可視性

C++プログラミングにおいて、変数を理解する上で最も重要な2つの概念が「有効範囲(スコープ)」と「記憶域期間(ストレージ期間)」です。

  • 有効範囲 (Scope): その変数名(識別子)が、コードのどの場所から「見える」か、「アクセスできる」かを定義するルールです。
  • 記憶域期間 (Storage Duration): その変数が、メモリ上で「いつ生成」され、「いつ破棄」されるか(生存期間)を定義するルールです。

これら2つは似ていますが、異なる概念であり、正しく理解することがバグの少ないコードを書くための鍵となります。

目次

有効範囲(スコープ)とは?

スコープは、中括弧 {} によって作られる「ブロック」と密接に関連しています。C++には主に以下のスコープが存在します。

  • ブロックスコープ: for 文、if 文、while 文、あるいは単なる {} の内側で宣言された変数は、そのブロックの内部でのみ有効です。
  • ローカルスコープ(関数スコープ): 関数の内部で宣言された変数は、その関数の内部でのみ有効です。
  • グローバルスコープ(名前空間スコープ): どの関数の外側でも宣言された変数は、グローバルスコープを持ち、プログラムの多くの場所からアクセスできます。

スコープと「シャドーイング」

内側のスコープで外側のスコープと同じ名前の変数を宣言すると、内側では外側の変数が「隠されます」。これをシャドーイング (shadowing) と呼びます。

スコープ解決演算子 (::)

:: 演算子は、スコープを明示的に指定するためのものです。

  • std::cout のように、:: の左側に名前空間 (std) を指定すると、その名前空間に属するメンバー (cout) にアクセスできます。
  • ::appVersion のように、:: の左側を空にすると、グローバルスコープに強制的にアクセスできます。

スコープのサンプルコード

#include <iostream>
#include <string>

// --- グローバルスコープ ---
// どの関数からもアクセス可能
std::string appVersion = "v1.0.0 (Global)";

// この関数はグローバルスコープの appVersion を参照する
void displayAppVersion() {
    std::cout << "  (displayAppVersion) Version: " << appVersion << "\n";
}

int main() {
    std::cout << "--- スコープのデモ ---\n";

    // 1. グローバルスコープの変数にアクセス
    std::cout << "(main) グローバル: " << appVersion << "\n";

    // 2. ローカルスコープ(シャドーイング)
    // グローバル変数と同じ名前で、main関数のローカル変数を宣言
    std::string appVersion = "v2.0.0 (main Local)";
    std::cout << "(main) ローカル: " << appVersion << "\n"; // "v2.0.0" が表示される

    // 3. ブロックスコープ
    if (true) {
        // この 'appVersion' は if ブロック内でのみ有効
        int appVersion = 99; // 型が異なってもシャドーイングは発生する
        std::cout << "  (ifブロック) ブロック: " << appVersion << "\n"; // "99" が表示される
    } // if ブロックの 'appVersion' (99) はここで破棄される

    std::cout << "(main) ローカルに戻る: " << appVersion << "\n"; // "v2.0.0" が表示される

    // 4. スコープ解決演算子 (::)
    // '::' を使うと、ローカル変数に隠されていてもグローバル変数にアクセスできる
    std::cout << "(main) ::appVersion: " << ::appVersion << "\n"; // "v1.0.0" が表示される
    
    // 5. 関数呼び出し
    displayAppVersion(); // この関数はローカル変数を「知らない」ため、グローバルを参照する

    return 0;
}

記憶域期間 (Storage Duration) とは?

記憶域期間は、オブジェクト(変数)がメモリ上にどれだけ長く存在するかを決定します。

自動記憶域期間 (Automatic Storage Duration)

関数やブロックの内部で(static を付けずに)宣言された変数が対象です。

  • 生存期間: そのブロックが実行されたときに生成され、ブロックを抜けたときに自動的に破棄されます。
  • 特徴: 一般に「スタック」と呼ばれるメモリ領域に配置されます。関数が呼び出されるたびに、新しい変数が生成されます。

静的記憶域期間 (Static Storage Duration)

以下の2種類が対象です。

  1. グローバルスコープで宣言された変数。
  2. static キーワードを付けて宣言されたローカル変数。
  • 生存期間: プログラムの実行が開始される前に生成され、プログラムが終了するまで存在し続けます。
  • 特徴:
    • グローバル変数は、プログラム全体で値を共有します。
    • static ローカル変数は、その関数内からしかアクセスできません(スコープはローカル)が、その値は関数呼び出しをまたいで保持されます。

記憶域期間のサンプルコード

#include <iostream>

// 'g_callCount' は静的記憶域期間を持つ (グローバル)
int g_callCount = 1000;

void logEvent() {
    // 'autoCounter' は自動記憶域期間を持つ
    // logEventが呼び出されるたびに '1' で初期化され、
    // 関数を抜けると破棄される
    int autoCounter = 1;
    
    // 'staticCounter' は静的記憶域期間を持つ
    // 最初の呼び出し時に '0' で初期化され、
    // 関数を抜けても値は保持される
    static int staticCounter = 0; 
    
    autoCounter += 5; // 常に 1 + 5 = 6
    staticCounter += 1; // 1, 2, 3... と増えていく
    
    std::cout << "  (関数内) auto: " << autoCounter;
    std::cout << ", static: " << staticCounter << "\n";
}

int main() {
    std::cout << "--- 1回目の呼び出し ---\n";
    logEvent();
    
    std::cout << "--- 2回目の呼び出し ---\n";
    logEvent();
    
    std::cout << "--- 3回目の呼び出し ---\n";
    logEvent();
    
    return 0;
}

実行結果:

--- 1回目の呼び出し ---
  (関数内) auto: 6, static: 1
--- 2回目の呼び出し ---
  (関数内) auto: 6, static: 2
--- 3回目の呼び出し ---
  (関数内) auto: 6, static: 3

autoCounter は毎回 6 にリセットされますが、staticCounter は呼び出しをまたいで値が保持されていることがわかります。

オブジェクトの生存期間

自動記憶域期間と静的記憶域期間のライフサイクルを比較すると、以下のようになります。

項目自動記憶域期間 (例: int autoVar;)静的記憶域期間 (例: static int sVar; int gVar;)
生成属するブロック(関数など)の実行が開始された時プログラムの実行が開始される前
初期化宣言文が実行された時(毎回)プログラム開始時(グローバル)または関数が初めて呼び出された時(ローカルstatic)に1回だけ
破棄属するブロックが終了した時プログラムが終了した時

参照を返却する関数

関数は int& のように「参照」を返すことができます。これは、関数が内部の変数そのもの(の別名)を呼び出し元に渡し、呼び出し元がその変数を直接変更できるようにする強力な機能です。

しかし、この機能は記憶域期間を理解していないと重大なバグ(未定義動作)を引き起こします。

絶対に、自動記憶域期間を持つローカル変数への参照を返してはいけません。

// (危険な例:絶対に避けるべきコード)
int& getDangerousReference() {
    int localValue = 100;
    // localValue は自動記憶域期間を持つ
    return localValue; // この関数が終了した瞬間に localValue は破棄される
} // 戻り値は「破棄されたメモリ」を指す「ダングリング参照」となる

参照を返す関数の正しい使い方は、関数よりも長く生存するオブジェクト(例:static 変数)への参照を返すことです。

正しい例:静的変数の参照を返す

static ローカル変数は静的記憶域期間を持つため、関数が終了しても破棄されません。

#include <iostream>

/**
 * @brief システムの単一の「セッションカウンター」への参照を返す
 * @return セッションカウンターへの参照
 */
int& getSessionCounter() {
    // 'counter' は静的記憶域期間を持つ
    // この変数はプログラム終了時まで存在する
    static int counter = 1000; 
    
    // 静的変数への参照を返却するのは安全
    return counter;
}

int main() {
    std::cout << "現在のカウンター: " << getSessionCounter() << "\n"; // 1000

    // 参照を介して静的変数を直接変更する
    getSessionCounter() = 2000; 

    std::cout << "変更後のカウンター: " << getSessionCounter() << "\n"; // 2000
    
    // 別の変数に参照を代入
    int& session = getSessionCounter();
    session++; // 2000 -> 2001

    std::cout << "再変更後のカウンター: " << getSessionCounter() << "\n"; // 2001

    return 0;
}

まとめ

  • **有効範囲(スコープ)**は、変数が「見える」範囲(主に {} で決まる)です。
  • 記憶域期間は、変数がメモリ上で「いつまで存在するか」です。
  • **自動記憶域期間(ローカル変数)**は、関数・ブロックの終了時に破棄されます。
  • **静的記憶域期間(グローバル変数, static 変数)**は、プログラムの終了時まで存在し続けます。
  • static ローカル変数は、スコープはローカルですが、記憶域期間は静的であるため、関数呼び出しをまたいで値を保持します。
  • **参照を返す関数(int& func())**は、自動記憶域期間を持つ変数(普通のローカル変数)への参照を返してはいけません。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次