在區分隱藏和重寫之前,先來理一理關於繼承的東西。。。
【繼承】
繼承是面向對象復用的重要手段,是類型之間的關系建模。通過繼承一個類,共享公有的東西,實現各自本質不同的東西。簡單的說,繼承就是指一個對象直接使用另一對象的屬性和方法。C++中的繼承關系就好比現實生活中的父子關系,繼承一套房子通常比白手起家自己掙要容易得多。所以原始類被稱為父類或基類,繼承類稱為子類或派生類,而子類又可以當成父類,可再被其它類繼承。這種關系和java是一樣道理,不過C++多了一個麻煩的地方就是它還支持多繼承,於是就引發出很多坑人的地方。
1.繼承的方式:
(1) 公有繼承(public)
基類的公有和保護成員被子類繼承時,它們都保持原有的狀態,而基類的私有成員同樣被繼承下來,只是在子類表現為私有,子類不能訪問。
(2)私有繼承(private)
它的特點是基類的公有和保護成員被子類繼承時,都會成為子類的私有成員;基類的私有成員也被繼承下來,但不能被該子類訪問。
(3)保護繼承(protected)
它的特點是基類的公有成員和保護成員被子類繼承時,都會成為子類的保護成員,子類的子類可通過保護成員函數或友元訪問;基類的私有成員被繼承下來仍然是私有的,依舊不能被子類訪問。
private能夠對外部和子類保密,即除了成員所在的類本身可以訪問之外,別的都不能直接訪問。protected能夠對外部保密,但允許子類直接訪問這些成員。不難看出protected限定符是因為繼承才能表現出作用。
各種繼承方式下各種成員關系變化如下圖
繼承方式就像一張‘網’,被繼承后都成了跟‘網’權限“相同的”和比“網” ’“小”的。
總結一下:
1. public繼承是一個接口繼承,保持is-a原則,每個父類可用的成員對子類也可用,因為每個子類對象也都是一個父類對象。
2. protetced/private繼承是一個實現繼承,基類的部分成員並未完全成為子類接口的一部分,是 has-a 的關系原則,所以非特殊情況下不會使用這兩種繼承關系,在絕大多數的場景下使用的都是公有繼承。
3. 不管是哪種繼承方式,在派生類內部都可以訪問基類的公有成員和保護成員,但是基類的私有成員存在但是在子類中不可見(不能
訪問)。
4. 使用關鍵字class時默認的繼承方式是private,使用struct時默認的繼承方式是public,不過最好要顯示地寫出繼承方式。
2.賦值兼容規則
要點:
1. 子類對象可以賦值給父類對象(支持對象切片)
2. 父類對象不能賦值給子類對象
3. 父類的指針/引用可以指向子類對象
4. 子類的指針/引用不能指向父類對象(可通過強制類型轉換完成)
例子:
1 #include <iostream> 2 #include <string> 3 using namespace std; 4 5 class Person 6 { 7 public: 8 void Show() 9 { 10 cout<<_name<<endl; 11 } 12 protected: 13 string _name; 14 }; 15 16 struct Student : public Person 17 { 18 public: 19 void Print() 20 { 21 cout<<_name<<endl; 22 } 23 24 //private: 25 public: 26 string _id; 27 }; 28 29 void test1() 30 { 31 Person p; 32 Student s; 33 34 p = s; // 不是隱式類型轉換 -- 切片處理-編譯器天然支持 -- is-a 35 Person* pP = &s; //通過 36 Person& p1 = s; //通過 37 //上面都屬於一種向上類型的轉換,編譯器默認支持。 38 //s = p; //報錯,類型不匹配 39 Student* p3 = (Student*)&p; //編譯通過,通過p3進行操作會出錯 40 //p3->_id = 10; //該句執行完雖給p3->_id賦了值,但在整個程序結束時程序會崩掉,因為強轉為了子類的指針,那么編譯器就會按子類 41 //的大小對該指針作解釋,這樣就多‘占用’一塊內存,而它可能是用來執行其他的活動的,但p3->id卻指向這塊非法的內存。 42 Student& r3 = (Student&)p; //編譯通過,用r3進行操作出錯 43 //Student& r3 = p; //出錯 44 //r3._id = 10; //運行出錯,同上面道理 45 46 }
注:p = s操作時會將子類對象獨有的(非繼承的部分)函數和變量自動“切去”,子類只留下繼承來的基類原有的“切片”來對基類的對象進行賦值。
3.多繼承
多繼承是指 一個子類有兩個或以上直接父類時稱這個繼承關系為多繼承。這種繼承方式使一個子類可以繼承多個父類的特性。多繼承可以看作是單繼承的擴展。派生類具有多個基類,派生類與每個基類之間的關系仍可看作是一個單繼承。
多繼承下派生類的構造函數與單繼承下派生類構造函數相似,但是注意幾點:
①子類構造函數必須同時負責該派生類所有基類構造函數的調用。
②派生類的參數個數必須包含完成所有基類初始化所需的參數個數。
③子類繼承來的部分在內存中是按照聲明定義的順序存放的
4.棱形繼承& 虛繼承
棱形繼承如下圖

