C++繼承與菱形繼承
繼承是⾯向物件復⽤的重要⼿段。
通過繼承定義⼀個類,繼承是型別之間的關係建模,共享公有的東西,實現各⾃本質不同的東西。
我們類成員訪問限定符有三種:
public(公有),protected(保護),private(私有)
我們的繼承關係也有三種:
public(公有繼承),protected(保護繼承),private(私有繼承)
下面是一個簡單的繼承:
class A//基類(父類)
{
public :
int _a ;
};
//B為派生類(子類),public為繼承關係
class B : public A
{
public :
int _b;
};
總結:
- 基類的私有成員在派⽣類中是不能被訪問的,如果⼀些基類成員不想被基類物件直接訪問,但需要在派⽣類中能訪問,就定義為保護成員。可以看出保護成員限定符是因繼承才出現的。
- public繼承是⼀個介面繼承,保持is-a(包含了所有父類的性質)原則,每個⽗類可⽤的成員對⼦類也可⽤,因為每個⼦類物件也都是⼀個⽗類物件。
- protetced/private繼承是⼀個實現繼承,基類的部分成員並未完全成為⼦類接⼜的⼀部分,是 has-a(有父類的性質) 的關係原則,所以⾮特殊情況下不會使⽤這兩種繼承關係,在絕⼤多數的場景下使⽤的都是公有繼承。
- 不管是哪種繼承⽅式,在派⽣類內部都可以訪問基類的公有成員和保護成員,但是基類的私有成員存在但是在⼦類中不可見(不能訪問)。
- 使⽤關鍵字class時預設的繼承⽅式是private,使⽤struct時預設的繼承⽅式是public,不過最好顯⽰的寫出繼承⽅式。
- 在實際運⽤中⼀般使⽤都是public繼承。
繼承與轉換–賦值相容規則–public繼承(前提條件)
1. ⼦類物件可以賦值給⽗類物件(切割/切⽚)
2. ⽗類物件不能賦值給⼦類物件
3. ⽗類的指標/引⽤可以指向⼦類物件
4. ⼦類的指標/引⽤不能指向⽗類物件(可以通過強制型別轉換完成)
class A//基類(父類)
{
public :
int _a ;
};
//B為派生類(子類),public為繼承關係
class B : public A
{
public :
int _b;
};
void Test()
{
A p;
B s;
}
// 1.子類物件可以賦值給父類物件(切割 /切口)
p = s ;
// 2.父類物件不能賦值給子類物件
//s = p;
// 3.父類的指標/引用可以指向子類物件
Person* p1 = &s;
Person& r1 = s;
// 4.子類的指標/引用不能指向父類物件(可以通過強制型別轉換完成)
Student* p2 = (Student*)& p;
Student& r2 = (Student&) p;
隱藏(重定義)
如果父類子類中有相同的函式,子類物件呼叫該會呼叫哪一個呢
class A//基類(父類)
{
public:
void Fun()
{
}
public :
int _a ;
};
//B為派生類(子類),public為繼承關係
class B : public A
{
public :
void Fun()
{
}
public :
int _b;
};
void Test()
{
A p;
B s;
s.Fun()
}
我們發現這個函式會先呼叫子類自己的函式
我們把在不同作用域(在父類與子類中)相同的函式名叫做重定義(或隱藏)
繼承體系中的作⽤域
1. 在繼承體系中基類和派⽣類都有獨⽴的作⽤域。
2. ⼦類和⽗類中有同名成員,⼦類成員將遮蔽⽗類對成員的直接訪問。(在⼦類成員函式中,可以使⽤基類::基類成員 訪問)–隱藏 –重定義
我們會先構造父類,在構造子類,析構時會先析構子類,在析構父類。
菱形繼承
單繼承&多重繼承
1. 單繼承–⼀個⼦類只有⼀個直接⽗類時稱這個繼承關係為單繼承
2. 多繼承–⼀個⼦類有兩個或以上直接⽗類時稱這個繼承關係為多繼承
菱形繼承是多繼承
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;
};
#include<iostream>
#include<windows.h>
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;
};
int main()
{
D b;
b.B::_a = 1;
b.C::_a = 2;
b._b = 3;
b._c = 4;
b._d = 5;
system("pause");
return 0;
}
菱形繼承的物件模型
菱形繼承的記憶體分配:
sizeof(b)的結果是20;
我們可以看出D中有兩份_a浪費空間
當我們 d._a時就會有呼叫不明確的問題,這就是二義性與數時據冗餘。
所以菱形繼承存在二義性和資料冗餘性。
那麼這個問題怎麼解決呢
解決方案1:加作用域限定符
如圖我們要修改B或者C中的_a我們就要加作用域
解決方案2:虛繼承——解決菱形繼承的二義性和資料冗餘問題
虛繼承
虛繼承——解決菱形繼承的⼆義性和資料冗餘的問題
1. 虛繼承解決了在菱形繼承體系⾥⾯⼦類物件包含多份⽗類物件的資料冗餘和浪費空間的問題。
2. 虛繼承體系看起來好複雜,在實際應⽤我們通常不會定義如此複雜的繼承體系。⼀般不到萬不得已都不要定義菱形結構的虛繼承體系結構,因為使⽤虛繼承解決資料冗餘問題也帶來了效能上的損耗。
什麼是虛繼承?
如上圖,虛繼承即讓B和C在繼承A時加上virtural關鍵字,這裡需要記住不是D使用虛繼承
那麼虛繼承在這裡是如何解決這個問題的
#include<iostream>
#include<windows.h>
using namespace std;
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;
};
int main()
{
D b;
b._a = 1;
b._a = 2;
b._b = 3;
b._c = 4;
b._d = 5;
cout << sizeof(b) << endl;
system("pause");
return 0;
}
這個程式輸出的結果是24
我們把A叫做虛基類,虛基表存放的是虛基類的偏移量
我們發現這樣修改的_a的值_a都會改變,這樣_a就只有一份
我們可以看見在B和C中不再儲存A中的內容,而是儲存了一份偏移地址,然後將A的資料儲存在一個公共位置處這樣保證了資料冗餘性的降低同時,我們也能直接的使用b._a來呼叫A裡的_a。
所以它的物件模型是如圖:
sizeof(b)=24.