C/C++符號隱藏與依賴管理(一):代碼符號隱藏

C語言中全局變量和函數(shù)的符號是默認(rèn)外部可訪問的几蜻。

只要我們知道一個全局變量或者函數(shù)的聲明,我們就可以在當(dāng)前的編譯單元中直接使用它体斩,即使它定義在另一個編譯單元中梭稚,甚至是定義在另一個軟件庫中。由于符號全局可訪問絮吵,鏈接器會在鏈接期幫我們跨編譯單元找到對應(yīng)的符號并進(jìn)行鏈接弧烤。

C語言這種默認(rèn)的全局可訪問性看起來使用簡單,但卻在實踐中引起了很多麻煩蹬敲。

首先暇昂,全局可訪問性增加了代碼符號的沖突幾率。為了避免符號沖突伴嗡,在大的C項目中我們必須為所有全局變量和函數(shù)起很長的名字急波,一般需要加上“子系統(tǒng)名”或者“模塊名”之類的前綴。這樣導(dǎo)致代碼不夠簡潔闹究,而且生成的二進(jìn)制還會占用更多的空間幔崖。

其次,全局可訪問性讓使用extern的成本很低。extern為使用外部符號提供了一種直通車機(jī)制赏寇,這種做法繞過了別人提供的頭文件吉嫩,可以直接引用對方本不想不暴露的符號。這不僅造成一種間接的隱式依賴嗅定,而且還導(dǎo)致了潛在的安全風(fēng)險自娩。

extern不加控制的項目,其依賴關(guān)系最終肯定會變成一團(tuán)亂麻渠退。更進(jìn)一步忙迁,extern會造成全局變量和函數(shù)原型的重復(fù)聲明碎乃,這不僅破壞了DRY(Don't Repeat Yourself)原則梅誓,還為代碼埋下了潛在的安全問題梗掰。

我已經(jīng)不止一次在非常關(guān)注可靠性的項目中目睹過全局變量的維護(hù)者修改了變量類型及穗,如將U32 g_ports[MAX_NUM]修改為U16 g_ports[MAX_NUM]埂陆,但是不小心遺漏了某處extern U32 g_ports[MAX_NUM]猜惋,然后引起了各種難以定位的內(nèi)存和復(fù)位問題著摔。

所以谍咆,我們需要遵守的第一條重要的原則是:盡量避免使用extern關(guān)鍵字私股。

extern只在很少幾種情況下是有用的倡鲸,例如明確要鏈接某些第三方的沒有頭文件的二進(jìn)制庫,或者調(diào)用匯編編寫的函數(shù)以及訪問編譯器/鏈接器自動生成的符號等逼争。

盡力消滅代碼中的extern絕對會改善你的設(shè)計誓焦,但是這并沒有改變C語言會將符號置為全局可見的事實杂伟。這時我們需要另一個非常重要的關(guān)鍵字static來幫忙赫粥。

static是C語言中僅有的用于隱藏符號的手段傅是,因此用好它的意義十分重要喧笔。

static在C語言中主要有兩種作用。1)對于函數(shù)內(nèi)的局部變量浆劲,它指示該變量的內(nèi)存不在棧上牌借,而在全局靜態(tài)區(qū)膨报。2)對于全局變量和函數(shù)來說现柠,它指示對應(yīng)的符號可見性被約束在本編譯單元內(nèi)弛矛,不會暴露出去丈氓。

對于符號隱藏,我們主要使用static的第二個用途饮怯。由于使用static修飾的全局變量和函數(shù)的符號不會被導(dǎo)出硕淑,所以我們可以給這些變量和函數(shù)起更精煉的名字置媳,同時編譯器也會幫我們做更好的優(yōu)化拇囊,生成更小的二進(jìn)制寥袭。

更重要的是传黄,盡量多的使用static會讓我們改善設(shè)計,進(jìn)而得到符合Modular C風(fēng)格的設(shè)計识埋。

Modular C風(fēng)格的設(shè)計最基本的就是將狀態(tài)(全局變量)和無需暴露的函數(shù)通過static隱藏到編譯單元內(nèi)部窒舟,只將真正的API接口聲明到頭文件中惠豺。由于使用static修飾的符號是沒法extern的耕腾,結(jié)合上一條建議,強制使用方只能顯示的通過包含對應(yīng)的頭文件來調(diào)用開放的API狼纬,這樣代碼自然變得更加的模塊化疗琉。

所以盈简,我們給出C語言符號隱藏另一個原則:盡可能多的使用static關(guān)鍵字來封裝細(xì)節(jié)柠贤,讓代碼遵從Modular C的設(shè)計風(fēng)格臼勉。

