当前位置:   article > 正文

C++虚函数与静态、动态绑定_在构造函数里,不会进行动态绑定

在构造函数里,不会进行动态绑定

一些基础概念:

C++继承与多态一:继承的本质和原理、访问限定表、派生类的构造过程_Rain的博客-CSDN博客

C++继承与多态二:重载,隐藏,覆盖、继承中的类型转换_Rain的博客-CSDN博客

        

        覆盖:如果派生类中的方法,和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法,自动处理成虚函数,它们之间成为覆盖关系;也就是说派生类会在自己虚函数表中将从基类继承来的虚函数进行替换,替换成派生类自己的。

        静态绑定:编译时期的多态,通过函数的重载以及模板来实现,也就是说调用函数的地址在编译时期我们就可以确定,在汇编代码层次,呈现的就是 call  函数名;

        动态绑定:运行时期的多态,通过派生类重写基类的虚函数来实现。在汇编代码层次,呈现的就是 call 寄存器,寄存器的值只有运行起来我们才可以确定。

不存在虚函数

  1. #include <iostream>
  2. #include <typeinfo>
  3. class Base
  4. {
  5. public:
  6. Base(int data = 10): ma(data) {}
  7. ~Base() {};
  8. void show() {
  9. std::cout << "Base::show()" << std::endl;
  10. }
  11. void show(int data) {
  12. std::cout << "Base::show()" << data << std::endl;
  13. }
  14. protected:
  15. int ma;
  16. };
  17. class Derive :public Base
  18. {
  19. public:
  20. Derive(int data) :Base(data), mb(data) {}
  21. ~Derive() {}
  22. void show() {
  23. std::cout << "Derive::show()" << std::endl;
  24. }
  25. private:
  26. int mb;
  27. };
  28. int main() {
  29. Derive d(50);
  30. Base *pb = &d;
  31. pb->show();//静态(编译时期)绑定(函数调用) Base::show (06F12E4h)
  32. pb->show(10);//Base::show (06F12BCh)
  33. std::cout << "Base size:" << sizeof(Base) << std::endl;//4
  34. std::cout << "Derive size:" << sizeof(Derive) << std::endl;//8
  35. std::cout << typeid(pb).name() << std::endl;//class Base *
  36. std::cout << typeid(*pb).name() << std::endl;//class Base
  37. return 0;
  38. }

打断点,F5进入调试,点击反汇编

 可以看到调用的都是基类的show(),在编译阶段已经生成指令调用Base下的show;

可以看到结果:
因为pb是Base类型的指针,所以调用的都是Base类的成员方法;
基类Base只有一个数据成员ma,所以大小只有4字节;
派生类Derive继承了ma,其次还有自己的mb,所以有8字节;
pb的类型是一个class Base *;
*pb的类型是一个class Base。
为了更好地理解上述过程,我们简单画图如下:
在这里插入图片描述

为什么Base *类型的指针,Derive类型的对象,调用方法的时候是Base而不是Derive呢?
原因如上图:
Derive类继承了Base类,导致了派生类的大小要比基类大,而pb的类型是基类的指针,所以通过pb调用方法时只能访问到Derive中从Base继承而来的方法,访问不到自己重写的方法(指针的类型限制了指针解引用的能力)

