關(guān)鍵字struct
是C++
繼承自C
語言的一項遺產(chǎn)锌介。作為更加貼切的詞匯巾表,class
被引入C++
猛计,用來表現(xiàn)類。這個決策造成的結(jié)果是:一種語言提供了兩個關(guān)鍵字來表示完全一致的概念绵估。在什么情況下應(yīng)該使用誰炎疆,社區(qū)內(nèi)并無定論,甚至C++
的發(fā)明者Bjarne Stroustrup
也無法給出毫不含糊的建議国裳。
一種流行的看法
如果只是名字的差別形入,那么毫無疑問,在任何時候都應(yīng)該使用class
缝左,畢竟它更直觀亿遂、準(zhǔn)確。
很多C++
開發(fā)者可能并不同意這一點渺杉。從他們的邏輯和經(jīng)驗出發(fā)蛇数,struct
仍然只應(yīng)該用來定義那些只有數(shù)據(jù),沒有行為的“類”—— 它們事實上是C
語言中的結(jié)構(gòu)體是越;而class
則應(yīng)該用來定義真正的類——那些有行為的家伙耳舅。甚至有團(tuán)隊規(guī)定: 一旦你使用struct
來定義一個類型,則其就不應(yīng)該有任何行為倚评;否則浦徊,就需要使用class
馏予。
如果使用C++
開發(fā)一套庫,卻要提供一套可供C
語言調(diào)用的接口盔性,這樣的規(guī)定是合理的霞丧。畢竟,C
語言是不認(rèn)識超出自己理解范圍的任何C++
語法的纯出。
如果不是基于此類目的蚯妇,這種規(guī)定就是自找麻煩敷燎。一個類型是不是應(yīng)該有行為暂筝,是動態(tài)的。盡管最初你定義它的時候硬贯,它是以持有數(shù)據(jù)為主要目的焕襟。但你無法確保隨后你不會因為某種目的需要為其添加一個方法。
比如饭豹,最初的時候你定義了這樣一個數(shù)據(jù)結(jié)構(gòu)鸵赖,由于它是進(jìn)程間通信的消息包,所以在你看來拄衰,它應(yīng)該是一個純粹的數(shù)據(jù)類——結(jié)構(gòu)體它褪。于是你將其定義為:
struct PDU
{
time_t timestamp;
data_t today;
int value;
};
隨后你發(fā)現(xiàn),在使用的過程中翘悉,總是需要重復(fù)這樣的代碼:
PDU pdu;
pdu.timestamp = now();
pdu.today = today();
pdu.value = value;
其中茫打,前兩個字段的設(shè)置方式是確定的,都是取系統(tǒng)的當(dāng)前時間和日期妖混。作為一個專業(yè)程序員老赤,這種重復(fù)讓你無法忍受,所以你決定實現(xiàn)一個類似于C
語言的初始化函數(shù):
void init_pdu(PDU& pdu, int value)
{
pdu.timestamp = now();
pdu.today = today();
pdu.value = value;
}
然后制市,你就可以這樣來調(diào)用:
PDU pdu;
init_pdu(pdu, 5);
這是一種典型的C
語言處理手法抬旺。如果你是個更老練的C++
程序員,會選擇使用構(gòu)造函數(shù):
struct PDU
{
explicit PDU(int value)
: timespace(::now())
, today(::today())
, value(value)
{}
time_t timestamp;
data_t today;
int value;
};
這種做法沒有帶來任何副作用:沒有任何額外的內(nèi)存和性能開銷祥楣。甚至开财,嚴(yán)格來講,由于使用了初始化列表误褪,它的性能還略微提高了床未。
除此之外,你還收獲了一些其它好處:
- 它的代碼和數(shù)據(jù)被毫無爭議振坚、無法分割的放在了一起薇搁,有著更好的內(nèi)聚性和可理解性;
- 具備某種強(qiáng)制性:你永遠(yuǎn)也不可能忘記調(diào)用構(gòu)造函數(shù);
- 客戶代碼更加的簡潔,直觀渡八。比如:
PDU pdu(5);
基于這些理由啃洋,我們讓類型PDU
有了一個行為传货。按照之前的規(guī)定,我們需要把它從struct
改成class
宏娄。這似乎不難做到问裕,但問題在于:為什么我們需要關(guān)注這件事情?如果從一開始它就是一個class
孵坚,哪怕它沒有任何行為粮宛,我們寶貴的時間和精力就不會被無謂的消耗。
這還不算完卖宠。當(dāng)你將一個類型由struct
改為class
后巍杈,所有對其進(jìn)行聲明的地方都需要做同步的修改。否則扛伍,編譯器將會發(fā)出警告筷畦。作為一個紀(jì)律嚴(yán)明的專業(yè)軟件公司的雇員,你被要求消除掉所有告警刺洒。于是鳖宾,更多的精力被毫無價值的浪費了。
所以逆航,以這種原則來區(qū)分struct
還是class
不會帶來任何好處鼎文,只會帶來一堆麻煩。于是因俐,就不難得出這樣的結(jié)論:我們只應(yīng)該堅持使用其中一個拇惋。
問題是:哪一個?
接口定義時的差別
除了名字不同之外,class
和struct
唯一的差別是:默認(rèn)可見性女揭。這體現(xiàn)在定義時和繼承時蚤假。struct
在定義一個成員,或者繼承時吧兔,如果不指明磷仰,則默認(rèn)為public
; 而class
則默認(rèn)為 private
境蔼。
但這不是我要討論的重點灶平,介紹語言的基礎(chǔ)特性并不是本文的目標(biāo),重點是這樣的差別會產(chǎn)生出不同的代碼箍土。
比如逢享,現(xiàn)在要定義一個純虛類,用兩個不同的關(guān)鍵字吴藻,會導(dǎo)致如下不同的結(jié)果:
class Interface
{
public:
virtual int invoke() = 0;
virtual ~Interface() {}
};
struct Interface
{
virtual int invoke() = 0;
virtual ~Interface() {}
};
兩者差別很小瞒爬,你或許并不在意。但對我而言,一個純虛類侧但,從邏輯上本來就是一個只有公開方法聲明矢空、沒有實現(xiàn)細(xì)節(jié)的接口類。它所聲明的一切都應(yīng)該是公開的禀横。在這樣的契約關(guān)系下屁药,如果再通過public
指明其公開性,這屬于畫蛇添足柏锄。
懶惰的我討厭冗余酿箭,討厭重復(fù)。更何況從平衡和美感的角度看趾娃,那個橫立的public
就像潔白墻面上的一沫蚊子血缭嫡,顯得格外刺眼。
繼承時的差別
而這并非故事的全部茫舶。struct
的默認(rèn)公開性還體現(xiàn)在繼承時:像成員一樣械巡,如果未指明刹淌,struct
對于父類的繼承默認(rèn)為public
繼承饶氏。而class
則恰恰相反。這個規(guī)則本身沒有問題有勾,問題在于我們?nèi)绾芜x擇疹启。
作為一個有經(jīng)驗的C++
程序員,在至少百分之九十以上的情況下蔼卡,都會使用公有繼承(對于很多C++程序員喊崖,這個比例是百分之百)。這就意味著雇逞,在絕大多數(shù)情況下(如果不是全部的話)荤懂,我們都要一遍遍的書寫public
——這不是一個理性的選擇。(Typing does take time, doesn't it?
)
class Derived
: public Base1
, public Base2
, public Base3
{
// more code
};
而一旦我們換作使用struct
來定義一個類塘砸,則所有不必要的 public
聲明就自然省略:
struct Derived
: Base1
, Base2
, Base3
{
// more code
};
確實干凈多了节仿,不是嗎?
實體類定義時的差別
實體類不同于接口類,往往存在私有數(shù)據(jù)(沒有數(shù)據(jù)掉蔬,只有實現(xiàn)的實體類也往往意味著壞味道廊宪,一些表現(xiàn)算法的策略類除外),而class
的默認(rèn)私有性女轿,讓這種場景成為它出彩的機(jī)會箭启。
class Foo
{
int a;
double b;
public:
Foo(int);
void doSomething();
};
這個類把私有數(shù)據(jù)定義在前面,把公開方法定義在后面蛉迹,所以可以利用 class
的默認(rèn)私有性傅寡。
但這樣的定義布局并不只是順序上的差異。我們的認(rèn)知習(xí)慣和閱讀順序決定了我們總是希望把更重要的、更希望人們了解的信息擺在一目了然的位置荐操。而不是讓別人穿越重重迷霧才能找到自己的關(guān)注點大猛。我們希望別人更容易理解我們的意圖,而不是試圖挑戰(zhàn)別人的智商和耐心淀零。
所以挽绩,信息擺放的順序就成了一件有所謂的事情。你如果認(rèn)為私有實現(xiàn)細(xì)節(jié)更為重要驾中,那就把私有數(shù)據(jù)擺在前面唉堪。否則,就把公開方法置于前列肩民。
對于把程序理解為“數(shù)據(jù)結(jié)構(gòu)+算法”的程序員唠亚,盡管正在使用面向?qū)ο蟮脑亍邦悺保瑓s依然會認(rèn)為理解一個程序的前提是理解它的數(shù)據(jù)結(jié)構(gòu)持痰。在這樣的價值驅(qū)動下灶搜,把私有數(shù)據(jù)擺在前面就是完全合情合理的。數(shù)年前工窍,我就曾在一本C++
相關(guān)的書中讀到過這樣的建議割卖。
但對于越來越確信信息隱藏對軟件之重要的我,則更傾向于認(rèn)為公開接口才是了解一個模塊最關(guān)鍵的知識患雏。當(dāng)試圖去使用一個模塊時鹏溯,我總是會優(yōu)先查看它的測試用例(如果有的話),然后再去看它的公開接口聲明淹仑。一般而言丙挽,這對于理解它能做什么,以及如何使用它已經(jīng)足夠匀借;接口聲明處的私有元素反而會干擾我對一個模塊的理解颜阐。只有在好奇心的驅(qū)使下,我才會進(jìn)一步去看看它的實現(xiàn)吓肋。
基于這樣的認(rèn)知凳怨,當(dāng)定義一個類的時候,我會把公開方法定義在前面蓬坡。至于私有的內(nèi)容猿棉,我總是會竭盡所能的不希望引起人們的注意,寧愿付出一些代價屑咳,也要把它們藏到別人在類聲明里看不到的地方萨赁,更不會放在前面擾亂視聽。
所以兆龙,我仍然會選擇struct
來定義實體類杖爽。如下:
struct Foo
{
Foo(int);
void doSomething();
private:
int a;
double b;
};
保持一致
無論是接口敲董,還是實現(xiàn)類;無論是一個類以行為為中心慰安,還是以數(shù)據(jù)為中心腋寨,使用 struct
而不是 class
都會給你的編程帶來一定的便利。
基于某些原因——無論是遺留系統(tǒng)的阻力化焕,還是個人偏好的牽引——在了解了這所有的一切之后萄窜,你可能仍然選擇使用class
。這沒有太大問題撒桨,畢竟你的程序你做主查刻。
但需要強(qiáng)調(diào)的是,無論你喜歡class
還是struct
凤类,都應(yīng)該堅持選擇其中一個穗泵,而不是混合使用(比如不要在定義數(shù)據(jù)類的時候使用 struct
,定義行為類的時候使用class
)谜疤。否則佃延,在大量使用前導(dǎo)聲明的情況下,一旦某個使用struct
的類改為class
夷磕,或反過來履肃,所有的前導(dǎo)聲明都需要做相應(yīng)修改∑笮浚或許編譯器并不認(rèn)為這種不一致是一種錯誤榆浓,但那些不斷騷擾你的警告亦會讓你不勝其煩于未。
總結(jié)
為什么要以這么大的篇幅討論struct
和class
?
首先撕攒,因為我所寫的所有C++
相關(guān)文章都會使用struct
來定義類(在實際項目中也一直如此)。如果不在這里給出說明烘浦,一些人可能會感到困惑抖坪。
其次是因為此問題在社區(qū)內(nèi)尚無定論,很多團(tuán)隊在給出struct
和 class
的選擇問題時闷叉,給出的選擇理由并不成立擦俐。一次深入的討論對于社區(qū)是有價值的。
最后握侧,通過這樣的討論蚯瞧,我希望闡述的并非結(jié)論,而是其背后所遵守價值觀和原則品擎。這會有助于理解我其它文章的內(nèi)容埋合。無論話題如何變化,我都會遵守一致的價值觀和原則萄传。