您好,登錄后才能下訂單哦!
這篇“C++中多態怎么實現和使用”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“C++中多態怎么實現和使用”文章吧。
賦值兼容說的是在使用基類對象的地方可以使用公有繼承類的對象來代替。賦值兼容是一種默認的行為,不需要進行顯式轉換就能夠實現。
就比如在派生類拷貝構造函數的參數初始化列表中,我們會直接使用派生類對象作為基類拷貝構造函數的參數,而不會報錯,這就是賦值兼容的表現。賦值兼容主要表現在:
派生類的對象可以賦值給基類對象
派生類的對象可以初始化基類的引用
派生類對象的地址可以賦值給指向基類的指針
但,發生賦值兼容之后,只能使用從基類繼承的成員
#include <iostream> using namespace std; class PERSON { public: PERSON(char *name_ = "***",char sex_ = '*') :name(name_),sex(sex_){} void display() { cout<<"The name is "<<name<<endl; cout<<"The sex is "<<sex<<endl; } protected: char *name; char sex; }; class STUDENT:public PERSON { public: STUDENT(char *name_ = "***",char sex_ = '*',char *num_ = "100") :PERSON(name_,sex_),num(num_){} void display() { cout<<"The name is "<<name<<endl; cout<<"The sex is "<<sex<<endl; cout<<"The num is "<<num<<endl; } private: char *num; }; int main() { STUDENT st("zhangsan",'x',"100"); st.display(); PERSON per = st; per.display(); PERSON &per2 = st; per2.display(); PERSON *per3 = &st; per3->display(); return 0; }
結果為:
The name is zhangsan
The sex is x
The num is 100
The name is zhangsan
The sex is x
The name is zhangsan
The sex is x
The name is zhangsan
The sex is x
上邊的程序可以看出,基類對象,引用和指針都可以使用派生類對象或者指針進行賦值,從而進行訪問。
其實也可以將基類指針強制轉換為派生類指針,進行訪問,但這種形式絕不是賦值兼容:
int main() { PERSON per("zhangsan",'x'); per.display(); STUDENT *st = static_cast<STUDENT *>(&per); st->display(); return 0; }
結果為:
The name is zhangsan
The sex is x
The name is zhangsan
The sex is x
The num is 夽@
上邊的程序中,是將基類的指針強制轉換派生類的指針,從而調用派生類的對象。
從實際上來說,該過程只是將以基類地址其實的一段內存交給了派生類的指針,因為類對象只存儲數據成員,因此能夠對應訪問到從基類繼承到的數據成員。
但同時不確定原來基類成員后邊的空間有什么東西,結果為出現部分亂碼。
如果將基類和派生類位置對調就是賦值兼容了。
C++ 中的多態主要說的是,在面向對象中,接口的多種不同的實現方式。
C++ 中的多態是接口多種不同的實現方式。而我們之前提到過的函數重載也是接口的多種不同的實現方式,因此也可以稱之為多態,只是函數重載是在編譯階段通過 name mangling 實現的,所以叫做靜多態。
而不在編譯階段而是在運行階段決定的多態就稱為動多態。動多態的形成條件為:
父類中有虛函數
子類 override 父類中的的虛函數
通過已被子類對象賦值的父類指針或引用,調用公有接口
class classname { virtual datatype func(argu); }
#include <iostream> using namespace std; class PERSON { public: PERSON(char *name_ = "***",char sex_ = '*') :name(name_),sex(sex_){} virtual void display() { cout<<"The name is "<<name<<endl; cout<<"The sex is "<<sex<<endl; } protected: char *name; char sex; }; class STUDENT:public PERSON { public: STUDENT(char *name_ = "***",char sex_ = '*',char *num_ = "100") :PERSON(name_,sex_),num(num_){} virtual void display() { cout<<"The name is "<<name<<endl; cout<<"The sex is "<<sex<<endl; cout<<"The num is "<<num<<endl; } protected: char *num; }; class POSTGRADUATE:public STUDENT { public: POSTGRADUATE(char *name_ = "***",char sex_ = '*',char *num_ = "***",char *job_ = "***") :STUDENT(name_,sex_,num_),job(job_){} virtual void display() { cout<<"The name is "<<name<<endl; cout<<"The sex is "<<sex<<endl; cout<<"The num is "<<num<<endl; cout<<"The job is "<<job<<endl; } protected: char *job; }; int main() { POSTGRADUATE po("zhsangsan",'x',"100","paper"); po.display(); PERSON *per = &po; per->display(); STUDENT *st = &po; st->display(); return 0; }
結果為:
The name is zhsangsan
The sex is x
The num is 100
The job is paper
The name is zhsangsan
The sex is x
The num is 100
The job is paper
The name is zhsangsan
The sex is x
The num is 100
The job is paper
在基類中聲明虛函數時需要使用 virtual 關鍵字,在類外實現虛函數時,不用再加 virtual
在派生類中重新定義此函數的過程稱為 override,此過程要求函數的要素全都不能發生改變,包括函數名,返回值類型,形參個數和類型,只有函數體可以改變
當基類中的函數成員被聲明為 virtual 時,其派生類中完全相同的函數都會變為虛函數,原則上派生類中的虛函數不用使用 virtual 關鍵字,但是為了程序的可讀性,可以在派生類的對應函數前加上 virtual
定義一個指向基類的指針,并使其指向其子類對象的地址,通過該指針調用虛函數,此時調用的就是指針變量指向的對象
子類中 override 的函數,可以為任意訪問類型
通過多態就避免了賦值兼容的問題
在虛函數的使用中,需要在派生類中 override 基類中的虛函數,表明該函數是從基類 override 得到的,override 的含義表明:
override 的函數要素全都不能發生改變
包括函數名,返回值類型,形參個數和類型
只有函數體可以改變
而有時為了可讀性,也為了防止編寫時出錯,可以通過在函數后添加 override 關鍵字表明這是 override 得到的。如上邊的例子中:
virtual void display() override
使用上邊的形式可以嚴格語法書寫。
對于一些抽象基類來說,我們并不需要在其中的虛函數中編寫什么語句,因此可以將之寫成純虛函數。
class classname { virtual datatype func(argu) = 0; }
如上例所示,可以將 STUDENT 中的 display 函數定義為純虛函數:
virtual void display() = 0;
只是此時不能夠調用 STUDENT 中的該函數了。
對于純虛函數而言:
含有純虛函數的類,稱為抽象基類,不能夠創建該類對象,該類只能被繼承,提供公共接口
純虛函數的聲明形式就包含了聲明和實現
如果一個類中聲明了純虛函數,而在派生類中沒有定義該函數,則該虛函數在派生類中仍然是純虛函數,派生類仍然為抽象基類,這意味著在第一次繼承的時候一定要定義該函數
從這個角度看,才算是虛函數的正確用法,直接用來聲明為純虛基類,從而被繼承
含有虛函數的類,析構函數也應該聲明為虛函數。
#include <iostream> using namespace std; class PERSON { public: PERSON(char *name_ = "***",char sex_ = '*') :name(name_),sex(sex_){} virtual void display() { cout<<"The name is "<<name<<endl; cout<<"The sex is "<<sex<<endl; } ~PERSON(){cout<<"PERSON"<<endl;} protected: char *name; char sex; }; class STUDENT:public PERSON { public: STUDENT(char *name_ = "***",char sex_ = '*',char *num_ = "100") :PERSON(name_,sex_),num(num_){} virtual void display() { cout<<"The name is "<<name<<endl; cout<<"The sex is "<<sex<<endl; cout<<"The num is "<<num<<endl; } ~STUDENT(){cout<<"STUDENT"<<endl;} protected: char *num; }; int main() { { STUDENT st("zhsangsan",'x',"100"); st.display(); } cout<<"****************"<<endl; PERSON *p = new STUDENT("zhsangsan",'x',"100"); p->display(); delete p; return 0; }
結果為:
The name is zhsangsan
The sex is x
The num is 100
STUDENT
PERSON
****************
The name is zhsangsan
The sex is x
The num is 100
PERSON
此時如果將析構函數聲明為 virtual:
virtual ~PERSON(){cout<<"PERSON"<<endl;}
結果為:
The name is zhsangsan
The sex is x
The num is 100
STUDENT
PERSON
****************
The name is zhsangsan
The sex is x
The num is 100
STUDENT
PERSON
可以看出,對于堆對象來說,含有虛函數的類對象析構與棧對象析構是有所差別的。為了防止這種情況出現,最好是將含有虛函數的析構函數聲明為 virtual。
因為虛函數是用在繼承中的,因此只有類成員函數才能聲明為虛函數
靜態成員函數不能是虛函數
內聯函數不能是虛函數
構造函數不能是虛函數
析構函數可以是虛函數且通常聲明為虛函數
(Run Time Type Identification,RTTI) 也叫運行時類型信息,也是通過多態實現的。
typeid 返回包含操作數數據類型信息的 type_info 對象的一個引用,信息中包括數據類型的名稱。要使用 typeid,需要在函數中包含:
#include <typeinfo>
type_info 重載了操作符 ==,!= 用來進行比較
函數 name() 返回類型名稱
type_info 的拷貝和賦值都是私有的,因此不可拷貝和賦值
#include <iostream> #include <typeinfo> using namespace std; typedef void (*Func)(); class Base1 { }; class Base2 { public: virtual ~Base2(){} }; class Derive1:public Base1 { }; class Derive2:public Base2 { }; int main() { cout<<typeid(int).name()<<endl; cout<<typeid(double).name()<<endl; cout<<typeid(char *).name()<<endl; cout<<typeid(char **).name()<<endl; cout<<typeid(const char *).name()<<endl; cout<<typeid(const char * const ).name()<<endl; cout<<"********************"<<endl; cout<<typeid(Func).name()<<endl; cout<<typeid(Base1).name()<<endl; cout<<typeid(Base2).name()<<endl; cout<<typeid(Derive1).name()<<endl; cout<<typeid(Derive2).name()<<endl; cout<<"********************"<<endl; Derive1 d; Base1 &b = d; cout<<typeid(b).name()<<endl; cout<<typeid(d).name()<<endl; cout<<"********************"<<endl; Derive2 dd; Base2 &bb = dd; cout<<typeid(bb).name()<<endl; cout<<typeid(dd).name()<<endl; cout<<"********************"<<endl; Base1 *p = &d; cout<<typeid(p).name()<<endl; cout<<typeid(*p).name()<<endl; cout<<typeid(d).name()<<endl; cout<<boolalpha<<(typeid(*p)== typeid(d))<<endl; cout<<"********************"<<endl; Base2 *pp = ⅆ cout<<typeid(pp).name()<<endl; cout<<typeid(*pp).name()<<endl; cout<<typeid(dd).name()<<endl; cout<<boolalpha<<(typeid(*pp)== typeid(dd))<<endl; cout<<"********************"<<endl; return 0; }
結果為:
i
d
Pc
PPc
PKc
PKc
********************
PFvvE
5Base1
5Base2
7Derive1
7Derive2
********************
5Base1
7Derive1
********************
7Derive2
7Derive2
********************
P5Base1
5Base1
7Derive1
false
********************
P5Base2
7Derive2
7Derive2
true
********************
從上邊可以看出,在 typeid 涉及到虛函數時,利用指針得到的結果就可能出現差別,因此在使用 typeid 時需要注意:
確保基類中至少定義了一個虛函數(虛析構也可)
在涉及到虛函數時,盡量不要將 typeid 應用于指針,而是應用于引用,或者解引用的指針
typeid 是一個運算符,而不是函數
typeid 運算符返回的 type_info 類型,其拷貝構造函數和賦值運算函數都聲明為 private,因此不能用于 stl 容器。也因此我們一般不直接保存 type_info,而是保存 type_info 的 name 信息
Notice how the type that typeid considers for pointers is the pointer type itself(both a and b are of type class Base *). However, when typeid is applied to objects(like *a and *b) typeid yields their dynamic type (i.e. the type of their most derived complete object).
If the type typeid evaluates is a pointer preceded by the dereference operator (*), and this pointer has a null value, typeid throws a bad_typeid exception.
在之前的文章中,我們簡單介紹過 static_cast,reininterpreter_cast,const_cast 的用法,當時還剩下一個 dynamic_cast。
dynamic_cast 是一種運行時的類型轉換方式,因此用于運行時的轉換判斷。該轉換能夠檢查指針所指向的類型,然后判斷這一類型與轉換的目標類型是否相同,如果是返回對象地址,如果不是返回 NULL。
dynamic_cast 常用于多態繼承中,來判斷父類指針的真實指向。
#include <iostream> #include <typeinfo> using namespace std; class A { public: virtual ~A(){} }; class B:public A { }; class C:public A { }; class D { }; int main() { B b; A *pa = &b; B *pb = dynamic_cast<B*>(pa); //成功 cout<<pb<<endl; C *pc = dynamic_cast<C*>(pa); //成功 安全 cout<<pc<<endl; D *pd = dynamic_cast<D*>(pa); //成功 安全 cout<<pd<<endl; pb = static_cast<B*>(pa); //成功 cout<<pb<<endl; pc = static_cast<C*>(pa); //成功 不安全 cout<<pc<<endl; pb = reinterpret_cast<B*>(pa); //成功 不安全 cout<<pb<<endl; pc = reinterpret_cast<C*>(pa); //成功 不安全 cout<<pc<<endl; pd = reinterpret_cast<D*>(pa); //成功 不安全 cout<<pd<<endl; return 0; }
結果為:
0x61fe8c
0
0
0x61fe8c
0x61fe8c
0x61fe8c
0x61fe8c
0x61fe8c
在上述幾種類型轉換中,dynamic_cast 的轉換用法算是比較安全的,因為這種轉換方式是先比較再返回的,而 reininterpreter_cast 則是最不安全的,因為這種轉換方式不做類型檢查直接將源類型重新解釋為目標類型,容易出錯。
但 dynamic_cast 的目標類型必須是類的指針或者引用。
之前介紹函數重載,也就是靜多態是通過 name mangling 實現的,而 C++ 的動多態則是通過虛函數表(virtual table)實現的。這個表中主要是一個類的虛函數的地址表,這張表包含了繼承,override 的情況。在實際使用中,在含有虛函數的類對象中,該表會被分配到該對象的內存中,用于指明實際所要調用的函數。
C++ 編譯器保證虛函數表的指針存在于實例對象的最前面,這表示實例對象的地址就是該虛函數表的位置,然后就可以遍歷其中的函數指針,進行調用。
#include <iostream> #include <typeinfo> using namespace std; class Base { public: void f() { cout << "Base::f" << endl; } void g() { cout << "Base::g" << endl; } void h() { cout << "Base::h" << endl; } private: int data; }; int main() { Base b; cout<<"sizeof(Base) = "<<sizeof(Base)<<endl; cout<<"sizeof(b) = "<<sizeof(b)<<endl; return 0; }
結果為:
sizeof(Base) = 4
sizeof(b) = 4
如果基類中存在虛函數,則為:
#include <iostream> #include <typeinfo> using namespace std; class Base { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } private: int data; }; int main() { Base b; cout<<"sizeof(Base) = "<<sizeof(Base)<<endl; cout<<"sizeof(b) = "<<sizeof(b)<<endl; return 0; }
結果為:
sizeof(Base) = 8
sizeof(b) = 8
可以看出有虛函數的基類的大小會比沒有虛函數的基類大小多出一個指針的大小。這個多出來的指針就是虛函數表的位置。
#include <iostream> #include <typeinfo> using namespace std; class Base { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } private: int data; }; typedef void (*FUNC)(void); int main() { Base b; cout<<"sizeof(Base) = "<<sizeof(Base)<<endl; cout<<"sizeof(b) = "<<sizeof(b)<<endl; cout<<&b<<endl; cout<<*((int **)*(int *)(&b)+0)<<endl; cout<<*((int **)*(int *)(&b)+1)<<endl; cout<<*((int **)*(int *)(&b)+2)<<endl; cout<<*((int **)*(int *)(&b)+3)<<endl; FUNC pf = NULL; pf = (FUNC)*((int **)*(int *)(&b)+0); pf(); pf = (FUNC)*((int **)*(int *)(&b)+1); pf(); pf = (FUNC)*((int **)*(int *)(&b)+2); pf(); return 0; }
結果為:
sizeof(Base) = 8
sizeof(b) = 8
0x61fe94
0x4029f0
0x402a24
0x402a58
0x3a434347
Base::f
Base::g
Base::h
上面的程序中:
先將 &b 轉換為 int *(這樣能夠保證 +1 一次增加一個指針的大小),取得虛函數表的地址
然后,再次取址就得到了第一個虛函數的地址,也就是 Base::f
最后再轉換為 int **,通過 +1,+2 后取址,就能夠得到 Base::g,Base::h
#include <iostream> #include <typeinfo> using namespace std; class Base { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } private: int data; }; class Derive:public Base { virtual void f1() { cout << "Base::f1" << endl; } virtual void g1() { cout << "Base::g1" << endl; } virtual void h2() { cout << "Base::h2" << endl; } }; typedef void (*FUNC)(void); int main() { Derive b; cout<<"sizeof(Base) = "<<sizeof(Base)<<endl; cout<<"sizeof(Derive) = "<<sizeof(Derive)<<endl; cout<<"sizeof(b) = "<<sizeof(b)<<endl; cout<<&b<<endl; cout<<*((int **)*(int *)(&b)+0)<<endl; cout<<*((int **)*(int *)(&b)+1)<<endl; cout<<*((int **)*(int *)(&b)+2)<<endl; cout<<*((int **)*(int *)(&b)+3)<<endl; cout<<*((int **)*(int *)(&b)+4)<<endl; cout<<*((int **)*(int *)(&b)+5)<<endl; cout<<*((int **)*(int *)(&b)+6)<<endl; FUNC pf = NULL; pf = (FUNC)*((int **)*(int *)(&b)+0); pf(); pf = (FUNC)*((int **)*(int *)(&b)+1); pf(); pf = (FUNC)*((int **)*(int *)(&b)+2); pf(); pf = (FUNC)*((int **)*(int *)(&b)+3); pf(); pf = (FUNC)*((int **)*(int *)(&b)+4); pf(); pf = (FUNC)*((int **)*(int *)(&b)+5); pf(); return 0; }
結果為:
sizeof(Base) = 8
sizeof(Derive) = 8
sizeof(b) = 8
0x61fe94
0x402ae0
0x402b14
0x402b48
0x402b94
0x402bc8
0x402bfc
0x3a434347
Base::f
Base::g
Base::h
Base::f1
Base::g1
Base::h2
在上邊的例子中,派生類沒有 override 任何父類的函數,并又重新定義了幾個虛函數,因此對于派生類來說:
虛函數按照其聲明順序在表中存放
父類的虛函數在子類的虛函數前邊
#include <iostream> #include <typeinfo> using namespace std; class Base { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } private: int data; }; class Derive:public Base { virtual void f() { cout << "Base::f1" << endl; } virtual void g1() { cout << "Base::g1" << endl; } virtual void h2() { cout << "Base::h2" << endl; } }; typedef void (*FUNC)(void); int main() { Derive b; cout<<"sizeof(Base) = "<<sizeof(Base)<<endl; cout<<"sizeof(Derive) = "<<sizeof(Derive)<<endl; cout<<"sizeof(b) = "<<sizeof(b)<<endl; cout<<&b<<endl; cout<<*((int **)*(int *)(&b)+0)<<endl; cout<<*((int **)*(int *)(&b)+1)<<endl; cout<<*((int **)*(int *)(&b)+2)<<endl; cout<<*((int **)*(int *)(&b)+3)<<endl; cout<<*((int **)*(int *)(&b)+4)<<endl; cout<<*((int **)*(int *)(&b)+5)<<endl; FUNC pf = NULL; pf = (FUNC)*((int **)*(int *)(&b)+0); pf(); pf = (FUNC)*((int **)*(int *)(&b)+1); pf(); pf = (FUNC)*((int **)*(int *)(&b)+2); pf(); pf = (FUNC)*((int **)*(int *)(&b)+3); pf(); pf = (FUNC)*((int **)*(int *)(&b)+4); pf(); return 0; }
結果為:
sizeof(Base) = 8
sizeof(Derive) = 8
sizeof(b) = 8
0x61fe94
0x402b54
0x402ad4
0x402b08
0x402b88
0x402bbc
0x3a434347
Base::f1
Base::g
Base::h
Base::g1
Base::h2
在上邊的例子中,派生類 override 了父類的 f 函數,并又重新定義了幾個虛函數,因此對于派生類來說:
override 的 f 函數被放到了虛函數表中原來基類虛函數的位置
沒有 override 的函數不變
Base *b = new Derive(); b->f();
這段代碼的實際過程為:
明確 b 類型
通過指向虛函數表的指針和偏移量,來匹配虛函數的地址
根據地址調用虛函數
以上就是關于“C++中多態怎么實現和使用”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。