基类定义虚函数

  1. #include <iostream>
  2. #include <typeinfo>
  3. class Base
  4. {
  5. public:
  6. Base(int data = 10): ma(data) {}
  7. ~Base() {};
  8. //虚函数
  9. virtual void show() {
  10. std::cout << "Base::show()" << std::endl;
  11. }
  12. void show(int data) {
  13. std::cout << "Base::show()" << data << std::endl;
  14. }
  15. protected:
  16. int ma;
  17. };
  18. class Derive :public Base
  19. {
  20. public:
  21. Derive(int data) :Base(data), mb(data) {}
  22. ~Derive() {}
  23. void show() {
  24. std::cout << "Derive::show()" << std::endl;
  25. }
  26. private:
  27. int mb;
  28. };
  29. int main() {
  30. Derive d(50);
  31. Base *pb = &d;
  32. /*
  33. pb->show();
  34. pb 指针是base类型,如果发现Base中的show是虚函数,就进行动态绑定
  35. mov ecx,dword ptr [pb]
  36. 00292B01 8B 45 D4 mov eax,dword ptr [pb] //将pb指向的内存前4个字节放入ecx寄存器,pb指向derive对象,前四个字节即vfptr,将虚函数表地址加载到eax
  37. 00292B04 8B 10 mov edx,dword ptr [eax] //将eax 的前四个字节 即Derive::show 加载到edx中
  38. 00292B06 8B F4 mov esi,esp
  39. 00292B08 8B 4D D4 mov ecx,dword ptr [pb]
  40. 00292B0B 8B 02 mov eax,dword ptr [edx]
  41. 00292B0D FF D0 call eax //虚函数的地址
  42. 00292B0F 3B F4 cmp esi,esp
  43. 00292B11 E8 9C E7 FF FF call __RTC_CheckEsp (02912B2h)
  44. 我们可以看到这一次,汇编码call的就不是确切的函数地址了,而是寄存器eax;
  45. 那么就很好理解了:
  46. eax寄存器里存放的是什么内容,编译阶段根本无从知晓,只能在运行的时候确定;
  47. 故,动态绑定。
  48. pb->show(10); 如果发现show是普通函数,就进行静态绑定 call Base::show
  49. */
  50. pb->show();//
  51. pb->show(10);//
  52. std::cout << "Base size:" << sizeof(Base) << std::endl;//8
  53. std::cout << "Derive size:" << sizeof(Derive) << std::endl;//12
  54. std::cout << typeid(pb).name() << std::endl;//class Base *
  55. /*
  56. pb的类型:Base类型,查看Base中有没有虚函数
  57. 1)Base中没有虚函数*pb识别的就是编译时期的类型 *pb 就是Base类型
  58. (2) Base中有虚函数,*pb识别的就是运行时期的类型 RTTI类型:Derive
  59. */
  60. std::cout << typeid(*pb).name() << std::endl;//class Derive
  61. return 0;
  62. }


在我们添加了virtual关键字后,对应的函数就变成了虚函数;
那么,一个类添加了虚函数,对这个类有什么影响呢?

  • 首先,如果类里面定义了虚函数,那么编译阶段,编译器给这个类类型产生一个唯一的vftable虚函数表,虚函数表中主要存储的内容是:RTTI(Run-time Type Information)指针和虚函数的地址,当程序运行时,每一张虚函数表都会加载到内存的.rodata区;
  • 一个类里面定义了虚函数,那么这个类定义的对象,在运行时,内存中会多存储一个vfptr虚函数指针,指向了对应类型的虚函数表vftable;
  • 一个类型定义的n个对象,他们的vfptr指向的都是同一张虚函数表;
  • 一个类里面虚函数的个数,不影响对象内存的大小(vfptr),影响的是虚函数表的大小。
  • 如果派生类中的方法和从基类继承来的某个方法中返回值、函数名以及参数列表都相同,且基类的方法是virtual,那么派生类的这个方法,自动处理成虚函数

图示如下:(以Base为例)

在这里插入图片描述

虚函数表
1、RTTI,存放的是类型信息,也就是(Base或者Derive)
2、偏移地址:虚函数指针相对于对象内存空间的偏移,一般vfptr都在0偏移位置
3、下面的函数时虚函数入口地址

  在Derive类中,由于重写了show(),因此在Derive的虚函数表中,是使用子类的show()方法代替了Base类的show()

VS的工具来查看虚函数表的有关信息

1 找到

2 在打开的窗口中切换到当前工程所在目录:

