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