不會吧!全局變量在 main 前的初始化,竟然是靜態、動態兩步走?
全局變量在 main 函數前初始化,這個大家都知道,但是,全局變量的初始化方式卻是一個容易被忽視但又至關重要的細節,全局變量的初始化可以分為靜態初始化和動態初始化兩種方式。
一、 什么是全局變量初始化?
全局變量是在所有函數體之外聲明的變量。
初始化是指為變量賦予其初始值的過程,它們的內存空間在程序啟動時就會被分配,并且它們的初始化過程發生在 main 函數執行之前。
這個初始化過程可以分為兩個不同的階段:靜態初始化 (Static Initialization) 和 動態初始化 (Dynamic Initialization)。
這里需要知道的是: static 局部變量 、static 類成員變量 和全局變量它們都是具有靜態生命周期的變量!
二、 靜態初始化:編譯鏈接時的確定性
靜態初始化是全局變量初始化的第一個階段,它發生在程序加載之前,由編譯器和鏈接器在可執行文件中預先安排。
這個階段的特點是:
(1) 初始化值必須常量表達式
靜態初始化的值必須是在編譯時就能完全確定的常量。這包括字面量(如 10, "hello")、const 常量、枚舉值,以及由這些常量組成的算術表達式等。(C++11及后續標準中,常量初始化被明確定義為靜態初始化的一部分,用于優化常量表達式的處理。)
(2) 零初始化:
如果全局變量(或靜態變量)沒有被顯式地初始化,編譯器會對其進行零初始化。這意味著整型變量會被初始化為 0,浮點型為 0.0,指針為 nullptr (或 NULL),布爾值為 false,聚合類型(如數組、結構體)的每個成員都會被遞歸地零初始化。
(3) 實現方式:
編譯器通常會將靜態初始化的值直接寫入可執行文件的特定段(如 .data 段用于顯式初始化的非零值,.bss 段用于零初始化的值)。程序加載時,這些段的內容會被直接映射到內存中,無需執行額外的代碼。
簡單來說,靜態初始化就像是在"設計圖紙"階段就已經確定好的固定參數,直接"印"在了最終的產品上。
示例:
c++復制代碼
#include
int g_zero_initialized; // 靜態初始化:零初始化為 0
int g_explicit_static = 10; // 靜態初始化:用常量表達式 10 初始化
const char* g_message = "Hello"; // 靜態初始化:用字符串字面量(常量)初始化
const int g_const_val = 5 * 2; // 靜態初始化:用常量表達式初始化
int main() {
std::cout << "g_zero_initialized: " << g_zero_initialized << std::endl; // 輸出 0
std::cout << "g_explicit_static: " << g_explicit_static << std::endl; // 輸出 10
std::cout << "g_message: " << g_message << std::endl; // 輸出 Hello
std::cout << "g_const_val: " << g_const_val << std::endl; // 輸出 10return 0;
}
靜態初始化的局限性在于它只能處理常量表達式(例如示例當中的 5 * 2)。如果初始值依賴于運行時計算(如函數調用或隨機數生成),就無法使用靜態初始化,轉而需要動態初始化。
三、 動態初始化:程序啟動時的靈活性
靜態初始化只能處理常量表達式,但有時我們需要用更復雜的方式來初始化全局變量,比如調用函數、使用非 const 全局變量的值,或者初始化一個類的全局對象并調用其構造函數。這時,動態初始化 就派上用場了。
動態初始化發生在靜態初始化完成之后,main 函數開始執行之前。
它的特點是:
(1) 初始化值可以是非常量:
動態初始化允許使用函數調用、其他變量的值(即使它們本身是動態初始化的)或者需要運行時計算的表達式來初始化全局變量。
(2) 執行時機:
在程序啟動過程中,靜態初始化完成后,但在 main 函數執行前,會有一段特殊的啟動代碼(runtime startup code)負責執行這些動態初始化操作。
(3) C++ 類對象:
全局類對象的構造函數調用通常屬于動態初始化(除非構造函數非常簡單且滿足特定條件,可能被優化為靜態初始化)
簡單來說,動態初始化就像是在產品組裝完成后、正式使用前,進行的"開機設置"或"首次配置"。
示例:
#include <iostream>
#include <string>
#include <cmath>
#include <ctime>
// 靜態初始化(零初始化)
int g_some_value;
// 動態初始化 - 需要運行時計算
double g_pi = acos(-1.0); // acos不是常量表達式
// 動態初始化 - 需要調用函數
time_t g_start_time = time(nullptr); // time()函數調用
// 動態初始化 - 依賴其他全局變量 (可能引發順序問題)
// int g_dependent_value = g_some_value + 5; // 如果g_some_value也是動態初始化,需注意順序
// C++ 類對象的動態初始化
classMyClass {
public:
MyClass(const std::string& name) : name_(name) {
std::cout << "構造函數執行: " << name_ << std::endl;
}
std::string getName()const{ return name_; }
private:
std::string name_;
};
std::string get_username(){
// 模擬獲取用戶名
return"默認用戶名";
}
MyClass g_my_object(get_username()); // 調用構造函數和get_username(),動態初始化
intmain(){
std::cout << "main 函數開始執行..." << std::endl;
std::cout << "g_pi: " << g_pi << std::endl;
std::cout << "g_start_time: " << g_start_time << std::endl;
// std::cout << "g_dependent_value: " << g_dependent_value << std::endl;
std::cout << "g_my_object name: " << g_my_object.getName() << std::endl;
// 即使 g_some_value 在 main 之前被動態初始化賦值(如果它是動態的話)
// 這里訪問它時,它已經完成了初始化
g_some_value = 100; // 在 main 中修改
std::cout << "g_some_value in main: " << g_some_value << std::endl;
return0;
}
輸出:(VS2022)
構造函數執行: 默認用戶名
main 函數開始執行...
g_pi: 3.14159
g_start_time: 1744354404
g_my_object name: 默認用戶名
g_some_value in main: 100
四、 為什么區分靜態和動態初始化
區分這兩個階段主要是為了效率和靈活性的平衡:
- 靜態初始化效率高: 它在編譯時確定值,程序加載時映射到內存,不增加程序啟動時間。對于大量簡單的全局數據,這是最優的方式。
- 動態初始化提供靈活性: 它允許進行復雜的初始化操作,適應更多場景,但會稍微增加程序啟動的開銷。
五、 靜態初始化順序災難
這個概念里面的靜態指的是生命周期:靜態存儲期(指的是變量的生命周期從程序開始時分配內存,直到程序結束時才釋放)
動態初始化的一個潛在問題是初始化順序。在不同的編譯單元(不同的 .cpp 文件)中定義的全局變量,它們的動態初始化順序在 C++ 標準中并沒有嚴格規定。如果你在一個編譯單元的動態初始化中,依賴了另一個編譯單元中需要動態初始化的全局變量,就可能因為后者的初始化尚未完成而出錯,這就是所謂的"靜態初始化順序災難"。
避免方法:
- 盡量使用靜態初始化: 如果可能,優先使用常量表達式進行靜態初始化。
- 局部靜態變量: 將全局變量改為函數內的靜態變量,利用其首次調用時才初始化的特性來保證依賴關系。
MyClass& get_global_object()
{
static MyClass instance(get_username()); // 在首次調用時才進行動態初始化
return instance;
}
六、 靜態 vs 動態初始化
特性 | 靜態初始化 | 動態初始化 |
初始化時機 | 編譯時(概念上) | 運行時(程序啟動時) |
初始值類型 | 常量表達式 | 可能涉及函數調用或運行時計算 |
性能開銷 | 幾乎無開銷 | 可能有運行時開銷 |
適用場景 | 固定值,如配置參數 | 依賴環境或動態計算的值 |
潛在問題 | 無 | 初始化順序問題 |
總體來說,我覺得我們開發當中要注意這幾點:
- 第一盡量使用靜態初始化以提高性能并避免初始化順序問題。
- 第二如果必須依賴運行時環境,確保初始化邏輯簡單且無依賴關系。
- 第三對于復雜的初始化需求,可以將邏輯封裝到函數中,并在程序啟動時顯式調用。
七、 總結
- 全局變量的初始化是一個分為靜態初始化和動態初始化的有序過程,發生在 main 函數執行之前。
- 靜態初始化處理常量表達式和零初始化,在編譯鏈接時確定,效率高。
- 動態初始化處理非常量表達式、函數調用和類對象構造,在程序啟動時執行,靈活性強。