C:\Program Files (x86)\Microsoft Visual Studio\2017\Community>cd  C:\Users\Admin\source\repos\C++test\

 3 输入命令:cl XXX.cpp /d1reportSingleClassLayoutXX(第一个XXX表示源文件的名字,第二个代表你想查看的类类型,我这里就是Derive)

以看到class Derived的对象的内存布局,在派生类对象的开始包含了基类Base的对象,其中有一个虚表指针,指向的就是下面的Derived::$vftable@ (virtual function table),表中包含了Derived类中所有的虚函数

多重继承、多继承 的虚函数表

1 内存分布


假设有一个基类ClassA,一个继承了该基类的派生类ClassB,并且基类中有虚函数,派生类实现了基类的虚函数。
我们在代码中运用多态这个特性时,通常以两种方式起手:
(1) ClassA *a = new ClassB();
(2) ClassB b; ClassA *a = &b;
以上两种方式都是用基类指针去指向一个派生类实例,区别在于第1个用了new关键字而分配在堆上,第2个分配在栈上
这里写图片描述

请看上图,不同两种方式起手仅仅影响了派生类对象实例存在的位置。
以左图为例,ClassA *a是一个栈上的指针。
该指针指向一个在堆上实例化的子类对象。基类如果存在虚函数,那么在子类对象中,除了成员函数与成员变量外,编译器会自动生成一个指向**该类的虚函数表(这里是类ClassB)**的指针,叫作虚函数表指针。通过虚函数表指针,父类指针即可调用该虚函数表中所有的虚函数。


2 类的虚函数表与类实例的虚函数指针


首先不考虑继承的情况。如果一个类中有虚函数,那么该类就有一个虚函数表。
这个虚函数表是属于类的,所有该类的实例化对象中都会有一个虚函数表指针去指向该类的虚函数表。
从第一部分的图中我们也能看到,一个类的实例要么在堆上,要么在栈上。也就是说一个类可以有很多很多个实例。但是!一个类只能有一个虚函数表。在编译时,一个类的虚函数表就确定了,这也是为什么它放在了只读数据段中。
这里写图片描述

3 多态代码及多重继承情况

在第二部分中,我们讨论了在没有继承的情况下,虚函数表的逻辑结构。
那么在有继承情况下,只要基类有虚函数,子类不论实现或没实现,都有虚函数表。

  1. #include <iostream>
  2. using namespace std;
  3. class ClassA
  4. {
  5. public:
  6. ClassA() { cout << "ClassA::ClassA()" << endl; }
  7. virtual ~ClassA() { cout << "ClassA::~ClassA()" << endl; }
  8. void func1() { cout << "ClassA::func1()" << endl; }
  9. void func2() { cout << "ClassA::func2()" << endl; }
  10. virtual void vfunc1() { cout << "ClassA::vfunc1()" << endl; }
  11. virtual void vfunc2() { cout << "ClassA::vfunc2()" << endl; }
  12. private:
  13. int aData;
  14. };
  15. class ClassB : public ClassA
  16. {
  17. public:
  18. ClassB() { cout << "ClassB::ClassB()" << endl; }
  19. virtual ~ClassB() { cout << "ClassB::~ClassB()" << endl; }
  20. void func1() { cout << "ClassB::func1()" << endl; }
  21. virtual void vfunc1() { cout << "ClassB::vfunc1()" << endl; }
  22. private:
  23. int bData;
  24. };
  25. class ClassC : public ClassB
  26. {
  27. public:
  28. ClassC() { cout << "ClassC::ClassC()" << endl; }
  29. virtual ~ClassC() { cout << "ClassC::~ClassC()" << endl; }
  30. void func2() { cout << "ClassC::func2()" << endl; }
  31. virtual void vfunc2() { cout << "ClassC::vfunc2()" << endl; }
  32. private:
  33. int cData;
  34. };
  35. int main()
  36. {
  37. ClassC c;
  38. return 0;
  39. }


