大多數(shù) C++ 關(guān)鍵字使用起來都是比較簡單的,但也有少數(shù)相對(duì)復(fù)雜,static 便是其中的一個(gè)代表。
標(biāo)準(zhǔn)往往會(huì)避免為語言增加新的關(guān)鍵字,而是復(fù)用已有的。這使得 static 如今已存在十幾種不同的意思,可以修飾全局,也可以修飾局部;可以修飾函數(shù),也可以修飾變量;還可以和 inline、const、constexpr、constinit 等關(guān)鍵字組合起來使用。
許多 C++ devs 對(duì)其都只處于一個(gè)淺層次的理解,不全面也不深入,用來不明所以。通過本文能夠彌補(bǔ)此部分知識(shí)點(diǎn)。
1
內(nèi)存布局
程序由指令和數(shù)據(jù)構(gòu)成,指令和數(shù)據(jù)又存儲(chǔ)在不同的段內(nèi),我們首先要了解執(zhí)行程序的內(nèi)存布局,才能從一個(gè)更底層的視角來理解 static。
常見的程序分段如表格所示:
Sections | Meaning |
---|---|
.code /.text |
代碼段,存儲(chǔ)編譯后的機(jī)器指令 |
.data |
數(shù)據(jù)段,通常存儲(chǔ)全局變量、靜態(tài)局部變量和常量 |
.bss |
Block Started by Symbol的縮寫,用來存儲(chǔ)未初始化的全局變量和靜態(tài)局部變量,實(shí)際并不占據(jù)空間,僅是請求加載器在程序加載到內(nèi)存時(shí),預(yù)留一些空間 |
.rodata |
read-only data的縮寫,通常存儲(chǔ)靜態(tài)常量 |
程序分段有利于區(qū)分指令和數(shù)據(jù),指令通常是只讀的,數(shù)據(jù)是可讀寫或只讀的,分到不同的段內(nèi),設(shè)置不同的權(quán)限,可以防止程序指令被無意修改。
一個(gè)程序編譯后的內(nèi)存布局圖可能如下圖所示:
![89815cb8-3a56-11ee-9e74-dac502259ad0.jpg](https://file1.elecfans.com//web2/M00/A1/18/wKgaomToQ76AfqvtAADFGl0rog8061.jpg)
可以看到,程序被分成了六個(gè)主要的內(nèi)存區(qū)域。因?yàn)榇a指令、數(shù)據(jù)段等往往是固定大小的,所以處于低地址;堆棧可能會(huì)在程序執(zhí)行時(shí)動(dòng)態(tài)增長,所以處于高地址。
同時(shí),不同段的數(shù)據(jù)還代表著不同的存儲(chǔ)時(shí)期。
2
存儲(chǔ)時(shí)期
C++中不同的數(shù)據(jù)存在四個(gè)存儲(chǔ)時(shí)期,分別為 automatic, static, thread 和 dynamic。
automatic 主要指的就是棧上的數(shù)據(jù),它能夠在進(jìn)入某個(gè)作用域時(shí)自動(dòng)申請內(nèi)存,并在離開時(shí)自動(dòng)釋放內(nèi)存。
static 指的主要是 .data/.bss/.rodata 段的數(shù)據(jù),這些數(shù)據(jù)在程序執(zhí)行時(shí)就申請內(nèi)存,等到程序結(jié)束時(shí)才釋放。
而 thread 存儲(chǔ)時(shí)期是 C++11才有的,只有 thread_local 修飾的數(shù)據(jù)才屬此類,它們在線程開始時(shí)申請內(nèi)存,線程結(jié)束時(shí)釋放內(nèi)存。
dynamic 則表示堆上的數(shù)據(jù),也就是使用 new/malloc 申請的內(nèi)存,這些內(nèi)存必須手動(dòng)釋放。當(dāng)然,通過智能指針這些數(shù)據(jù)的生命周期也能夠自動(dòng)管理。
不要把這些存儲(chǔ)時(shí)期與編譯期/運(yùn)行期混淆,它們是編譯原理的概念,按那種尺度來劃分,則存在編譯期-加載期-運(yùn)行期三個(gè)不同的時(shí)期。
編譯期指的是編譯器處理代碼的時(shí)期,該時(shí)期的數(shù)據(jù)符號(hào)地址被翻譯成絕對(duì)地址,是最早就確定的數(shù)據(jù)。constexpr/constint 所修飾的數(shù)據(jù),一般就是在這一時(shí)期分配的內(nèi)存。
編譯后的程序存儲(chǔ)在硬盤上,準(zhǔn)備執(zhí)行時(shí)操作系統(tǒng)需要將它們讀取到 RAM 中,這個(gè)時(shí)期就叫加載期。.data/.rodata 段的數(shù)據(jù)就是在這一時(shí)期分配內(nèi)存的,一個(gè)常見的誤區(qū)就是認(rèn)為 static 數(shù)據(jù)是處于編譯期。
運(yùn)行期是程序已經(jīng)運(yùn)行,指令已經(jīng)開始被CPU處理了。一些額外的內(nèi)存需要分配給現(xiàn)在才存在的數(shù)據(jù),比如 .bss 和堆棧數(shù)據(jù)就屬于這一時(shí)期。
內(nèi)存布局和存儲(chǔ)時(shí)期搞清楚了,下面需要理解鏈接的基本概念。
3
鏈接
不同的數(shù)據(jù)作用范圍也不盡相同,如何調(diào)整數(shù)據(jù)的作用范圍?就是通過那些變量修飾符,它們能改變數(shù)據(jù)的鏈接方式。
每個(gè) .cpp/.cc/.cxx... 文件稱為一個(gè) TU(Translation Units,翻譯單元),有些數(shù)據(jù)只在當(dāng)前 TU 使用,有些數(shù)據(jù)還需要在其他 TUs 使用,前者稱為內(nèi)部鏈接,后者稱為外部鏈接。
每個(gè) TU 就是一個(gè)模塊,鏈接就是將這些模塊組合起來,同時(shí)保證其中所引用的各種符號(hào)都在正確的位置上。
只有在當(dāng)前 TU 中使用的數(shù)據(jù)才需要內(nèi)部鏈接,局部的那些數(shù)據(jù)屬于無鏈接。我們需要關(guān)注的是一個(gè)變量的內(nèi)部鏈接和外部鏈接是如何指定的。
先說內(nèi)部鏈接,這些名稱能夠在整個(gè) TU 使用,以下是其規(guī)則:
-
命名空間下以 static 修飾的變量、變量模板、函數(shù)和函數(shù)模板;
-
命名空間下以 const 修飾的變量;
-
匿名 union 的數(shù)據(jù)成員;
-
匿名空間下的所有名稱。
再說外部鏈接,這些名稱能夠在不同 TUs 間使用,外部鏈接的名稱甚至能夠和其他語言生成的 TUs 鏈接。規(guī)則如下:
-
命名空間下沒有以 static 修飾的函數(shù),沒有額外修飾的變量和以 extern 修飾的變量;
-
命名空間下的枚舉名稱;
-
命名空間下的類名稱,包含它們的成員函數(shù)、(const) static 數(shù)據(jù)成員、嵌套類/枚舉、首次引入的友元函數(shù);
-
命名空間下以 static 修飾的非函數(shù)模板;
-
首次在block scope 下的函數(shù)名、以 extern 修飾的變量名。
暫時(shí)先留個(gè)大概印象,后面還會(huì)再次以具體例子介紹有些規(guī)則。
4
以 static修飾變量
前置概念介紹完了,下面從不同方面來進(jìn)行討論 static 關(guān)鍵字。本節(jié)關(guān)注于修飾變量,這又有全局和局部之分。
4.1
以static修飾全局變量
全局變量處于 static存儲(chǔ)時(shí)期,也對(duì)應(yīng)于加載期,在 main()執(zhí)行之前就已為這些變量分配了內(nèi)存。
如果一個(gè)全局變量被 extern 修飾,則它具有外部鏈接,能夠被其他 TUs 使用。相反,如果一個(gè)全局變量被 static 修飾,它具有內(nèi)部鏈接,只能在當(dāng)前 TU 使用。
一個(gè)例子:
//tu-one.cpp
externintvar_1=42;//externallinkage
staticintvar_2=24;//internallinkage
//tu-two.cpp
#include
//referstothevar_1definedinthetu-one.cpp
externintvar_1;
intmain(){
std::cout<"
";//prints42
}
若是再考慮組合 const 進(jìn)行修飾,情況則又不相同。
如果一個(gè)全局變量沒有使用const 修飾,那么它默認(rèn)就有 extern 鏈接,無需多此一舉再加上 extern 修飾。而對(duì)于這樣一個(gè)變量,如何改變它的外部鏈接方式?只需使用 static 修飾,就可以將它變成內(nèi)部鏈接。
如果一個(gè)全局變量使用了 const/constexpr 修飾,則它默認(rèn)就有了 static 鏈接。此時(shí)如果再加上 static 修飾,也是多此一舉。其他文件此時(shí)將無法訪問該全局變量,如何改變呢?前面加上 extern 修飾,就可以讓它變成外部鏈接。
以上內(nèi)容的一個(gè)例子:
//tu-one.cpp
intvar_1=42;//externallinkage by default
externintvar_2=42;//sameasvar_1,butit'sredundant.
staticintvar_3=42;//internallinkage
constintvar_4=42;//internallinkage by default
staticconstintvar_5=42;//sameasvar_4,butit'sredundant.
externconstintvar_6=42;//externallinkage
constexprintvar_7=42;//internallinkagebydefault
staticconstexprintvar_8=42;//sameasvar_7,butit'sredundant.
4.2
以static修飾局部變量
局部變量也要分情況討論一下,先說函數(shù)中的局部變量。
函數(shù)中局部變量的存儲(chǔ)時(shí)期為 automatic,此類變量無鏈接,使用時(shí)在棧上自動(dòng)分配內(nèi)存,離開作用域時(shí)自動(dòng)釋放,只能在當(dāng)前作用域使用。
如果為這樣的局部變量加上 static,就將其存儲(chǔ)時(shí)期由 automatic 改變成了 static,生命周期遍及整個(gè)程序的生命周期。這種變量實(shí)際是先在 .bss 段預(yù)留了空間,等到首次進(jìn)入該函數(shù),才真正為其分配內(nèi)存,此時(shí)初始化的時(shí)機(jī)就不是加載期,而且延遲到了運(yùn)行期,所以這種方式也叫惰性初始化。
這種局部靜態(tài)變量就相當(dāng)于全局變量,不同之處在于它是無鏈接,可見性僅在當(dāng)前函數(shù),而且可以延遲初始化。
4.3
以static修飾成員變量
如果一個(gè)類成員變量以 static修飾,那么該變量只是一個(gè)聲明,需要額外提供定義,類的所有對(duì)象共享此類變量。
classS{
staticintx;//declaration
};
intS::x=0;//definition,initialize outside the class
為什么需要在外部定義呢?
因?yàn)?static 對(duì)象必須滿足 ODR(One Definition Rule),而類一般是在頭文件中聲明,該頭文件可能會(huì)被多個(gè) TUs 包含,每個(gè)對(duì)象必須具備唯一的定義,否則在編譯鏈接時(shí)會(huì)出現(xiàn)問題。所以將它作為一個(gè)聲明,定義在類外部單獨(dú)指定。
但是在某些時(shí)候也可以不用定義,比如:
//Examplefromcppref
structS
{
staticconstintx=0;//staticdatamember
//adefinitionoutsideofclassisrequiredifitisodr-used
};
constint&f(constint&r);
intn=b?(1,S::x)//S::xisnotodr-usedhere
:f(S::x);//S::xisodr-usedhere:adefinitionisrequired
只有在 ODR-used 時(shí)才必須要提供定義,在不需要 lvalue 的表達(dá)式中,它可以直接使用 S::x 的值,此時(shí)經(jīng)歷了 lvalue-to-rvalue 的隱式轉(zhuǎn)換。相反,在需要 lvalue 的表達(dá)式中,則必須提供定義。
注:ODR-used 是標(biāo)準(zhǔn)用來指必須為實(shí)體提供定義的術(shù)語,因?yàn)樗皇潜仨毜模枰蕾嚽榫秤懻?,所以不單?dú)使用 used 來描述。比如一個(gè)虛函數(shù)是非 ODR-used,而一個(gè)純虛函數(shù)是 ODR-used,使用時(shí)必須提供定義。模板只在使用時(shí)才實(shí)例化,這里的使用準(zhǔn)確的描述也應(yīng)該是 ODR-used。
如果嫌在外部定義麻煩,在 C++17 可以采用 inline 來光明正大地違背 ODR,它能夠告訴鏈接器,我想在多個(gè) TUs 之間擁有相同的定義。
classS{
//since C++17
inlinestaticintx=42;
};
在 C++20,由于 constexpr 會(huì)隱式 inline,所以還可以這么寫:
classS{
//since C++20
staticconstexprintx=42;
};
另外,在 C++98,如果以 static const 修飾一個(gè)整型成員數(shù)據(jù),那么也可以在類內(nèi)直接初始化,并且可以保證初始化是在編譯期完成的。
//C++98
structS{
staticconstintx=42;//OK
constinty=42;//since C++11, default member initializer
};
對(duì)于非 static 數(shù)據(jù)成員,自 C++11 開始支持 default member initializer,于是也可以直接在類內(nèi)直接初始化。
5
以static修飾函數(shù)
函數(shù)也分全局函數(shù)和成員函數(shù),以 static 修飾時(shí)也要分別討論。
5.1
以static修飾全局函數(shù)
正常情況下,全局函數(shù)默認(rèn)是外部鏈接,可以通過前置聲明在多個(gè) TUs 使用。
如果以 static 修飾全局函數(shù),則將其鏈接方式變?yōu)閮?nèi)部鏈接,只能在當(dāng)前 TU 使用。
一個(gè)小例子:
//tu-one.cpp
#include
staticvoidfoo(){
std::cout<"internallinkage
";
};
voidbar(){
std::cout<"externallinkage
";
}
//tu-two.cpp
externvoidfoo();
externvoidbar();//referstothebar()definedinthetu-one.cpp
intmain(){
foo();//Error,undefinedreferenceto'foo()'
bar();//OK
}
5.2
以static修飾成員函數(shù)
之前在【洞悉C++函數(shù)重載決議】里已經(jīng)講解過本節(jié)內(nèi)容。
以 static 修飾的成員函數(shù)增加了一個(gè)隱式對(duì)象參數(shù),它并不是 this 指針,而是為了重載決議能夠正常運(yùn)行所定義的一個(gè)可以匹配任何參數(shù)的對(duì)象參數(shù)。這樣的成員函數(shù)無法訪問其他的非靜態(tài)成員名稱,因?yàn)槟切┟Q都與對(duì)象綁定。
當(dāng)時(shí)編寫的一個(gè)示例:
structS{
voidf(long){
std::cout<"memberversion
";
}
staticvoidf(int){
std::cout<"staticmemberversion
";
}
};
intmain(){
Ss;
s.f(1);//staticmemberversion
}
6
static修飾變量對(duì) Lambdas捕獲參數(shù)的影響
如果是全局變量,那么 Lambdas 無需捕獲便可以直接使用:
intx=42;
intmain(){
//youdon'tneed tocapturea globalvariable
[]{returnx;}();
}
但如果是局部變量,由于它的存儲(chǔ)時(shí)期為 automatic,就必須捕獲才能使用:
intmain(){
intx=42;
//youhavetocapturealocalvariable
[&x]{returnx;}();
}
但如果使用 static 修飾該局部變量,就無需再進(jìn)行捕獲:
intmain(){
staticintx=42;
//OK
[]{returnx;}();
}
同理,const/constexpr/constinit 修飾的變量在某些時(shí)候也無需再進(jìn)行捕獲:
constinitintm=42;
intmain(){
constexprintx=42;
constintn=42;
//OK
[]{returnm+x+n;}();
}
7
static constexpr, static constinit
請大家注意我上節(jié)最后一句的用詞,是「在某些時(shí)候」也無需進(jìn)行捕獲,使用那些詞修飾并非一定是可以無需捕獲。
準(zhǔn)確地說,非 ODR-used 的數(shù)據(jù)無需捕獲,它可以直接把那個(gè)常量隱式捕獲。
intmain(){
constexprstd::string_viewx="foo";
[]{x;}(); //OK,xisnotodr-used
[]{x.data();}();//error:x.data()isodr-used
}
此時(shí)就可以借助 static constexpr,就可以強(qiáng)保證 Lambdas 可以隱式捕獲該數(shù)據(jù):
intmain(){
staticconstexprstd::string_viewx="foo";
[]{x;}();//OK,xisnotodr-used
[]{x.data();};//OK,x.data()isnot odr-used
}
可以理解為此時(shí)捕獲的不是 lvalue,而是經(jīng)由 lvalue-to-rvalue 的那個(gè)值。
static constinit 也是同理,事實(shí)上 constinit 在局部使用必須添加 static,它只能修飾靜態(tài)存儲(chǔ)期或是線程存儲(chǔ)期的數(shù)據(jù)。
在類中使用 static constexpr 修飾數(shù)據(jù),可以保持?jǐn)?shù)據(jù)既是編譯期,又能夠所有對(duì)象共享一份數(shù)據(jù)。
template<intN>
structS{
staticconstexprintx=N;
};
constinit 和 constexpr 大多時(shí)候都是同理,只是前者是可讀可寫,后者是只讀,除非有不同的意義,否則討論的 constexpr 用法也適用于 constinit。后文不再提及。
static constexpr 的另一個(gè)用處是「強(qiáng)保證」發(fā)生于編譯期。constexpr 本身只是「弱保證」,它并不一定發(fā)生于編譯期。
它們的其他用處見第9節(jié)。
8
static const vsconstexpr
前面講過,C++ 以 const 修飾全局變量會(huì)默認(rèn)為內(nèi)部鏈接,所以 static 可以省略不寫。但是局部變量不可省,因?yàn)?static 修飾局部變量時(shí)的意義是改變局部變量的存儲(chǔ)時(shí)期,此時(shí)的 static const必須完整寫出。
全局變量本身的存儲(chǔ)時(shí)期就是 static,加上 const 表示只讀,此時(shí)以 static 修飾的意義是指定其鏈接方式。
局部變量本身的存儲(chǔ)時(shí)期是 automatic,無鏈接,加上 const 依舊表示只讀,此時(shí)以 static 修飾的意義是指定其存儲(chǔ)時(shí)期。
所以對(duì)于全局 (static) const 數(shù)據(jù)來說,是在加載期就分配內(nèi)存了(如存儲(chǔ)時(shí)期那節(jié)所強(qiáng)調(diào),不要誤以為它發(fā)生在編譯期)。而對(duì)于局部 static const 數(shù)據(jù)來說,它實(shí)際分配內(nèi)存是在首次使用時(shí),實(shí)際可能發(fā)生于運(yùn)行期。
constexpr 修飾的變量則不同,它們發(fā)生于編譯期,這其實(shí)是要早于 static const 修飾的變量。
但是經(jīng)過優(yōu)化,它們展現(xiàn)的效果是差不多的,相對(duì)來說,更推薦使用 constexpr。
intmain(){
staticconstintm=42;//sinceC++98
constexprintn=42;//sinceC++11
returnm+n;
}
9
Solvingthe"Static Initialization Order Fiasco"
SIOF是存儲(chǔ)時(shí)期為 static 的這類數(shù)據(jù)在跨 TUs 時(shí)相互依賴所導(dǎo)致的問題,因?yàn)椴煌?TUs 中的這些數(shù)據(jù)初始化順序沒有規(guī)定,引用的數(shù)據(jù)可能還沒初始化。
因此全局變量、靜態(tài)變量、靜態(tài)成員變量都可能會(huì)引起這個(gè)問題。
看如下例子:
//tu-one.cpp
autoget_value(intval)->int{
returnval*2;
}
autotu_one_x=get_value(42);
//tu-two.cpp
#include
externinttu_one_x;
autotu_two_x=tu_one_x;
intmain(){
std::cout<"tu_two_x:"<"
";
}
tu_one_x 跨 TUs 使用,tu_two_x 依賴于它。在編譯時(shí)初始化 tu_two_x,如果此時(shí) tu_one_x 還未初始化,那么結(jié)果就偏離預(yù)期。
這依賴于編譯順序,你只有50%的幾率獲得預(yù)期結(jié)果。
![89a16a76-3a56-11ee-9e74-dac502259ad0.png](https://file1.elecfans.com//web2/M00/A1/18/wKgaomToQ76AMMFeAACJpRC-kHs406.png)
解決策略一是使用局部靜態(tài)變量替代全局變量。前面講過,局部靜態(tài)變量相當(dāng)于全局變量,而且可以延遲初始化。
//tu-one.cpp
autoget_value(intval)->int{
returnval*2;
}
//autotu_one_x=get_value(42);
autoget_x()->int&{
staticautox=get_value(42);
returnx;
}
//tu-two.cpp
#include
autoget_x()->int&;
autotu_two_x=get_x();
intmain(){
std::cout<"tu_two_x:"<"
";
}
局部靜態(tài)靜態(tài)會(huì)在首次訪問時(shí)初始化,因此在初始化 tu_two_x 之前,就先把 tu_one_x 初始化了。于是不會(huì)再有 SOIF:
解決策略二是借助 constinit。前面依舊講過,constinit 發(fā)生于編譯期,而存儲(chǔ)時(shí)期為 static 的數(shù)據(jù)實(shí)際發(fā)生于加載期,SOIF 只是加載期的問題,只要將初始化時(shí)期錯(cuò)開,體現(xiàn)一前一后,就能夠指定順序,從而解決該問題。
//tu-one.cpp
constexprautoget_value(intval)->int{
returnval*2;
}
constinitautotu_one_x=get_value(42);
//tu-two.cpp
#include
externconstinitinttu_one_x;
autotu_two_x=tu_one_x;
intmain(){
std::cout<"tu_two_x:"<"
";
}
此時(shí) tu_one_x 初始化于編譯期,而 tu_two_x 初始化于加載期,所以也不會(huì)存在SOIF。
但是你無法使用 extern constexpr,像如下這樣寫會(huì)編譯失?。?/span>
//tu-two.cpp
externconstexprinttu_one_x;//error:notadefinition
autotu_two_x=tu_one_x;
因?yàn)?constinit 修飾的數(shù)據(jù)是可讀可寫的,而 constexpr 修飾的數(shù)據(jù)是只讀的,定義時(shí)必須要給初值。這里這種寫法被視為只是一個(gè)聲明。
雖然無法使用 extern constexpr,但也是可以借助 constexpr 來解決 SOIF 的,只不過要把所有的實(shí)現(xiàn)全部放到頭文件,然后在另一個(gè)實(shí)現(xiàn)文件中包含該頭文件。本節(jié)最后有一個(gè)相關(guān)例子。
使用 static 修飾的變量與全局變量同理,也提供一個(gè)例子:
//tu-one.h
structS{
staticinttu_one_x;//declaration
};
//tu-one.cpp
#include"tu-one.h"
autoget_value(intval)->int{
returnval*2;
}
//definition
intS::tu_one_x=get_value(42);
//tu-two.cpp
#include"tu-one.h"
#include
staticautotu_two_x=S::tu_one_x;
intmain(){
std::cout<"tu_two_x:"<"
";
}
它們的存儲(chǔ)時(shí)期也是 static,所以也會(huì)產(chǎn)生 SOIF。tu_two_x 的初始化依賴于 S::tu_one_x,因此你也有 50% 的幾率得到正確結(jié)果。
![89dbcba8-3a56-11ee-9e74-dac502259ad0.png](https://file1.elecfans.com//web2/M00/A1/18/wKgaomToQ76AFDU-AADIPBslPUw549.png)
通過使用 static constinit,也得以解決此問題。
//tu-one.h
structS{
staticconstinitinttu_one_x;//declaration
};
//tu-one.cpp
#include"tu-one.h"
constexprautoget_value(intval)->int{
returnval*2;
}
//definition
intS::tu_one_x=get_value(42);
//tu-two.cpp
#include"tu-one.h"
#include
staticautotu_two_x=S::tu_one_x;
intmain(){
std::cout<"tu_two_x:"<"
";
}
使用 constinit,所有相關(guān)操作也都得是編譯期完成,所以 get_value() 也加上了 constexpr 修飾。那么 static 在此時(shí)主要指的是所修飾數(shù)據(jù)在所有對(duì)象之間共享,constinit 將它的初始化時(shí)間提前到了編譯期。
![89f06126-3a56-11ee-9e74-dac502259ad0.png](https://file1.elecfans.com//web2/M00/A1/18/wKgaomToQ76AeX2rAACLIUO8-hc570.png)
但是你不能把 tu_two_x 也以 static constinit 修飾,因?yàn)榫幾g期的值發(fā)生在鏈接之間,在編譯期就得確定,而 tu_two_x 的值又來自于另一個(gè)文件,編譯時(shí)根本就不知道所依賴的那個(gè)常量值。
同理這里也可以使用 static constexpr,但是 constexpr 沒有 constinit 靈活,它是 const 的,所以定義時(shí)就必須跟著初始化。
//tu-one.h
constexprautoget_value(intval)->int{
returnval*2;
}
structS{
staticconstexprinttu_one_x=get_value(42);//definition
};
//tu-two.cpp
#include"tu-one.h"
#include
staticautotu_two_x=S::tu_one_x;
intmain(){
std::cout<"tu_two_x:"<"
";
}
結(jié)果和使用 static constinit 完全相同。
10
static inline
static 能夠指示編譯器數(shù)據(jù)只在單個(gè) TU 使用,即內(nèi)部鏈接;與之相反,inline 能夠指示編譯器數(shù)據(jù)需要在多個(gè) TUs 使用,此時(shí)即使違背了 ODR,也不應(yīng)該報(bào)錯(cuò),屬于外部鏈接。
那如果它們組合使用,會(huì)有怎樣的效果?
讓我們寫個(gè)例子來對(duì)比一下不同的情況編譯之后到底產(chǎn)生了什么內(nèi)容。
////Thisexampleisadaptedfromhttps://gist.github.com/htfy96/50308afc11678d2e3766a36aa60d5f75
//header.hpp
inlineintonly_inline(){return42;}
staticintonly_static(){return42;}
staticinlineintstatic_inline(){return42;}
//tu-one.cpp
#include"header.hpp"
autoget_value_one()->int{
returnstatic_inline()+only_inline()+only_static();
}
//tu-two.cpp
#include"header.hpp"
autoget_value_one()->int;
autoget_value_two()->int{
returnstatic_inline()+only_inline()+only_static();
}
automain()->int{
returnget_value_one()+get_value_two();
}
先編譯 tu-one.cpp,并查看生成的目標(biāo)文件的符號(hào)表:
lkimuk@cppmore:~/Desktop/demo$g++-ctu-one.cpp
lkimuk@cppmore:~/Desktop/demo$readelf-sWtu-one.o|c++filt-t
Symboltable'.symtab'contains8entries:
Num:ValueSizeTypeBindVisNdxName
0:00000000000000000NOTYPELOCALDEFAULTUND
1:00000000000000000FILELOCALDEFAULTABStu-one.cpp
2:00000000000000000SECTIONLOCALDEFAULT2.text
3:00000000000000000SECTIONLOCALDEFAULT6.text._Z11only_inlinev
4:000000000000000011FUNCLOCALDEFAULT2only_static()
5:000000000000000b11FUNCLOCALDEFAULT2static_inline()
6:000000000000000011FUNCWEAKDEFAULT6only_inline()
7:000000000000001636FUNCGLOBALDEFAULT2get_value_one()
readelf 用來查看 Linux 上可執(zhí)行文件的結(jié)構(gòu),-s 表示顯示符號(hào)表,-W 表示以寬格式顯示。c++filt 是 gnu 提供的反 Name Demangling 工具,可以顯示未經(jīng)修飾的函數(shù)名稱。
可以看到,only_static() 和 static_inline() 的綁定方式都是 LOCAL,表示僅在當(dāng)前文件可見;only_inline() 的綁定方式為 WEAK,表示該符號(hào)可被覆蓋,所以在其他文件中也是可見的;get_value_one() 的綁定方式是 GLOBAL,也表示在所有文件中可見。
再來編譯 tu-two.cpp,也查看其目標(biāo)文件的符號(hào)表:
lkimuk@cppmore:~/Desktop/demo$g++-ctu-two.cpp
lkimuk@cppmore:~/Desktop/demo$readelf-sWtu-two.o|c++filt-t
Symboltable'.symtab'contains10entries:
Num:ValueSizeTypeBindVisNdxName
0:00000000000000000NOTYPELOCALDEFAULTUND
1:00000000000000000FILELOCALDEFAULTABStu-two.cpp
2:00000000000000000SECTIONLOCALDEFAULT2.text
3:00000000000000000SECTIONLOCALDEFAULT6.text._Z11only_inlinev
4:000000000000000011FUNCLOCALDEFAULT2only_static()
5:000000000000000b11FUNCLOCALDEFAULT2static_inline()
6:000000000000000011FUNCWEAKDEFAULT6only_inline()
7:000000000000001636FUNCGLOBALDEFAULT2get_value_two()
8:000000000000003a29FUNCGLOBALDEFAULT2main
9:00000000000000000NOTYPEGLOBALDEFAULTUNDget_value_one()
和 tu-two.o 的情況相似,這里不再贅述。
具體來看鏈接之后的情況:
lkimuk@cppmore:~/Desktop/demo$g++tu-one.otu-two.o-omain
lkimuk@cppmore:~/Desktop/demo$readelf-sWmain|c++filt-t
Symboltable'.dynsym'contains6entries:
Num:ValueSizeTypeBindVisNdxName
0:00000000000000000NOTYPELOCALDEFAULTUND
1:00000000000000000FUNCGLOBALDEFAULTUND__libc_start_main@GLIBC_2.34(3)
2:00000000000000000NOTYPEWEAKDEFAULTUND_ITM_deregisterTMCloneTable
3:00000000000000000NOTYPEWEAKDEFAULTUND__gmon_start__
4:00000000000000000NOTYPEWEAKDEFAULTUND_ITM_registerTMCloneTable
5:00000000000000000FUNCWEAKDEFAULTUND__cxa_finalize@GLIBC_2.2.5(2)
Symboltable'.symtab'contains43entries:
Num:ValueSizeTypeBindVisNdxName
0:00000000000000000NOTYPELOCALDEFAULTUND
1:00000000000000000FILELOCALDEFAULTABSScrt1.o
2:000000000000035832OBJECTLOCALDEFAULT3__abi_tag
3:00000000000000000FILELOCALDEFAULTABScrtstuff.c
4:00000000000010700FUNCLOCALDEFAULT14deregister_tm_clones
5:00000000000010a00FUNCLOCALDEFAULT14register_tm_clones
6:00000000000010e00FUNCLOCALDEFAULT14__do_global_dtors_aux
7:00000000000040281OBJECTLOCALDEFAULT25completed.0
8:de00OBJECTLOCALDEFAULT20__do_global_dtors_aux_fini_array_entry
9:00000000000011200FUNCLOCALDEFAULT14frame_dummy
10:dd80OBJECTLOCALDEFAULT19__frame_dummy_init_array_entry
11:00000000000000000FILELOCALDEFAULTABStu-one.cpp
12:000000000000112911FUNCLOCALDEFAULT14only_static()
13:000000000000113411FUNCLOCALDEFAULT14static_inline()
14:00000000000000000FILELOCALDEFAULTABStu-two.cpp
15:000000000000116e11FUNCLOCALDEFAULT14only_static()
16:000000000000117911FUNCLOCALDEFAULT14static_inline()
17:00000000000000000FILELOCALDEFAULTABScrtstuff.c
18:00000000000021d80OBJECTLOCALDEFAULT18__FRAME_END__
19:00000000000000000FILELOCALDEFAULTABS
20:00000000000020040NOTYPELOCALDEFAULT17__GNU_EH_FRAME_HDR
21:de80OBJECTLOCALDEFAULT21_DYNAMIC
22:00000000000040000OBJECTLOCALDEFAULT23_GLOBAL_OFFSET_TABLE_
23:00000000000040280NOTYPEGLOBALDEFAULT24_edata
24:00000000000040180NOTYPEWEAKDEFAULT24data_start
25:00000000000020004OBJECTGLOBALDEFAULT16_IO_stdin_used
26:00000000000000000FUNCWEAKDEFAULTUND__cxa_finalize@GLIBC_2.2.5
27:00000000000011a829FUNCGLOBALDEFAULT14main
28:00000000000040200OBJECTGLOBALHIDDEN24__dso_handle
29:00000000000011c80FUNCGLOBALHIDDEN15_fini
30:00000000000000000FUNCGLOBALDEFAULTUND__libc_start_main@GLIBC_2.34
31:000000000000116311FUNCWEAKDEFAULT14only_inline()
32:000000000000104038FUNCGLOBALDEFAULT14_start
33:00000000000010000FUNCGLOBALHIDDEN11_init
34:00000000000040280OBJECTGLOBALHIDDEN24__TMC_END__
35:000000000000113f36FUNCGLOBALDEFAULT14get_value_one()
36:000000000000118436FUNCGLOBALDEFAULT14get_value_two()
37:00000000000040180NOTYPEGLOBALDEFAULT24__data_start
38:00000000000040300NOTYPEGLOBALDEFAULT25_end
39:00000000000040280NOTYPEGLOBALDEFAULT25__bss_start
40:00000000000000000NOTYPEWEAKDEFAULTUND_ITM_deregisterTMCloneTable
41:00000000000000000NOTYPEWEAKDEFAULTUND__gmon_start__
42:00000000000000000NOTYPEWEAKDEFAULTUND_ITM_registerTMCloneTable
請注意觀察,鏈接之后 only_static() 和 static_inline() 存在兩份拷貝,說明它在每個(gè) TU 都存在一份拷貝。而 only_inline() 只存在一份拷貝,說明 inline 的名稱的確能夠跨 TUs。
全局變量也只存在一份,說明外部鏈接是起作用的。
于是能夠得出結(jié)論,static 是內(nèi)部鏈接,inline 是外部鏈接,static inline 和 static 效果一樣,此時(shí)加上 inline,僅僅是告訴編譯器,可以嘗試內(nèi)聯(lián)一下代碼。
11
總結(jié)
本文深入全面系統(tǒng)地介紹了 static 關(guān)鍵字的方方面面,涉及內(nèi)容又多又雜,又廣又深。
static 是 C++ 中最復(fù)雜的關(guān)鍵字之一,有多達(dá)十幾種不同的意思,而且涉及編譯知識(shí),許多使用形式意思非常細(xì)微。
所有相關(guān)內(nèi)容幾乎都包含在本文當(dāng)中,具體總結(jié)大家就得自己歸納一下了。
-
cpu
+關(guān)注
關(guān)注
68文章
10911瀏覽量
213152 -
存儲(chǔ)
+關(guān)注
關(guān)注
13文章
4359瀏覽量
86213 -
C++
+關(guān)注
關(guān)注
22文章
2114瀏覽量
73895 -
編譯器
+關(guān)注
關(guān)注
1文章
1642瀏覽量
49318
原文標(biāo)題:深入理解 C++ “static” 關(guān)鍵字
文章出處:【微信號(hào):CPP開發(fā)者,微信公眾號(hào):CPP開發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評(píng)論請先 登錄
相關(guān)推薦
深入理解C語言:循環(huán)語句的應(yīng)用與優(yōu)化技巧
![<b class='flag-5'>深入理解</b><b class='flag-5'>C</b>語言:循環(huán)語句的應(yīng)用與優(yōu)化技巧](https://file1.elecfans.com/web2/M00/FC/CD/wKgZomaWI5uASgBaAABuQHdMO4I302.png)
C語言關(guān)鍵字分別發(fā)生在哪個(gè)階段
C++新手容易犯的十個(gè)編程錯(cuò)誤
深入理解FPD-link III ADAS解串器HUB產(chǎn)品
![<b class='flag-5'>深入理解</b>FPD-link III ADAS解串器HUB產(chǎn)品](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
使用邊緣AI和Sitara處理器進(jìn)行關(guān)鍵字檢測
![使用邊緣AI和Sitara處理器進(jìn)行<b class='flag-5'>關(guān)鍵字</b>檢測](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
技術(shù)干貨驛站 ▏深入理解C語言:基本數(shù)據(jù)類型和變量
![技術(shù)干貨驛站 ▏<b class='flag-5'>深入理解</b><b class='flag-5'>C</b>語言:基本數(shù)據(jù)類型和變量](https://file1.elecfans.com/web2/M00/FC/CD/wKgZomaWI5uASgBaAABuQHdMO4I302.png)
快速掌握C語言關(guān)鍵字
![快速掌握<b class='flag-5'>C</b>語言<b class='flag-5'>關(guān)鍵字</b>](https://file.elecfans.com/web2/M00/9B/3D/poYBAGQjnauAVXOgAABFcEbXdEE684.png)
深入理解渲染引擎:打造逼真圖像的關(guān)鍵
![<b class='flag-5'>深入理解</b>渲染引擎:打造逼真圖像的<b class='flag-5'>關(guān)鍵</b>](https://file.elecfans.com/web2/M00/4E/DC/poYBAGLCjeiALm_WAAAYmfR7Qec474.png)
inline關(guān)鍵字被優(yōu)化導(dǎo)致此類函數(shù)被布局在flash內(nèi),怎么處理?
《深入理解FFmpeg閱讀體驗(yàn)》
深入理解 FPGA 的基礎(chǔ)結(jié)構(gòu)
深入理解數(shù)據(jù)備份的關(guān)鍵原則:應(yīng)用一致性與崩潰一致性的區(qū)別
![<b class='flag-5'>深入理解</b>數(shù)據(jù)備份的<b class='flag-5'>關(guān)鍵</b>原則:應(yīng)用一致性與崩潰一致性的區(qū)別](https://file1.elecfans.com/web2/M00/C4/A2/wKgaomXueUOAUC9kAAUkG4ifnAc542.png)
嵌入式C語言面試大挑戰(zhàn)
![嵌入式<b class='flag-5'>C</b>語言面試大挑戰(zhàn)](https://file1.elecfans.com/web2/M00/C3/99/wKgaomXmudSAfByVAAAaS7J-56c861.png)
評(píng)論