目錄
-
關鍵字與操作符
static
const
#define
typedef
using -
指針與引用
引用與指針的區別和聯系
為什么傳引用比傳指針安全?
野指針
智能指針 -
類
空類默認成員函數
友元函數和友元類 -
多態與虛函數
C++多態性實現原理
動態綁定
虛函數表和虛函數指針
純虛函數
override和final
回避虛函數機制
不能聲明為虛函數的成員函數
override和overwrite的區別
關鍵字與操作符
該部分內容包括:
- static
- const
- #define
- typedef
- using
指針與引用
該部分內容包括:
- 引用與指針的區別和聯系
- 為什么傳引用比傳指針安全?
- 野指針
- 智能指針
引用與指針的區別和聯系
- 指針是實體,會為其分配內存,且可以允許多級指針
- 引用創建時必須初始化且不可變(只能初始化一次故不用const),指針創建時無須初始化但最好初始化以防止NULL
- 二者自增(++)結果不同,引用是值進行自增,而指針是地址進行自增;
- sizeof 結果不同,sizeof 引用得到的是所指向的變量(對象)的大小,而sizeof 指針得到的是指針本身的大小
- 引用訪問是直接訪問原對象,指針則是間接訪問
- 作為參數給函數傳參不同,引用傳參會比指針傳參更安全,原因見下一問
- 聯系
- 引用的內部使用指針實現的
- 引用是受了限制的指針
為什么傳引用比傳指針安全?
- 引用在創建的同時必須初始化,保證引用的對象是有效的,所以不存在NULL引用;而指針在定義的時候不必初始化,所以,指針則可以是NULL,可以在定義后面的任何地方重新賦值
- 引用一旦被初始化為指向一個對象,它就不能被改變為另一個對象的引用;而指針在任何時候都可以改變為指向另一個對象
- 引用的創建和銷毀並不會調用類的拷貝構造函數
因為不存在空引用,並且引用一旦被初始化為指向一個對象,它就不能被改變為另一個對象的引用,所以比指針安全。
由於 const 指針仍然存在空指針,並且有可能產生野指針,所以還是不安全。解決方案是智能指針。
野指針
野指針不是NULL指針,是未初始化或者未清零的指針,它指向的內存地址不是程序員所期望的,可能指向了受限的內存。
成因:
1)指針變量沒有被初始化
2)指針指向的內存被釋放了,但是指針沒有置NULL
3)指針超過了變量的作用范圍,比如b[10],指針b+11
介紹一下智能指針
智能指針,將基本類型指針封裝為類對象指針(這個類肯定是個模板,以適應不同基本類型的需求),並在析構函數里編寫delete語句刪除指針指向的內存空間。
智能指針就是一種棧上創建的對象,函數退出時會調用其析構函數,這個析構函數里面往往就是一堆計數之類的條件判斷,如果達到某個條件,就把真正指針指向的空間給釋放了。
使用普通指針,容易造成堆內存泄露(忘記釋放),二次釋放,野指針,程序發生異常時內存泄露等問題等,使用智能指針能更好的管理堆內存。
注意:不能將指針直接賦值給一個智能指針,一個是類,一個是指針。
常用的智能指針
智能指針在C++11版本之后提供,包含在頭文件
在C++98中,有
std::auto_ptr
,但它有很多問題。 不支持復制(拷貝構造函數)和賦值(operator =),但復制或賦值的時候不會提示出錯。所以可能會造成程序崩潰。
auto_ptr<string> p1(new string ("auto") );//#1
auto_ptr<string> p2; //#2
p2 = p1; //#3
在語句#3中,p2接管string對象的所有權后,p1的所有權將被剝奪,可防止p1和p2的析構函數試圖刪同—個對象;
如果再訪問p1指向的內容則會導致程序崩潰,因為p1不再指向有效的數據。std::auto_ptr
被unique_ptr代替。
- unique_ptr
某個時刻只能有一個unique_ptr指向一個給定的對象。
不支持復制和賦值,但比auto_ptr好,直接賦值會編譯出錯。實在想賦值的話,需要使用std::move
。 - shared_ptr
基於引用計數的智能指針,提供所有權共享的智能指針,在最后一個指向共享對象的shared_ptr被銷毀時釋放共享對象。 - weak_ptr
用來解決shared_ptr循環引用的問題,見參考4。
弱智能指針對象,它不控制所指向對象生存期的智能指針,它指向由一個shared_ptr管理的智能指針。
將一個weak_ptr綁定到一個shared_ptr對象,不會改變shared_ptr的引用計數。
一旦最后一個所指向對象的shared_ptr被銷毀,所指向的對象就會被釋放,即使此時有weak_ptr指向該對象,所指向的對象依然被釋放。
類
該部分內容包括:
- 空類默認成員函數
- 友元函數和友元類
空類缺省有哪些成員函數
- 構造函數
- 析構函數
- 拷貝構造函數
- 賦值函數(operator=)
友元函數和友元類
友元提供了不同類的成員函數之間、類的成員函數和一般函數之間進行數據共享的機制。
通過友元,一個不同函數或者另一個類中的成員函數可以訪問類中的私有成員和保護成員。
友元的正確使用能提高程序的運行效率,但同時也破壞了類的封裝性和數據的隱藏性,導致程序可維護性變差。
詳情見參考5
- 友元函數
友元函數是可以訪問類的私有成員的非成員函數。它是定義在類外的普通函數,不屬於任何類,但是需要在類的定義中加以聲明。
friend 類型 函數名(形式參數);
一個函數可以是多個類的友元函數,只需要在各個類中分別聲明。
- 友元類
友元類的所有成員函數都是另一個類的友元函數,都可以訪問另一個類中的隱藏信息(包括私有成員和保護成員)。
friend class 類名;
使用友元類時注意:
(1) 友元關系不能被繼承。
(2) 友元關系是單向的,不具有交換性。若類B是類A的友元,類A不一定是類B的友元,要看在類中是否有相應的聲明。
(3) 友元關系不具有傳遞性。若類B是類A的友元,類C是B的友元,類C不一定是類A的友元,同樣要看類中是否有相應的申明
多態與虛函數
多態是“一個接口,多種實現”,通過派生類重寫父類的虛函數,實現了接口的重用。
多態包括編譯時多態和運行時多態,編譯時多態體現在運算符重載上,運行時多態是通過繼承和虛函數來體現的。
類的繼承中使用虛函數來重寫(override)基類中的函數。
在實際開發時,一般面向接口開發,操作父類指針即可。運行時父類指針指向子類對象並可訪問子類同名函數,這時父類的虛函數就提供了接口。
該部分內容包括:
- C++多態性實現原理
- 動態綁定
- 純虛函數
- override和final
- 回避虛函數機制
- 不能聲明為虛函數的成員函數
- override和overwrite的區別
C++多態性實現原理
在繼承體系下,將父類的某個函數給成虛函數(即加上virtual關鍵字),在派生類中對這個虛函數進行重寫,利用父類的指針或引用調用虛函數。通過指向派生類的基類指針或引用,訪問派生類中同名覆蓋成員函數。對於虛函數調用來說,每一個對象內部都有一個虛表指針,在構造子類對象時,執行構造函數中進行虛表的創建和虛表指針的初始化,該虛表指針被初始化為本類的虛表。所以在程序中,不管你的對象類型如何轉換,但該對象內部的虛表指針是固定的,所以呢,才能實現動態的對象函數調用,這就是C++多態性實現的原理。
普通函數是靜態編譯的,沒有運行時多態。
動態綁定與靜態綁定
靜態綁定和動態綁定是C++多態性的一種特性。
1)對象的靜態類型和動態類型
靜態類型:對象在聲明時采用的類型,在編譯時確定
動態類型:當前對象所指的類型,在運行期決定,對象的動態類型可變,靜態類型無法更改
2)靜態綁定和動態綁定
靜態綁定:綁定的是對象的靜態類型,函數依賴於對象的靜態類型,在編譯期確定
動態綁定:綁定的是對象的動態類型,函數依賴於對象的動態類型,在運行期確定
只有虛函數才使用的是動態綁定,其他的全部是靜態綁定。
何時發生動態綁定
只有通過指向派生類的基類指針或引用,訪問派生類中同名覆蓋成員函數,才能發生動態綁定。若使用靜態對象調用虛函數,是無法發生動態綁定,這時只能調用基類的成員函數。
詳情可見參考3。
純虛函數
基類中為其派生類保留一個名字,以便派生類根據需要進行定義。
包含一個純虛函數的類被稱為抽象類,比如說“動物”這樣一個類不便於定義具體成員函數,其子類“長頸鹿”、“大象”等才應該定義具體的成員函數,這時候“動物”類適合定義定義為抽象類,成員函數創建為純虛函數以提供接口。
純虛函數的形式如下:
virtual returnType function() = 0;
抽象類不可以實例化,但可以定義指針。
虛函數的使用場景
函數傳參時,直接傳基類指針,在函數中即可直接對基類指針操作,而不用考慮具體是哪個派生類,這樣便於函數的封裝。
void func(Base *base){...}
虛函數表和虛函數指針
虛函數vtable表屬於類(有點像一個類里面的staic成員變量);不同編譯器的vtable存放位置不同,如gcc編譯器的實現中虛函數表vtable存放在可執行文件的只讀數據段。
虛函數指針屬於類的實例化對象,大小固定;vptr存放在棧中。
override和final說明符
C++11中可以使用override關鍵字來說明派生類中的虛函數以方便查錯。
void f1() const override; // 子類虛函數重寫f1
為了拒絕在子類中重寫父類的函數,可以使用final,方便查錯。
void f1() const final; // 父類函數不允許子類重寫f1
如何回避虛函數機制
某些情況希望對虛函數的調用不要進行動態綁定,而是強迫執行虛函數的某個特定版本。
這時候可以使用作用域運算符::
。
DerivedClass child;
double x = child -> BaseClass::function();
哪些類中的成員函數不能聲明為虛函數
- 靜態成員函數
- 內聯函數
- 構造函數
- 靜態成員函數不能定義為虛函數
因為靜態成員函數沒有this指針,並且靜態成員函數可以通過類名來訪問。
又因為虛函數是放在對象的虛表里面的,同一個類中的所有對象雖然共用同一張虛表,但是類名無法找到虛表。 - 內聯函數不能定義為虛函數
因為內聯函數沒有地址,而虛表里面存放的就是虛函數的地址。 - 構造函數不能定義為虛函數
因為虛函數是存放在對象的虛表里面,如果將構造函數定義為虛函數,則構造函數也必須存放在虛表里面,但是此時對象都還沒有創建也就沒有所謂的虛表。
override和overwrite的區別
1)override,派生類覆蓋基類的虛函數,實現接口的重用,返回值類型必須相同
特征:不同范圍(基類和派生類)、函數名字相同、參數相同、基類中必須有virtual關鍵字(必須是虛函數)
2)overwrite,派生類屏蔽了其同名的基類函數,返回值類型可以不同
特征:不同范圍(基類和派生類)、函數名字相同、參數不同或者參數相同且無virtual關鍵字
在子類中overwrite直接覆蓋掉同名成員函數,對於子類對象來說,父類的同名函數是不可見的,就無法實現父類指針或引用調用子類同名函數了。
值得注意的是,C++中是沒有overwrite這個術語的。
參考內容
- 常見C++筆試面試題整理. https://mp.weixin.qq.com/s/hONnHSkR8Qk4doIK0aYHiQ
- C++面試基礎題匯總. https://www.cnblogs.com/277223178dudu/p/10750434.html
- 虛函數調用的幾種方式. https://www.cnblogs.com/lsgxeva/p/7692567.html
- weak_ptr這個智能指針有什么用. https://segmentfault.com/q/1010000004078858
- 【C++基礎之十】友元函數和友元類. https://blog.csdn.net/m0_38126105/article/details/78757872
- 圖說C++對象模型:對象內存布局詳解. https://www.cnblogs.com/QG-whz/p/4909359.html#_label1