请看上面代码
(1) ClassA是基类, 有普通函数: func1() func2() 。虚函数: vfunc1() vfunc2() ~ClassA()
(2) ClassB继承ClassA, 有普通函数: func1()。虚函数: vfunc1() ~ClassB()
(3) ClassC继承ClassB, 有普通函数: func2()。虚函数: vfunc2() ~ClassB()
基类的虚函数表和子类的虚函数表不是同一个表。下图是基类实例与多态情形下,数据逻辑结构。注意,虚函数表是在编译时确定的,属于类而不属于某个具体的实例。虚函数在代码段,仅有一份
ClassB继承与ClassA,其虚函数表是在ClassA虚函数表的基础上有所改动的,变化的仅仅是在子类中重写的虚函数。如果子类没有重写任何父类虚函数,那么子类的虚函数表和父类的虚函数表在内容上是一致的

  1. ClassA *a = new ClassB();
  2. a->func1(); // "ClassA::func1()" 隐藏了ClassB的func1()
  3. a->func2(); // "ClassA::func2()"
  4. a->vfunc1(); // "ClassB::vfunc1()" 重写了ClassA的vfunc1()
  5. a->vfunc2(); // "ClassA::vfunc2()"

这个结果不难想象,看上图,ClassA类型的指针a能操作的范围只能是黑框中的范围,之所以实现了多态完全是因为子类的虚函数表指针与虚函数表的内容与基类不同
这个结果已经说明了C++的隐藏、重写(覆盖)特性。

同理,也就不难推导出ClassC的逻辑结构图了
类的继承情况是: ClassC继承ClassB,ClassB继承ClassA
这是一个多次单继承的情况。(多重继承)

这里写图片描述

 

4、多继承下的虚函数表 (同时继承多个基类)

多继承是指一个类同时继承了多个基类,假设这些基类都有虚函数,也就是说每个基类都有虚函数表,那么该子类的逻辑结果和虚函数表是什么样子呢?

  1. #include <iostream>
  2. using namespace std;
  3. class ClassA1
  4. {
  5. public:
  6. ClassA1() { cout << "ClassA1::ClassA1()" << endl; }
  7. virtual ~ClassA1() { cout << "ClassA1::~ClassA1()" << endl; }
  8. void func1() { cout << "ClassA1::func1()" << endl; }
  9. virtual void vfunc1() { cout << "ClassA1::vfunc1()" << endl; }
  10. virtual void vfunc2() { cout << "ClassA1::vfunc2()" << endl; }
  11. private:
  12. int a1Data;
  13. };
  14. class ClassA2
  15. {
  16. public:
  17. ClassA2() { cout << "ClassA2::ClassA2()" << endl; }
  18. virtual ~ClassA2() { cout << "ClassA2::~ClassA2()" << endl; }
  19. void func1() { cout << "ClassA2::func1()" << endl; }
  20. virtual void vfunc1() { cout << "ClassA2::vfunc1()" << endl; }
  21. virtual void vfunc2() { cout << "ClassA2::vfunc2()" << endl; }
  22. virtual void vfunc4() { cout << "ClassA2::vfunc4()" << endl; }
  23. private:
  24. int a2Data;
  25. };
  26. class ClassC : public ClassA1, public ClassA2
  27. {
  28. public:
  29. ClassC() { cout << "ClassC::ClassC()" << endl; }
  30. virtual ~ClassC() { cout << "ClassC::~ClassC()" << endl; }
  31. void func1() { cout << "ClassC::func1()" << endl; }
  32. virtual void vfunc1() { cout << "ClassC::vfunc1()" << endl; }
  33. virtual void vfunc2() { cout << "ClassC::vfunc2()" << endl; }
  34. virtual void vfunc3() { cout << "ClassC::vfunc3()" << endl; }
  35. };
  36. int main()
  37. {
  38. ClassC c;
  39. return 0;
  40. }