現(xiàn)在我們轉(zhuǎn)向C++。得益于C++的面向?qū)ο筇匦云靶唬覀冇辛祟愐约皩?yīng)的訪問性控制關(guān)鍵字private氓扛、protectedpublic幢尚。

這些關(guān)鍵字可以修飾類的成員以及類的繼承關(guān)系,從而對內(nèi)和對外呈現(xiàn)出不同級別的可訪問性理茎。這些關(guān)鍵字的用法在各種教科書中都有,本文不做更多介紹础倍。 推薦大家熟練掌握這些關(guān)鍵字的用法忆家,記得千萬不要把類中的一切都公開出去(雖然我見過很多人確實這么做的)。

記住一個原則卸例,那就是盡可能多的使用private關(guān)鍵字

除了類旦装,C++語言還有一個用于隱藏信息極好的特性,那就是命名空間namespace呻袭。namespace讓我們能夠?qū)Ψ柗诸悾瑢⑵淇刂圃讵毩⒌拿臻g中,而不用像C語言中那樣靠增加名字前綴來避免符號沖突栈拖。

遺憾的是C++中命名空間是沒有可訪問性控制的,也就是說命名空間中的符號全部是公開的,外部通過命名空間路徑都是可以訪問到的器仗。

不過C++語言提供了匿名命名空間的特性暴心,凡是在匿名命名空間中的符號都是不導(dǎo)出的弹沽。也就是說匿名命名空間中的符號只在本編譯單元內(nèi)部可見,外部是不能使用的丽已。其作用類似于C語言中的static,但是寫起來更加簡潔嘁灯。

// example.cpp

namespace {
    struct Port {
        // ...
    };

    Port ports[MAX_NUM];

    unsigned int getRateOf(const Port& port) {
        // ...
    }
}

unsigned int getPortRate(unsigned int portId) {
    // ...
}

如上面例子中:Port羹奉、portsgetRateOf只能在"example.cpp"中訪問病蛉,而getPortRate則在該編譯單元外也可以使用俗孝。

因此對于C++語言革骨,我們推薦:盡可能使用命名空間來管理符號农尖,尤其是使用匿名命名空間來隱藏符號

C++語言為了兼容C良哲,仍舊使用頭文件機(jī)制發(fā)布API盛卡。為了在C++的頭文件中更好的隱藏符號,我們在這里先來區(qū)分兩個概念:“可見性”與“可訪問性”筑凫。

以下面這個Storage類定義的頭文件“Storage.h”為例:

// Storage.h

#include "StorageType.h"

class Storage {
public:
    Storage();
    unsigned int getCharge() const;
private:
    bool isValid() const;
private:
    StorageType type;
    unsigned int capacity; 
    static unsigned int totalCapacity;
};

用戶只要包含這個頭文件滑沧,就可以看到Storage類中的所有的方法聲明以及成員變量定義。因此從可見性上來說巍实,這個類的所有函數(shù)聲明和成員變量的定義都是外部可見的滓技。然而從可訪問性上來說,我們只能訪問這個類的公開的構(gòu)造函數(shù)Storage()getCharge()接口棚潦。

從上面的例子中可以看到令漂,C++頭文件中類定義對外的可見性和可訪問性是不一致的。

當(dāng)可見性大于可訪問性的時候丸边,帶來的問題是:當(dāng)我們修改了類的私有函數(shù)或者成員變量定義(用戶可見但是不可訪問的符號)時叠必,事實上并不會影響用戶對該類的使用方式,然而所有使用該類的用戶卻被迫要承擔(dān)重新編譯的負(fù)擔(dān)原环。

為了避免上面的問題挠唆,降低客戶重新編譯的負(fù)擔(dān),我們需要在頭文件中盡量少的暴露信息嘱吗。對類來說需要盡量讓其外部可見性和可訪問性在頭文件中趨于一致玄组。

那要怎么做呢?主要有以下手段:

  • 可以將類的靜態(tài)私有(static private)成員直接轉(zhuǎn)移到類實現(xiàn)文件中的匿名命名空間中定義谒麦;

如上例中的static unsigned int totalCapacity是不需要定義到類的頭文件中的俄讹,可以直接定義到該類實現(xiàn)文件的匿名命名空間中。

// Storage.cpp

#include "Storage.h"

namespace
{
    // remove "static unsigned int totalCapacity" in Storage.h, and define it here
    unsigned int totalCapacity = 0;
}

