生活随笔
收集整理的這篇文章主要介紹了
C/C++学习之路: 多态
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
C/C++學習之路: 多態
目錄
多態基本概念向上類型轉換及問題如何實現動態綁定抽象基類和純虛函數純虛函數和多繼承虛析構函數重寫,重載,重定義
1. 多態基本概念
多態是面向對象程序設計語言中數據抽象和繼承之外的第三個基本特征。多態性(polymorphism)提供接口與具體實現之間的另一層隔離。多態性改善了代碼的可讀性和組織性,同時也使創建的程序具有可擴展性,項目不僅在最初創建時期可以擴展,而且當項目在需要有新的功能時也能擴展。c++支持編譯時多態(靜態多態)和運行時多態(動態多態),運算符重載和函數重載就是編譯時多態,而派生類和虛函數實現運行時多態。靜態多態和動態多態的區別就是函數地址是早綁定(靜態聯編)還是晚綁定(動態聯編)。 如果函數的調用,在編譯階段就可以確定函數的調用地址,并產生代碼,就是靜態多態(編譯時多態),就是說地址是早綁定的。而如果函數的調用地址不能編譯不能在編譯期間確定,而需要在運行時才能決定,就屬于晚綁定(動態多態,運行時多態)。
class Caculator {
public:void setA(int a
) {this->mA
= a
;}void setB(int b
) {this->mB
= b
;}void setOperator(string oper
) {this->mOperator
= oper
;}int getResult() {if (this->mOperator
== "+") {return mA
+ mB
;} else if (this->mOperator
== "-") {return mA
- mB
;} else if (this->mOperator
== "*") {return mA
* mB
;} else if (this->mOperator
== "/") {return mA
/ mB
;}}private:int mA
;int mB
;string mOperator
;
};
class AbstractCaculator {
public:void setA(int a
) {this->mA
= a
;}virtual void setB(int b
) {this->mB
= b
;}virtual int getResult() = 0;protected:int mA
;int mB
;
};
class PlusCaculator : public AbstractCaculator {
public:virtual int getResult() {return mA
+ mB
;}
};
class MinusCaculator : public AbstractCaculator {
public:virtual int getResult() {return mA
- mB
;}
};
class MultipliesCaculator : public AbstractCaculator {
public:virtual int getResult() {return mA
* mB
;}
};void DoBussiness(AbstractCaculator
*caculator
) {int a
= 10;int b
= 20;caculator
->setA(a
);caculator
->setB(b
);cout
<< "計算結果:" << caculator
->getResult() << endl
;delete caculator
;
}
2. 向上類型轉換及問題
1. 問題
對象可以作為自己的類或者作為它的基類的對象來使用,還能通過基類的地址來操作它。取一個對象的地址(指針或引用),并將其作為基類的地址來處理,這種稱為向上類型轉換。父類引用或指針可以指向子類對象,通過父類指針或引用來操作子類對象。
class Animal {
public:void speak() {cout
<< "動物在唱歌..." << endl
;}
};class Dog : public Animal {
public:void speak() {cout
<< "小狗在唱歌..." << endl
;}
};void DoBussiness(Animal
&animal
) {animal
.speak();
}void test3() {Dog dog
;DoBussiness(dog
);
}
我們給DoBussiness傳入的對象是dog,而不是animal對象,輸出的結果應該是Dog::speak。
2. 問題解決(虛函數,virtual function)
把函數體與函數調用相聯系稱為綁定(捆綁,binding)當綁定在程序運行之前(由編譯器和連接器)完成時,稱為早綁定(early binding),C語言中只有一種函數調用方式,就是早綁定。上面的問題就是由于早綁定引起的,因為編譯器在只有Animal地址時并不知道要調用的正確函數。編譯是根據指向對象的指針或引用的類型來選擇函數調用。這個時候由于DoBussiness的參數類型是Animal&,編譯器確定了應該調用的speak是Animal::speak的,而不是真正傳入的對象Dog::speak。解決方法就是遲綁定(遲捆綁,動態綁定,運行時綁定,late binding),意味著綁定要根據對象的實際類型,發生在運行。C++語言要實現這種動態綁定,必須有某種機制來確定運行時對象的類型并調用合適的成員函數。C++動態多態性是通過虛函數來實現的,虛函數允許子類(派生類)重新定義父類(基類)成員函數,而子類(派生類)重新定義父類(基類)虛函數的做法稱為覆蓋(override),或者稱為重寫。對于特定的函數進行動態綁定,c++要求在基類中聲明這個函數的時候使用virtual關鍵字,動態綁定也就對virtual函數起作用.為創建一個需要動態綁定的虛成員函數,可以簡單在這個函數聲明前面加上virtual關鍵字,定義時候不需要.如果一個函數在基類中被聲明為virtual,那么在所有派生類中它都是virtual的.在派生類中virtual函數的重定義稱為重寫(override),Virtual關鍵字只能修飾成員函數,構造函數不能為虛函數注意: 僅需要在基類中聲明一個函數為virtual.調用所有匹配基類聲明行為的派生類函數都將使用虛機制。
class Animal {
public:virtual void speak() {cout
<< "動物在唱歌..." << endl
;}
};class Dog : public Animal {
public:virtual void speak() {cout
<< "小狗在唱歌..." << endl
;}
};void DoBussiness(Animal
&animal
) {animal
.speak();
}void test3() {Dog dog
;DoBussiness(dog
);
}
3. 如何實現動態綁定
當編譯器發現我們的類中有虛函數的時候,編譯器會創建一張虛函數表,把虛函數的函數入口地址放到虛函數表中,并且在類中秘密增加一個指針,這個指針就是虛表指針(縮寫vptr),這個指針是指向對象的虛函數表。在多態調用的時候,根據vptr指針,找到虛函數表來實現動態綁定。在編譯階段,編譯器秘密增加了一個vptr指針,但是此時vptr指針并沒有初始化指向虛函數表(vtable),在對象構建的時候,也就是在對象初始化調用構造函數的時候。編譯器首先默認會在我們所編寫的每一個構造函數中,增加一些vptr指針初始化的代碼。如果沒有提供構造函數,編譯器會提供默認的構造函數,那么就會在默認構造函數里做此項工作,初始化vptr指針,使之指向本對象的虛函數表。子類繼承基類,子類繼承了基類的vptr指針,這個vptr指針是指向基類虛函數表,當子類調用構造函數,使得子類的vptr指針指向了子類的虛函數表。當子類無重寫基類虛函數時:
Animal
* animal
= new Dog
;animal
->fun1();當程序執行到這里,會去animal指向的空間中尋找vptr指針,通過vptr指針找到func1函數,此時由于子類并沒有重寫也就是覆蓋基類的func1函數,所以調用func1時,仍然調用的是基類的func1
.
當子類重寫基類虛函數時:
Animal
* animal
= new Dog
;animal
->fun1();當程序執行到這里,會去animal指向的空間中尋找vptr指針,通過vptr指針找到func1函數,由于子類重寫基類的func1函數,所以調用func1時,調用的是子類的func1
.
多態的成立條件: 有繼承子類重寫父類虛函數函數:返回值,函數名字,函數參數,必須和父類完全一致(析構函數除外),子類中virtual關鍵字可寫可不寫,建議寫類型兼容,父類指針,父類引用 指向 子類對象
4. 抽象基類和純虛函數
在設計時,常常希望基類僅僅作為其派生類的一個接口。這就是說,僅想對基類進行向上類型轉換,使用它的接口,而不希望用戶實際的創建一個基類的對象。
同時創建一個純虛函數允許接口中放置成員原函數,而不一定要提供一段可能對這個函數毫無意義的代碼。
做到這點,可以在基類中加入至少一個純虛函數(pure virtual function),使得基類稱為抽象類(abstract class).
純虛函數使用關鍵字virtual,并在其后面加上=0。如果試圖去實例化一個抽象類,編譯器則會阻止這種操作。當繼承一個抽象類的時候,必須實現所有的純虛函數,否則由抽象類派生的類也是一個抽象類。Virtual void fun() = 0;告訴編譯器在vtable中為函數保留一個位置,但在這個特定位置不放地址。 建立公共接口目的是為了將子類公共的操作抽象出來,可以通過一個公共接口來操縱一組類,且這個公共接口不需要事先(或者不需要完全實現),可以創建一個公共類。
class AbstractDrinking{
public:virtual void Boil() = 0;virtual void Brew() = 0;virtual void PourInCup() = 0;virtual void PutSomething() = 0;void MakeDrink(){Boil();Brew();PourInCup();PutSomething();}
};
class Coffee : public AbstractDrinking{
public:virtual void Boil(){cout
<< "煮農夫山泉!" << endl
;}virtual void Brew(){cout
<< "沖泡咖啡!" << endl
;}virtual void PourInCup(){cout
<< "將咖啡倒入杯中!" << endl
;}virtual void PutSomething(){cout
<< "加入牛奶!" << endl
;}
};
class Tea : public AbstractDrinking{
public:virtual void Boil(){cout
<< "煮自來水!" << endl
;}virtual void Brew(){cout
<< "沖泡茶葉!" << endl
;}virtual void PourInCup(){cout
<< "將茶水倒入杯中!" << endl
;}virtual void PutSomething(){cout
<< "加入食鹽!" << endl
;}
};
void DoBussiness(AbstractDrinking
* drink
){drink
->MakeDrink();delete drink
;
}void test(){DoBussiness(new Coffee
);cout
<< "--------------" << endl
;DoBussiness(new Tea
);
}
5. 純虛函數和多繼承
絕大數面向對象語言都不支持多繼承,但是絕大數面向對象對象語言都支持接口的概念,c++中沒有接口的概念,但是可以通過純虛函數實現接口。接口類中只有函數原型定義,沒有任何數據定義。多重繼承接口不會帶來二義性和復雜性問題。接口類只是一個功能聲明,并不是功能實現,子類需要根據功能說明定義功能實現。注意:除了析構函數外,其他聲明都是純虛函數。
6. 虛析構函數
1. 虛析構函數作用
虛析構函數是為了解決基類的指針指向派生類對象,并用基類的指針刪除派生類對象。
class People{
public:People(){cout
<< "構造函數 People!" << endl
;}virtual void showName() = 0;virtual ~People(){cout
<< "析構函數 People!" << endl
;}
};class Worker : public People{
public:Worker(){cout
<< "構造函數 Worker!" << endl
;pName
= new char[10];}virtual void showName(){cout
<< "打印子類的名字!" << endl
;}~Worker(){cout
<< "析構函數 Worker!" << endl
;if (pName
!= NULL){delete pName
;}}
private:char* pName
;
};void test(){People
* people
= new Worker
;people
->~People();
}
2. 純虛析構函數
純虛析構函數在c++中是合法的,但是在使用的時候有一個額外的限制:必須為純虛析構函數提供一個函數體。那么問題是:如果給虛析構函數提供函數體了,那怎么還能稱作純虛析構函數呢?純虛析構函數和非純析構函數之間唯一的不同之處在于純虛析構函數使得基類是抽象類,不能創建基類的對象。如果類的目的不是為了實現多態,作為基類來使用,就不要聲明虛析構函數,反之,則應該為類聲明虛析構函數。
class A{
public:virtual ~A();
};A::~A(){}
class B{
public:virtual ~B() = 0;
};B::~B(){}void test(){A a
; B b
;
}
7. 重寫,重載,重定義
重載,同一作用域的同名函數 同一個作用域參數個數,參數順序,參數類型不同和函數返回值,沒有關系const也可以作為重載條件 //do(const Teacher& t){} do(Teacher& t) 重定義(隱藏) 有繼承子類(派生類)重新定義父類(基類)的同名成員(非virtual函數) 重寫(覆蓋) 有繼承子類(派生類)重寫父類(基類)的virtual函數函數返回值,函數名字,函數參數,必須和基類中的虛函數一致
總結
以上是生活随笔為你收集整理的C/C++学习之路: 多态的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。