ClassA1是第一个基类,拥有普通函数func1(),虚函数vfunc1() vfunc2()。
ClassA2是第二个基类,拥有普通函数func1(),虚函数vfunc1() vfunc2(),vfunc4()。
ClassC依次继承ClassA1、ClassA2。普通函数func1(),虚函数vfunc1() vfunc2() vfunc3()。

 在多继承情况下,有多少个基类就有多少个虚函数表指针,前提是基类要有虚函数才算上这个基类。
如图,虚函数表指针01指向的虚函数表是以ClassA1的虚函数表为基础的,子类的ClassC::vfunc1(),和vfunc2()的函数指针覆盖了虚函数表01中的虚函数指针01的位置、02位置。当子类有多出来的虚函数时,添加在第一个虚函数表中。注意:
1.子类虚函数会覆盖每一个父类的每一个同名虚函数。
2.父类中没有的虚函数而子类有,填入第一个虚函数表中,且用父类指针是不能调用。
3.父类中有的虚函数而子类没有,则不覆盖。仅子类和该父类指针能调用

虚基类和多重继承 

什么是多重继承

多重继承,很好理解,一个派生类如果只继承一个基类,称作单继承;
一个派生类如果继承了多个基类,称作多继承。
如图所示:

在这里插入图片描述

多重继承的优点
这个很好理解:
多重继承可以做更多的代码复用!
派生类通过多重继承,可以得到多个基类的数据和方法,更大程度的实现了代码复用。

关于菱形继承的问题
凡事有利也有弊,对于多继承而言,也有自己的缺点。
我们先通过了解菱形继承来探究多重继承的缺点:
菱形继承是多继承的一种情况,继承方式如图所示:在这里插入图片描述

从图中我们可以看到:
类B类C类A单继承而来;
类D类B类C多继承而来。
那么这样继承会产生什么问题呢?
我们来看代码:

  1. #include <iostream>
  2. using namespace std;
  3. class A
  4. {
  5. public:
  6. A(int data) :ma(data) { cout << "A()" << endl; }
  7. ~A() { cout << "~A()" << endl; }
  8. protected:
  9. int ma;
  10. };
  11. class B :public A
  12. {
  13. public:
  14. B(int data) :A(data), mb(data) { cout << "B()" << endl; }
  15. ~B() { cout << "~B()" << endl; }
  16. protected:
  17. int mb;
  18. };
  19. class C :public A
  20. {
  21. public:
  22. C(int data) :A(data), mc(data) { cout << "C()" << endl; }
  23. ~C() { cout << "~C()" << endl; }
  24. protected:
  25. int mc;
  26. };
  27. class D :public B, public C
  28. {
  29. public:
  30. D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }
  31. ~D() { cout << "~D()" << endl; }
  32. protected:
  33. int md;
  34. };
  35. int main()
  36. {
  37. D d(10);
  38. return 0;
  39. }

 

通过运行结果,我们发现了问题:
对于基类A而言,构造了两次,析构了两次!
并且,通过分析各个派生类的内存布局我们可以看到:

在这里插入图片描述

对于派生类D来说,间接继承的基类A中的数据成员ma重复了!
这对资源来说是一种浪费与消耗。
(如果多继承的数量增加,那么派生类中重复的数据也会增加!)

查看D类的内存布局:

其他多重继承的情况

除了菱形继承外,还有其他多重继承的情况,也会出现相同的问题

在这里插入图片描述

比如说图中呈现的:半圆形继承。

如何解决多重继承的问题


通过分析我们知道了,多重继承的主要问题是,通过多重继承,有可能得到重复的基类数据,并且可能重复的构造和析构同一个基类对象。
那么如何能够避免重复现象的产生呢?
答案就是:=》虚基类。

什么是虚基类
要理解虚基类,我们首先需要认识virtual关键字的使用场景:

修饰成员方法时:产生虚函数;
修饰继承方式时:产生虚基类。
对于被虚继承的类,称作虚基类。
比如说:

  1. class A
  2. {
  3. XXXXXX;
  4. };
  5. class B : virtual public A
  6. {
  7. XXXXXX;
  8. };

 对于这个示例而言,B虚继承了A,所以把A称作虚基类。

