1. 程式人生 > >C++【繼承與多型】

C++【繼承與多型】

兩個類間的關係:組合與繼承

繼承:可以使用現有類的所有功能,並在無需編寫原來的類的情況下對這些功能進行擴充套件。

通過繼承建立的新類稱為“子類”或“派生類”。   被繼承的類稱為“基類”,“父類”或“超類”。

繼承的作用:程式碼的複用

訪問許可權

派生類物件怎麼構造

在派生類建構函式的初始化列表中,指定基類成員的構造方式

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類的成員,並修改。