0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

深入理解C++ “static”關(guān)鍵字

CPP開發(fā)者 ? 來源:CPP開發(fā)者 ? 2023-08-14 12:25 ? 次閱讀

大多數(shù) C++ 關(guān)鍵字使用起來都是比較簡單的,但也有少數(shù)相對復(fù)雜,static 便是其中的一個代表。

標(biāo)準(zhǔn)往往會避免為語言增加新的關(guān)鍵字,而是復(fù)用已有的。這使得 static 如今已存在十幾種不同的意思,可以修飾全局,也可以修飾局部;可以修飾函數(shù),也可以修飾變量;還可以和 inline、const、constexpr、constinit 等關(guān)鍵字組合起來使用。

許多 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)存時,預(yù)留一些空間
.rodata read-only data的縮寫,通常存儲靜態(tài)常量

程序分段有利于區(qū)分指令和數(shù)據(jù),指令通常是只讀的,數(shù)據(jù)是可讀寫或只讀的,分到不同的段內(nèi),設(shè)置不同的權(quán)限,可以防止程序指令被無意修改。

一個程序編譯后的內(nèi)存布局圖可能如下圖所示:

89815cb8-3a56-11ee-9e74-dac502259ad0.jpg

可以看到,程序被分成了六個主要的內(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)存必須手動釋放。當(dāng)然,通過智能指針這些數(shù)據(jù)的生命周期也能夠自動管理。

不要把這些存儲時期與編譯期/運行期混淆,它們是編譯原理的概念,按那種尺度來劃分,則存在編譯期-加載期-運行期三個不同的時期。

編譯期指的是編譯器處理代碼的時期,該時期的數(shù)據(jù)符號地址被翻譯成絕對地址,是最早就確定的數(shù)據(jù)。constexpr/constint 所修飾的數(shù)據(jù),一般就是在這一時期分配的內(nèi)存。

編譯后的程序存儲在硬盤上,準(zhǔn)備執(zhí)行時操作系統(tǒng)需要將它們讀取到 RAM 中,這個時期就叫加載期。.data/.rodata 段的數(shù)據(jù)就是在這一時期分配內(nèi)存的,一個常見的誤區(qū)就是認(rèn)為 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ù)只在當(dāng)前 TU 使用,有些數(shù)據(jù)還需要在其他 TUs 使用,前者稱為內(nèi)部鏈接,后者稱為外部鏈接。

每個 TU 就是一個模塊,鏈接就是將這些模塊組合起來,同時保證其中所引用的各種符號都在正確的位置上。

只有在當(dāng)前 TU 中使用的數(shù)據(jù)才需要內(nèi)部鏈接,局部的那些數(shù)據(jù)屬于無鏈接。我們需要關(guān)注的是一個變量的內(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 關(guān)鍵字。本節(jié)關(guān)注于修飾變量,這又有全局和局部之分。

4.1

以static修飾全局變量

全局變量處于 static存儲時期,也對應(yīng)于加載期,在 main()執(zhí)行之前就已為這些變量分配了內(nèi)存。

如果一個全局變量被 extern 修飾,則它具有外部鏈接,能夠被其他 TUs 使用。相反,如果一個全局變量被 static 修飾,它具有內(nèi)部鏈接,只能在當(dāng)前 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 修飾,那么它默認(rèn)就有 extern 鏈接,無需多此一舉再加上 extern 修飾。而對于這樣一個變量,如何改變它的外部鏈接方式?只需使用 static 修飾,就可以將它變成內(nèi)部鏈接。

如果一個全局變量使用了 const/constexpr 修飾,則它默認(rèn)就有了 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)存,離開作用域時自動釋放,只能在當(dāng)前作用域使用。

如果為這樣的局部變量加上 static,就將其存儲時期由 automatic 改變成了 static,生命周期遍及整個程序的生命周期。這種變量實際是先在 .bss 段預(yù)留了空間,等到首次進入該函數(shù),才真正為其分配內(nèi)存,此時初始化的時機就不是加載期,而且延遲到了運行期,所以這種方式也叫惰性初始化。

這種局部靜態(tài)變量就相當(dāng)于全局變量,不同之處在于它是無鏈接,可見性僅在當(dāng)前函數(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 是標(biāo)準(zhǔn)用來指必須為實體提供定義的術(shù)語,因為它不是必須的,需要依賴情境討論,所以不單獨使用 used 來描述。比如一個虛函數(shù)是非 ODR-used,而一個純虛函數(shù)是 ODR-used,使用時必須提供定義。模板只在使用時才實例化,這里的使用準(zhǔn)確的描述也應(yīng)該是 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ù)默認(rèn)是外部鏈接,可以通過前置聲明在多個 TUs 使用。

如果以 static 修飾全局函數(shù),則將其鏈接方式變?yōu)閮?nèi)部鏈接,只能在當(dāng)前 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)成員名稱,因為那些名稱都與對象綁定。

