`

构造函数调用虚函数

    博客分类:
  • C++
阅读更多
在构造函数中调用虚成员函数,虽然这是个不很常用的技术,但研究一下可以加深对虚函数机制及对象构造过程的理解。这个问题也和一般直观上的认识有所差异。先看看下面的两个类定义。

struct C180
{
 C180() {
  foo();
  this->foo();
 }
 virtual foo() {
  cout << "<< C180.foo this: " << this << " vtadr: " << *(void**)this << endl;
 }
};
struct C190 : public C180
{
 C190() {}
 virtual foo() {
  cout << "<< C190.foo this: " << this << " vtadr: " << *(void**)this << endl;
 }
};

  父类中有一个虚函数,并且父类在它的构造函数中调用了这个虚函数,调用时它采用了两种方法一种是直接调用,一种是通过this指针调用。同时子类又重写了这个虚函数。

  我们可以来预测一下如果构造一个C190的对象会发生什么情况。

  我们知道,在构造一个对象时,过程是这样的:
        1) 首先会按对象的大小得到一块内存(在heap上或在stack上),
        2) 把指向这块内存的指针做为this指针来调用类的构造函数,对这块内存进行初始化。
        3) 如果对象有父类就会先调用父类的构造函数(并依次递归),如果有多个父类(多重继承)会依次对父类的构造函数进行调用,并会适当的调整this指针的位置。在调用完所有的父类的构造函数后,再执行自己的代码。

  照上面的分析构造C190时也会调用C180的构造函数,这时在C180构造函数中的第一个foo调用为静态绑定,会调用到C180::foo()函数。第二个foo调用是通过指针调用的,这时多态行为会发生,应该调用的是C190::foo()函数。

  执行如下代码:

C190 obj;
obj.foo();

  结果为:

<< C180.foo this: 0012F7A4 vtadr: 0045C404
<< C180.foo this: 0012F7A4 vtadr: 0045C404
<< C190.foo this: 0012F7A4 vtadr: 0045C400

  和我们的分析大相径庭。前2行是构造C190时的输出,后1行是我们用静态绑定方式调用的C190::foo()函数。第2行的输出说明多态行为并没有象预期的那样发生。而且比较输出的最后一列,发现在调用C180的构造函数时对象对应的虚表和构造后对象对应的虚表不是同一个。其实这正是奥秘的所在。

  为此我查了一下C++标准规范。在12.7.3条中有明确的规定。这是一种特例,在这种情况下,即在构造子类时调用父类的构造函数,而父类的构造函数中又调用了虚成员函数,这个虚成员函数即使被子类重写,也不允许发生多态的行为。即,这时必须要调用父类的虚函数,而不子类重写后的虚函数。

  我想这样做的原因是因为在调用父类的构造函数时,对象中属于子类部分的成员变量是肯定还没有初始化的,因为子类构造函数中的代码还没有被执行。如果这时允许多态的行为,即通过父类的构造函数调用到了子类的虚函数,而这个虚函数要访问属于子类的数据成员时就有可能出错。

  我们看看VC7.1生成的汇编代码就可以很容易的理解这个行为了。

这是C190的构造函数:

01 00426FE0 push ebp
02 00426FE1 mov ebp,esp
03 00426FE3 sub esp,0CCh
04 00426FE9 push ebx
05 00426FEA push esi
06 00426FEB push edi
07 00426FEC push ecx
08 00426FED lea edi,[ebp+FFFFFF34h]
09 00426FF3 mov ecx,33h
10 00426FF8 mov eax,0CCCCCCCCh
11 00426FFD rep stos dword ptr [edi]
12 00426FFF pop ecx
13 00427000 mov dword ptr [ebp-8],ecx
14 00427003 mov ecx,dword ptr [ebp-8]
15 00427006 call 0041D451
16 0042700B mov eax,dword ptr [ebp-8]
17 0042700E mov dword ptr [eax],45C400h
18 00427014 mov eax,dword ptr [ebp-8]
19 00427017 pop edi
20 00427018 pop esi
21 00427019 pop ebx
22 0042701A add esp,0CCh
23 00427020 cmp ebp,esp
24 00427022 call 0041DDF2
25 00427027 mov esp,ebp
26 00427029 pop ebp
27 0042702A ret

  开始部分的指令在前面几篇中陆续解释过,这里不再详述。我们看看第15是对父类的构造函数C180::C180()的调用,根据前文的说明,我们知道此时ecx中放的是this指针,也就是C190对象的地址。这时如果跳到this指针批向的地址看看会发现值为0xcccccccc即没有初始化,虚表指针也没有被初始化。那么我们跟着跳到C180的构造函数看看。

01 00427040 push ebp
02 00427041 mov ebp,esp
03 00427043 sub esp,0CCh
04 00427049 push ebx
05 0042704A push esi
06 0042704B push edi
07 0042704C push ecx
08 0042704D lea edi,[ebp+FFFFFF34h]
09 00427053 mov ecx,33h
10 00427058 mov eax,0CCCCCCCCh
11 0042705D rep stos dword ptr [edi]
12 0042705F pop ecx
13 00427060 mov dword ptr [ebp-8],ecx
14 00427063 mov eax,dword ptr [ebp-8]
15 00427066 mov dword ptr [eax],45C404h
16 0042706C mov ecx,dword ptr [ebp-8]
17 0042706F call 0041DA8C
18 00427074 mov ecx,dword ptr [ebp-8]
19 00427077 call 0041DA8C
20 0042707C mov eax,dword ptr [ebp-8]
21 0042707F pop edi
22 00427080 pop esi
23 00427081 pop ebx
24 00427082 add esp,0CCh
25 00427088 cmp ebp,esp
26 0042708A call 0041DDF2
27 0042708F mov esp,ebp
28 00427091 pop ebp
29 00427092 ret

  看看第15行,在this指针的位置也就是对象的起始处,填入了一个4字节的值0x0045C404,其实这就是我们前面的打印过的C180的虚表地址。第16、17行和18、19行分别调用了两次foo()函数,用的都是静态绑定。这个就有点奇怪,因为对后一个调用我们使用了this指针,照理应该是动态绑定才对。可这里却是静态绑定,为什么编译器要做这个优化?我们继承往后看。

这个函数执行完后,我们再回到C190构造函数中,我们接着看C190构造函数汇编代码的第17行,这里又在对象的起始处重新填入了0x0045C400,覆盖了原来的值,而这个值就是我们前面打印过的真正的C190的虚表地址。

  也就是说VC7.1是通过在调用构造函数的真正代码前把对象的虚指针值设置为指向对应类的虚表来实现C++规范的相应语义。C++标准中只规定了行为,并不规定具体编译器在实现这一行为时所用的方法。象我们上面看到的,即使是通过this指针调用,编译器也把它优化为静态绑定,也就是说即使不做这个虚指针的调整也不会有错。之所以要调整我想可能是防止在被调用的虚成员中又通过this指针来调用其他的虚函数,不过谁会这么变态呢?

  还有值得一提的是,VC7.1中有一个扩展属性可以用来抑制编译器产生对虚指针进行调整的代码。我们可以在C180类的声明中加入这个属性。

struct __declspec(novtable) C180
{
 C180() {
  foo();
  this->foo();
 }
 virtual foo() {
  cout << "<< C180.foo this: " << this << " vtadr: " << *(void**)this << endl;
 }
};

  这样再执行前面的代码,输出就会变成:

<< C180.foo this: 0012F7A4 vtadr: CCCCCCCC
<< C180.foo this: 0012F7A4 vtadr: CCCCCCCC
<< C190.foo this: 0012F7A4 vtadr: 0045C400

  由于编译器抑制了对虚指针的调整所以在调C180的构造函数时虚指针的值没有初始化,这时我们才看到多亏编译器把第二个通过this指针对foo的调用优化成了静态绑定,否则由于虚指针没有初始化一定会出现一个指针异常的错误,这就回答我们上面的那个问题。

  在这种情况下产生的汇编代码我就不列了,有兴趣的朋友可以自己去看一看。另外对于析构函数的调用,也请有兴趣的朋友自行分析一下。

  另外这个属性在ATL的代码中大量的使用。在ATL中接口一般为纯虚基类,如果不用这个优化属性,由于在子类即实现类的构造函数中要调用父类的构造函数,而编译器产生的父类构造函数又要设置虚指针的值。所以编译器必须要把父类的虚表构建出来。而实际上这个虚表是没有任何意义的,因为ATL的纯虚接口类的虚函数都是无实现的。这样不仅仅是多了几行无用的设值指令,同时也浪费了空间。有兴趣的朋友可以自行验证一下。
分享到:
评论

相关推荐

    构造函数不能声明为虚函数的原因及分析

    问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。 2. 从使用角度,虚函数主要用于在信息不全的情况下,能使...

    C++ 虚继承对基类构造函数调用顺序的影响

    继承作为面向对象编程的一种基本特征,其使用频率...  假设derived 虚继承自base类,那么derivd与base是一种“has a”的关系,即derived类有一个指向base类的vptr。(貌似有些牵强!某些编译器确实如此)  因此虚

    c++ 构造函数中调用虚函数的实现方法

    下面小编就为大家带来一篇c++ 构造函数中调用虚函数的实现方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧

    基类子类虚函数的调用顺序问题

    在虚函数的基类与子类构造函数中调用虚函数。当生成子类对象时,函数的调用顺序是怎样的

    C# 构造函数如何调用虚方法

    在C#中,用virtual关键字修饰的方法(属性、事件)称为虚方法(属性、事件),表示该方法可以由派生类重写(override)。虚方法是.NET中的重要概念,可以说在某种程度上,虚方法使得多态成为可能。 然而虚方法的使用...

    虚函数被类的构造析构函数和成员函数调用虚函数的执行过程

    代码如下:#include class base{public:  base() { std::cout&lt;&lt;std::endl; std::cout&lt;&lt;“base constructor”&lt;&lt;std::endl; func1(); std::cout&lt;&lt;std::endl; } ... func

    C#中派生类调用基类构造函数用法分析

    主要介绍了C#中派生类调用基类构造函数用法,实例分析了派生类调用基类构造函数的技巧,具有一定参考借鉴价值,需要的朋友可以参考下

    完全掌握C++编程中构造函数使用的超级学习教程

    构造函数是一种可初始化其类的实例的成员函数。构造函数具有与类相同的名称,没有返回值。构造函数可以具有任意数量...虚函数指针指向类中的虚函数表,确保虚函数正确地调用绑定代码。 它执行自己函数体中的所有代码。

    c++模拟测试题

    调用了 A 的构造函数 6 次。N 12、派生类后于基类构造,先于基类释放。Y 13、类是对象的抽象,对象是类的具体化。Y 14、二进制文件中数据的存放格式和计算机内存中存储的格式是一致的。Y 15、流提取运算符和流插入...

    详解C++中如何将构造函数或析构函数的访问权限定为private

     当我们在程序中声明一个对象时,编译器为调用构造函数(如果有的话),而这个调用将通常是外部的,也就是说它不属于class对象本身的调用,假如构造函数是私有的,由于在class外部不允许访问私有成员,所以这将导致...

    c++多态性与虚函数练习题

    2、熟练掌握虚函数的作用及其使用方法。 3、掌握静态关联和动态关联的概念和用法。 4、理解纯虚函数和抽象类的概念和用法。 (二)实验内容 1、定义一个类A,在A中有两个私有的整型变量a和b,定义构造函数对a和b进行...

    类中的函数分类与虚函数的原理

    构造函数:不可以虚化(重写),在子类的构造函数前自动被调用一次父类的构造函数. 析构函数:当[DELETE]类指针时 非虚会从[此类]一直释放到基类,为虚时会从被赋于的[NEW类]的析构函数一直释放到基类. 总得来说释放[方向...

    是谁在叫c++运行程序.exe

    Cat包含构造函数、析构函数和虚函数sound Leopard包含构造函数、析构函数和虚函数sound 要求:定义一个Felid类指针,采用动态内存分配的方式建议一个Cat对象,并且调用sound函数,之后撤销该对象,采用动态内存...

    c++基础语法:构造函数与析构函数

    说实话c++还是以前在学校的时候用过的,从毕业到现在一直用...现在主要看看继承中的构造函数和析构函数的调用: 代码如下:class A {} ;class B : public A{};class C : public B{}; c * ptr = new C() ;delete

    C++_虚函数和纯虚函数区别

    一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。

    C++ 课程作业 继承与派生 (motorcycle类设计(虚基类))

    声名一个基类vehicle,有私有成员maxspeed和weight,公有成员run...注意:构造函数和析构函数中均为cout语句,说明哪个构造/析构函数被调用。 该题重点和难点在于构造函数的设计,需考虑怎么给基类及最远基类传递参数。

    VC学习文档 当对两个值做比较的时候,将常量放在前面,如果逻辑错误的话,编译时就会出错,例如:

    子类继承父类以后,当子类创建对象时先调用父类的构造函数再调用子类的构造函数,删除对象时先调用子类的析构函数再调用父类的析构函数。 关于函数的覆盖和虚函数的解释可以参考工程中的Main实例 当父类中某函数被...

    C++虚函数及虚函数表简析

    C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。...

Global site tag (gtag.js) - Google Analytics