C/C++の関数内のstatic変数について

C/C++の関数内で定義されたstatic変数についてまとめました。

 

1 bssセクションに保存される

関数内でstaticをつけて宣言された変数はコンパイル時にbssセクション(静的領域)に確保されます。

.bssセクションに設置された変数は0で初期化されます。

バッファ等の大きい領域を必要とする変数はヒープや静的領域に確保します。

void func()
{
  static char buffer[4096];
}

staticをつけずにスタックに大きなバッファを確保したコードは、実行時にスタックがオーバーフローしてしまいます。

void func()
{
  char buffer[1 * 1024 * 1024]; /* Stack overflow. */
  memcpy(buffer, "hello", 6);   /* SIGSEGV */
}

int main()
{
  func();
  return 0;
}

ただし、staticをつけるとスレッドセーフではなくなるので(staticをつけていない場合はスレッド毎に用意されたスタックを用いる)、変数が大きな領域を必要としない場合はstaticをつけないでおくか、大きな領域を必要とする場合はstaticを付けつつ別途staticなロック機構を用意する必要があります。

 

上のコードの場合はmemcpyをロック機構で保護する必要があります。

2 staticがついたPOD型の初期化

Stackoverflowの投稿によれば、staticがついたPOD型の変数は変数のスコープに入る前に初期化されると規格で規定されています。

GCCでは.init_arrayで初期化するようになっています。

POD型とはコンストラクタを持たない型です。

基本型やコンストラクタを定義していないstructが該当します。

 

以下のコードでbufferは.init_arrayに配置され、プログラム実行時にゼロで初期化されます。

static void func()
{
  static int buffer[SIZE];
}

gcc -Sの結果は以下の通りです。

.LFE1024:
        .size   _GLOBAL__sub_I_main, .-_GLOBAL__sub_I_main
        .section        .init_array,"aw"
        .align 8
        .quad   _GLOBAL__sub_I_main
        .local  _ZZL4funcvE6buffer
        .comm   _ZZL4funcvE6buffer,134217728,32
        .hidden __dso_handle
        .ident  "GCC: (Ubuntu 5.2.1-22ubuntu2) 5.2.1 20151010"
        .section        .note.GNU-stack,"",@progbits

以下のコードはbuffer0番目の要素を0で初期化し、残りの要素を0で初期化する意味合いになります。

最近のGCCでは上述のコードと同じ内容になります(0番目の要素を0で初期化し、残りの要素を初期化するコードは生成されません)。

static void func()
{
  static int buffer[SIZE] = { 0 };
}

以下のコードでは.init_arrayに設置されたbufferは、_ZZL4funcvE6bufferを指すobjectとなり、0番目の要素を1で初期化し、残りの要素を0で初期化するコードになります(.long 1と.zero 残りのサイズ)。

なおstaticをつけない場合は0番目の要素を1に設定し、残りの部分はmemsetで0に設定するコードが生成されます。

static void func()
{
  static int buffer[SIZE] = { 1 };
}
.LFE1024:
        .size   _GLOBAL__sub_I_main, .-_GLOBAL__sub_I_main
        .section        .init_array,"aw"
        .align 8
        .quad   _GLOBAL__sub_I_main
        .data
        .align 32
        .type   _ZZL4funcvE6buffer, @object
        .size   _ZZL4funcvE6buffer, 134217728
_ZZL4funcvE6buffer:
        .long   1
        .zero   134217724
        .hidden __dso_handle
        .ident  "GCC: (Ubuntu 5.2.1-22ubuntu2) 5.2.1 20151010"
        .section        .note.GNU-stack,"",@progbits

3 staticがついたPOD型でない型の初期化

staticがついたPOD型でない型は最初の関数実行時に初期化されます。

関数外で定義したstaticな変数とは異なり、関数内で定義したstaticな変数は関数が初めて実行される際に初期化されます。

1. __cxa_guard_acquire
2. 初期化したかを確認し、してない場合は初期化する
3. __cxa_guard_release

__cxa_guard_acquire/__cxa_guard_releaseはpthread_mutex_tでロックを取る為、スレッドセーフなコードが生成されます。

例えば以下のコードは次のような処理になります。

MyClass *MyClass::singleton()
{
  static MyClass gMyClass;
  return &gMyClass;
}
1. __cxa_guard_acquire
2. MyClass()でgMyClassを初期化
3. __cxa_guard_release
4. return &gMyClass

__cxa_guard_acquire/__cxa_guard_releaseはあくまで初期化周りのロックとなります。

3.1 シングルトンパターンの実装

先ほどのスレッドセーフなコード生成を利用して、シングルトンパターンを実装できます。

MyClass *MyClass::singleton()
{
  static MyClass gMyClass;
  return &gMyClass;
}

上のコードでは、関数を呼び出す度に__cxa_guard_acquire/__cxa_guard_releaseと変数初期化確認の処理が実行されます。

関数外に出してやればこれらの処理は実行されません。

static MyClass gMyClass;

MyClass *MyClass::singleton()
{
  return &gMyClass;
}

関数外のstaticな変数はmain関数が呼ばれる前に実行される為、main関数以降で初期化した場合には問題になります。

以下のようにstaticな初期化関数と終了化関数をクラスで定義して、singleton関数を使用する前に初期化すると良いでしょう。

static MyClass *gMyClass = nullptr;

MyClass *MyClass::singleton()
{
  return gMyClass;
}

void MyClass::initialize()
{
  gMyClass = new MyClass();
}

void MyClass::finalize()
{
  delete gMyClass;
}