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