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