大多數(shù) C++ 關鍵字使用起來都是比較簡單的,但也有少數(shù)相對復雜,static 便是其中的一個代表。
標準往往會避免為語言增加新的關鍵字,而是復用已有的。這使得 static 如今已存在十幾種不同的意思,可以修飾全局,也可以修飾局部;可以修飾函數(shù),也可以修飾變量;還可以和 inline、const、constexpr、constinit 等關鍵字組合起來使用。
許多 C++ devs 對其都只處于一個淺層次的理解,不全面也不深入,用來不明所以。通過本文能夠彌補此部分知識點。
1
內(nèi)存布局
程序由指令和數(shù)據(jù)構(gòu)成,指令和數(shù)據(jù)又存儲在不同的段內(nèi),我們首先要了解執(zhí)行程序的內(nèi)存布局,才能從一個更底層的視角來理解 static。
常見的程序分段如表格所示:
Sections | Meaning |
---|---|
.code /.text |
代碼段,存儲編譯后的機器指令 |
.data |
數(shù)據(jù)段,通常存儲全局變量、靜態(tài)局部變量和常量 |
.bss |
Block Started by Symbol的縮寫,用來存儲未初始化的全局變量和靜態(tài)局部變量,實際并不占據(jù)空間,僅是請求加載器在程序加載到內(nèi)存時,預留一些空間 |
.rodata |
read-only data的縮寫,通常存儲靜態(tài)常量 |
程序分段有利于區(qū)分指令和數(shù)據(jù),指令通常是只讀的,數(shù)據(jù)是可讀寫或只讀的,分到不同的段內(nèi),設置不同的權(quán)限,可以防止程序指令被無意修改。
一個程序編譯后的內(nèi)存布局圖可能如下圖所示:
可以看到,程序被分成了六個主要的內(nèi)存區(qū)域。因為代碼指令、數(shù)據(jù)段等往往是固定大小的,所以處于低地址;堆棧可能會在程序執(zhí)行時動態(tài)增長,所以處于高地址。
同時,不同段的數(shù)據(jù)還代表著不同的存儲時期。
2
存儲時期
C++中不同的數(shù)據(jù)存在四個存儲時期,分別為 automatic, static, thread 和 dynamic。
automatic 主要指的就是棧上的數(shù)據(jù),它能夠在進入某個作用域時自動申請內(nèi)存,并在離開時自動釋放內(nèi)存。
static 指的主要是 .data/.bss/.rodata 段的數(shù)據(jù),這些數(shù)據(jù)在程序執(zhí)行時就申請內(nèi)存,等到程序結(jié)束時才釋放。
而 thread 存儲時期是 C++11才有的,只有 thread_local 修飾的數(shù)據(jù)才屬此類,它們在線程開始時申請內(nèi)存,線程結(jié)束時釋放內(nèi)存。
dynamic 則表示堆上的數(shù)據(jù),也就是使用 new/malloc 申請的內(nèi)存,這些內(nèi)存必須手動釋放。當然,通過智能指針這些數(shù)據(jù)的生命周期也能夠自動管理。
不要把這些存儲時期與編譯期/運行期混淆,它們是編譯原理的概念,按那種尺度來劃分,則存在編譯期-加載期-運行期三個不同的時期。
編譯期指的是編譯器處理代碼的時期,該時期的數(shù)據(jù)符號地址被翻譯成絕對地址,是最早就確定的數(shù)據(jù)。constexpr/constint 所修飾的數(shù)據(jù),一般就是在這一時期分配的內(nèi)存。
編譯后的程序存儲在硬盤上,準備執(zhí)行時操作系統(tǒng)需要將它們讀取到 RAM 中,這個時期就叫加載期。.data/.rodata 段的數(shù)據(jù)就是在這一時期分配內(nèi)存的,一個常見的誤區(qū)就是認為 static 數(shù)據(jù)是處于編譯期。
運行期是程序已經(jīng)運行,指令已經(jīng)開始被CPU處理了。一些額外的內(nèi)存需要分配給現(xiàn)在才存在的數(shù)據(jù),比如 .bss 和堆棧數(shù)據(jù)就屬于這一時期。
內(nèi)存布局和存儲時期搞清楚了,下面需要理解鏈接的基本概念。
3
鏈接
不同的數(shù)據(jù)作用范圍也不盡相同,如何調(diào)整數(shù)據(jù)的作用范圍?就是通過那些變量修飾符,它們能改變數(shù)據(jù)的鏈接方式。
每個 .cpp/.cc/.cxx... 文件稱為一個 TU(Translation Units,翻譯單元),有些數(shù)據(jù)只在當前 TU 使用,有些數(shù)據(jù)還需要在其他 TUs 使用,前者稱為內(nèi)部鏈接,后者稱為外部鏈接。
每個 TU 就是一個模塊,鏈接就是將這些模塊組合起來,同時保證其中所引用的各種符號都在正確的位置上。
只有在當前 TU 中使用的數(shù)據(jù)才需要內(nèi)部鏈接,局部的那些數(shù)據(jù)屬于無鏈接。我們需要關注的是一個變量的內(nèi)部鏈接和外部鏈接是如何指定的。
先說內(nèi)部鏈接,這些名稱能夠在整個 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 修飾的變量名。
暫時先留個大概印象,后面還會再次以具體例子介紹有些規(guī)則。
4
以 static修飾變量
前置概念介紹完了,下面從不同方面來進行討論 static 關鍵字。本節(jié)關注于修飾變量,這又有全局和局部之分。
4.1
以static修飾全局變量
全局變量處于 static存儲時期,也對應于加載期,在 main()執(zhí)行之前就已為這些變量分配了內(nèi)存。
如果一個全局變量被 extern 修飾,則它具有外部鏈接,能夠被其他 TUs 使用。相反,如果一個全局變量被 static 修飾,它具有內(nèi)部鏈接,只能在當前 TU 使用。
一個例子:
//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 進行修飾,情況則又不相同。
如果一個全局變量沒有使用const 修飾,那么它默認就有 extern 鏈接,無需多此一舉再加上 extern 修飾。而對于這樣一個變量,如何改變它的外部鏈接方式?只需使用 static 修飾,就可以將它變成內(nèi)部鏈接。
如果一個全局變量使用了 const/constexpr 修飾,則它默認就有了 static 鏈接。此時如果再加上 static 修飾,也是多此一舉。其他文件此時將無法訪問該全局變量,如何改變呢?前面加上 extern 修飾,就可以讓它變成外部鏈接。
以上內(nèi)容的一個例子:
//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ù)中局部變量的存儲時期為 automatic,此類變量無鏈接,使用時在棧上自動分配內(nèi)存,離開作用域時自動釋放,只能在當前作用域使用。
如果為這樣的局部變量加上 static,就將其存儲時期由 automatic 改變成了 static,生命周期遍及整個程序的生命周期。這種變量實際是先在 .bss 段預留了空間,等到首次進入該函數(shù),才真正為其分配內(nèi)存,此時初始化的時機就不是加載期,而且延遲到了運行期,所以這種方式也叫惰性初始化。
這種局部靜態(tài)變量就相當于全局變量,不同之處在于它是無鏈接,可見性僅在當前函數(shù),而且可以延遲初始化。
4.3
以static修飾成員變量
如果一個類成員變量以 static修飾,那么該變量只是一個聲明,需要額外提供定義,類的所有對象共享此類變量。
classS{
staticintx;//declaration
};
intS::x=0;//definition,initialize outside the class
為什么需要在外部定義呢?
因為 static 對象必須滿足 ODR(One Definition Rule),而類一般是在頭文件中聲明,該頭文件可能會被多個 TUs 包含,每個對象必須具備唯一的定義,否則在編譯鏈接時會出現(xiàn)問題。所以將它作為一個聲明,定義在類外部單獨指定。
但是在某些時候也可以不用定義,比如:
//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 時才必須要提供定義,在不需要 lvalue 的表達式中,它可以直接使用 S::x 的值,此時經(jīng)歷了 lvalue-to-rvalue 的隱式轉(zhuǎn)換。相反,在需要 lvalue 的表達式中,則必須提供定義。
注:ODR-used 是標準用來指必須為實體提供定義的術(shù)語,因為它不是必須的,需要依賴情境討論,所以不單獨使用 used 來描述。比如一個虛函數(shù)是非 ODR-used,而一個純虛函數(shù)是 ODR-used,使用時必須提供定義。模板只在使用時才實例化,這里的使用準確的描述也應該是 ODR-used。
如果嫌在外部定義麻煩,在 C++17 可以采用 inline 來光明正大地違背 ODR,它能夠告訴鏈接器,我想在多個 TUs 之間擁有相同的定義。
classS{
//since C++17
inlinestaticintx=42;
};
在 C++20,由于 constexpr 會隱式 inline,所以還可以這么寫:
classS{
//since C++20
staticconstexprintx=42;
};
另外,在 C++98,如果以 static const 修飾一個整型成員數(shù)據(jù),那么也可以在類內(nèi)直接初始化,并且可以保證初始化是在編譯期完成的。
//C++98
structS{
staticconstintx=42;//OK
constinty=42;//since C++11, default member initializer
};
對于非 static 數(shù)據(jù)成員,自 C++11 開始支持 default member initializer,于是也可以直接在類內(nèi)直接初始化。
5
以static修飾函數(shù)
函數(shù)也分全局函數(shù)和成員函數(shù),以 static 修飾時也要分別討論。
5.1
以static修飾全局函數(shù)
正常情況下,全局函數(shù)默認是外部鏈接,可以通過前置聲明在多個 TUs 使用。
如果以 static 修飾全局函數(shù),則將其鏈接方式變?yōu)閮?nèi)部鏈接,只能在當前 TU 使用。
一個小例子:
//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ù)增加了一個隱式對象參數(shù),它并不是 this 指針,而是為了重載決議能夠正常運行所定義的一個可以匹配任何參數(shù)的對象參數(shù)。這樣的成員函數(shù)無法訪問其他的非靜態(tài)成員名稱,因為那些名稱都與對象綁定。
當時編寫的一個示例:
structS{
voidf(long){
std::cout<"memberversion
";
}
staticvoidf(int){
std::cout<"staticmemberversion
";
}
};
intmain(){
Ss;
s.f(1);//staticmemberversion
}
6
static修飾變量對 Lambdas捕獲參數(shù)的影響
如果是全局變量,那么 Lambdas 無需捕獲便可以直接使用:
intx=42;
intmain(){
//youdon'tneed tocapturea globalvariable
[]{returnx;}();
}
但如果是局部變量,由于它的存儲時期為 automatic,就必須捕獲才能使用:
intmain(){
intx=42;
//youhavetocapturealocalvariable
[&x]{returnx;}();
}
但如果使用 static 修飾該局部變量,就無需再進行捕獲:
intmain(){
staticintx=42;
//OK
[]{returnx;}();
}
同理,const/constexpr/constinit 修飾的變量在某些時候也無需再進行捕獲:
constinitintm=42;
intmain(){
constexprintx=42;
constintn=42;
//OK
[]{returnm+x+n;}();
}
7
static constexpr, static constinit
請大家注意我上節(jié)最后一句的用詞,是「在某些時候」也無需進行捕獲,使用那些詞修飾并非一定是可以無需捕獲。
準確地說,非 ODR-used 的數(shù)據(jù)無需捕獲,它可以直接把那個常量隱式捕獲。
intmain(){
constexprstd::string_viewx="foo";
[]{x;}(); //OK,xisnotodr-used
[]{x.data();}();//error:x.data()isodr-used
}
此時就可以借助 static constexpr,就可以強保證 Lambdas 可以隱式捕獲該數(shù)據(jù):
intmain(){
staticconstexprstd::string_viewx="foo";
[]{x;}();//OK,xisnotodr-used
[]{x.data();};//OK,x.data()isnot odr-used
}
可以理解為此時捕獲的不是 lvalue,而是經(jīng)由 lvalue-to-rvalue 的那個值。
static constinit 也是同理,事實上 constinit 在局部使用必須添加 static,它只能修飾靜態(tài)存儲期或是線程存儲期的數(shù)據(jù)。
在類中使用 static constexpr 修飾數(shù)據(jù),可以保持數(shù)據(jù)既是編譯期,又能夠所有對象共享一份數(shù)據(jù)。
template<intN>
structS{
staticconstexprintx=N;
};
constinit 和 constexpr 大多時候都是同理,只是前者是可讀可寫,后者是只讀,除非有不同的意義,否則討論的 constexpr 用法也適用于 constinit。后文不再提及。
static constexpr 的另一個用處是「強保證」發(fā)生于編譯期。constexpr 本身只是「弱保證」,它并不一定發(fā)生于編譯期。
它們的其他用處見第9節(jié)。
8
static const vsconstexpr
前面講過,C++ 以 const 修飾全局變量會默認為內(nèi)部鏈接,所以 static 可以省略不寫。但是局部變量不可省,因為 static 修飾局部變量時的意義是改變局部變量的存儲時期,此時的 static const必須完整寫出。
全局變量本身的存儲時期就是 static,加上 const 表示只讀,此時以 static 修飾的意義是指定其鏈接方式。
局部變量本身的存儲時期是 automatic,無鏈接,加上 const 依舊表示只讀,此時以 static 修飾的意義是指定其存儲時期。
所以對于全局 (static) const 數(shù)據(jù)來說,是在加載期就分配內(nèi)存了(如存儲時期那節(jié)所強調(diào),不要誤以為它發(fā)生在編譯期)。而對于局部 static const 數(shù)據(jù)來說,它實際分配內(nèi)存是在首次使用時,實際可能發(fā)生于運行期。
constexpr 修飾的變量則不同,它們發(fā)生于編譯期,這其實是要早于 static const 修飾的變量。
但是經(jīng)過優(yōu)化,它們展現(xiàn)的效果是差不多的,相對來說,更推薦使用 constexpr。
intmain(){
staticconstintm=42;//sinceC++98
constexprintn=42;//sinceC++11
returnm+n;
}
9
Solvingthe"Static Initialization Order Fiasco"
SIOF是存儲時期為 static 的這類數(shù)據(jù)在跨 TUs 時相互依賴所導致的問題,因為不同 TUs 中的這些數(shù)據(jù)初始化順序沒有規(guī)定,引用的數(shù)據(jù)可能還沒初始化。
因此全局變量、靜態(tài)變量、靜態(tài)成員變量都可能會引起這個問題。
看如下例子:
//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 依賴于它。在編譯時初始化 tu_two_x,如果此時 tu_one_x 還未初始化,那么結(jié)果就偏離預期。
這依賴于編譯順序,你只有50%的幾率獲得預期結(jié)果。
解決策略一是使用局部靜態(tài)變量替代全局變量。前面講過,局部靜態(tài)變量相當于全局變量,而且可以延遲初始化。
//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)會在首次訪問時初始化,因此在初始化 tu_two_x 之前,就先把 tu_one_x 初始化了。于是不會再有 SOIF:
解決策略二是借助 constinit。前面依舊講過,constinit 發(fā)生于編譯期,而存儲時期為 static 的數(shù)據(jù)實際發(fā)生于加載期,SOIF 只是加載期的問題,只要將初始化時期錯開,體現(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:"<"
";
}
此時 tu_one_x 初始化于編譯期,而 tu_two_x 初始化于加載期,所以也不會存在SOIF。
但是你無法使用 extern constexpr,像如下這樣寫會編譯失敗:
//tu-two.cpp
externconstexprinttu_one_x;//error:notadefinition
autotu_two_x=tu_one_x;
因為 constinit 修飾的數(shù)據(jù)是可讀可寫的,而 constexpr 修飾的數(shù)據(jù)是只讀的,定義時必須要給初值。這里這種寫法被視為只是一個聲明。
雖然無法使用 extern constexpr,但也是可以借助 constexpr 來解決 SOIF 的,只不過要把所有的實現(xiàn)全部放到頭文件,然后在另一個實現(xiàn)文件中包含該頭文件。本節(jié)最后有一個相關例子。
使用 static 修飾的變量與全局變量同理,也提供一個例子:
//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:"<"
";
}
它們的存儲時期也是 static,所以也會產(chǎn)生 SOIF。tu_two_x 的初始化依賴于 S::tu_one_x,因此你也有 50% 的幾率得到正確結(jié)果。
通過使用 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,所有相關操作也都得是編譯期完成,所以 get_value() 也加上了 constexpr 修飾。那么 static 在此時主要指的是所修飾數(shù)據(jù)在所有對象之間共享,constinit 將它的初始化時間提前到了編譯期。
但是你不能把 tu_two_x 也以 static constinit 修飾,因為編譯期的值發(fā)生在鏈接之間,在編譯期就得確定,而 tu_two_x 的值又來自于另一個文件,編譯時根本就不知道所依賴的那個常量值。
同理這里也可以使用 static constexpr,但是 constexpr 沒有 constinit 靈活,它是 const 的,所以定義時就必須跟著初始化。
//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ù)只在單個 TU 使用,即內(nèi)部鏈接;與之相反,inline 能夠指示編譯器數(shù)據(jù)需要在多個 TUs 使用,此時即使違背了 ODR,也不應該報錯,屬于外部鏈接。
那如果它們組合使用,會有怎樣的效果?
讓我們寫個例子來對比一下不同的情況編譯之后到底產(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,并查看生成的目標文件的符號表:
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 表示顯示符號表,-W 表示以寬格式顯示。c++filt 是 gnu 提供的反 Name Demangling 工具,可以顯示未經(jīng)修飾的函數(shù)名稱。
可以看到,only_static() 和 static_inline() 的綁定方式都是 LOCAL,表示僅在當前文件可見;only_inline() 的綁定方式為 WEAK,表示該符號可被覆蓋,所以在其他文件中也是可見的;get_value_one() 的綁定方式是 GLOBAL,也表示在所有文件中可見。
再來編譯 tu-two.cpp,也查看其目標文件的符號表:
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() 存在兩份拷貝,說明它在每個 TU 都存在一份拷貝。而 only_inline() 只存在一份拷貝,說明 inline 的名稱的確能夠跨 TUs。
全局變量也只存在一份,說明外部鏈接是起作用的。
于是能夠得出結(jié)論,static 是內(nèi)部鏈接,inline 是外部鏈接,static inline 和 static 效果一樣,此時加上 inline,僅僅是告訴編譯器,可以嘗試內(nèi)聯(lián)一下代碼。
11
總結(jié)
本文深入全面系統(tǒng)地介紹了 static 關鍵字的方方面面,涉及內(nèi)容又多又雜,又廣又深。
static 是 C++ 中最復雜的關鍵字之一,有多達十幾種不同的意思,而且涉及編譯知識,許多使用形式意思非常細微。
所有相關內(nèi)容幾乎都包含在本文當中,具體總結(jié)大家就得自己歸納一下了。
-
cpu
+關注
關注
68文章
10829瀏覽量
211193 -
存儲
+關注
關注
13文章
4266瀏覽量
85686 -
C++
+關注
關注
22文章
2104瀏覽量
73503 -
編譯器
+關注
關注
1文章
1618瀏覽量
49057
原文標題:深入理解 C++ “static” 關鍵字
文章出處:【微信號:CPP開發(fā)者,微信公眾號:CPP開發(fā)者】歡迎添加關注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論