C++多重繼承及多態性

1、多重繼承的二義性問題

舉例ios

#include <iostream>
using namespace std;
class BaseA {
public:	void fun() { cout << "A.fun" << endl; }
};
class BaseB {
public:
	void fun() { cout << "B.fun" << endl; }
	void tun() { cout << "B.tun" << endl; }
};
class Derived :public BaseA, public BaseB {
public:
	void tun() { cout << "D.tun" << endl; }
	void hun() { fun(); } //此處調用出現二義性,編譯沒法經過
};
int main() {
	Derived d, * p = &d; d.hun();
	return 0;
}

類名限定函數

void hun() { BaseA::fun(); } //改寫上述代碼的第14行,用BaseA類名限定調用的函數
d.BaseB::fun();	//派生類對象調用基類同名函數時,使用類名限定
p->BaseB::fun(); //派生類指針調用基類同名函數時,使用類名限定

名字支配規則

若是存在兩個或多個包含關係的做用域,外層聲明瞭一個名字,而內層沒有再次聲明相同的名字,則外層名字在內層可見;若是在內層聲明瞭相同的名字,則外層名字在內層不可見——隱藏(屏蔽)規則。spa

在類的派生層次結構中,基類的成員和派生類新增的成員都具備類做用域,兩者的做用域不一樣:基類在外層,派生類在內層。若是派生類聲明瞭一個和基類成員同名的新成員,則派生類的新成員就會屏蔽基類的同名成員,直接使用成員名只能訪問到派生類新增的成員。(如需使用從基類繼承的成員,應當使用基類名限定)指針

#include <iostream>
using namespace std;
class Base {
public:
	void fun() { cout << "A.fun" << endl; }
	Base(int x = 1, int y = 2) :x(x), y(y) {}
	int x, y;
};

class Derived :public Base{
public:
	void tun() { cout << "D.tun" << endl; }
	void fun() { cout << "D.fun" << endl; }
	Derived(int x = 0) :x(x) {}
	int x;
};
int main() {
	Derived d, * p = &d;
	d.fun();	//輸出的結果爲	D.fun
	cout << p->x << " " << p->y << " " << p->Base::x << endl; //輸出爲 0 2 1
	d.Base::fun(); //輸出爲	A.fun
}

2、虛基類

虛基類的使用目的:在繼承間接基類時只保留一份成員。code

聲明虛基類須要在派生類定義時,指定繼承方式的時候聲明,只須要在訪問標號(public、protected、private繼承方式)前加上virtual關鍵字。注意:爲了保證虛基類在派生類中只繼承依次,應當在該基類的全部直接派生類中聲明爲虛基類,不然仍會出現屢次繼承。對象

派生類不只要負責對直接基類進行初始化,還要負責對虛基類初始化;若多重繼承中沒有虛基類,則派生類只須要對間接基類進行初始化,而對基類的初始化由各個間接基類完成(會所以產生多個基類的副本保存在各個間接基類中)。繼承

#include <iostream>
using namespace std;
class Base {
public:
	Base(int n) { nv = n; cout << "Member of Base" << endl; }
	void fun() { cout << "fun of Base" << endl; }
private:
	int nv;
};
class A:virtual public Base {	//聲明Base爲虛基類,做爲間接基類都須要使用virtual關鍵字
public:
	A(int a) :Base(a) { cout << "Member of A" << endl; }
private:
	int na;
};
class B :virtual public Base { //聲明Base爲虛基類,做爲間接基類都須要使用virtual關鍵字
public:
	B(int b) :Base(b) { cout << "Member of B" << endl; }
private:
	int nb;
};
class Derived :public A, public B {
public:
	Derived(int n) :Base(n), A(n), B(n) { cout << "Member of Derived" << endl; }
    //派生類的構造函數初始化列表,先調用基類Base的構造函數,再依次調用間接基類A、B的構造函數
    //因爲虛基類Base中沒有默認構造函數(容許無參構造),因此從Base類繼承的全部派生類的構造函數初始化表中都須要顯式調用基類(包括間接基類)的構造函數,完成初始化
private:
	int nd;
};
int main() {
	Derived de(3); de.fun();//不會產生二義性
	return 0;
}

