1.final
final在繼承和多態中都可以使用,在繼承中是指不想將自己被繼承,在多態中是指不想該函數被重寫,比較簡單,下面是一些使用例子。
2.純虛函數
當我們需要抽象一個類的時候,我們就需要用到純虛函數。所謂抽象的類是指高度概括的,需要針對不同事物有不同處理的。如植物是一種抽象的類,而像蘋果、香蕉就是具象的,單獨討論植物太過龐大,沒有太大意義,因此我們的重心放在由植物具象出來的蘋果,我們可以具體討論它的成分、營養價值等。理解了這個例子,就能理解為什么有抽象類,純虛函數的存在了。
這就是一個純虛函數,就是在虛函數后面加上 = 0,它對應的類就叫抽象類。注意,只要有一個純虛函數,這個類就叫抽象類,抽象類不能被實例化,就算你不打算用這個純虛函數。唯一能做的就是調用這個類里面的static成員,因為它們不需要實例化就能調用
這么做的意義就在于純虛函數對應的類本身就高度抽象,實例化它沒有意義。但我們可以討論將它具象化的事物,這就要用到虛函數的重寫功能。我們可以理解,純虛函數存在的意義是依賴于虛函數的性質存在的,這里需要我們深刻思考。
3.繼承、多態難點
繼承、多態的用法、意義幾乎講的差不多了,絕大多數情況下已經夠用了,只不過在極少數情況下仍有一些坑。
(1)多態調用重寫的函數
先看下面的代碼,想想結果是什么
#include <iostream>
using namespace std;class A
{
public:virtual void test(int a = 0){}
};class B final : public A
{
public:void test(int a){cout << a << endl;}
};int main()
{A* a = new B;a->test();return 0;
}
不少人會想,這難道不報錯嗎?但結果是
我們需要知道,當構成多態和重寫時,調用函數是以父類聲明+子類定義進行的,對于三個類及以上都是如此,這個父類指的是構成多態的父類
我們也可以進一步理解為什么只需要父類寫virtual,子類可以不寫,因為子類的函數聲明根本沒有意義(在多態中),寫不寫都是以父類的聲明為標準。但是在多態語法以外就不會出現這種反直覺處理情況了。
(2)繼承調用父類函數時this的類型變化
先看看下面的代碼,想想test2的隱含的this指針是B*還是A*
#include <iostream>
using namespace std;class A
{
public:virtual void test(int a = 0){} void test2(){test();}
};class B final : public A
{
public:void test(int a = 1){cout << a << endl;}
};int main()
{B* a = new B;a->test2();return 0;
}
既然是B*調用函數,那理所應當應該是B*為形參來接受啊,但實際不是這么理解的。
當子類去調用父類的成員函數時,隱含的指針類型始終是父類的。要理解這里,我們假設這個指針的類型是子類的,那如果子類又寫了一個一模一樣的函數構成隱藏,那么就會因為參數和假設的函數完全相同而報錯,所以是行不通的。
當子類調用父類時,this指針會發生一次賦值兼容轉換,這里是從B*賦值兼容轉換為A*,賦值兼容轉換為指針只會影響訪問的方式,指針的值,指向的內容都不會改變。但學了多態之后,我們是否可以將這種特性和多態的形成條件結合起來呢?上面這段代碼就是如此。
結合上一個易錯點,這段代碼的最終結果是
(3)多態訪問限制的特殊處理
先看看下面的代碼,看看是否能夠正常訪問
#include <iostream>
using namespace std;class A
{
public:virtual void Test(){cout << "A" << endl;}
};class B : public A
{
private:void Test(){cout << "B" << endl;}
};int main()
{A* p = new B;p->Test();return 0;
}
很多人以為p的類型是A*,A訪問不了B,但其實程序運行沒有問題
我們要理解訪問限定符限制的是什么,是防止其它類調用private的函數,這里p是一個指針,本身就指向B對象的空間,只不過訪問方式按A進行。由于符合多態的條件,就按虛函數表進行訪問。那么問題在于:B會不會阻止呢?
我們先看看什么情況是會阻止的
我們發現無論在A還是在main函數中,都沒有辦法調用B中的private成員,這也符合我們之前的預期。但是為什么A* p = new B;??p->Test();這種操作就可以呢?
事實上,這是多態中的特殊處理,當我們用父類的指針或引用來訪問子類的虛函數時,是會以父類的訪問限定符為標準的。子類的限制不會起到作用。同理,就算子時public,父是private,那么就無法訪問
一般建議都設為public
4.動靜態綁定
動靜態綁定都是為了定位一個函數,從反匯編的角度上講就是確定call的對應的地址是什么,只不過兩者的方式有一定的區別。
(1)動態綁定:多態調用函數的核心時動態綁定,也叫運行時綁定。也就是借助虛函數表,在這個函數指針數組中確定函數的地址。
(2)靜態綁定:我們平時寫的函數都可以認為是靜態綁定(包括函數重載、普通函數、模板函數),函數如果聲明定義在一起就在編譯后進符號表,如果聲明定義分離在兩個文件則在鏈接時進符號表,運行時是根據符號表來查找函數。
在多態中,在滿足動態綁定的情況下我們指定類域調用函數那就自動轉為靜態綁定,就失去了多態的特性。
#include <iostream>
using namespace std;class A
{
public:virtual void Test(){cout << "A" << endl;}
};class B final : public A
{
public:void Test(){cout << "B" << endl;}
};int main()
{A* p = new B;p->Test();p->A::Test();return 0;
}
運行結果
5.繼承、多態的一些知識點和處理技巧
(1)多用const修飾函數,保證匿名對象傳參可以調用函數
(2)函數第一句的指令理解為函數的地址,成員函數要打印它們的地址函數名前要加&,其余函數函數名就是它的地址(&可加可不加,但成員函數一定要加)
(3)cout打印地址很麻煩,char*不會打印地址,會按字符串去打印,這跟流插入的重載有關。有幾個關于函數指針的重載會導致出現bug,打印地址很受阻,最好使用printf
(4)關聯性強的類型之間支持隱式類型轉換,如整型家族+double(內置類型)、指針之間,有的支持強轉,如int和int*。
關聯性弱的自定義類型,想取頭地址,可以使用*((int*)&Base),虛函數表的地址就在類的開頭
(5)只有virtual修飾的成員函數才能叫作虛函數,而像static修飾的成員函數、全局函數都不能定義為虛函數,全局定義的虛函數沒有意義,static修飾的成員函數不屬于對象,就算加了virtual,也進不了虛函數表,沒有意義。
(6)virtual修飾的成員函數聲明定義分離時定義處不寫virtual
(7)友元不是成員函數,所以不能用virtual修飾
(8)多繼承可能有多張虛函數表,按繼承順序排序,但單繼承對應的就只有一張虛函數表,如果多繼承后自己又寫了虛函數,則默認放在第一張虛函數表后面
(9)如果不重寫虛函數,那共用同一個函數,如果所有的函數都不重寫,兩個類存的函數的地址都相同,但是這對應兩張虛函數表,開辟的是不同的空間
(10)虛函數表是在編譯期間就形成了。而多態是動態綁定(運行時綁定/晚期綁定),是因為編譯時編譯器只負責檢查語法錯誤,而不負責讀取內容,只有運行起來才知道函數調用的地址。