#include <iostream> #include <string> using namespace std; class A { public: int _a; }; class B : public A { public: int _b; }; class C : public A { public: int _c; }; class D : public B, public C { public: int _d; }; void test4() { D d; d.B::_a = 1; //添加域作用限定符指向修改_a d.C::_a = 2; //同理 //cout<<sizeof(D)<<endl; //cout<<sizeof(B)<<endl; }
從圖來看它帶來兩個問題:①數據冗余 ②二義性
D的對象模型里面保存了兩份A,當我們想要改動從A里繼承的_a時就會出現指向不明確問題,並且還存在數據冗余的問題,明明可以只要一份_a就好,但卻保存了兩份,浪費空間。
所以想要改動_a便要加上域作用限定符,但是這難免有些繁瑣,有一個更好的辦法來實現——虛繼承

class A { public: int _a; }; class B : virtual public A { public: int _b; }; class C : virtual public A { public: int _c; }; class D : public B, public C { public: int _d; };
關於虛繼承:
-
虛繼承即讓B和C在繼承A時 加上
virtural
關鍵字,注意不是D繼承B、C使用虛繼承。 -
虛繼承解決了在菱形繼承體系里面子類對象包含多份父類對象的數據冗和浪費空間的問題。
-
虛繼承和虛函數雖然用的是同一個關鍵字,然而它倆之間沒半毛錢關系。
- 虛繼承體系比較復雜,在實際應用我們通常不會定義如此復雜的繼承體系。應盡量避免定義菱形結構的虛繼承體系結構,因為使用虛繼承解決數據冗余問題也帶來了性能上的損耗
既然說虛繼承解決數據冗余問題,那怎么還說虛繼承帶來了損耗?
還是針對上面代碼,先來看看使用虛繼承和不使用虛繼承 類D的大小,
//普通繼承 cout<<sizeof(D)<<endl; //結果: 20 //使用虛繼承 cout<<sizeof(D)<<endl; //結果: 24
虛繼承后的結果確實大了,這就比較奇怪了,到底大在了啥地方。看看內存
從上圖不難看出兩點:
①派生對象d確實存放了繼承來的各個成員(_a, _b, _c),同時初始基類屬性_a被提出來放在了高地址處
②多出來了兩個值,68 58 2d 01 和 60 58 2d 01 似乎找到了d變大原因,看看它們所指向的地方會存放什么。
d._a = 1; d._b = 2; d._c = 3; d._d = 4;
一個是數值12,一個是20。這時候看看D的對象模型,發現d._b(2
)
的地址和 d._a(1)
地址之差是20,d._c(3)
的地址和 d._a(1)
地址之差是12。大膽猜測一下,這也許就是某個偏移量啊。
事實上,確實是的。每一個繼承自父類對象的實例中都存有一個指針,這個指針指向虛基表中的某一項,表項里面存的是一個偏移量。對象的實例可以通過自身的地址加上這個偏移量找到存放繼承自父類屬性的地址。
虛繼承基於這種機制雖然解決了冗余和二義性,但是也增大性能的開銷,應盡量避免使用。
【隱藏和重寫】
因為有類的繼承機制,在父類和子類之間就會隱藏和重寫這兩個東西。而隱藏(重定義)和重寫因字面上又有着相近的意思,導致它們比較容易被我們搞混淆。但其實重寫是建立在隱藏的基礎上的,它比隱藏多了一些限制。
對於隱藏,它的出現是在父子類擁有相同的成員(成員函數,成員屬性)的時候,此時父類的成員就會被隱藏起來(還存在),子類的成員得到訪問或調用
舉個例子,
class Person { public: void Show() { cout<<"Person::"<<_name<<endl; } protected: int _id; string _name; }; struct Student : public Person { public: void Show() { _id = 10; //Person::_id = 10; 要訪問加上類作用符 cout<<"Student::"<<_name<<endl; } public: int _id; }; void test1() { Person p; Student s; s.Show(); }
結果:
注意到此時只為子類的_id賦了值,並未給繼承來的_id賦值,如果需要訪問,則要加上類作用符“::”。通常不建議定義同名的成員。
再看看隱藏了函數的情況,
class AA { public: void f() { cout<<"AA::f()"<<endl; } }; class BB : public AA { public: void f(int a) { cout<<"BB::f()"<<endl; } }; void test2() { AA aa; BB bb; aa.f(); bb.f(); //會報錯 }
這個例子,乍一看,感覺AA::f() 和BB::f(int)兩個函數構成了重載,但其實BB類對象bb將f() 繼承下來,兩函數是構成了隱藏。因為它們壓根就不在同一作用域,所以就不存在重載這么一說了,此時bb.f()調用的是自己的 f(int) ,但卻未傳參數,所以這個程序就會報錯。
虛函數和重寫
虛函數:類的成員函數前面加virtual關鍵字,則這個成員函數稱為虛函數。
作用:基類的指針在操作它的多態類對象時,會根據不同的類對象,調用其相應的函數,這個函數就是虛函數。實際上就是用來實現多態的
關於虛函數:
1. 基類中定義了虛函數,在派生類中該函數始終保持虛函數的特性。
2. 只有類的成員函數才能定義為虛函數。
3. 靜態成員函數不能定義為虛函數。(原因戳這里)
4. 如果在類外定義虛函數,只能在聲明函數時加virtual,類外定義函數時不能加virtual。
5. 構造函數不能為虛函數 (雖然可以將operator=定義為虛函數,但是最好不要將operator=定義為虛函數,因為使用時容易引起混淆)
7. 不要在構造函數和析構函數里面調用虛函數,(在構造函數和析構函數中,對象是不完整的,可能會發生未定義的行為)具體原因戳這里
8. 最好把基類的析構函數聲明為虛函數。(why?繼承時,如果派生類有資源需要自己手動釋放,我們可以通過基類的指針或引用去釋放子類的資源,防止內存泄露。)
重寫就用到了虛函數。對於重寫(覆蓋),它是指在父類中有一個虛函數,然后子類重寫出一個和它形式(函數名,參數列表,返回值)完全相同的一個虛函數,

class Human { public : virtual void Drink() { cout<<"喝白開水"<< endl; } protected : string _name ; // 姓名 }; class Boss: public Human { public : virtual void Drink() //virtual 不寫同樣構成覆蓋 { cout<<"喝咖啡"<<endl; } protected : int _position; //職稱 };
來小結什么時候隱藏,什么時候重寫
隱藏的情況:
-
構成隱藏的兩成員函數都不在同一作用域
-
函數名相同,參數不同(無論有無virtual關鍵字,父類函數被隱藏)
- 如果派生類函數與基類函數參數相同,但是在基類函數中沒有virtual關鍵字,發生函數隱藏
重寫的情況:
-
不在同一作用域
-
子類的函數與父類的函數同名,並且參數列表也相同
-
父類函數必須virtual關鍵字,使之成為虛函數。(否則,父類的函數在子類中將被隱藏)