天天看點

C++中繼承與虛繼承

先談談繼承的概念

C++中繼承與虛繼承

面向對象的繼承就是如上圖這樣:         從一個類派生另外一個類,使前者所有的特征在後者中自動可用。它可以聲明一些類型,這些類型可以共享部分或全都以前所聲明的類型,它也可以從超過一個的基類型共享一些特征。 繼承是面向對象複用的重要手段。通過繼承定義一個類,繼承類型之間的關系模型,共享共有的東西,實作各自本質的不同的東西。 不過是把現實生活中執行個體用能表現事物特征的關鍵資料來代替(如現實生活中的汽車,發票;對應的抽象後就可以用車牌号,預定數量這樣的資料來表示現實生活中的事物) 繼承還有好幾種情況:

C++中繼承與虛繼承

下面看看具體細節

一.繼承的關系及繼承通路限定符

下面是三種繼承關系下基類成員在派生類中的通路變化

C++中繼承與虛繼承

那麼private和protected都是限定直接通路,那麼他們有什麼差別?

先看下面這個單繼承示例代碼在作解釋:

#include<iostream>
using namespace std;

class person    //父類/基類
{
public:
    person()
	{
		cout << "person" << endl;
	}

protected:
	int id;
private:
	int _age;
};
//class student:protected person
//class student:private person
class student :public person      //class 子類/派生類: 繼承關系 父類/基類
{
public:
	student()
	{
		cout<< person::id << endl;
		cout << _num << endl;
	}
public:
	int _num;
};
int main()
{
	person p;
	student s;
	return 0;
}
           

下圖發現類可以直接調用父類的id和person(),而不能調用父類中私有的_age成員

C++中繼承與虛繼承

這裡程式可以正常運作,但是子類中不可以輸出cout<<person::_age<<endl;,因為_age是父類中的私有成員(private),不可在類外直接通路;而id是父類中的保護成員(protected),id可以被子類繼承後直接通路,但不可以在子類和父類外的其它地方直接使用;進而可以看出來protected通路限定符是為繼承而産的。

要想通路類中的私有成員,隻能通過類中的其它通路權限的方法調用成員,然後在類外使用。

 總結 1.基類的私有成員在派生類中是不能被通路的.如果一些基類成員不想被基類對象直接通路.但需要在派生類中能通路.就定義為保護成員。可以看出保護成員限定符是因繼承才出現的。 2. public繼承是一個接口繼承,保持is-a原則.每個父類可用的成員對子類也可用,因為每個子類對象也都是一個父類對象。 3. protected / privat繼承是一個實作繼承,基類的部分成員并未完全成為子類接口的一部分,是hasa的關系原則,是以非特殊情況下不會使用這兩種繼承關系,在絕大多數的場景下使用的都是公有繼承。 4.不管是哪種繼承方式,在派生類内部都可以通路基類的公有成員和保護成員但是基類的私有成員存在但是在子類中不可見(不能通路) 5.使用關鍵字 class時預設的繼承方式是 private.使用 stret時預設的繼示方式是 public.不過最好顯示的寫出繼承方式 6.在實際運用中一般使用都使用 public繼承,極少場景下才會使用 protected和private繼承

二.指派相容規則

看下面示例代碼:

#include<iostream>
using namespace std;

class person
{
public:
	void Display()
	{
		cout << "person()" << endl;
	}

protected:
	string _name;
private:
	int _age;
};

class student :public person
{
public:
	int _num;
};
int main()
{
	person b;
	student a;
	
	//子類對象可以指派給父類對象(切片/切割)
	b = a;    //yes

	//父類對象不可以指派給子類對象
	//a = b; //no

	//父類指針/引用可以指向子類對象
	person* p1 = &a;
	person& p1 = a;
	
	//子類指針/引用不能指向父類對象(可以通過強制類型轉換來指向)
	//student* s1 = &b;            //no
	student* s1 = (student*)&b;    //yes
	//student& s2 = b;             //no
	student& s2 = (student&)b;   //yes

	return 0;
}
           

可以總結:

子類對象可以指派給父類對象(切片/切割)

父類對象不可以指派給子類對象

父類指針 / 引用可以指向子類對象

子類指針 / 引用不能指向父類對象(可以通過強制類型轉換來指向)

下面我們一條一條解釋

那什麼是切片/切割呢?看下圖

C++中繼承與虛繼承

子類可以指派給父類,是因為子類裡邊包含父類裡的全部成員變量,是以可以通過切片把父類的成員都賦予相應的值

父類賦給子類為什麼不可以呢?是因為父類裡面沒有子類獨有的那部分變量,是以無法給子類獨有的成員變量指派,是以不能用父類給子類指派

父類指針 / 引用可以指向子類對象

C++中繼承與虛繼承

現在我們知道子類指針 / 引用不能指向父類對象,因為子類比父類空間大,是以會越界,那強制類型轉換會出現什麼情況呢?

如果上面代碼的main函數内加上以下這兩句代碼程式會是什麼反應呢?

s1->_num = 10;

s2._num =  20;

解釋如下圖所示:

C++中繼承與虛繼承

繼承體系中的作用域:

1.在繼承體系中基類和派生類都有獨立的作用域。 2.子類和父類中有同名成員、子類成員将屏蔽父類對成員的直接通路。(在子類成員函數中,可以使用基類:基類成員通路)-隐藏-重定義 3.注意在實際中在繼承體系裡面最好不要定義同名的成員。

派生類中的預設成員函數 吓在繼承關系裡,如果派生類沒有顯示的定義6個預設成員函數(構造函數,拷貝構造,析構函數,指派運算符重載,取位址操作符重載,const修飾的取位址操作符重載),編譯系統則會預設的的合成這6個成員函數

//派生類構造函數和析構函數的構造規則。 
#include <iostream>
using namespace std;
class First           // 聲明基類
{
public:
	First()
		:a(0)
		, b(0)
	{}

	First(int x, int y)
		:a(x)
		, b(y)
	{}

	~First()
	{}

	void print()
	{
		cout << "\n a=" << a << "b = " << b;
	}
private:
	int a, b;
};
class Second : public First   //聲明基類Frist的公有派生類Second
{
public:
	Second()
		:First(1,1)
		,c(0)
		,d(0)
	{}

	Second(int x , int y)
		:First(x+1, y+1)
		, c(x)
		, d(y)
	{}

	Second(int x, int y, int m, int n)
		:First(m, n)
		, c(x)
		, d(y)
	{}

	~Second()
	{}
	void print()
	{
		First::print();
		cout << " c=" << c << " d= " << d;
	}
private:
	int c, d;
};
class Third:public Second           //聲明Second的公有派生類Third
{
public:
	Third()
		:e(0)
	{}
	Third(int x, int y)
		:Second(x,y)
	{}
	Third(int x, int y, int z,int m, int n)
		:Second(x, y,m,n)
		, e(z)
	{}

	~Third()
	{}

	void print()
	{
		Second::print();
		cout << " e=" << e;
	}

private:
	int e;
};

int main()
{
	Third l(3, 2,1);
	l.print();

}
           

通常情況下,當建立派生類對象時,首先要調用基類的構造函數,随後在調用派生類的構造函數,當撤銷派生類對象時,則先調用派生類的析構函數,随後調用基類的析構函數,遵循先調用的後釋放,後調用的先釋放。

看如上代碼總結出以下三點:

(1)“Third()”從這裡可以看出,當基類構造函數不帶參數時, 派生類不一定需要定義構造面數, 系統會自動的調用基類的無參構造函數; 然而當基類的構造函數那怕隻帶有一個參數, 它所有的派生類都必須定義構造函數,

甚至所定義的派生類構造函數的函數體可能為空, 它僅僅起參數的傳遞作用, 例如, 在上面的程式段中" Third(int x, int y)", 派生類 Third就不使用參數x和y, x和y隻是被傳遞給了要調用的基類構造函數Second

(2)若基類使用預設構造函數或不帶參數的構造函數, 則在派生類中定義構造函數時可略“:基類構造函數名(參數表)”, 此時若派生類也不需要構造函數, 則可不定義構造函數

3)如果派生類的基類也是一個派生類, 每個派生類隻需負責其直接基類資料成員的初始,依次上溯。

多繼承與菱形繼承會出現二義性和資料備援現象:

#include<iostream>
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 C, public B
{
public:
	int _d;
};

int main()
{
	D dd;
	cout << sizeof(dd) << endl;

	dd.B::_a = 1;
	dd._b = 3;

	dd.C::_a = 2;
	dd._c = 4;

	dd._d = 5;
	B bb;
	C cc;
	cout << sizeof(bb) << endl;

	//bb = dd;
	cc = dd;
	//A* pa = &dd;ⅆ
	//B* pb = &dd;
	//C* pc = &dd;ⅆ
	//D* pd = &dd;

	return 0;
}
           
C++中繼承與虛繼承

為什麼運作結果是這樣?看完下面的對象的記憶體分布也就明白了

C++中繼承與虛繼承

若要解決二義性和資料備援就得使用虛繼承(在繼承基類時,使用關鍵字virtual,如上面代碼所示)

C++中繼承與虛繼承

從上圖可以發現,當使用虛繼承後,通過虛基表就解決了資料二義性的問題。多了虛基表的空間是以sizeof(dd)就變成24了。

虛繼承體系看起來好複雜,在實際應用我們通常不會定義如此複雜的繼承體系。一般不到萬不得已都不要定義菱形結構的虛繼承體系結構,因為使用虛繼承解決資料備援問題也帶來了性能上的損牦。 

再看下面,如果main()裡面運作此代碼,各指針所指向的空間是什麼?

int main()
{
	D dd;

	dd.B::_a = 1;
	dd._b = 3;

	dd.C::_a = 2;
	dd._c = 4;

	dd._d = 5;
	B bb;
	C cc;

	A* pa = ⅆ
	B* pb = ⅆ
	C* pc = ⅆ
	D* pd = ⅆ

	return 0;
}
           
C++中繼承與虛繼承

補充幾點:

友元關系不能繼承,也就是說基類友元不能通路子類私有和保護成員。

基類定義了static成員,則整個繼承體系裡面隻有一個這樣的成員。無論派生出多少個子類,都隻有一個static成員執行個體

繼續閱讀