Storage::Storage() {
    // ...
}

bool Storage::isValid() const {
    if (this->capacity > totalCapacity) {
        // ...
    }
    // ...
}

unsigned Storage::int getCharge() const {
    if(this->isValid(this->capacity)) {
        // ...
    }
    // ...
}
  • 對于類的普通私有成員方法绕德,可以將它依賴的成員變量當(dāng)做參數(shù)傳給它患膛,這樣它就可以變成類的靜態(tài)私有函數(shù)。然后就可以依照前面的方法將其移到類實現(xiàn)文件中的匿名命名空間中耻蛇;

如上例中類的bool isValid() const私有成員方法的實現(xiàn)中訪問了類的成員變量this->capacity踪蹬。我們修改isValid方法的實現(xiàn),將capacity作為參數(shù)傳遞給它臣咖,這樣isValid在類中的聲明就可以變?yōu)?code>static bool isValid(unsigned int capacity)跃捣,實現(xiàn)變?yōu)椋?/p>

// Storage.cpp

bool Storage::isValid(unsigned int capacity) {
    if (capacity > totalCapacity) {
        // ...
    }
    // ...
}

現(xiàn)在我們就已經(jīng)可以參照前面的原則,將類的私有靜態(tài)成員搬移到實現(xiàn)文件的匿名命名空間中夺蛇,將其在頭文件中的聲明刪除疚漆。

// Storage.h

#include "StorageType.h"

class Storage {
public:
    Storage();
    unsigned int getCharge() const;
private:
    StorageType type;
    unsigned int capacity; 
};
// Storage.cpp

#include "Storage.h"

namespace
{
    unsigned int totalCapacity = 0;

    bool isValid(unsigned int capacity) {
        if (capacity > totalCapacity) {
            // ...
        }
        // ...
    }    
}

Storage::Storage() {
    // ...
}

unsigned Storage::int getCharge() const {
    if(isValid(this->capacity)) {
        // ...
    }
    // ...
}

經(jīng)過上面的操作,類中的私有方法和靜態(tài)私有成員都從頭文件移到了實現(xiàn)文件的匿名命名空間中了。那么最后剩下的類的非靜態(tài)私有成員變量能否也隱藏起來呢娶聘?

方法是有的闻镶,就是使用PIMPL(pointer to implementation)方法。

  • 可以使用PIMPL方法隱藏類的私有成員丸升。

對于上例铆农,使用PIMPL后實現(xiàn)如下:

// storage.h

class Storage {
public:
    Storage();
    unsigned int getCharge() const;
    ~Storage();
private:
    class Impl;
    Impl* p_impl{nullptr}; 
};
// Storage.cpp

#include "Storage.h"
#include "StorageType.h"

namespace
{
    unsigned int totalCapacity = 0;

    bool isValid(unsigned int capacity) {
        if (capacity > 0) {
            // ...
        }
        // ...
    } 
}

class Storage::Impl {
public:
    Impl() {
        // original implmentation of Storage::Storage()
    }

    unsigned int getCharge() const {
        // original implmentation of Storage::getCharge()
    }
private:
    StorageType type;
    unsigned int capacity;     
};

Storage::Storage() : p_impl(new Impl()){
}

Storage::~Storage(){
    if(p_impl) delete p_impl;
}

unsigned int Storage::getCharge() const {
    return p_impl->getCharge();
}

可以看到,使用PIMPL方法就是把所有的調(diào)用都委托到一個內(nèi)部類(本例中的Impl)的指針上发钝。

由于指針的類型只用做前置聲明顿涣,所以使用PIMPL手法的類的私有成員只用包含一個內(nèi)部類的前置聲明和一個成員指針即可。而Impl類則包含了原來類的所有真正的成員和函數(shù)實現(xiàn)酝豪。因為Impl類可以實現(xiàn)在cpp文件中,所以達(dá)到了進(jìn)一步隱藏信息的效果精堕。

從上例我們看到孵淘,由于Storage類的所有私有成員都轉(zhuǎn)移到了內(nèi)部的Impl類中,所以Storage類的頭文件中不再需要包含"StorageType.h"歹篓,只用在實現(xiàn)文件中包含即可瘫证。因此使用PIMPL手法,可以解決頭文件耦合與物理依賴傳遞的問題庄撮。

不過背捌,通過代碼示例也可以看到使用PIMPL方法是有成本的,它增加了間接函數(shù)調(diào)用和動態(tài)內(nèi)存分配的開銷洞斯。而且由于代碼多了一層封裝毡庆,導(dǎo)致整體復(fù)雜度上升了。因此除非解決某些嚴(yán)重的物理依賴問題烙如,一般不會大面積使用該手法么抗。

