兩個類間的關係:組合與繼承
繼承:可以使用現有類的所有功能,並在無需編寫原來的類的情況下對這些功能進行擴展。
通過繼承創建的新類稱爲“子類”或“派生類”。 被繼承的類稱爲“基類”,“父類”或“超類”。
繼承的作用:代碼的複用
訪問權限
派生類對象怎麼構造
在派生類構造函數的初始化列表中,指定基類成員的構造方式
class Base
{
public:
Base(int data) :ma(data)
{
cout << "Base()" << endl;
}
~Base(){ cout << "~Base()" << endl; }
protected:
int ma;
};
class Derive : public Base
{
public:
// “Base”: 沒有合適的默認構造函數可用
Derive(int data) :Base(data), mb(data)
{
cout << "Derive()" << endl;
}
~Derive(){ cout << "~Derive()" << endl; }
private:
int mb;
};
同名成員的訪問方式(基類和派生類的同名成員方法的關係)
重載:在基類或者派生類同一個類作用域當中的。
函數名相同,參數列表不同
隱藏:方法分佈在基類和派生類中
方法名字相同,就說派生類的同名方法把基類的同名方法給隱藏了
覆蓋/重寫:方法分佈在基類和派生類中
基類的方法是虛函數,派生類的方法和基類的虛函數 返回值相同,函數名相同,參數列表也相同,那麼此時派生類的這個函數自動處理成虛函數,和基類的函數成爲覆蓋關係, 在虛函數表中進行覆蓋!
不可以的
派生類對象 = 基類對象
派生類指針(同引用) =》 基類對象可以
基類對象 = 派生類對象
基類指針(同引用)=》 派生類對象
判斷是否爲動態綁定
沒有virtual , 一定是靜態綁定
有virtual,而且用指針&引用 =》 動態綁定 call eax => 運行時多態
有virtual,但是用對象調用 =》 靜態綁定 call 0x9009876多態:基類指針(引用)指向派生類對象
實現虛函數需要的的兩個條件
1.取地址 2.依賴對象
對象的生命週期【構造函數完 == 析構函數開始】
構造函數和析構函數內部都不能發生動態綁定,多態!(調用虛函數不發生動態綁定)
虛函數在構造函數開始需要做的事
1. 棧幀開闢 2.棧幀初始化 3.vftable 存入 vfptr裏面
class Base
{
public:
// 構造函數和析構函數內部都不能發生動態綁定,多態!
// 對象 【構造函數完,析構函數開始】
Base(int data) :ma(data)
{
// 1. 棧幀開闢 2.棧幀初始化 3.vftable=》vfptr裏面
cout << "Base()" << endl;
//clear();
this->show();
}
virtual ~Base()
{
this->show();
cout << "~Base()" << endl;
}
void clear()
{ memset(this, 0, sizeof(*this)); }
virtual void show(int i=10)
{
cout << "Base::show" << endl;
}
private:
int ma;
};
編譯時期(靜態指針指向) 方法的訪問權限。基類方法的訪問限定
運行時期 RTTI 運行時的類型信息方法的訪問權限問題 編譯階段確定 基類方法的訪問限定
方法參數的壓棧,是編譯時候確定。具體調用哪個方法,動態綁定,是運行時期確定
虛函數表:
繼承結構中,構造函數剛開始的時候,每一層構造函數會把自己類型的虛函數表的地址填入vfptr裏面
純虛函數------含有純虛函數的類是抽象類
class Man // 擁有純虛函數的類 =》 抽象類
{
public:
Man(int id, string name)
:_id(id), _name(name){}
// 基類提供的這個虛函數,就是爲所有派生類提供統一的虛函數接口,
// 讓派生類自己去重寫的
virtual void show() = 0; // 虛函數 純虛函數
protected:
int _id;
string _name;
};
基類沒有函數可以寫成純虛函數的情況下,析構函數可以寫成純虛函數。在類外面需要重寫析構函數(標明作用域)
class Base
{
public:
Base(int data) :ma(data){ cout << "Base()" << endl; }
virtual ~Base() = 0;
virtual void show(int i=10)
{
cout << "Base::show" << endl;
}
private:
int ma;
};
Base::~Base()
{
cout << "~Base()" << endl;
}
虛繼承
多重繼承/菱形繼承 ,產生的問題 :派生類有多份基類的數據。所以用虛繼承來解決該問題
/**
普通繼承(沒有使用虛基類)
*/
// 基類A
class A
{
public:
int dataA;
};
class B : public A
{
public:
int dataB;
};
class C : public A
{
public:
int dataC;
};
class D : public B, public C
{
public:
int dataD;
};
上面是一個簡單的多繼承例子,我們啓動Visual Studio命令提示功能,切換到NormalInheritance.cpp文件所在目錄,輸入一下命令:c1 NormalInheritance.cpp /d1reportSingleClassLayoutD
我們可以看到class D的內存佈局如下:
從類D的內存佈局可以看到A派生出B和C,B和C中分別包含A的成員。再由B和C派生出D,此時D包含了B和C的成員。這樣D中就總共出現了2個A成員。大家注意到左邊的幾個數字,這幾個數字表明瞭D中各成員在D中排列的起始地址,D中的五個成員變量(B::dataA、dataB、C::dataA、dataC、dataD)各佔用4個字節,sizeof(D) = 20。
爲了跟後文加以比較,我們再來看看B和C的內存佈局:
class A
{
public:
A(int data) :ma(data){ cout << "A()" << endl; }
~A(){ cout << "~A()" << endl; }
protected:
int ma;
};
////////////////////////////////////////////////////
class B : virtual public A
{
public:
B(int data) :A(data), mb(data){ cout << "B()" << endl; }
~B(){ cout << "~B()" << endl; }
protected:
int mb;
};
class C : virtual public A
{
public:
C(int data) :A(data), mc(data){ cout << "C()" << endl; }
~C(){ cout << "~C()" << endl; }
protected:
int mc;
};
////////////////////////////////////////////////////
class D : public B, public C
{
public:
D(int data) :md(data),A(data), B(data), C(data)
{
cout << "D()" << endl;
}
~D(){ cout << "~D()" << endl; }
void show(){ cout << ma << endl; }
protected:
int md;
};
int main()
{
cout << sizeof(D) << endl;
D d(10);
d.show();
/*
通過D類的vbptr,在vbtable尋找A類vbptr的偏移量,找到A類的vbptr,修改A類的成員的值
*/
int *p = (int*)&d;
int *q = (int*)*p;
q += 1;
int offset = *q;
char *p1 = (char*)&d;
p1 += offset;
*(int*)p1 = 30;
d.show();
return 0;
}
VirtualInheritance.cpp和NormalInheritance.cpp的不同點在與B和C繼承A時使用了virtual關鍵字,也就是虛繼承。同樣,我們看看B、C、D類的內存佈局情況:
我們可以看到,菱形繼承體系中的子類在內存佈局上和普通多繼承體系中的子類類有很大的不一樣。對於類B和C,sizeof的值變成了12,除了包含類A的成員變量dataA外還多了一個指針vbptr,類D除了繼承B、C各自的成員變量dataB、dataA和自己的成員變量外,還有兩個分別屬於B、C的指針。
由上圖,我們可以發現。虛繼承之後,虛基類(A)的數據,搬到派生類對象的後面,原來的地方放一個vbptr(虛基類指針)
上圖爲D類的內存佈局,虛函數表保存的是B和C各自vbptr到A::ma的字節數,也就是偏移量。而正是因爲這裏保存的便宜量,我們纔可以在D類裏通過指針訪問A類的成員,並修改。