如何自學(xué)C?
本次回答邀請(qǐng)boolean,由他來(lái)分享自己在學(xué)習(xí)C++過(guò)程中踩過(guò)的那些坑~相信對(duì)自學(xué)C++的朋友會(huì)有幫助~
如果,將編程語(yǔ)言比作武功秘籍,C++無(wú)異于《九陰真經(jīng)》。《九陰真經(jīng)》威力強(qiáng)大、博大精深,經(jīng)中所載內(nèi)功、輕功、拳、掌、腿、刀法、劍法、杖法、鞭法、指爪、點(diǎn)穴密技、療傷法門(mén)、閉氣神功、移魂大法等等,無(wú)所不包,C++亦如是。C++跟《九陰真經(jīng)》一樣,如果使用不當(dāng),很容易落得跟周芷若、歐陽(yáng)鋒、梅超風(fēng)等一樣走火入魔。這篇文章總結(jié)了在學(xué)習(xí)C++過(guò)程中容易走火入魔的一些知識(shí)點(diǎn)。為了避免篇幅浪費(fèi),太常見(jiàn)的誤區(qū)(如指針和數(shù)組、重載、覆蓋、隱藏等)在本文沒(méi)有列出,文中的知識(shí)點(diǎn)也沒(méi)有前后依賴關(guān)系,各個(gè)知識(shí)點(diǎn)基本是互相獨(dú)立,并沒(méi)有做什么鋪墊,開(kāi)門(mén)見(jiàn)山直接進(jìn)入正文。
目錄1 函數(shù)聲明和對(duì)象定義
2 靜態(tài)對(duì)象初始化順序
3 類型轉(zhuǎn)換
3.1 隱式轉(zhuǎn)換
3.2 顯示轉(zhuǎn)換
4 inline內(nèi)聯(lián)
5 名稱查找
5.1 受限名稱查找
5.2 非受限名稱查找
6 智能指針
6.1 std::auto_ptr
6.2 std::shared_ptr
6.3 std::unique_ptr
7 lambda表達(dá)式
1 函數(shù)聲明和對(duì)象定義
對(duì)象定義寫(xiě)成空的初始化列表時(shí),會(huì)被解析成一個(gè)函數(shù)聲明。可以采用代碼中的幾種方法定義一個(gè)對(duì)象。
2 靜態(tài)對(duì)象初始化順序
在同一個(gè)編譯單元中,靜態(tài)對(duì)象的初始化次序與其定義順序保持一致。對(duì)于作用域?yàn)槎鄠€(gè)編譯單元的靜態(tài)對(duì)象,不能保證其初始化次序。如下代碼中,在x.cpp和y.cpp分別定義了變量x和y,并且雙方互相依賴。
x.cpp中使用變量y來(lái)初始化x
y.cpp中使變量x來(lái)初始化y
如果初始化順序不一樣,兩次執(zhí)行的結(jié)果輸出不一樣,如下所示:
如果我們需要指定依賴關(guān)系,比如y依賴x進(jìn)行初始化,可以利用這樣一個(gè)特性來(lái)實(shí)現(xiàn):函數(shù)內(nèi)部的靜態(tài)對(duì)象在函數(shù)第一次調(diào)用時(shí)初始化,且只被初始化一次。使用該方法,訪問(wèn)靜態(tài)對(duì)象的唯一途徑就是調(diào)用該函數(shù)。改寫(xiě)后代碼如下所示:
getX()函數(shù)返回x對(duì)象
y對(duì)象使用x對(duì)象進(jìn)行初始化
打印x和y值。通過(guò)這種方式,就保證了x和y的初始化順序。
3 類型轉(zhuǎn)換
這里只描述自定義類的類型轉(zhuǎn)換,不涉及如算數(shù)運(yùn)算的類型自動(dòng)提升等。
3.1 隱式轉(zhuǎn)換
C++自定義類型在以下兩種情況會(huì)發(fā)生隱式轉(zhuǎn)換:
1) 類構(gòu)造函數(shù)只有一個(gè)參數(shù)或除第一個(gè)參數(shù)外其他參數(shù)有默認(rèn)值;2) 類實(shí)現(xiàn)了operator type()函數(shù);
上面定義了一個(gè)Integer類,Integer(int)構(gòu)造函數(shù)可以將int隱式轉(zhuǎn)換為Integer類型。operator int()函數(shù)可以將Integer類型隱式轉(zhuǎn)換為int。從下面代碼和輸出中可以看出確實(shí)發(fā)生了隱式的類型轉(zhuǎn)換。
隱式類型轉(zhuǎn)換在某些場(chǎng)景中確實(shí)比較方便,如:
a、運(yùn)算符重載中的轉(zhuǎn)換,如可以方便的使Integer類型和內(nèi)置int類型進(jìn)行運(yùn)算
b、條件和邏輯運(yùn)算符中的轉(zhuǎn)換,如可以使智能指針像原生裸指針一樣進(jìn)行條件判斷
隱式類型轉(zhuǎn)換在帶來(lái)便利性的同時(shí)也帶來(lái)了一些坑,如下所示:
構(gòu)造函數(shù)隱式轉(zhuǎn)換帶來(lái)的坑。上述代碼定義了一個(gè)Array類,并重載了operator==運(yùn)算符。本意是想比較兩個(gè)數(shù)組,但是if(arr1 == arr2)誤寫(xiě)成了f(arr1 == arr2[0]),編譯器不會(huì)抱怨,arr2[0]會(huì)轉(zhuǎn)換成一個(gè)臨時(shí)Array對(duì)象然后進(jìn)行比較。
operator type()帶來(lái)的坑。上述String類存在到const char *的隱式轉(zhuǎn)換,strcat函數(shù)返回時(shí)String隱身轉(zhuǎn)換成const char *,而String對(duì)象已經(jīng)被銷毀,返回的const char *指向無(wú)效的內(nèi)存區(qū)域。這也是std::string不提提供const char *隱式轉(zhuǎn)換而專門(mén)提供了c_str()函數(shù)顯示轉(zhuǎn)換的原因。
3.2 顯示轉(zhuǎn)換
正是由于隱式轉(zhuǎn)換存在的坑,C++提供explicit關(guān)鍵字來(lái)阻止隱式轉(zhuǎn)換,只能進(jìn)行顯示轉(zhuǎn)換,分別作用域構(gòu)造函數(shù)和operator(),如下所示:
1) explicit Ctor(const type &);2) explicit operator type();
用explicit改寫(xiě)Integer類后,需要進(jìn)行顯示轉(zhuǎn)換才能與int進(jìn)行運(yùn)算,如下:
為了保持易用性,C++11中explicit operator type()在條件運(yùn)算中,可以進(jìn)行隱式轉(zhuǎn)換,這就是為什么C++中的智能指針如shared_ptr的operator bool()加了explicit還能直接進(jìn)行條件判斷的原因。下面代碼來(lái)自shared_ptr源碼。
4 inline內(nèi)聯(lián)
內(nèi)聯(lián)類似于宏定義,在調(diào)用處直接展開(kāi)被調(diào)函數(shù),以此來(lái)代替函數(shù)調(diào)用,在消除宏定義的缺點(diǎn)的同時(shí)又保留了其優(yōu)點(diǎn)。內(nèi)聯(lián)有以下幾個(gè)主要特點(diǎn):
a、內(nèi)聯(lián)可以發(fā)生在任何時(shí)機(jī),包括編譯期、鏈接期、運(yùn)行時(shí)等;
b、編譯器很無(wú)情,即使你加了inline,它也可能拒絕你的inline;
c、編譯器很多情,即使你沒(méi)有加inline,它也可能幫你實(shí)施inline;
d、不合理的inline會(huì)導(dǎo)致代碼臃腫。
使用內(nèi)聯(lián)時(shí),需要注意以下幾個(gè)方面的誤區(qū):
1)inline函數(shù)需顯示定義,不能僅在聲明時(shí)使用inline。類內(nèi)實(shí)現(xiàn)的成員函數(shù)是inline的。
2)通過(guò)函數(shù)指針對(duì)inline函數(shù)進(jìn)行調(diào)用時(shí),編譯器有可能不實(shí)施inline
3)編譯器可能會(huì)拒絕內(nèi)聯(lián)虛函數(shù),但可以靜態(tài)確定的虛函數(shù)對(duì)象,多數(shù)編譯器可以inline
4)inline函數(shù)有局部靜態(tài)變量時(shí),可能無(wú)法內(nèi)聯(lián)
5)直接遞歸無(wú)法inline,應(yīng)轉(zhuǎn)換成迭代或者尾遞歸。下面分別以遞歸和迭代實(shí)現(xiàn)了二分查找。
二分查找的遞歸方式實(shí)現(xiàn)。
二分查找的迭代方式實(shí)現(xiàn)。
分別調(diào)用二分查找的遞歸和迭代實(shí)現(xiàn),開(kāi)啟-O1優(yōu)化,通過(guò)查看匯編代碼和nm查看可執(zhí)行文件可執(zhí)行文件符號(hào),只看到了遞歸版本的call指令和函數(shù)名符號(hào),說(shuō)明遞歸版本沒(méi)有內(nèi)聯(lián),而迭代版本實(shí)施了內(nèi)聯(lián)展開(kāi)。
6)構(gòu)造函數(shù)和析構(gòu)函數(shù)可能無(wú)法inline,即使函數(shù)體很簡(jiǎn)單
表面上構(gòu)造函數(shù)定義為空且是inline,但編譯器實(shí)際會(huì)生成如右側(cè)的偽代碼來(lái)構(gòu)造基類成分和成員變量,從而不一定能實(shí)施inline。
5 名稱查找
C++中名稱主要分為以下幾類:
a) 受限型名稱:使用作用域運(yùn)算符(::)或成員訪問(wèn)運(yùn)算符(.和->)修飾的名稱。如:::std、std::sort、penguin.name、this->foo等。
b) 非受限型名稱:除了受限型名稱之外的名稱。如:name、foo
c) 依賴型名稱:依賴于形參的名稱。如:vector<T>::iterator
d) 非依賴型名稱:不屬于依賴型名稱的名稱。如:vector<int>::iterator
5.1 受限名稱查找
受限名稱查找是在一個(gè)受限作用域進(jìn)行的,查找作用域由限定的構(gòu)造對(duì)象決定,如果查找作用域是類,則查找范圍可以到達(dá)基類。
5.2 非受限名稱查找
5.2.1 普通查找:由內(nèi)向外逐層查找,存在繼承體系時(shí),先查找該類,然后查找基類作用域,最后才逐層查找外圍作用域
5.2.2 ADL(argument-dependent lookup)查找:又稱koenig查找,由C++標(biāo)準(zhǔn)委員會(huì)Andrew Koenig定義了該規(guī)則——如果名稱后面的括號(hào)里提供了一個(gè)或多個(gè)類類型的實(shí)參,那么在名稱查找時(shí),ADL將會(huì)查找實(shí)參關(guān)聯(lián)的類和命名空間。
根據(jù)類型C的實(shí)參c,ADL查找到C的命名空間ns,找到了foo的定義。
了解了ADL,現(xiàn)在來(lái)看個(gè)例子,下面代碼定義了一個(gè)Integer類和重載了operator<運(yùn)算符,并進(jìn)行一個(gè)序列排序。
上面的代碼輸出什么? 1 1 5 10嗎。上面的代碼無(wú)法編譯通過(guò),提示如下錯(cuò)誤
operator<明明在全局作用于有定義,為什么找不到匹配的函數(shù)?前面的代碼片段,應(yīng)用ADL在ns內(nèi)找不到自定義的operator<的定義,接著編譯器從最近的作用域std內(nèi)開(kāi)始向外查找,編譯器在std內(nèi)找到了operator<的定義,于是停止查找。定義域全局作用域的operator<被隱藏了,即名字隱藏。名字隱藏同樣可以發(fā)生在基類和子類中。好的實(shí)踐:定義一個(gè)類時(shí),應(yīng)當(dāng)將其相關(guān)的接口(包括自由函數(shù))也放入到與類相同的命名空間中。
把operator<定義移到ns命名空間后運(yùn)行結(jié)果正常
再來(lái)看一個(gè)名稱查找的例子。
這段代碼編譯時(shí)提示如下錯(cuò)誤,我們用int *實(shí)例化D1的模板參數(shù)并給m_value賦值,編譯器提示無(wú)法將int *轉(zhuǎn)換成int類型,也就是m_value被實(shí)例化成了int而不是int *。
我們將代碼改動(dòng)一下,將D2繼承B<int>改為B<T>,代碼可以順利編譯并輸出。
D1和D2唯一的區(qū)別就是D1繼承自B<int>,D2繼承自B<T>。實(shí)例化后,為何D1.m_value類型是int,而D2.m_value類型是int *。在分布式事務(wù)領(lǐng)域有二階段提交,在并發(fā)編程設(shè)計(jì)模式中二階段終止模式。在C++名稱查找中也存在一個(gè)二階段查找。
二階段查找(two-phase lookup):首次看到模板定義的時(shí)候,進(jìn)行第一次查找非依賴型名稱。當(dāng)實(shí)例化模板的時(shí)候,進(jìn)行第二次查找依賴型名稱。
D1中查找T時(shí),基類B<int>是非依賴型名稱,無(wú)需知道模板實(shí)參就確定了T的類型。
D2中查找T時(shí),基類B<T>是依賴型名稱,在實(shí)例化的時(shí)候才會(huì)進(jìn)行查找。
6 智能指針
6.1 std::auto_ptr
std::auto_ptr是C++98智能指針實(shí)現(xiàn),復(fù)制auto_ptr時(shí)會(huì)轉(zhuǎn)移所有權(quán)給目標(biāo)對(duì)象,導(dǎo)致原對(duì)象會(huì)被修改,因此不具備真正的復(fù)制語(yǔ)義,不能將其放置到標(biāo)準(zhǔn)容器中。auto_ptr在c++11中已經(jīng)被標(biāo)明棄用,在c++17中被移除。
6.2 std::shared_ptr
std::shared_ptr采用引用計(jì)數(shù)共享指針指向?qū)ο笏袡?quán)的智能指針,支持復(fù)制語(yǔ)義。每次發(fā)生復(fù)制行為時(shí)會(huì)遞增引用計(jì)數(shù),當(dāng)引用計(jì)數(shù)遞減至0時(shí)其管理的對(duì)象資源會(huì)被釋放。但shared_ptr也存在以下幾個(gè)應(yīng)用方面的陷阱。
1)勿通過(guò)傳遞裸指針構(gòu)造share_ptr
這段代碼通過(guò)一個(gè)裸指針構(gòu)造了兩個(gè)shared_ptr,這兩個(gè)shared_ptr有著各自不同的引用計(jì)數(shù),導(dǎo)致原始指針被釋放兩次,引發(fā)未定義行為。
2)勿直接將this指針構(gòu)造shared_ptr對(duì)象
這段代碼使用同一個(gè)this指針構(gòu)造了兩個(gè)沒(méi)有關(guān)系的shared_ptr,在離開(kāi)作用域時(shí)導(dǎo)致重復(fù)析構(gòu)問(wèn)題,和1)是一個(gè)道理。當(dāng)希望安全的將this指針托管到shared_ptr時(shí),目標(biāo)對(duì)象類需要繼承std::enable_shared_from_this<T>模板類并使用其成員函數(shù)shared_from_this()來(lái)獲得this指針的shared_ptr對(duì)象。如下所示:
3)請(qǐng)勿直接使用shared_ptr互相循環(huán)引用,如實(shí)在需要請(qǐng)將任意一方改為weak_ptr。
代碼運(yùn)行結(jié)果,沒(méi)有看到打印任何內(nèi)容,析構(gòu)函數(shù)沒(méi)有被調(diào)用。最終你我都沒(méi)有jump,完美的結(jié)局。但是現(xiàn)實(shí)就是這么殘酷,C++的世界不允許他們不jump,需要將其中一個(gè)shared_ptr改為weak_ptr后資源才能正常釋放。
4)優(yōu)先使用make_shared而非直接構(gòu)造shared_ptr。make_shared主要有以下幾個(gè)優(yōu)點(diǎn):
a、可以使用auto自動(dòng)類型推導(dǎo)。
shared_ptr<Object> sp(new Object());
auto sp = make_shared<Object>();
b、減少內(nèi)存管理器調(diào)用次數(shù)。shared_ptr的內(nèi)存結(jié)構(gòu)如下圖所示,包含了兩個(gè)指針:一個(gè)指向其所指的對(duì)象,一個(gè)指向控制塊內(nèi)存。
這條語(yǔ)句會(huì)調(diào)用兩次內(nèi)存管理器,一次用于創(chuàng)建Object對(duì)象,一次用于創(chuàng)建控制塊。如果使用make_shared會(huì)一次性分配內(nèi)存同時(shí)保存Object和控制塊。
c、防止內(nèi)存泄漏。
這段代碼可能發(fā)生內(nèi)存泄漏。一般情況下,這段代碼的調(diào)用順序如下:
new Handler() ① 在堆上創(chuàng)建Handler對(duì)象
shared_ptr() ②創(chuàng)建shared_ptr
getData() ③調(diào)用getData()函數(shù)
但是編譯器可能不按照上述①②③的順序來(lái)生成調(diào)用代碼。可能產(chǎn)生①③②的順序,此時(shí)如果③getData()產(chǎn)生異常,而new Handler對(duì)象指針還沒(méi)有托管到shared_ptr中,于是內(nèi)存泄漏發(fā)生。使用make_shared可以避免這個(gè)問(wèn)題。
這條語(yǔ)句在運(yùn)行期,make_shared和getData肯定有一個(gè)會(huì)先調(diào)用。如果make_shared先調(diào)用,在getData被調(diào)用前動(dòng)態(tài)分配的Hander對(duì)象已經(jīng)被安全的存儲(chǔ)在返回的shared_ptr對(duì)象中,接著即使getData產(chǎn)生了異常shared_ptr析構(gòu)函數(shù)也能正常釋放Handler指針對(duì)象。如果getData先調(diào)用并產(chǎn)生了異常,make_shared則不會(huì)被調(diào)用。
但是make_shared并不是萬(wàn)能的,如不能指定自定義刪除器,此時(shí)可以先創(chuàng)建shared_ptr對(duì)象再傳遞到函數(shù)中。
6.3 std::unique_ptr
std::unique_ptr是獨(dú)占型智能指針,僅支持移動(dòng)語(yǔ)義,不支持復(fù)制。默認(rèn)情況下,unique_ptr有著幾乎和裸指針一樣的內(nèi)存開(kāi)銷和指令開(kāi)銷,可以替代使用裸指針低開(kāi)銷的場(chǎng)景。
1)與shared_ptr不同,unique_ptr可以直接指向一個(gè)數(shù)組,因?yàn)閡nique_ptr對(duì)T[]類型進(jìn)行了特化。如果shared_ptr指向一個(gè)數(shù)組,需要顯示指定刪除器。
2)與shared_ptr不同,unique_ptr指定刪除器時(shí)需要顯示指定刪除器的類型。
7 lambda表達(dá)式
1)捕獲了變量的lambda表達(dá)式無(wú)法轉(zhuǎn)換為函數(shù)指針。
2)對(duì)于按值捕獲的變量,其值在捕獲的時(shí)候就已經(jīng)確定了(被復(fù)制到lambda閉包中)。而對(duì)于按引用捕獲的變量,其傳遞的值等于lamdba調(diào)用時(shí)的值。
3)默認(rèn)情況下,lambda無(wú)法修改按值捕獲的變量。如果需要修改,需要使用mutable顯示修飾。這其實(shí)也好理解,lambda會(huì)被編譯器轉(zhuǎn)換成operator() const的函數(shù)對(duì)象。
4)lambda無(wú)法捕捉靜態(tài)存儲(chǔ)的變量。