最后,一個完備的PIMPL實現(xiàn)會借助unique_ptr類型的智能指針亚铁。本例為了簡化示例所以采用了裸指針實現(xiàn)蝇刀,更完整和通用的PIMPL實現(xiàn)可以參見 https://en.cppreference.com/w/cpp/language/pimpl

到此徘溢,我們總結(jié)一下C/C++語言自身有關(guān)符號可見性控制的原則和方法:

1) 盡量避免使用extern關(guān)鍵字吞琐;
2) 對于C語言,盡可能多的使用static關(guān)鍵字來封裝細(xì)節(jié)然爆,讓代碼遵從Modular C的設(shè)計風(fēng)格站粟;
3)對于C++,盡可能多的使用private關(guān)鍵字施蜜;
4)對于C++卒蘸,盡可能使用命名空間來管理符號,尤其是使用匿名命名空間來隱藏符號;
5)頭文件盡量隱藏信息缸沃,縮小頭文件內(nèi)的符號可見性恰起。可以采取的手段有:
    - 將類的靜態(tài)私有成員轉(zhuǎn)移到實現(xiàn)文件的匿名命名空間中趾牧;
    - 在某些情況下检盼,可以將類的私有方法重構(gòu)成類的靜態(tài)私有方法,然后移入到實現(xiàn)文件的匿名命名空間中翘单;
    - 對于某些嚴(yán)重的頭文件耦合問題吨枉,可以選擇使用PIMPL方法,隱藏類的所有非公開成員及其依賴的頭文件哄芜;

C/C++符號隱藏與依賴管理(二):庫的符號隱藏

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末貌亭,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子认臊,更是在濱河造成了極大的恐慌圃庭,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件失晴,死亡現(xiàn)場離奇詭異剧腻,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)涂屁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門书在,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拆又,你說我怎么就攤上這事儒旬。” “怎么了遏乔?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵义矛,是天一觀的道長。 經(jīng)常有香客問我盟萨,道長凉翻,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任捻激,我火速辦了婚禮制轰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘胞谭。我一直安慰自己垃杖,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布丈屹。 她就那樣靜靜地躺著调俘,像睡著了一般伶棒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上彩库,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天肤无,我揣著相機(jī)與錄音,去河邊找鬼骇钦。 笑死宛渐,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的眯搭。 我是一名探鬼主播窥翩,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鳞仙!你這毒婦竟也來了寇蚊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤繁扎,失蹤者是張志新(化名)和其女友劉穎幔荒,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體梳玫,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年右犹,在試婚紗的時候發(fā)現(xiàn)自己被綠了提澎。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡念链,死狀恐怖盼忌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情掂墓,我是刑警寧澤谦纱,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站君编,受9級特大地震影響跨嘉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吃嘿,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一祠乃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧兑燥,春花似錦亮瓷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春除师,著一層夾襖步出監(jiān)牢的瞬間沛膳,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工馍盟, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留于置,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓贞岭,卻偏偏與公主長得像八毯,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子瞄桨,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 【轉(zhuǎn)載】C&C++——C函數(shù)與C++函數(shù)相互調(diào)用問題 C C++相互調(diào)用 在項目中融合C和C++有時是不可避免的话速,...
    天之道天知道閱讀 3,427評論 2 19
  • 重新系統(tǒng)學(xué)習(xí)下C++;但是還是少了好多知識點芯侥;socket泊交;unix;stl柱查;boost等廓俭; C++ 教程 | 菜...
    kakukeme閱讀 19,793評論 0 50
  • 幾種語言的特性 匯編程序:將匯編語言源程序翻譯成目標(biāo)程序編譯程序:將高級語言源程序翻譯成目標(biāo)程序解釋程序:將高級語...
    囊螢映雪的螢閱讀 2,867評論 1 5
  • 1.面向?qū)ο蟮某绦蛟O(shè)計思想是什么研乒? 答:把數(shù)據(jù)結(jié)構(gòu)和對數(shù)據(jù)結(jié)構(gòu)進(jìn)行操作的方法封裝形成一個個的對象。 2.什么是類淋硝?...
    少帥yangjie閱讀 4,988評論 0 14
  • 重點掌握 3 類對象和方法 對象就是一個物體 類的獨特存在就是一個實例雹熬,對實例進(jìn)行操作叫做方法。方法可以應(yīng)用于類或...
    Coder大雄閱讀 1,254評論 0 2