虚基类如何解决问题

那么虚基类如何解决上述多重继承产生的重复问题呢?
我们来看代码:

  1. #include <iostream>
  2. using namespace std;
  3. class A
  4. {
  5. public:
  6. A(int data) :ma(data) { cout << "A()" << endl; }
  7. ~A() { cout << "~A()" << endl; }
  8. protected:
  9. int ma;
  10. };
  11. class B :virtual public A
  12. {
  13. public:
  14. B(int data) :A(data), mb(data) { cout << "B()" << endl; }
  15. ~B() { cout << "~B()" << endl; }
  16. protected:
  17. int mb;
  18. };
  19. class C :virtual public A
  20. {
  21. public:
  22. C(int data) :A(data), mc(data) { cout << "C()" << endl; }
  23. ~C() { cout << "~C()" << endl; }
  24. protected:
  25. int mc;
  26. };
  27. class D :public B, public C
  28. {
  29. public:
  30. D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }
  31. ~D() { cout << "~D()" << endl; }
  32. protected:
  33. int md;
  34. };

 

 提示说:"A::A" : 没有合适的默认构造函数可用
为什么会这样呢?
我们可以这么理解:

刚开始BC单继承A的时候,实例化对象时,会首先调用基类的构造函数,也就是A的构造函数,到了D,由于多继承了BC,所以在实例化D的对象时,会首先调用BC的构造函数,然后调用自己(D)的。

但是这样会出现A重复构造的问题,所以,采用虚继承,把有关重复的基类A改为虚基类,这样的话,对于A构造的任务就落到了最终派生类D的头上,但是我们的代码中,对于D的构造函数:D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }并没有对A进行构造。
所以会报错。
那么我们就给D的构造函数,调用A的构造函数:
D(int data) :A(data), B(data), C(data), md(data) { cout << "D()" << endl; }
这一次再运行

 

我们会发现,问题解决了。







查看虚基类的内存布局

 我们可以看到当前B的内存空间:

当前B的内存空间里,前四个字节是vbptr(这个就代表里虚基类指针:virtual base ptr);
vfptr(虚函数指针)指向了vftable(虚函数表)一样,
vbptr(虚基类指针)指向了vbtable(虚基类表)。

vbtable(虚基类表)的布局也如图所示,
首先是偏移量0:表示了虚基类指针再内存布局中的偏移量;
接着是偏移量8:表示从虚基类中继承而来的数据成员在内存中的偏移量。

对比普通继承下的内存布局

我们可以对比没有虚继承下的B的内存布局来理解:

 我们把他们放在一起对比可以看到:

 继承虚基类的类(BC)会把自己从虚基类继承而来的数据ma放在自己内存的最末尾(偏移量最大),并在原来ma的位置填充一个vbptr(虚基类指针),这个指针指向了vbtable(虚基类表)。
理解了B,我们可以看看更为复杂的D

 可以看到,将ma移动到了末尾处,并在含有ma的地方,都用vbptr进行填充。
这样一来,就只有一个ma了!解决了多重继承的重复问题。

虚析构函数

析构函数:可以成为虚函数,调用时候对象存在。
虚析构函数:在析构函数前加上virtual关键字。

什么时候需要把基类的析构函数必须实现成虚函数?
       基类的指针(引用)指向堆上new出来的派生类对象的时候,delete调用析构函数的时候,必须发生动态绑定,否则会导致派生类的析构函数无法调用。
 

  1. #include <iostream>
  2. using namespace std;
  3. class Base
  4. {
  5. public:
  6. Base(int data) :ma(data)
  7. {
  8. cout << "Base()" << endl;
  9. }
  10. ~Base()
  11. {
  12. cout << "~Base()" << endl;
  13. }
  14. virtual void show()
  15. {
  16. cout << "call Base::show()" << endl;
  17. }
  18. protected:
  19. int ma;
  20. };
  21. class Derive : public Base
  22. {
  23. public:
  24. Derive(int data) :Base(data), mb(data), ptr(new int(data))
  25. {
  26. cout << "Derive()" << endl;
  27. }
  28. ~Derive()
  29. {
  30. delete ptr;
  31. cout << "~Derive() " << endl;
  32. }
  33. private:
  34. int mb;
  35. int *ptr;
  36. };
  37. int main()
  38. {
  39. Base *pb = new Derive(10);
  40. pb->show();//静态绑定pb Base* *pb Derive
  41. delete pb;
  42. return 0;
  43. }