當(dāng)時編寫的一個示例:

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é)最后一句的用詞,是在某些時候」也無需進行捕獲,使用那些詞修飾并非一定是可以無需捕獲。

準(zhǔn)確地說,非 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ù),可以保持?jǐn)?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 修飾全局變量會默認(rèn)為內(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 時相互依賴所導(dǎo)致的問題,因為不同 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é)果就偏離預(yù)期。

這依賴于編譯順序,你只有50%的幾率獲得預(yù)期結(jié)果。

89a16a76-3a56-11ee-9e74-dac502259ad0.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)會在首次訪問時初始化,因此在初始化 tu_two_x 之前,就先把 tu_one_x 初始化了。于是不會再有 SOIF:

89b8a81c-3a56-11ee-9e74-dac502259ad0.png

解決策略二是借助 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,像如下這樣寫會編譯失?。?/span>

//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é)最后有一個相關(guān)例子。

使用 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é)果。

89dbcba8-3a56-11ee-9e74-dac502259ad0.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ù)據(jù)在所有對象之間共享,constinit 將它的初始化時間提前到了編譯期。

89f06126-3a56-11ee-9e74-dac502259ad0.png

但是你不能把 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,也不應(yīng)該報錯,屬于外部鏈接。

那如果它們組合使用,會有怎樣的效果?

讓我們寫個例子來對比一下不同的情況編譯之后到底產(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)文件的符號表:

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,表示僅在當(dāng)前文件可見;only_inline() 的綁定方式為 WEAK,表示該符號可被覆蓋,所以在其他文件中也是可見的;get_value_one() 的綁定方式是 GLOBAL,也表示在所有文件中可見。