關於虛基類的說明:接口

  1. 一個類在一個類族中既能夠被用做虛基類,也能夠被用做非虛基類;
  2. 若是虛基類中沒有默認構造函數(或參數所有爲默認參數的),則在派生類中必須顯式聲明構造函數,並在初始化列表中列出對虛基類構造函數的調用;
  3. 在一個成員初始化列表中同時出現對虛基類和非虛基類構造函數的調用時,虛基類的構造函數先於非虛基類的構造函數執行。

3、虛函數

虛函數概念:被virtual關鍵字修飾的成員函數,即爲虛函數,其做用就是實現多態性。

虛函數使用說明:

  • 虛函數只能是類中的成員函數,且不能是靜態的;內存

  • virtual關鍵字只能在類體中使用,即使虛函數的實如今類體外定義,也不能帶上virtual關鍵字;ci

  • 當在派生類中定義了一個與基類虛函數同名的成員函數時,只要該函數的參數個數、類型、順序以及返回類型與基類中的徹底一致,則派生類的這個成員函數不管是否使用virtual關鍵字,它都將自動成爲虛函數;

  • 利用虛函數,能夠在基類和派生類中使用相同的函數名定義函數的不一樣實現,達到「一個接口,多種方式」的目的。當基類指針或引用對虛函數進行訪問時,系統將根據運行時指針(或引用)所指向(或引用)的實際對象來肯定調用的虛函數版本;

  • 使用虛函數並不必定產生多態性,也不必定使用動態聯編。如:在調用中對虛函數使用類名限定,能夠強制C++對該函數使用靜態聯編。

  • 在派生類中,當一個指向基類成員函數的指針指向一個虛函數,而且經過指向對象的基類指針(或引用)訪問這個虛函數時,仍將發生多態性。

    #include <iostream>
    using namespace std;
    class Base {
    public:
    	virtual void print() { cout << "Base-print" << endl; }
    };
    class Derived :public Base {
    public:
    	void print() { cout << "Derived-print" << endl; }
    };
    void display(Base* p, void(Base::* pf)()) {
    	(p->*pf)();
    }
    int main() {
    	Derived d; Base b;
    	display(&d, &Base::print);	//輸出Derived-print
    	display(&b, &Base::print);	//輸出Base-print
        return 0;
    }
  • 使用虛函數,系統要增長必定的空間開銷存儲虛函數表,可是系統在進行動態聯編時的時間開銷時不多的,所以,虛函數實現的多態性是高效的。

虛函數實現多態的條件(同時知足)

  1. 類之間的繼承關係知足賦值兼容規則;
  2. 改寫了同名的虛函數,但函數形參、返回類型要保持一致;
  3. 根據賦值兼容規則使用指針(或引用);
    • 使用基類指針(或引用)訪問虛函數;
    • 把指針(或引用)做爲函數參數,這個函數不必定是類的成員函數,能夠是普通函數,而且能夠重載。

虛析構函數

派生類的對象從內存中撤銷時通常先調用派生類的析構函數,而後再調用基類的析構函數。但若是用new運算符創建了派生類對象,且定義了一個基類的指針指向這個對象,那麼當用delete運算符撤銷對象時,系統會只執行基類的析構函數,而不執行派生類的析構函數,於是也沒法對派生類對象進行真正的撤銷清理工做。

若是但願delete關鍵字做用於基類指針時,也執行派生類的析構函數,則須要將基類的析構函數聲明爲虛函數。

若是將基類的析構函數聲明爲虛函數,則由該基類所派生的全部派生類的析構函數也都自動成爲虛函數,即便派生類的析構函數與基類的析構函數名字不相同。

C++支持虛析構函數,可是不支持虛構造函數,即構造函數不能聲明爲虛函數!

純虛函數

許多狀況下,不能在基類中爲虛函數給出一個有意義的定義,這時能夠將它說明爲純虛函數,將具體定義留給派生類去作。純虛函數的定義形式爲:virtual 返回類型 函數名(形式參數列表) = 0;

包含有純虛函數的類稱爲抽象類,一個抽象類只能做爲基類來派生新類,所以又稱爲抽象基類,抽象類不能定義對象(實體)。

4、多態性

