這篇文章主要給大家介紹了關於C++中繼承與多態的基礎虛函數類的相關資料,文中透過範例程式碼介紹的非常詳細,對大家的學習或工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。
前言
本文主要為大家介紹了關於C++中繼承與多態的基礎虛函數類別的相關內容,並分享出來供大家參考學習,下面話不多說了,來一起看看詳細的介紹吧。
虛函數類別
在繼承中我們經常提到虛擬繼承,現在我們來探究這種的虛函數,虛函數類別的成員函數前面加virtual關鍵字,則這個成員函數稱為虛函數,不要小看這個虛函數,他可以解決繼承中許多棘手的問題,而對於多態那他更重要了,沒有它就沒有多態,所以這個知識點非常重要,以及後面介紹的虛函數表都極其重要,一定要認真的理解~ 現在開始概念虛函數就又引出一個概念,那就是重寫(覆蓋),當在子類別的定義了一個與父類別完全相同的虛擬函數時,則稱子類別的這個函數重寫(也稱為覆蓋)了父類別的這個虛擬函數。這裡先提一下虛函數表,後面會講到的,重寫就是將子類別裡面的虛函數表裡的被重寫父類別的函數位址全都改成子類別函數的位址。
純虛函數
在成員函數的形參後面寫上=0,則成員函數為純虛函數。包含純虛函數的類別叫做抽象類別(也叫介面類別)
抽象類別不能實例化出物件。純虛函數在衍生類別中重新定義以後,衍生類別才能實例化出物件。
看一個例子:
class Person { virtual void Display () = 0; // 纯虚函数 protected : string _name ; // 姓名 }; class Student : public Person {};
先總結一下概念:
1.衍生類別重寫基類別的虛擬函數實現多態,要求函數名稱、參數列表、傳回值完全相同。 (協變除外)
2.基底類別中定義了虛函數,在衍生類別中該函數始終保持虛函數的特性。
3.只有類別的成員函數才能定義為虛函數。
4.靜態成員函數不能定義為虛函數。
5.如果在類別外定義虛擬函數,只能在宣告函數時加上virtual,類別外定義函數時不能加上virtual。
6.不要在建構子和析構函式裡面呼叫虛函數,在建構子和析構函式中,物件是不完整的,可能會發生未定義的行為。
7.最好把基底類別的析構函數宣告為虛函數。 (why?另外析構函數比較特殊,因為衍生類別的析構函式跟基底類別的析構函式名稱不一樣,但是構成覆寫,這裡是因為編譯器做了特殊處理)
#8.建構函數不能為虛擬函數,雖然可以將operator=定義為虛函數,但是最好不要將operator=定義為虛函數,因為容易使用時容易造成混淆.
上面概念大家可能都會問一句為什麼要這樣? 這些內容在接下來的知識裡都能找到答案~ 好了那麼我們今天的主角虛函數#登場!!!!
何為虛函數表,我們寫一個程序,調一個監視視窗就知道了。
下面是一個有虛擬函數的類別:
#include<iostream> #include<windows.h> using namespacestd; class Base { public: virtual void func1() {} virtual void func2() {} private: inta; }; void Test1() { Base b1; } int main() { Test1(); system("pause"); return0; }
我們現在點開b1的監視視窗
這裡面有一個_vfptr,而這個_vfptr指向的東西就是我們的主角,虛函數表。一會兒大家就知道了,無論是單繼承還是多繼承甚至於我們的菱形繼承虛函數表都會有不同的形態,虛函數表是一個很有趣的東西。
我們來研究單繼承的記憶體格局
#include<iostream> #include<windows.h> using namespace std; class Base { public: virtual void func1() { cout<< "Base::func1"<< endl; } virtual void func2() { cout<< "Base::func2"<< endl; } private: inta; }; class Derive:public Base { public: virtual void func1() { cout<< "Derive::func1"<< endl; } virtual void func3() { cout<< "Derive::func3"<< endl; } virtual void func4() { cout<< "Derive::func4"<< endl; } private: int b; };
fun1()重寫了父類別的
fun1() ,虛表裡存的是子類別的
fun1() ,接下來父類別的
fun2() ,子類別的
fun3() ,
fun4()都是虛函數,所以虛表裡會有4個元素,分別為子類別的
fun1() ,父類別
fun2() ,子類別
fun3() ,子類
fun4() 。然後我們調出監視視窗看我們想的到底對不對呢?
#
我预计应该是看到fun1()
,fun2()
,fun3()
,fun4()
的虚函数表,但是呢这里监视窗口只有两个fun1()
, fun2()
,难道我们错了????
这里并不是这样的,只有自己靠得住,我觉得这里的编译器有问题,那我们就得自己探索一下了。 但是在探索之前我们必须来实现一个可以打印虚函数表的函数。
typedef void(*FUNC)(void); void PrintVTable(int* VTable) { cout<< " 虚表地址"<<VTable<< endl; for(inti = 0;VTable[i] != 0; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i,VTable[i]); FUNC f = (FUNC)VTable[i]; f(); } cout<< endl; } int main() { Derive d1; PrintVTable((int*)(*(int*)(&d1))); system("pause"); return0; }
下图来说一下他的缘由:
我们来使用这个函数,该函数代码如下:
//单继承 class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } private: int a; }; class Derive :public Base { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } virtual void func4() { cout << "Derive::func4" << endl; } private: int b; }; typedef void(*FUNC)(void); void PrintVTable(int* VTable) { cout<< " 虚表地址"<<VTable<< endl; for(inti = 0;VTable[i] != 0; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i,VTable[i]); FUNC f = (FUNC)VTable[i]; f(); } cout<< endl; } int main() { Derive d1; PrintVTable((int*)(*(int*)(&d1))); //重点 system("pause"); return0; }
这里我就要讲讲这个传参了,注意这里的传参不好理解,应当细细的"品味".
PrintVTable((int*)(*(int*)(&d1)));
首先我们肯定要拿到d1的首地址,把它强转成int*,让他读取到前4个字节的内容(也就是指向虚表的地址),再然后对那个地址解引用,我们已经拿到虚表的首地址的内容(虚表里面存储的第一个函数的地址)了,但是此时这个变量的类型解引用后是int,不能够传入函数,所以我们再对他进行一个int*的强制类型转换,这样我们就传入参数了,开始函数执行了,我们一切都是在可控的情况下使用强转,使用强转你必须要特别清楚的知道内存的分布结构。
最后我们来看看输出结果:
到底打印的对不对呢? 我们验证一下:
这里我们通过&d1的首地址找到虚表的地址,然后访问地址查看虚表的内容,验证我们自己写的这个函数是正确的。(这里VS还有一个bug,当你第一次打印虚表时程序可能会崩溃,不要担心你重新生成解决方案,再运行一次就可以了。因为当你第一次打印是你虚表最后一个地方可能没有放0,所以你就有可能停不下来然后崩溃。)我们可以看到d1的虚表并不是监视器里面打印的那个样子的,所以有时候VS也会有bug,不要太相信别人,还是自己靠得住。哈哈哈,臭美一下~
我们来研究一下多继承的内存格局
探究完了单继承,我们来看看多继承,我们还是通过代码调试的方法来探究对象模型
看如下代码:
class Base1 { public: virtual void func1() { cout << "Base1::func1" << endl; } virtual void func2() { cout << "Base1::func2" << endl; } private: int b1; }; class Base2 { public: virtual void func1() { cout << "Base2::func1" << endl; } virtual void func2() { cout << "Base2::func2" << endl; } private: int b2; }; class Derive : public Base1, public Base2 { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual void func3() { cout << "Derive::func3" << endl; } private: int d1; }; typedef void(*FUNC) (); void PrintVTable(int* VTable) { cout << " 虚表地址>" << VTable << endl; for (int i = 0; VTable[i] != 0; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i, VTable[i]); FUNC f = (FUNC)VTable[i]; f(); } cout << endl; } void Test1() { Derive d1; //Base2虚函数表在对象Base1后面 int* VTable = (int*)(*(int*)&d1); PrintVTable(VTable); int* VTable2 = (int *)(*((int*)&d1 + sizeof (Base1) / 4)); PrintVTable(VTable2); } int main() { Test1(); system("pause"); return 0; }
现在我们现在知道会有两个虚函数表,分别是Base1和Base2的虚函数表,但是呢!我们的子类里的fun3()
函数怎么办?它是放在Base1里还是Base2里还是自己开辟一个虚函数表呢?我们先调一下监视窗口:
监视窗口又不靠谱了。。。。完全没有找到fun3().那我们直接看打印出来的虚函数表。
现在很清楚了,fun3()
在Base1的虚函数表中,而Base1是先继承的类,好了现在我们记住这个结论,当涉及多继承时,子类的虚函数会存在先继承的那个类的虚函数表里。记住了!
我们现在来看多继承的对象模型:
现在我们来结束一下上面我列的那么多概念现在我来逐一的解释为什么要这样.
1.为什么静态成员函数不能定义为虚函数?
因为静态成员函数它是一个大家共享的一个资源,但是这个静态成员函数没有this指针,而且虚函数变只有对象才能能调到,但是静态成员函数不需要对象就可以调用,所以这里是有冲突的.
2.为什么不要在构造函数和析构函数里面调用虚函数?
构造函数当中不适合用虚函数的原因是:在构造对象的过程中,还没有为“虚函数表”分配内存。所以,这个调用也是违背先实例化后调用的准则析构函数当中不适用虚函数的原因是:一般析构函数先析构子类的,当你在父类中调用一个重写的fun()
函数,虚函数表里面就是子类的fun()
函数,这时候已经子类已经析构了,当你调用的时候就会调用不到.
现在我在写最后一个知识点,为什么尽量最好把基类的析构函数声明为虚函数??
现在我们再来写一个例子,我们都知道平时正常的实例化对象然后再释放是没有一点问题的,但是现在我这里举一个特例:
我们都知道父类的指针可以指向子类,现在呢我们我们用一个父类的指针new一个子类的对象。
//多态 析构函数 class Base { public: virtual void func1() { cout << "Base::func1" << endl; } virtual void func2() { cout << "Base::func2" << endl; } virtual ~Base() { cout << "~Base" << endl; } private: int a; }; class Derive :public Base { public: virtual void func1() { cout << "Derive::func1" << endl; } virtual ~Derive() { cout << "~Derive"<< endl; } private: int b; }; void Test1() { Base* q = new Derive; delete q; } int main() { Test1(); system("pause"); return 0; }
这里面可能会有下一篇要说的多态,所以可能理解起来会费劲一点。
注意这里我先让父类的析构函数不为虚函数(去掉virtual),我们看看输出结果:
这里它没有调用子类的析构函数,因为他是一个父类类型指针,所以它只能调用父类的析构函数,无权访问子类的析构函数,这种调用方法会导致内存泄漏,所以这里就是有缺陷的,但是C++是不会允许自己有缺陷,他就会想办法解决这个问题,这里就运用到了我们下次要讲的多态。现在我们让加上为父类析构函数加上virtual,让它变回虚函数,我们再运行一次程序的:
诶! 子类的虚函数又被调用了,这里发生了什么呢?? 来我们老方法打开监视窗口。
刚刚这种情况就是多态,多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。这个我们下一个博客专门会总结多态.
当然虚函数的知识点远远没有这么一点,这里可能只是冰山一角,比如说菱形继承的虚函数表是什么样?然后菱形虚拟继承又是什么样子呢? 这些等我总结一下会专门写一个博客来讨论菱形继承。虚函数表我们应该已经知道是什么东西了,也知道单继承和多继承中它的应用,这些应该就足够了,这些其实都是都是为你让你更好的理解继承和多态,当然你一定到分清楚重写,重定义,重载的他们分别的含义是什么. 这一块可能有点绕,但是我们必须要掌握.
以上是介紹有關C++中繼承與多型態的基礎虛函數類的詳細內容。更多資訊請關注PHP中文網其他相關文章!