この記事では、C++ の基本的な仮想関数クラスの継承とポリモーフィズムに関する関連情報を、サンプル コードを通じて詳細に紹介します。この記事は、学習や仕事に必要な学習に役立ちます。以下のエディターと一緒に学びましょう。
はじめに
この記事では主に、C++ の継承とポリモーフィズムの基本的な仮想関数クラスに関する関連コンテンツを紹介し、参考と学習のために共有します。以下では多くを述べません。詳細を見てみましょう。
仮想関数クラス
継承では仮想継承についてよく言及しますが、仮想関数クラスのメンバー関数の前に仮想キーワードが追加されている場合の仮想関数について見てみましょう。これは仮想関数と呼ばれます。この仮想関数を過小評価しないでください。この関数は継承における多くの厄介な問題を解決できます。これがなければポリモーフィズムは存在しないため、この知識も非常に重要です。後で紹介する仮想関数テーブルと同様に、これらはすべて非常に重要であり、注意深く理解する必要があります。仮想関数の概念は、親とまったく同じ仮想関数を書き換える (上書きする) という別の概念につながります。クラスがサブクラス内で定義されている場合、それはサブクラスと呼ばれます。クラスのこの関数は、親クラスの仮想関数をオーバーライドします (オーバーライドとも呼ばれます)。まず仮想関数テーブルについて触れますが、後述しますが、書き換えとは、サブクラスの仮想関数テーブルにある親クラスのオーバーライドされた関数のアドレスをすべてサブクラスの関数のアドレスに変更することです。
純粋仮想関数
メンバー関数の仮引数の後に =0 と書くと、そのメンバー関数は純粋仮想関数になります。純粋な仮想関数を含むクラスは、抽象クラス (インターフェイス クラスとも呼ばれます) と呼ばれます
抽象クラスはオブジェクトをインスタンス化できません。純粋仮想関数が派生クラスで再定義された後でのみ、派生クラスはオブジェクトをインスタンス化できます。
例を見てみましょう:
class Person { virtual void Display () = 0; // 纯虚函数 protected : string _name ; // 姓名 }; class Student : public Person {};
まず概念を要約しましょう:
1. 派生クラスは、関数名、パラメーター リスト、および戻り値を必要とするポリモーフィズムを実現するために、基本クラスの仮想関数を書き換えます。値がまったく同じになるようにします。 (共分散を除く)
2. 仮想関数は基底クラスで定義され、その関数は派生クラスでも常に仮想関数の性質を維持します。
3. クラスのメンバー関数のみを仮想関数として定義できます。
4. 静的メンバー関数は仮想関数として定義できません。
5. クラスの外で仮想関数を定義する場合、関数の宣言時にのみ仮想を追加できます。クラスの外で関数を定義する場合は、仮想を追加できません。
6. コンストラクターとデストラクターでは仮想関数を呼び出さないでください。オブジェクトは不完全であり、未定義の動作が発生する可能性があります。
7. 基本クラスのデストラクターを仮想関数として宣言するのが最善です。 (なぜですか?また、派生クラスのデストラクター名は基底クラスのデストラクター名と異なりますが、オーバーライドに相当するため、デストラクターは特別です。これは、コンパイラーが特別な処理を行っているためです)
8.コンストラクターは仮想関数ではありません。なぜ上記の概念が必要なのか この内容の答えは次の知識で見つかります~ さて、それでは今日の主役
仮想関数
仮想関数テーブルとは何ですか?プログラムを作成し、監視ウィンドウを調整して知ることができます。
#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が指すものは私たちの主人公であるvirtualです関数テーブル。単一継承か多重継承かにかかわらず、ダイヤモンド型の継承仮想関数テーブルですら形状が異なることは誰でもすぐにわかります。仮想関数テーブルは非常に興味深いものです。
単一継承のメモリレイアウトを勉強しましょう
次のコードを注意深く見てください:
#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()
,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 中国語 Web サイトの他の関連記事を参照してください。