多態的含義:指同一操做做用於不一樣的對象時產生不一樣的結果。

  • 重載多態——函數重載、運算符重載

  • 強制多態——也稱類型轉換

    C++的基本數據類型之間轉換規則:char→short→int→unsigned→long→unsigned→float→double→long double

    能夠在表達式中使用3中強制類型轉換表達式:static_cast<T>(E)T(E)(T)E 其中E表明運算表達式(得到一個值),T表明一個類型標識符。強制多態使得類型檢查複雜化,尤爲在容許重載的狀況下,會致使沒法消解的二義性。

  • 類型參數化多態——模板(函數模板、類模板)

  • 包含多態——使用虛函數

    至少含有一個虛函數的類稱爲多態類,虛函數使得程序可以以動態聯編的方式達到執行結果的多態化。這種多態使用的背景是:派生類繼承基類的全部操做,或者說,基類的操做能被用於操做派生類的對象,當基類的操做不能適應派生類時,派生類就須要重載基類的操做;其表現爲C++容許用基類的指針接收派生類的地址或使用基類的引用綁定派生類的對象。

靜態聯編和動態聯編

聯編:將模塊或者函數合併在一塊兒生成可執行代碼的處理過程,同時對每一個模塊或者函數分配內存地址,而且對外部訪問也分配正確的內存地址。

靜態聯編:在編譯階段就將函數實現和函數調用綁定。靜態聯編在編譯階段就必須瞭解全部函數的或模塊執行所須要的信息,它對函數的選擇是基於指向對象的指針(或者引用)的類型。C語言中,全部的聯編都是靜態聯編,C++中通常狀況下的聯編也是靜態聯編。

動態聯編:在程序運行的時候才進行函數實現和函數調用的綁定稱之爲動態聯編(dynamic binding)

#include <iostream>
#define PI 3.14159265
using namespace std;
class Point {
public:
	Point(double x = 0, double y = 0) :x(x), y(y) {}
	double area_static() { return 0; }	//不是虛函數,只會在編譯期綁定,造成靜態聯編
	virtual double area_dynamic() { return 0; } //用虛函數聲明,則編譯時只作賦值兼容的合法性檢查,而不作綁定
private:
	double x, y;
};
class Circle :public Point {
public:
	Circle(double r = 1.0) :r(r) {} //因爲基類中的構造函數非必須顯式傳參,因此係統會自動調用基類帶默認參數的構造函數
	Circle(double x, double y, double r=1.0) :Point(x, y), r(r) {} //重載一個可傳座標點、半徑值參數的構造函數
	double area_static() { return PI * r * r; } //靜態聯編
	double area_dynamic() { return PI * r * r; } //動態聯編(仍爲虛函數),爲使可讀性更好,可在不缺省virutal關鍵字
private:
	double r;
};
int main() {
	Point o(2.5, 2.5); Circle c(2.5, 2.5, 1);
	Point* po = &o, * pc = &c, & y_c = c;
//下面五個所有爲靜態聯編,不管指針指向的是基類仍是派生類,因爲指針類型爲基類類型,且調用的不是虛函數,則統一綁定爲基類中的函數
	cout << "Point area =" << o.area_static() << endl;	//值爲0
	cout << "Circle area=" << c.area_static() << endl;	//值爲3.14159
	cout << "the o area from po:" << po->area_static() << endl; //值爲0
	cout << "the c area from pc:" << pc->area_static() << endl;	//值爲0
	cout << "the c area from cite y_c:" << y_c.area_static() << endl; //值爲0
//下面三個爲動態聯編,有指針(或引用)、虛函數,則所調用的虛函數會在運行時經過vptr指針找到虛函數表	,根據指針指向的實際對象(而非指針類型)來斷定調用誰的函數
	cout << "the o area from po:" << po->area_dynamic() << endl; //值爲0
	cout << "the c area from pc:" << pc->area_dynamic() << endl; //值爲3.14159
	cout << "the c area from cite y_c:" << y_c.area_dynamic() << endl; //值爲3.14159
    //強制使用靜態聯編
    cout << "the c area calculated by Point::area_():" << pc->Point::area_dynamic() << endl; //值爲0
	return 0;
}

動態聯編與虛函數

  • 當調用虛函數時,先經過vptr指針(編譯虛函數時,編譯器會爲類自動生成一個指向虛函數表的vptr指針)找到虛函數表,而後再找出虛函數的真正地址,再調用它

  • 派生類能繼承基類的虛函數表,並且只要是和基類同名(參數也相同)的成員函數,不管是否使用virtual聲明,它們都自動成爲虛函數。若是派生類沒有改寫繼承基類的虛函數,則函數指針調用基類的虛函數;若是派生類改寫了基類的虛函數,編譯器將從新爲派生類的虛函數創建地址,函數指針會調用改寫後的虛函數。

相關文章
相關標籤/搜索