C++ 基础概念、语法和易错点整理
目錄
基礎(chǔ)知識(shí)
構(gòu)造函數(shù)與析構(gòu)函數(shù)
虛函數(shù)
繼承
單例模式
重載、隱藏和重寫(xiě)(覆蓋)
vector 擴(kuò)容機(jī)制應(yīng)注意的問(wèn)題
STL 迭代器
前言
快秋招了,專(zhuān)門(mén)用一篇博客整理一下 C++ 的一些基礎(chǔ)概念、語(yǔ)法和細(xì)節(jié)。一次整理不完,基本上是遇到什么問(wèn)題就添加什么,會(huì)持續(xù)更新。
?
基礎(chǔ)知識(shí)
1. 引用和指針的區(qū)別?
(1) 指針指向變量的地址,而引用是變量的別名。
(2)?sizeof 引用得到的是所指向的變量(對(duì)象)的大小,而 sizeof 指針得到的是指針本身的大小。
(3)?引用在定義的時(shí)候必須進(jìn)行初始化,并且不能改變,即不能變成另一個(gè)變量的引用。指針在定義的時(shí)候不一定要初始化,并且可以改變指向的地址。(引用不能為 NULL )
(4)?有多級(jí)指針,但是沒(méi)有多級(jí)引用。
(5) 引用訪(fǎng)問(wèn)一個(gè)變量是直接訪(fǎng)問(wèn),而指針訪(fǎng)問(wèn)一個(gè)變量是間接訪(fǎng)問(wèn)。
(6) 引用底層是通過(guò)指針實(shí)現(xiàn)的。
(7) 作為參數(shù)時(shí)不同,傳指針的實(shí)質(zhì)是傳值,傳遞的值是指針的地址;傳遞引用的實(shí)質(zhì)是傳地址,傳遞的是變量的地址。
?
2. 指針參數(shù)傳遞和引用參數(shù)傳遞
(1)?指針參數(shù)傳遞的本質(zhì)是值傳遞,它所傳遞的是一個(gè)地址值。值傳遞的特點(diǎn)是,被調(diào)用函數(shù)對(duì)形參的任何操作都是作為局部變量進(jìn)行的,不會(huì)影響主調(diào)函數(shù)的實(shí)參變量的值(形參指針的地址值變了,實(shí)參的地址值不會(huì)變)。傳遞指針參數(shù)的本質(zhì)是值傳遞,這句討論的是指針的值。在函數(shù)體中通過(guò) *p 改變指針?biāo)赶虻刂返闹?#xff0c;這個(gè)過(guò)程會(huì)影響主調(diào)函數(shù)中實(shí)參變量的值。而在函數(shù)體中使 p 指向另一個(gè)地址,這個(gè)過(guò)程不會(huì)影響主函數(shù)中相應(yīng)實(shí)參。
(2) 引用參數(shù)傳遞過(guò)程中,被調(diào)用函數(shù)的形參也作為局部變量在棧中開(kāi)辟了內(nèi)存空間,但是這時(shí)存放的是由主調(diào)函數(shù)放進(jìn)來(lái)的實(shí)參變量的地址。被調(diào)用函數(shù)對(duì)形參的任何操作都會(huì)影響主調(diào)函數(shù)中的實(shí)參變量。
?
3. 形參和實(shí)參的區(qū)別
(1) 形參變量只有在被調(diào)用時(shí)才分配內(nèi)存單元,在調(diào)用結(jié)束時(shí),即刻釋放所分配的內(nèi)存單元。因此,形參只有在函數(shù)內(nèi)部有效。
(2) 實(shí)參可以是常量、變量、表達(dá)式、函數(shù)等。無(wú)論實(shí)參是那種類(lèi)型的量,在進(jìn)行函數(shù)調(diào)用時(shí),它們都必須是確定的值,以便將這些值傳遞給形參。
值傳遞:按值傳遞參數(shù)有一個(gè)形參向棧拷貝數(shù)據(jù)的過(guò)程,如果值傳遞的對(duì)象是類(lèi)對(duì)象或是大的結(jié)構(gòu)體對(duì)象,將耗費(fèi)一定的時(shí)間和空間(棧空間很寶貴)。
指針傳遞:按指針進(jìn)行傳遞時(shí),同樣有一個(gè)形參向棧拷貝數(shù)據(jù)的過(guò)程,但是拷貝的數(shù)據(jù)是一個(gè)地址。
引用傳遞:同樣有上述的拷貝過(guò)程,但傳的是實(shí)參變量的地址。該類(lèi)型的形參的任何操作都會(huì)影響主調(diào)函數(shù)中的實(shí)參變量。
從效率上講,指針傳遞和引用傳遞比值傳遞效率高。一般使用引用傳遞,代碼邏輯上更緊湊、清晰。
?
4. 聲明和定義
當(dāng)定義一個(gè)變量的時(shí)候,就包含了對(duì)該變量聲明的過(guò)程,同時(shí)在內(nèi)存中申請(qǐng)了一塊內(nèi)存空間。如果在多個(gè)文件中使用相同的變量,為了避免重復(fù)定義,就必須將聲明和定義分離開(kāi)來(lái)。
(1) 變量的聲明和定義
從編譯原理上來(lái)說(shuō),聲明是僅僅告訴編譯器,有個(gè)類(lèi)型的變量會(huì)被使用,但編譯器并不會(huì)為它分配任何內(nèi)存,而定義會(huì)分配內(nèi)存。ps.變量的聲明和定義方式默認(rèn)是局部的。
(2) 函數(shù)的聲明和定義
聲明:一般在頭文件中,告訴編譯器有一個(gè)函數(shù)叫 asd(),讓編譯器知道這個(gè)函數(shù)的存在。
定義:一般在源文件中,具體就是函數(shù)的實(shí)現(xiàn)過(guò)程。
函數(shù)的聲明和定義方式默認(rèn)都是 extern,即函數(shù)默認(rèn)是全局的。
?
5. static 的用法和作用
靜態(tài)變量存儲(chǔ)的位置
(1) static 可以保持變量?jī)?nèi)容的持久。存儲(chǔ)在靜態(tài)數(shù)據(jù)區(qū)的變量會(huì)在程序剛開(kāi)始運(yùn)行時(shí)就完成初始化(也是唯一的一次初始化),共有兩種變量存儲(chǔ)在靜態(tài)存儲(chǔ)區(qū):全局變量和 static 變量,只不過(guò)和全局變量比起來(lái),static 可以控制變量的可見(jiàn)范圍。
(2) 函數(shù)中的 static 變量的作用范圍為該函數(shù)體內(nèi),該變量的內(nèi)存只被分配一次,因此其值在下次調(diào)用時(shí)仍然維持上次的值。
?
靜態(tài)函數(shù):在函數(shù)的返回類(lèi)型前加上 static 關(guān)鍵字。
特點(diǎn):靜態(tài)函數(shù)與普通函數(shù)不同,它只能在聲明它的文件當(dāng)中可見(jiàn),不能被其他文件可用。
?
類(lèi)中的靜態(tài)成員變量
(3)?在類(lèi)中的 static 成員變量屬于整個(gè)類(lèi)所擁有,類(lèi)的所有對(duì)象只有一份拷貝。
(4) static 成員變量必須要在類(lèi)外進(jìn)行初始化,static 修飾的變量先于對(duì)象存在,所以 static 修飾的變量要在類(lèi)外初始化。
?
類(lèi)中的靜態(tài)函數(shù)成員
(5)?靜態(tài)成員函數(shù)的多態(tài)可以通過(guò)重載來(lái)實(shí)現(xiàn)。可以通過(guò)類(lèi)名調(diào)用靜態(tài)成員函數(shù),例如:Point::output();?。
(6)?類(lèi)的對(duì)象可以使用靜態(tài)成員函數(shù),類(lèi)的非靜態(tài)成員可以調(diào)用靜態(tài)成員函數(shù),但是在類(lèi)的靜態(tài)成員函數(shù)中不能引用非靜態(tài)成員。
(7)?由于 static 修飾的類(lèi)成員屬于類(lèi),不屬于對(duì)象,因此 static 類(lèi)成員函數(shù)是沒(méi)有 this 指針的,this 指針是指向本對(duì)象的指針。因?yàn)闆](méi)有 this 指針,所以 static 類(lèi)成員函數(shù)不能訪(fǎng)問(wèn)非 static 的類(lèi)成員,只能訪(fǎng)問(wèn) static 修飾的類(lèi)成員。
(8)?函數(shù)不能同時(shí)聲明為靜態(tài)和虛函數(shù),例如:virtual?static void output(int a); 。static 成員函數(shù)不能被 virtual 修飾,static 成員不屬于任何對(duì)象或?qū)嵗?#xff0c;所以加上 virtual 沒(méi)有任何意義。
?
6. extern 的用法
(1) extern 修飾變量
如果文件 a.c 需要使用 b.c 中的一個(gè)變量 int asd,就可以在 a.c 中聲明 extern int asd; ,然后就可以使用變量 asd。
(2) extern 修飾函數(shù)
如果文件 a.c 需要使用 b.c 中的一個(gè)函數(shù)。例如在 b.c 中原型是 int asd(int a),那么就可以在 a.c 中聲明 extern int asd(int a);,然后調(diào)用 asd() 完成任務(wù)。
(3)?函數(shù)的聲明和定義方式默認(rèn)都是 extern 的,即函數(shù)默認(rèn)是全局的。
(4) extern 修飾符可用于指示 C 或者 C++ 函數(shù)的調(diào)用規(guī)范
若在 C++ 中調(diào)用 C 的庫(kù)函數(shù),就需要在 C++ 程序中用 extern "C"?聲明要引用的函數(shù)。這是給鏈接器用的,告訴鏈接器在鏈接的時(shí)候用 C 函數(shù)規(guī)范來(lái)鏈接。主要原因是 C++ 和 C 程序編譯完成后在目標(biāo)代碼中命名規(guī)則不同。
?
7. const
(1) 可以使用 const 阻止一個(gè)變量被改變。通常需要對(duì)它進(jìn)行初始化,因?yàn)橐院缶蜎](méi)有機(jī)會(huì)再去改變它了。
(2)?對(duì)指針而言,可以指定指針本身為 const,也可以指定指針?biāo)赶虻臄?shù)據(jù)為 const,或者將它們同時(shí)指定為 const。
(3) 在一個(gè)函數(shù)聲明中,const 可以修飾形參,表明它在函數(shù)內(nèi)部不可以改變它的值。
(4) 對(duì)于類(lèi)的成員函數(shù),若指定為 const 類(lèi)型,則表明它是一個(gè)常函數(shù),不能修改類(lèi)的成員變量,類(lèi)的常對(duì)象只能訪(fǎng)問(wèn)類(lèi)的常成員函數(shù)。這是因?yàn)橐粋€(gè)沒(méi)有聲明為 const 的成員函數(shù)被看作是將要修改對(duì)象中數(shù)據(jù)成員的函數(shù),而且編譯器不允許它被一個(gè) const 對(duì)象調(diào)用。因此,const 對(duì)象只能調(diào)用 const 成員函數(shù)。
(5)?同時(shí)使用 static 和 const 修飾一個(gè)函數(shù)時(shí),會(huì)報(bào) cannot have cv-qualifier 的錯(cuò)誤(在?C++ 中?cv?限定符指 const 和 volatile)。這是因?yàn)?strong> static 表示靜態(tài)的,const 表示靜態(tài)不變的。因?yàn)?const 已經(jīng)是靜態(tài)的了,所以這兩個(gè)放在一起就像你用兩個(gè) static 修飾同一個(gè)變量。
(6)?const 成員函數(shù)可以訪(fǎng)問(wèn)非 const 對(duì)象的非 const 數(shù)據(jù)成員、const 數(shù)據(jù)成員,也可以訪(fǎng)問(wèn) const 對(duì)象內(nèi)的所有數(shù)據(jù)成員。?
(7)?非 const 成員函數(shù)可以訪(fǎng)問(wèn)非 const 對(duì)象的非 const 數(shù)據(jù)成員和 const 數(shù)據(jù)成員(可以訪(fǎng)問(wèn),但是嘗試修改 const 數(shù)據(jù)成員會(huì)報(bào)錯(cuò)),但不可以訪(fǎng)問(wèn) const 對(duì)象的任意數(shù)據(jù)成員。
例:const Asd & Asd::test( const Asd &a) const 第一個(gè)const:確保返回的Asd對(duì)象在以后的使用中不能被修改。 第二個(gè)const:確保此方法不修改傳遞的參數(shù)a。 第三個(gè)const:保證此方法不修改調(diào)用它的對(duì)象,const對(duì)象只能調(diào)用const成員函數(shù),不能調(diào)用非const函數(shù)。?
8. 指針和 const 的用法
(1) 當(dāng) const 修飾指針時(shí),const 的位置不同,它修飾的對(duì)象也會(huì)有所不同。const 與指針的結(jié)合使用,有兩種情況:一是用 const 修飾指針(常指針),即修飾存儲(chǔ)在指針里的地址;二是修飾指針指向的對(duì)象(常量)。為了防止混淆使用,采用 "靠近" 原則,即 const 離哪個(gè)量近則修飾哪個(gè)量。如果 const 修飾符離變量近,則表達(dá)的意思為指向常量的指針;如果離指針近,則表示指向變量的常指針。
(2) const int * p1 或者 int const *p1。在這兩種情況下,const 離 int 近,所以修飾的是指針指向的值(常量)。不可以改變 p1 所指對(duì)象的值,但是可以讓 p1 改變所指向的對(duì)象,即指向常量的指針。
(3) int * const p2 中?const 修飾 p2 的值,所以 p2 的值不可以改變,即 p2 指向一個(gè)固定的地址(常指針)。而 p2 所指對(duì)象的值是可以改變的。
(4) const int * const p3 表示的是 p3 是一個(gè)常指針,它指向的對(duì)象的值是一個(gè)常量。
?
9. #define 與 inline 的區(qū)別
(1) #define 是關(guān)鍵字,inline 是函數(shù)。
(2) 宏定義在預(yù)處理階段進(jìn)行文本替換,inline 函數(shù)在編譯階段進(jìn)行替換。
(3) inline 函數(shù)有類(lèi)型檢查,比宏定義更安全。
?
#define 與 const 的區(qū)別
(1) const 定義的常量是帶數(shù)據(jù)類(lèi)型的,而 #define 定義只是個(gè)常數(shù)而不帶類(lèi)型。
(2) #define 只在預(yù)處理階段起作用,做簡(jiǎn)單的文本替換。而 const 在編譯、鏈接過(guò)程中起作用。
(3) #define 只是簡(jiǎn)單的字符串替換,沒(méi)有類(lèi)型檢查。而 const 是有數(shù)據(jù)類(lèi)型的。
(4) #define 預(yù)處理后,占用代碼段空間,const 占用數(shù)據(jù)段空間。
(5) const 不能重定義,而 #define 可以通過(guò) #undef 取消某個(gè)符號(hào)的定義,進(jìn)行重定義。
(6) #define 可以用來(lái)防止文件重復(fù)引用。
?
10. 野指針和懸空指針
野指針:就是指針指向的位置是不可知的(隨機(jī)的、不正確的、沒(méi)有明確限制的)。指針變量在定義時(shí)如果未初始化,其值是隨機(jī)的。或者指針變量的值是別的變量的地址,意味著指針指向了一個(gè)地址是不確定的變量,此時(shí)去解引用就是去訪(fǎng)問(wèn)了一個(gè)不確定的地址,所以結(jié)果是不可知的。
懸空指針是:一個(gè)指針的指向?qū)ο笠驯粍h除,那么就成了懸空指針。
野指針的成因:
(1)?指針變量未初始化
任何指針變量剛被創(chuàng)建時(shí)不會(huì)自動(dòng)成為 NULL 指針,它的缺省值是隨機(jī)的。所以,指針變量在創(chuàng)建的同時(shí)應(yīng)當(dāng)被初始化,要么將指針設(shè)置為 NULL,要么讓它指向合法的內(nèi)存。
(2)?指針釋放后之后未置空
有時(shí)指針在 free 或 delete 后未賦值 NULL,便會(huì)讓程序員以為是合法的。free 和 delete 只是把指針?biāo)傅膬?nèi)存給釋放掉,但并沒(méi)有對(duì)指針本身做處理。此時(shí)指針指向的就是 "垃圾" 內(nèi)存。釋放后的指針應(yīng)立即將指針置為 NULL,防止產(chǎn)生 "野指針"。
(3)?指針操作超越變量作用域。
?
11. C 語(yǔ)言 struct 和 C++ struct 區(qū)別
(1) C 語(yǔ)言中,struct 是用戶(hù)自定義數(shù)據(jù)類(lèi)型(UDT),在 C++ 中 struct 是抽象數(shù)據(jù)類(lèi)型(ADT),支持成員函數(shù)的定義,C++ 中的 struct 能繼承,能實(shí)現(xiàn)多態(tài)。
(2) C 語(yǔ)言中的 struct 是沒(méi)有權(quán)限設(shè)置的,且 struct 中只能是一些變量的集合體,可以封裝數(shù)據(jù),但是不能隱藏?cái)?shù)據(jù),而且成員不可以是函數(shù)。?
(3) C++ 中,struct 的成員默認(rèn)訪(fǎng)問(wèn)權(quán)限是 public,class 中的默認(rèn)訪(fǎng)問(wèn)權(quán)限是 private。
?
12. C/C++的編譯過(guò)程
- (1) 預(yù)處理
- 使用 -E 選項(xiàng):gcc -E hello.c?-o hello.i
- 預(yù)處理階段的過(guò)程有:頭文件展開(kāi),宏替換,條件編譯,去掉注釋等。
- (2) 編譯
- 使用 -S 選項(xiàng):gcc -S hello.c?-o hello.s
- 編譯階段的工作是通過(guò)詞法分析和語(yǔ)法分析將高級(jí)語(yǔ)言翻譯成機(jī)器語(yǔ)言,生成對(duì)應(yīng)的匯編代碼。
- (3) 匯編
- 使用 -c 選項(xiàng):gcc -c hello.c?-o hello.o
- 匯編階段將源文件翻譯成二進(jìn)制文件。
- (4) 鏈接?gcc hello.o?-o a.out
- 鏈接過(guò)程將二進(jìn)制文件與需要用到的庫(kù)鏈接。連接后便可以生成可執(zhí)行文件。
?
13. C++類(lèi)型轉(zhuǎn)換
(1) static_cast 能進(jìn)行基礎(chǔ)類(lèi)型之間的轉(zhuǎn)換。主要可以完成:
①?用于類(lèi)層次結(jié)構(gòu)中父類(lèi)和子類(lèi)之間指針或引用的轉(zhuǎn)換。進(jìn)行向上強(qiáng)制轉(zhuǎn)換是安全的(將派生類(lèi)引用或指針轉(zhuǎn)換為基類(lèi)引用或指針)。進(jìn)行向下強(qiáng)制轉(zhuǎn)換(將基類(lèi)引用或指針轉(zhuǎn)換為派生類(lèi)引用或指針),是不安全的。如果不使用顯示類(lèi)型轉(zhuǎn)換,則向下強(qiáng)制轉(zhuǎn)換是不允許的。
②?用于基本數(shù)據(jù)類(lèi)型之間的轉(zhuǎn)換,如把 int 轉(zhuǎn)換成 char,把 int 轉(zhuǎn)換成 enum。這種轉(zhuǎn)換的安全性也要開(kāi)發(fā)人員來(lái)保證。
③ 把空指針轉(zhuǎn)換成目標(biāo)類(lèi)型的空指針。
④ 把任何類(lèi)型的表達(dá)式轉(zhuǎn)換成 void 類(lèi)型。
(2)?const_cast 主要作用是:修改類(lèi)型的 const 或 volatile 屬性。使用該運(yùn)算方法可以返回一個(gè)指向非常量的指針(或引用),然后通過(guò)該指針(或引用)對(duì)它的數(shù)據(jù)成員任意改變。
需要注意的是:const_cast 不是用于去除變量的常量性,而是去除指向常數(shù)對(duì)象的指針或引用的常量性,它去除常量性的對(duì)象必須是指針或引用。
(3) reinterpret_cast 它可以把一個(gè)指針轉(zhuǎn)換成一個(gè)整數(shù),也可以把一個(gè)整數(shù)轉(zhuǎn)換成一個(gè)指針(先把一個(gè)指針轉(zhuǎn)換成一個(gè)整數(shù),再把該整數(shù)轉(zhuǎn)換成原類(lèi)型的指針,還可以得到原先的指針值)。
(4) dynamic_cast 主要用在繼承體系中的安全向下轉(zhuǎn)換。它能安全的將指向基類(lèi)的指針或引用轉(zhuǎn)換為指向子類(lèi)的指針或引用,并且能知道類(lèi)型轉(zhuǎn)換是否成功。轉(zhuǎn)換失敗會(huì)返回 NULL(轉(zhuǎn)換對(duì)象為指針)或拋出異常 bad_cast(轉(zhuǎn)型對(duì)象為引用)。dynamic_cast 會(huì)動(dòng)用運(yùn)行時(shí)信息(RTTI,Run Time Type Info)來(lái)進(jìn)行類(lèi)型安全檢查,因此 dynamic_cast 存在一定的效率損失。當(dāng)使用 dynamic_cast 時(shí),該類(lèi)型必須含有虛函數(shù),這是因?yàn)?dynamic_cast 使用了存儲(chǔ)在 VTABLE 中的信息來(lái)判斷實(shí)際的類(lèi)型。
在 C++ 中,typeid 用于返回指針或引用所指對(duì)象的實(shí)際類(lèi)型(頭文件是#include <typeinfo>)。typeid 運(yùn)算符用來(lái)獲取一個(gè)數(shù)據(jù)類(lèi)型或者表達(dá)式的類(lèi)型信息,運(yùn)行時(shí)獲知變量類(lèi)型名稱(chēng),可以使用 typeid(變量).name()。類(lèi)型信息對(duì)于編程語(yǔ)言非常重要,它描述了數(shù)據(jù)的各種屬性:
對(duì)于基本類(lèi)型(int、float 等 C++?內(nèi)置類(lèi)型)的數(shù)據(jù),類(lèi)型信息所包含的內(nèi)容比較簡(jiǎn)單,主要是指數(shù)據(jù)的類(lèi)型。
對(duì)于類(lèi)類(lèi)型的數(shù)據(jù)(也就是對(duì)象),類(lèi)型信息是指對(duì)象所屬的類(lèi)、所包含的成員、所在的繼承關(guān)系等。
?
14. C++ 模板底層原理
編譯器并不是把函數(shù)模板處理成能夠處理任何類(lèi)型的函數(shù);編譯器從函數(shù)模板通過(guò)具體類(lèi)型產(chǎn)生不同的函數(shù);編譯器會(huì)對(duì)函數(shù)模板進(jìn)行兩次編譯:①?在聲明的地方對(duì)模板代碼本身進(jìn)行編譯;②?在調(diào)用的地方對(duì)參數(shù)替換后的代碼進(jìn)行編譯。函數(shù)模板要被實(shí)例化后才能成為真正的函數(shù)。
?
15. mutable 關(guān)鍵字和 volatile 關(guān)鍵字
如果需要在 const 成員方法中修改一個(gè)成員變量的值,那么需要將這個(gè)成員變量修飾為 mutable。即用 mutable 修飾的成員變量不受 const 成員方法的限制。被?mutable 修飾的變量,將永遠(yuǎn)處于可變的狀態(tài),即使在一個(gè) const 函數(shù)中。
volatile 關(guān)鍵字是一種類(lèi)型修飾符,用它聲明的類(lèi)型變量表示可以被某些編譯器未知的因素更改,比如:操作系統(tǒng)、硬件或者其他線(xiàn)程等。遇到 volatile 關(guān)鍵字聲明的變量,編譯器對(duì)訪(fǎng)問(wèn)該變量的代碼不再進(jìn)行優(yōu)化,從而可以提供對(duì)特殊地址的穩(wěn)定訪(fǎng)問(wèn)。聲明語(yǔ)法為:int volatile number; 當(dāng)要使用 volatile 聲明的變量的值時(shí),即使之前的指令剛剛讀取過(guò)數(shù)據(jù),而且立即保存了下來(lái),系統(tǒng)還是要重新從它的內(nèi)存讀取數(shù)據(jù)。volatile 可以用在以下場(chǎng)景:
(1) 中斷服務(wù)程序中修改的供其他程序檢測(cè)的變量需要加 volatile。
(2) 多線(xiàn)程環(huán)境下各線(xiàn)程間共享的標(biāo)志應(yīng)該加 volatile。
(3) 存儲(chǔ)器映射的硬件寄存器通常也要加 volatile。
?
16. 一個(gè)函數(shù)調(diào)用另一個(gè)函數(shù)時(shí)棧的變化
esp 是堆棧(stack)指針寄存器,指向堆棧頂部。ebp 是基址指針寄存器,指向當(dāng)前堆棧底部。
(1) 調(diào)用者把被調(diào)用函數(shù)所需的參數(shù)按從右向左依次壓入棧中。
(2) 調(diào)用者使用 call 指令調(diào)用被調(diào)函數(shù),并把 call 指令的下一條指令的地址當(dāng)成返回地址壓入棧中(這個(gè)壓棧操作隱含在 call 指令中)。
(3) 被調(diào)函數(shù)會(huì)先保存調(diào)用者函數(shù)的棧底地址(push ebp),然后再保存調(diào)用者函數(shù)的棧頂?shù)刂?#xff0c;即,當(dāng)前被調(diào)用函數(shù)的棧底地址(mov ebp, esp)。
(4) 在被調(diào)函數(shù)中,從 ebp 的位置處開(kāi)始存放被調(diào)函數(shù)中的局部變量和臨時(shí)變量,并且這些變量的地址按照定義時(shí)的順序依次減小,即:先定義的變量先入棧,后定義的變量后入棧。
(5) 當(dāng)被調(diào)用的函數(shù)完成了相應(yīng)的功能后,mov esp ebp,將 ebp 的值賦給 esp,也就等于將 esp 指向 ebp,銷(xiāo)毀被調(diào)用函數(shù)的棧幀。再調(diào)用?pop ebp,ebp 出棧,將棧中保存的 main 函數(shù)的基址賦值給 ebp。
(6) 將之前保存的函數(shù)返回地址出棧,返回 main 函數(shù)繼續(xù)運(yùn)行程序。
?
17. 回調(diào)函數(shù)
回調(diào)函數(shù)就是一個(gè)通過(guò)函數(shù)指針調(diào)用的函數(shù)。如果你把函數(shù)的指針(地址)作為參數(shù)傳遞給另一個(gè)函數(shù),當(dāng)這個(gè)指針被用來(lái)調(diào)用其所指向的函數(shù)時(shí),我們就稱(chēng)這個(gè)函數(shù)是回調(diào)函數(shù)。調(diào)用者與被調(diào)用者分開(kāi),調(diào)用者不需要關(guān)心誰(shuí)被調(diào)用,調(diào)用者需要知道的,只是存在一個(gè)具有某種特定原型、某些限制條件的被調(diào)用函數(shù)。
?
?
構(gòu)造函數(shù)與析構(gòu)函數(shù)
構(gòu)造函數(shù),是一種特殊的方法。主要用來(lái)在創(chuàng)建對(duì)象時(shí)初始化對(duì)象, 即為對(duì)象成員變量賦初始值。特別的是一個(gè)類(lèi)可以有多個(gè)構(gòu)造函數(shù) ,可根據(jù)其參數(shù)個(gè)數(shù)的不同或參數(shù)類(lèi)型的不同來(lái)區(qū)分它們,即構(gòu)造函數(shù)的重載。
構(gòu)造函數(shù)初始化時(shí),可以在函數(shù)體內(nèi)進(jìn)行賦值初始化,也可以使用列表完成初始化。對(duì)于在函數(shù)體內(nèi)初始化,是在所有的數(shù)據(jù)成員被分配內(nèi)存空間后才進(jìn)行的。而列表初始化是給數(shù)據(jù)成員分配內(nèi)存空間時(shí)就進(jìn)行初始化。
析構(gòu)函數(shù)與構(gòu)造函數(shù)相反,當(dāng)對(duì)象結(jié)束其生命周期,如對(duì)象所在的函數(shù)已調(diào)用完畢時(shí),系統(tǒng)自動(dòng)執(zhí)行析構(gòu)函數(shù)。析構(gòu)函數(shù)往往用來(lái)做 "清理善后"?的工作。析構(gòu)函數(shù)沒(méi)有參數(shù),也沒(méi)有返回值,而且不能重載,每個(gè)類(lèi)中只能有一個(gè)析構(gòu)函數(shù)。
構(gòu)造函數(shù)的執(zhí)行順序和析構(gòu)函數(shù)的執(zhí)行順序:
(1) 構(gòu)造函數(shù)順序
① 基類(lèi)構(gòu)造函數(shù)。如果有多個(gè)基類(lèi),則構(gòu)造函數(shù)的調(diào)用順序是某類(lèi)在派生表中出現(xiàn)的順序,而不是它們?cè)诔蓡T初始化表中的順序。
② 成員類(lèi)對(duì)象構(gòu)造函數(shù)。如果有多個(gè)成員類(lèi)對(duì)象(如:string)則構(gòu)造函數(shù)的調(diào)用順序是對(duì)象在類(lèi)中被聲明的順序,而不是它們出現(xiàn)在成員初始化表中的順序。
③ 派生類(lèi)構(gòu)造函數(shù)。
(2) 析構(gòu)函數(shù)順序
① 調(diào)用派生類(lèi)的析構(gòu)函數(shù)。
② 調(diào)用成員類(lèi)對(duì)象的析構(gòu)函數(shù)。
③ 調(diào)用基類(lèi)的析構(gòu)函數(shù)。
?
構(gòu)造函數(shù)和析構(gòu)函數(shù)可以調(diào)用虛函數(shù)嗎?
(1) 在 C++ 中,提倡不在構(gòu)造函數(shù)和析構(gòu)函數(shù)中調(diào)用虛函數(shù)。
(2) 構(gòu)造函數(shù)和析構(gòu)函數(shù)調(diào)用虛函數(shù)時(shí)都不使用動(dòng)態(tài)聯(lián)編,如果在構(gòu)造函數(shù)或析構(gòu)函數(shù)中調(diào)用虛函數(shù),則運(yùn)行的是構(gòu)造函數(shù)或析構(gòu)函數(shù)本身所在類(lèi)的定義的版本。
(3) 因?yàn)楦割?lèi)對(duì)象會(huì)在子類(lèi)之前進(jìn)行構(gòu)造,此時(shí)子類(lèi)部分的數(shù)據(jù)成員還未初始化,因此調(diào)用子類(lèi)的虛函數(shù)是不安全的,所以 C++ 不會(huì)進(jìn)行動(dòng)態(tài)聯(lián)編。
(4) 析構(gòu)函數(shù)是用來(lái)銷(xiāo)毀一個(gè)對(duì)象的,在銷(xiāo)毀一個(gè)對(duì)象時(shí)調(diào)用析構(gòu)函數(shù)沒(méi)有任何意義。
?
?
虛函數(shù)
簡(jiǎn)單地說(shuō),那些被 virtual 關(guān)鍵字修飾的成員函數(shù),就是虛函數(shù)。虛函數(shù)的作用是實(shí)現(xiàn)多態(tài)性,多態(tài)性是將接口與實(shí)現(xiàn)進(jìn)行分離;用形象的語(yǔ)言來(lái)解釋就是實(shí)現(xiàn)一個(gè)方法,但因個(gè)體差異,而采用不同的策略。
使用基類(lèi)引用或指針指向派生類(lèi)對(duì)象時(shí),想要根據(jù)其指向的對(duì)象調(diào)用相應(yīng)的方法時(shí),就要將相應(yīng)的函數(shù)聲明為虛函數(shù)。沒(méi)有 virtual 關(guān)鍵字,程序根據(jù)引用類(lèi)型或指針類(lèi)型調(diào)用方法。virtual 關(guān)鍵字告訴程序要根據(jù)引用或指針指向的對(duì)象的類(lèi)型來(lái)調(diào)用方法。
?
靜態(tài)綁定與動(dòng)態(tài)綁定
綁定,又稱(chēng)聯(lián)編,是使一個(gè)計(jì)算機(jī)程序的不同部分彼此關(guān)聯(lián)的過(guò)程。根據(jù)進(jìn)行綁定所處階段的不同,有兩種不同的綁定方法,靜態(tài)綁定和動(dòng)態(tài)綁定。
(1) 靜態(tài)綁定在編譯階段完成,所有綁定過(guò)程都在程序開(kāi)始之前完成。靜態(tài)綁定具有執(zhí)行速度快的特點(diǎn),因?yàn)樵诔绦蜻\(yùn)行前,編譯程序能夠進(jìn)行代碼優(yōu)化。函數(shù)重載(包括成員函數(shù)重載和派生類(lèi)對(duì)基類(lèi)函數(shù)的重載)就是靜態(tài)綁定。靜態(tài)綁定對(duì)函數(shù)的選擇是基于指向?qū)ο蟮闹羔樆蛞玫念?lèi)型,而與指針或引用實(shí)際指向的對(duì)象無(wú)關(guān),這也是靜態(tài)綁定的限定性。
(2)?動(dòng)態(tài)綁定是在程序運(yùn)行時(shí)動(dòng)態(tài)地進(jìn)行。如果編譯器在編譯階段不確切地知道把發(fā)送到對(duì)象的消息和實(shí)現(xiàn)消息的哪段代碼具體聯(lián)系到一起,而是運(yùn)行時(shí)才把函數(shù)調(diào)用與函數(shù)具體聯(lián)系在一起,就稱(chēng)作動(dòng)態(tài)綁定(這虛函數(shù)的實(shí)現(xiàn))。相對(duì)于靜態(tài)綁定,動(dòng)態(tài)綁定是在編譯后綁定,也稱(chēng)晚綁定,又稱(chēng)運(yùn)行時(shí)識(shí)別。動(dòng)態(tài)綁定具有靈活性好、更高級(jí)、更自然的問(wèn)題抽象、易于擴(kuò)充和易于維護(hù)等特點(diǎn)。通過(guò)動(dòng)態(tài)綁定,可以動(dòng)態(tài)地根據(jù)指針或引用指向的對(duì)象實(shí)際類(lèi)型來(lái)選擇調(diào)用的函數(shù)。
?
構(gòu)造函數(shù)為什么不能為虛函數(shù)
虛函數(shù)用于實(shí)現(xiàn)運(yùn)行時(shí)的多態(tài),需要使用到 vtable 虛函數(shù)表(是在編譯期間創(chuàng)建的)。在調(diào)用相應(yīng)虛函數(shù)時(shí),會(huì)使用到指向 vtable 的指針 vptr(創(chuàng)建對(duì)象的時(shí)候創(chuàng)建 vptr),而這個(gè)指針存儲(chǔ)在類(lèi)對(duì)象的內(nèi)存空間中。如果構(gòu)造函數(shù)是虛函數(shù),就需要使用 vtable 來(lái)調(diào)用構(gòu)造函數(shù)。但是,此時(shí)對(duì)象還沒(méi)有實(shí)例化,沒(méi)有虛函數(shù)指針,所以找不到 vtable。所以構(gòu)造函數(shù)不能是虛函數(shù)。
虛析構(gòu)函數(shù)是為了防止內(nèi)存泄露。具體地說(shuō),如果派生類(lèi)中申請(qǐng)了內(nèi)存空間,并在其析構(gòu)函數(shù)中對(duì)這些內(nèi)存空間進(jìn)行釋放。假設(shè)基類(lèi)中采用的是非虛析構(gòu)函數(shù),當(dāng)刪除基類(lèi)指針指向的派生類(lèi)對(duì)象時(shí),不會(huì)觸發(fā)動(dòng)態(tài)綁定,所以只會(huì)調(diào)用基類(lèi)的析構(gòu)函數(shù),而不會(huì)調(diào)用派生類(lèi)的析構(gòu)函數(shù)。在這種情況下,派生類(lèi)中申請(qǐng)的空間得不到釋放就會(huì)產(chǎn)生內(nèi)存泄露。
?
哪些函數(shù)不能是虛函數(shù)
(1) 構(gòu)造函數(shù):當(dāng)有虛函數(shù)時(shí),每個(gè)類(lèi)都有一個(gè)虛函數(shù)表,每一個(gè)對(duì)象都有一個(gè)虛函數(shù)表指針,而虛函數(shù)表指針是在構(gòu)造函數(shù)中初始化的。
(2) 靜態(tài)成員函數(shù):靜態(tài)函數(shù)不屬于對(duì)象而是屬于類(lèi),靜態(tài)成員函數(shù)沒(méi)有this指針,因此靜態(tài)函數(shù)設(shè)置為虛函數(shù)沒(méi)有任何意義。
(3) 友元函數(shù):友元函數(shù)不屬于類(lèi)的成員函數(shù),不能被繼承,也不需要表現(xiàn)出多態(tài)性。因此,友元函數(shù)不能是虛函數(shù)。
(4) 普通函數(shù):普通函數(shù)不屬于類(lèi)的成員函數(shù),不能被繼承,也不需要表現(xiàn)出多態(tài)性。因此,普通函數(shù)不能是虛函數(shù)。
?
?
繼承
若邏輯上 B 是 A 的一種,則應(yīng)該使用繼承。繼承建立一種 is-a 關(guān)系(is-a-kind-of 關(guān)系)。例如:可以從水果類(lèi)中派生出榴蓮類(lèi),榴蓮類(lèi)有自己的特性(外殼帶刺、果肉綿軟、有核等),但是其他的水果并沒(méi)有。派生類(lèi)對(duì)象也是一個(gè)基類(lèi)對(duì)象,可以對(duì)基類(lèi)對(duì)象執(zhí)行的操作,也可以對(duì)派生類(lèi)對(duì)象執(zhí)行。
若邏輯上 B 是 A 的一部分,則應(yīng)該使用組合而不是繼承,組合建立一種 has-a 關(guān)系。例如:眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是頭(Head)的一部分,所以類(lèi) Head 應(yīng)該由類(lèi) Eye、Nose、Mouth、Ear 組合而成,而不是派生。
private 和 protected 之間的區(qū)別只有在基類(lèi)派生的類(lèi)中才會(huì)表現(xiàn)出來(lái)。派生類(lèi)的成員可以直接訪(fǎng)問(wèn)基類(lèi)的保護(hù)成員,但不能直接訪(fǎng)問(wèn)基類(lèi)的私有成員。對(duì)外,保護(hù)成員的行為與私有成員相似。對(duì)外,保護(hù)成員的行為與公有成員相似。
?
多繼承的優(yōu)缺點(diǎn)
比如有三個(gè)類(lèi),人類(lèi)-士兵類(lèi)-步兵類(lèi),三個(gè)依次繼承,這樣的繼承稱(chēng)為單繼承。
class?Person?{}; class Soldier :public Person {}; class Infantryman :public Soldier {};多繼承是如果一個(gè)類(lèi)有多個(gè)基類(lèi),比如農(nóng)民工類(lèi)繼承了農(nóng)民類(lèi)和工人類(lèi)。多繼承會(huì)出現(xiàn)菱形繼承的情況。
class?Worker?{}; class Farmer {}; class MigrantWorker:public Worker,public Farmer {};多繼承的有點(diǎn)很明顯,就是派生類(lèi)對(duì)象可以調(diào)用多個(gè)基類(lèi)中的接口。缺點(diǎn)是當(dāng)兩個(gè)或多個(gè)基類(lèi)中有同名的成員時(shí),如果直接訪(fǎng)問(wèn)該成員,就會(huì)產(chǎn)生命名沖突,編譯器不知道使用哪個(gè)基類(lèi)的成員。這個(gè)時(shí)候需要在成員名字前面加上類(lèi)名和域解析符?::,以顯式地指明到底使用哪個(gè)類(lèi)的成員,消除二義性。
若類(lèi) B、C 繼承了類(lèi) A,同時(shí)類(lèi) D 繼承類(lèi) B 和類(lèi) C。此時(shí),就出現(xiàn)了菱形繼承的問(wèn)題。在實(shí)例化 D 的時(shí)候,就會(huì)繼承兩個(gè) A 的成員,造成數(shù)據(jù)的冗余,為了解決這個(gè)問(wèn)題,引入了虛繼承的方式,即:如果 B 和 C 是虛繼承 A 的話(huà),那么實(shí)例化 D 以后,D 中只有一份 A 的數(shù)據(jù)成員,不會(huì)產(chǎn)生冗余數(shù)據(jù)。
class B:virtual public A// 虛基類(lèi) {}; class C:virtual public A?{}; class D:public B,public C?{};?
在成員函數(shù)中調(diào)用 delete this會(huì)出現(xiàn)什么問(wèn)題?
(1) 在類(lèi)對(duì)象的內(nèi)存空間中,只有數(shù)據(jù)成員和虛函數(shù)表指針,并不包含代碼內(nèi)容,類(lèi)的成員函數(shù)單獨(dú)放在代碼段中。在調(diào)用成員函數(shù)時(shí),隱含傳遞一個(gè) this 指針,讓成員函數(shù)知道當(dāng)前是哪個(gè)對(duì)象在調(diào)用它。當(dāng)調(diào)用 delete this 時(shí),類(lèi)對(duì)象的內(nèi)存空間被釋放。在 delete this 之后進(jìn)行的任何操作,只要不涉及到 this 指針的內(nèi)容,都能正常運(yùn)行。一旦涉及到 this 指針,如操作數(shù)據(jù)成員,調(diào)用虛函數(shù)等,就會(huì)產(chǎn)生不可預(yù)期的結(jié)果。
(2) delete this 之后釋放了類(lèi)對(duì)象的內(nèi)存空間,這段內(nèi)存應(yīng)該已經(jīng)還給系統(tǒng),不再屬于這個(gè)進(jìn)程。從邏輯上看,應(yīng)該發(fā)生指針錯(cuò)誤,無(wú)訪(fǎng)問(wèn)權(quán)限之類(lèi)的問(wèn)題。但這個(gè)問(wèn)題涉及到操作系統(tǒng)的內(nèi)存管理策略。delete this 釋放了類(lèi)對(duì)象的內(nèi)存空間,但是內(nèi)存空間并沒(méi)有被系統(tǒng)收回。此時(shí)這段內(nèi)存是可以訪(fǎng)問(wèn)的,但是其中的值卻是不確定的。當(dāng)你獲取數(shù)據(jù)成員時(shí),可能得到的是一串很長(zhǎng)的未初始化的隨機(jī)數(shù);訪(fǎng)問(wèn)虛函數(shù)表,指針無(wú)效的可能性非常高,容易造成系統(tǒng)崩潰。
(3) 如果在類(lèi)的析構(gòu)函數(shù)中調(diào)用 delete this 會(huì)導(dǎo)致堆棧溢出。原因很簡(jiǎn)單,delete 的本質(zhì)是 "為將要被釋放的內(nèi)存調(diào)用一個(gè)或多個(gè)析構(gòu)函數(shù),然后再釋放內(nèi)存"。delete this 會(huì)去調(diào)用本對(duì)象的析構(gòu)函數(shù),而析構(gòu)函數(shù)中又調(diào)用 delete this,形成無(wú)限遞歸,造成堆棧溢出,系統(tǒng)崩潰。
?
this 指針調(diào)用成員函數(shù)
當(dāng)在類(lèi)的非靜態(tài)成員函數(shù)訪(fǎng)問(wèn)類(lèi)的非靜態(tài)成員時(shí),編譯器會(huì)自動(dòng)將對(duì)象的地址作為隱含參數(shù)傳遞給函數(shù),這個(gè)隱含參數(shù)就是 this 指針。即使程序員沒(méi)有寫(xiě) this 指針,編譯器在鏈接的時(shí)候也會(huì)加上 this 的,對(duì)各成員的訪(fǎng)問(wèn)都是通過(guò) this 的。 調(diào)用過(guò)程中,this 指針首先入棧,然后成員函數(shù)的參數(shù)從右向左入棧,最后函數(shù)返回地址入棧。
?
?
單例模式
單例模式(Singleton Pattern)是最簡(jiǎn)單的設(shè)計(jì)模式之一。這種類(lèi)型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式。這種模式涉及到一個(gè)單一的類(lèi),該類(lèi)負(fù)責(zé)創(chuàng)建自己的對(duì)象,同時(shí)確保只有單個(gè)對(duì)象被創(chuàng)建。這個(gè)類(lèi)提供了一種訪(fǎng)問(wèn)其唯一的對(duì)象的方式,可以直接訪(fǎng)問(wèn),不需要實(shí)例化該類(lèi)的對(duì)象。
#include <iostream> using namespace std;class Singleton {private:static Singleton *p;Singleton(){cout << "構(gòu)造" <<endl;}~Singleton(){cout << "析構(gòu)" <<endl;}Singleton(const Singleton&);Singleton operator= (const Singleton&); //聲明但是不實(shí)現(xiàn)public:int a, b;static Singleton *get(){static Singleton s;return &s;}void print(){cout << a << " " << b <<endl; } };Singleton* Singleton::p = NULL;int main() {Singleton *s1 = Singleton::get();cout << "Address:" << s1 <<endl;s1->print();s1->a = 1;s1->b = 2;s1->print();Singleton *s2 = Singleton::get();cout << "Address:" << s2 <<endl;s2->print(); s2->a = 2;s2->print();return 0; }?
?
重載、隱藏和重寫(xiě)(覆蓋)
重載 Overload:函數(shù)名字相同,特征標(biāo)(形參)不同。返回類(lèi)型可以相同也可以不同,但只有返回值不同不能達(dá)到重載的目的,且會(huì)報(bào)錯(cuò)。函數(shù)必須在同一個(gè)域(類(lèi))中。
隱藏 Hide:派生類(lèi)中的函數(shù)會(huì)隱藏父類(lèi)中所有的同名函數(shù)。
重寫(xiě) Override:子類(lèi)對(duì)父類(lèi)函數(shù)的重新編寫(xiě),返回值和特征標(biāo)(形參)都不能變,只能改變內(nèi)部代碼。被 override 的函數(shù)必須在父類(lèi)中,且為 virtual。
?
重載
#include <iostream> using namespace std; //重載 class Asd {private:int a;public:Asd(int b):a(b){}void test(){cout << "Asd test \n";}void test(int a){cout << "Asd test [int] \n";}int test(int a){cout << "Asd test int \n"; return 1;}void test(double a){cout << "Asd test [double] \n";}virtual int geta() {return a;}virtual int seta(int b) { a = b; } };int main() {Asd aa(1);aa.test();aa.test(1);aa.test(1.2);return 0; }正如前面描述一樣,只有返回值不同不能達(dá)到重載的目的,且會(huì)報(bào)錯(cuò)。注釋掉相應(yīng)代碼就可以正常運(yùn)行了。
?
隱藏
#include <iostream> using namespace std; //隱藏 class Asd {private:int a;public:Asd(int b):a(b){}void test(){cout << "Asd test \n";}void test(int a){cout << "Asd test [int] \n";}void test(double a){cout << "Asd test [double] \n";}virtual int geta() {return a;}int seta(int b) { a = b; }int seta(char *a) { cout << a <<endl;} };class Bsd:public Asd {public:Bsd(int a):Asd(a){}void test(){cout << "Bsd test\n";}int geta() final {return Asd::geta();} };int main() {Bsd aa(1);char a[] = "Output sting";aa.seta(3);aa.seta(a);aa.test();aa.test(1);//aa.test(1.2);cout << aa.geta() <<endl;return 0; }派生類(lèi) Bsd 公有繼承 Asd,同時(shí)聲明了一個(gè)父類(lèi)中包含的函數(shù) test()。此時(shí),在派生類(lèi)中的函數(shù)會(huì)隱藏父類(lèi)中所有的同名函數(shù)。如果調(diào)用別父類(lèi)中的 test() 函數(shù)也會(huì)報(bào)錯(cuò)。
?
重寫(xiě)(覆蓋)
管理虛方法:override 和 final
在 C++11 之后,可使用虛說(shuō)明符 override 指出你要覆蓋的一個(gè)虛函數(shù):將其放在參數(shù)列表后面。如果聲明于基類(lèi)方法不匹配,編譯器將視為錯(cuò)誤。說(shuō)明符 final 解決了另一個(gè)問(wèn)題。你可能想禁止派生類(lèi)覆蓋特定的虛方法,為此可在參數(shù)列表后面加上 final。
虛方法對(duì)實(shí)現(xiàn)多態(tài)類(lèi)層次結(jié)構(gòu)很重要,讓基類(lèi)引用或指針能夠根據(jù)指向的對(duì)象類(lèi)型調(diào)用相應(yīng)的方法,但虛方法也帶來(lái)了一些編程陷阱。例如,假設(shè)基類(lèi)聲明了一個(gè)虛方法,而你決定在派生類(lèi)中提供不同的版本,這將覆蓋舊版本。如果特征標(biāo)不匹配且沒(méi)有使用 override 進(jìn)行說(shuō)明,將隱藏而不是覆蓋舊版本。
#include <iostream> using namespace std;class Asd {private:int a;public:Asd(int b):a(b){}virtual void test(){cout << "Asd test\n";}void test(int a){cout << "Asd test int \n";}void test(double a){cout << "Asd test float " << a <<"\n";}virtual int geta() {return a;}virtual int seta(int b) { a = b; }int seta(char *a) { cout << a <<endl;} };class Bsd:public Asd {public:Bsd(int a):Asd(a){}//void test(int a){cout << "Bsd test\n";}void test(int a) {cout << "Bsd test\n";}int geta() final {return Asd::geta();}//int seta(int b) final {Asd::seta(b);}//int seta(char *a) override { cout << a <<endl;} };int main() {Asd asd(1);asd.test();asd.test(1);Bsd aa(1);char a[] = "Output sting";aa.seta(1);aa.seta(a);cout << aa.geta() <<endl;aa.test(1);return 0; }總結(jié):
(1) 對(duì)父類(lèi)不聲明為 virtual 成員函數(shù)添加 final 和 override 會(huì)報(bào)錯(cuò)。
(2) 對(duì)一個(gè)父類(lèi)成員函數(shù)進(jìn)行重寫(xiě)后,父類(lèi)中重載的同名函數(shù)不能使用。
(3) final 禁止派生類(lèi)重寫(xiě)其虛方法。
(4) override 指出一個(gè)要重寫(xiě),如果特征標(biāo)與基類(lèi)不匹配,編譯器將視為錯(cuò)誤。
在 C++ 中 override 和 final 是說(shuō)明符而不是關(guān)鍵字,它們是具有特殊含義的標(biāo)識(shí)符。這意味著編譯器根據(jù)上下文確定它們是否有特殊含義;在其他上下文中,可將它們用作常規(guī)標(biāo)識(shí)符,如變量名或枚舉。
標(biāo)識(shí)符
標(biāo)識(shí)符是用來(lái)標(biāo)識(shí)變量、函數(shù)、類(lèi)、模塊,或任何其他用戶(hù)自定義項(xiàng)目的名稱(chēng),用它來(lái)命名程序正文中的一些實(shí)體,比如函數(shù)名、變量名、類(lèi)名、對(duì)象名等。
關(guān)鍵字
關(guān)鍵字就是預(yù)先定義好的標(biāo)識(shí)符,C++?編譯器對(duì)其進(jìn)行特殊處理。關(guān)鍵字又稱(chēng)為保留字,這些保留字不能作為常量名、變量名或其他標(biāo)識(shí)符名稱(chēng)。
?
?
vector 擴(kuò)容機(jī)制應(yīng)注意的問(wèn)題
問(wèn)題1:當(dāng) vector 中的容量是 10 時(shí),已經(jīng)插入了 9 個(gè)元素了,再插入一個(gè)元素會(huì)不會(huì)引起擴(kuò)容?
問(wèn)題2:當(dāng) vector 中有備用空間時(shí),能不能引起擴(kuò)容?
下面是一段簡(jiǎn)單的代碼,就是在 push_back() 插入的過(guò)程中調(diào)用 capacity() 函數(shù),它的功能是返回容器當(dāng)前已分配空間的元素的個(gè)數(shù)。
#include <iostream> #include <vector> using namespace std;int main() {vector<int> asd;cout << asd.capacity() <<endl;for(int i = 0; i < 20; i++) { asd.push_back(i);cout << asd.capacity() << " ?";}cout << endl;return 0; }可以看到,當(dāng)插入一個(gè)元素的時(shí)候,只分配一個(gè)空間(引發(fā)擴(kuò)容)。插入第二個(gè)元素的時(shí)候,分配的空間為 2(引發(fā)擴(kuò)容)。插入第三個(gè)元素的時(shí)候,已分配空間的元素的個(gè)數(shù)為 4(引發(fā)擴(kuò)容),那么就會(huì)有一個(gè)備用空間。所以插入第 4 個(gè)元素是不會(huì)引起擴(kuò)容的。同理,當(dāng) vector 內(nèi)已分配的空間容量為 N 時(shí),插入第 N 個(gè)元素時(shí),是不會(huì)引發(fā)擴(kuò)容的。
當(dāng)使用 push_back() 將元素插入 vector 的尾端時(shí),該函數(shù)首先檢查是否還有備用空間,如果有就直接在備用空間上構(gòu)造元素,并調(diào)整迭代器 finish,使 vector 變大。如果沒(méi)有備用空間了,就擴(kuò)充空間(重新配置、移動(dòng)數(shù)據(jù)、釋放原空間)。以下代碼片段源自《STL源碼剖析》。
void push_back(const T& x){if(finish != end_of_storage) {construct(finish, x);++finish;}elseinser_aux(end(), x); }template <class T, class Alloc = alloc> void vector<T, Alloc>::insert_aux(iterator position, const T& x) {if(finish != end_of_storage) {construct(finish, *(finish-1));++finish;T x_copy = x;copy_backward(position, finish-2, finish-1);*position = x_copy;}else {const size_type old_size = size();const size_type len = old_size != 0 ? 2*old_size : 1;//以上配置原則:如果原大小為0,則配置1;如果原大小不為0,則配置原大小的兩倍。iterator new_start = data_allocator::allocate(len);iterator new_finish = new_start;try {//將原vector的內(nèi)容拷貝到新vectornew_finish = uninitialized_copy(start, position, new_start);construct(new_finish, x);++new_finish;//將安插點(diǎn)的原內(nèi)容也拷貝過(guò)來(lái)new_finish = uninitialized_copy(position, finish, new_finish);}catch(...) {destroy(new_start, new_finish);data_allocator::deallocate(new_start, len);throw;}//析構(gòu)并釋放原vectordestroy(begin(), end());deallocate();//調(diào)整迭代器,指向新vectorstart = new_start;finish = new_finish;end_of_storage = new_start + len;} }按上述源碼的配置原則:如果原大小為 0,則配置 1;如果原大小不為 0,則配置原大小的兩倍。對(duì)于第一個(gè)問(wèn)題而言,若是?vector 中的容量是 10,已經(jīng)插入了 9 個(gè)元素了,再插入一個(gè)元素是不會(huì)引發(fā)擴(kuò)容的,這是這個(gè)問(wèn)題的標(biāo)準(zhǔn)答案。不過(guò)面試官的問(wèn)題是容量是 10,與上面代碼中一直使用 push_back() 函數(shù)出現(xiàn)的容量不一樣,這就引出來(lái)另一個(gè)問(wèn)題:什么情況下會(huì)引起擴(kuò)容的?old_size?的變化?
?
改變vector容量的情況
void?shrink_to_fit();shrink_to_fit()?請(qǐng)求刪除未使用的容量,將?capacity()?減小為?size()。size() 函數(shù)返回目前 vector 使用的空間,capacity() 函數(shù)返回的是目前已分配空間。
#include <iostream> #include <vector> using namespace std;int main() {vector<int> asd;cout << asd.capacity() <<endl;for(int i = 0; i < 20; i++) {asd.push_back(i);cout << asd.capacity() << " ?";if(i == 4)asd.shrink_to_fit();}cout << endl;return 0; }可以看到,插入了第 5 個(gè)元素后,容量擴(kuò)充到了8,調(diào)用 shrink_to_fit() 后實(shí)際空間變成了 5。所以在插入下一個(gè)元素的時(shí)候,立馬引發(fā)了擴(kuò)容,將空間擴(kuò)展到了 10。
?
void?reserve(?size_type new_cap?);reserve()?將 vector 的容量增加到大于或等于的值?new_cap。
如果 new_cap 大于當(dāng)前的?capacity(),則分配新的存儲(chǔ),否則該方法不執(zhí)行任何操作。reserve()?不會(huì)更改?vector?的元素個(gè)數(shù),如果 new_cap 大于?capacity(),則所有迭代器(包括過(guò)去的迭代器)以及對(duì)元素的所有引用都將無(wú)效。否則,沒(méi)有迭代器或引用無(wú)效。
#include <iostream> #include <vector> using namespace std;int main() {vector<int> asd;cout << asd.capacity() <<endl;for(int i = 0; i < 20; i++) {asd.push_back(i);cout << asd.capacity() << " ?";if(i == 3)asd.reserve(7);}cout << endl;return 0; }以上代碼在插入 4 個(gè)元素后,調(diào)用?reserve()?函數(shù)調(diào)整了?vector 容量。根據(jù) vector 底層源碼可以知道,這個(gè)調(diào)整的容量就是?old_size,在下次進(jìn)行擴(kuò)充的時(shí)候,就會(huì)擴(kuò)展成?2*old_size。當(dāng)?vector 還有備用空間時(shí),可以調(diào)用?reserve()?函數(shù)完成擴(kuò)容(第二個(gè)問(wèn)題的答案)。
?
?
STL 迭代器
迭代器是一種抽象的設(shè)計(jì)理念,通過(guò)迭代器可以在不了解容器內(nèi)部原理的情況下遍歷容器。除此之外,STL 中迭代器最重要的作用是作為容器和 STL 算法的粘合劑。
迭代器提供了一個(gè)遍歷容器內(nèi)部所有元素的接口,因此迭代器內(nèi)部必須保存一個(gè)與容器相關(guān)聯(lián)的指針,然后重載各種運(yùn)算操作來(lái)遍歷,其中最重要的是 * 運(yùn)算符與 -> 運(yùn)算符,以及 ++、--? 等可能需要重載的運(yùn)算符。
unordered_map 容器實(shí)現(xiàn)了++,但是沒(méi)有實(shí)現(xiàn) --。例如下述代碼:
auto it = find_if(nums1.begin(), nums1.end(), [](auto &a){return a.second == 1;}); cout << (*(--it)).first << " " << (*(it)).second<<endl;會(huì)報(bào)以下錯(cuò)誤:
main.cpp:18:13: error: no match for ‘operator--’ (operand type is ‘std::__detail::_Node_iterator<std::pair<const int, int>, false, false>’)?
vector 的下標(biāo)運(yùn)算符和 map 的下標(biāo)運(yùn)算符
通過(guò)下標(biāo)訪(fǎng)問(wèn) vector 中的元素時(shí)不會(huì)做邊界檢查,即使越界了,程序也不會(huì)報(bào)錯(cuò)(前提是越界)。通過(guò)使用 at 函數(shù)不但可以通過(guò)下標(biāo)訪(fǎng)問(wèn) vector 中的元素,而且在 at 函數(shù)內(nèi)部會(huì)對(duì)下標(biāo)進(jìn)行邊界檢查。
map 的下標(biāo)運(yùn)算符 [ ] 的作用是:將 key 作為下標(biāo)去執(zhí)行查找,并返回相應(yīng)的值;如果不存在這個(gè) key,就將一個(gè)具有該 key 和 value 的默認(rèn)值插入這個(gè) map 中。
map 的 find 函數(shù):用 key 進(jìn)行查找,找到了返回相應(yīng)位置的迭代器;如果不存在該 key,則返回 end 迭代器。
總結(jié)
以上是生活随笔為你收集整理的C++ 基础概念、语法和易错点整理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 计算机网络:子网划分、子网掩码、CIDR
- 下一篇: C++ 20新特性