C/C++の関数内で定義されたstatic変数についてまとめました。
Table of Contents
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; }