没有调用派生类的析构函数,造成内存泄漏

问题: pb的类型是Base类型,因此delete调用析构函数先去Base中找Base::~Base(),对于析构函数的调用就是静态绑定,之间编译,没有机会调用派生类的析构函数,最后发生内存泄露。
解决方案: 将基类的析构函数定义为虚析构函数,派生类的析构函数自动成为虚函数。 pb的类型是Base类型,调用析构时去Base中找Base::~Base发现它为虚函数,发生动态绑定。派生类的虚函数表中:&Derive:: ~derive,用派生类析构函数将自己部分进行析构,再调用基类的析构函数将基类部分析构。

注意:构造函数不能是虚函数。

 (1)从存储空间角度 

        虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。

(2)从使用角度

虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。


(3)虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。


(4)构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它。但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。

(5)从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数  
从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数

深入动态绑定问题

虚函数的调用一定就是动态绑定吗?
在类的构造函数当中,调用虚函数,也是静态绑定。构造函数中调用其他函数,包括虚函数,不会发生动态绑定。

注意:
1.用对象本身调用虚函数,是静态绑定
2.动态绑定:虚函数前面必须是指针或引用调用才能发生动态绑定:基类指针指向基类对象,基类指针指向派生类对象,都是动态绑定。
3.如果不是通过指针或者引用来调用虚函数,那就是静态绑定

案例1:静态绑定

  1. class Base
  2. {
  3. public:
  4. Base(int data = 0):ma(data){}
  5. virtual void show()
  6. {
  7. cout << "Base::show()" << endl;
  8. }
  9. protected:
  10. int ma;
  11. };
  12. class Derive : public Base
  13. {
  14. public:
  15. Derive(int data = 0):Base(data), mb(data){}
  16. void show()
  17. {
  18. cout << "Derive::show()" << endl;
  19. }
  20. private:
  21. int mb;
  22. };
  23. int main()
  24. {
  25. Base b;
  26. Derive d;
  27. //静态绑定
  28. b.show();//虚函数 call Base::show();
  29. d.show();//虚函数 call Derive::show();
  30. return 0;
  31. }

转到反汇编:发现其为静态绑定

在这里插入图片描述

 案例2:基类指针指向基类对象

  1. Base b;
  2. Derive d;
  3. Base *pb1 = &b;//基类指针指向基类对象
  4. pb1->show();

转到反汇编:为动态绑定

在这里插入图片描述

案例3:基类指针指向派生类对象 

  1. Base b;
  2. Derive d;
  3. Base *pb2 = &d;//基类指针指向派生类对象
  4. pb2->show();

在这里插入图片描述

 转到反汇编:为动态绑定

案例4:基类指针指向基类对象,基类指针引用派生类对象

  1. Derive *pd1 = &d;
  2. pd1->show();
  3. Derive &rd1 = d;
  4. rd1.show();

与指针一样:都是动态绑定。

案例5:派生类指针调用派生类对象,派生类引用调用派生类对象

  1. Derive *pd1 = &d;
  2. pd1->show();
  3. Derive &rd1 = d;
  4. rd1.show();

转到反汇编:都是动态绑定。

案例6:强制类型转换

  1. Derive *pd2 = (Derive*)&b;
  2. pd2->show();

动态绑定

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/731439
推荐阅读
相关标签
  

闽ICP备14008679号