再來編譯 tu-two.cpp,也查看其目標(biā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() 存在兩份拷貝,說明它在每個 TU 都存在一份拷貝。而 only_inline() 只存在一份拷貝,說明 inline 的名稱的確能夠跨 TUs。

全局變量也只存在一份,說明外部鏈接是起作用的。

于是能夠得出結(jié)論,static 是內(nèi)部鏈接,inline 是外部鏈接,static inline 和 static 效果一樣,此時加上 inline,僅僅是告訴編譯器,可以嘗試內(nèi)聯(lián)一下代碼。

11

總結(jié)

本文深入全面系統(tǒng)地介紹了 static 關(guān)鍵字的方方面面,涉及內(nèi)容又多又雜,又廣又深。

static 是 C++ 中最復(fù)雜的關(guān)鍵字之一,有多達十幾種不同的意思,而且涉及編譯知識,許多使用形式意思非常細(xì)微。

所有相關(guān)內(nèi)容幾乎都包含在本文當(dāng)中,具體總結(jié)大家就得自己歸納一下了。


聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • cpu
    cpu
    +關(guān)注

    關(guān)注

    68

    文章

    10698

    瀏覽量

    209328
  • 存儲
    +關(guān)注

    關(guān)注

    13

    文章

    4122

    瀏覽量

    85271
  • C++
    C++
    +關(guān)注

    關(guān)注

    21

    文章

    2085

    瀏覽量

    73301
  • 編譯器
    +關(guān)注

    關(guān)注

    1

    文章

    1602

    瀏覽量

    48894

原文標(biāo)題:深入理解 C++ “static” 關(guān)鍵字

文章出處:【微信號:CPP開發(fā)者,微信公眾號:CPP開發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    深入理解FPD-link III ADAS解串器HUB產(chǎn)品

    電子發(fā)燒友網(wǎng)站提供《深入理解FPD-link III ADAS解串器HUB產(chǎn)品.pdf》資料免費下載
    發(fā)表于 09-06 09:58 ?0次下載
    <b class='flag-5'>深入理解</b>FPD-link III ADAS解串器HUB產(chǎn)品

    快速掌握C語言關(guān)鍵字

    C語言中的32個關(guān)鍵字你知道多少個呢?根據(jù)關(guān)鍵字的作用分為四類:數(shù)據(jù)類型關(guān)鍵字、控制語句關(guān)鍵字、存儲類型
    的頭像 發(fā)表于 07-06 08:04 ?193次閱讀
    快速掌握<b class='flag-5'>C</b>語言<b class='flag-5'>關(guān)鍵字</b>

    嵌入式C語言面試大挑戰(zhàn)

    C++static關(guān)鍵字除了具有C中的作用還有在類中的使用在類中,static可以用來修飾靜態(tài)數(shù)據(jù)成員和靜態(tài)成員方法靜態(tài)數(shù)據(jù)成員
    發(fā)表于 03-05 14:18 ?238次閱讀
    嵌入式<b class='flag-5'>C</b>語言面試大挑戰(zhàn)

    簡單總結(jié)一下嵌入式C++中常見的錯誤形式

    對于 C++ 類,一些關(guān)鍵字只要寫在 .h 中就好,cpp 中就不用再加上了,比如 virtual、static關(guān)鍵字,如果在 cpp 中多寫,編譯器會報錯。
    的頭像 發(fā)表于 02-23 09:40 ?347次閱讀

    在NVM和本地\"內(nèi)存中定義數(shù)組(靜態(tài) /global /local)的\"關(guān)鍵字是什么?

    在 NVM 和本地\"內(nèi)存中定義數(shù)組(靜態(tài) /global /local)的\"關(guān)鍵字是什么? 還有與 32 位對齊的關(guān)鍵字怎么樣。
    發(fā)表于 01-25 07:52

    深入理解FFmpeg閱讀體驗》+ 書收到了,嶄新的開篇

    今天收到了《深入理解FFmpeg》 嶄新的書,一個在2022年較近距離接觸過卻尚未深入研究的領(lǐng)域圖像處理。最近剛好在作這方面的研究,希望自己可以把握這次機會,好好學(xué)習(xí)下 FFMpeg,相信可以讓自己
    發(fā)表于 01-07 18:57

    你還記得這個C語言關(guān)鍵字嗎?

    當(dāng)你使用volatile關(guān)鍵字時,你告訴編譯器該變量的值可能會在程序的執(zhí)行過程中被外部因素更改,因此編譯器不應(yīng)該對該變量的讀寫進行優(yōu)化。下面是一些使用volatile的例子,以及對它們的一些文字描述
    的頭像 發(fā)表于 12-17 08:00 ?278次閱讀
    你還記得這個<b class='flag-5'>C</b>語言<b class='flag-5'>關(guān)鍵字</b>嗎?

    vlookup提取關(guān)鍵字匹配多個結(jié)果

    是它能夠基于關(guān)鍵字匹配提取多個結(jié)果。 當(dāng)使用VLOOKUP提取多個結(jié)果時,需要遵循以下幾個重要步驟。首先,您需要確保數(shù)據(jù)的組織和結(jié)構(gòu)正確。數(shù)據(jù)應(yīng)以表格格式排列,左側(cè)為關(guān)鍵字列,右側(cè)為相應(yīng)的信息列。 首先,讓我們考慮一個實際的例子。假設(shè)您有一個大型數(shù)據(jù)集
    的頭像 發(fā)表于 12-01 10:40 ?1992次閱讀

    static關(guān)鍵字的三種用法

    C語言中,關(guān)鍵字"static"可以用于不同的上下文,具有不同的作用。以下是"static"在C語言中的主要作用:1.靜態(tài)
    的頭像 發(fā)表于 11-10 08:00 ?414次閱讀
    <b class='flag-5'>static</b><b class='flag-5'>關(guān)鍵字</b>的三種用法

    IAR能否在整個工程中搜索關(guān)鍵字?

    像mdk可以在整個工程中搜索關(guān)鍵字 IAR應(yīng)該也有類似的功能吧,但是我貌似沒有找到
    發(fā)表于 11-08 07:21

    指向code區(qū)數(shù)組的指針需不需要加code關(guān)鍵字的聲明?

    指向code區(qū)數(shù)組的指針需不需要加code 關(guān)鍵字的聲明?
    發(fā)表于 11-02 06:16

    在嵌入式中定義變量時volatile關(guān)鍵字的作用是什么?

    在嵌入式中定義變量時volatile關(guān)鍵字的作用?
    發(fā)表于 10-24 07:50

    程序中變量加volatile關(guān)鍵字是做什么用途的?

    為什么變量定義要加上這個關(guān)鍵字
    發(fā)表于 10-13 07:15

    Java中對static關(guān)鍵詞的介紹

    static 是Java的一個關(guān)鍵字,可以用來修飾成員變量、修飾成員方法、構(gòu)造靜態(tài)代碼塊、實現(xiàn)靜態(tài)導(dǎo)包以及實現(xiàn)靜態(tài)內(nèi)部類,下面我們來分別介紹。 1、修飾成員變量 用 static 修飾成員變量
    的頭像 發(fā)表于 10-11 15:26 ?385次閱讀
    Java中對<b class='flag-5'>static</b><b class='flag-5'>關(guān)鍵</b>詞的介紹

    this關(guān)鍵字在Java中的用法

    this 關(guān)鍵字只能在方法內(nèi)部使用,表示對“調(diào)用方法的那個對象”的引用。 其實簡單來說 this 關(guān)鍵字就是表示當(dāng)前對象,下面我們來具體介紹 this 關(guān)鍵字在Java中的用法。 1、調(diào)用成員變量
    的頭像 發(fā)表于 10-10 16:49 ?457次閱讀
    this<b class='flag-5'>關(guān)鍵字</b>在Java中的用法