C++中构造函数和析构函数避免调用虚函数的问题

news/2024/5/19 0:01:29 标签: 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

和我们的分析大相径庭。第一行是在C180中运行foo()函数得到的,这里的foo()当然是调用C180中的foo()函数。
第二行是调用C190中的this->foo()得到的,此时this指向的应该是C190的虚表地址,按照调用规则,应该是动态绑定,即,此时若派生类对该虚函数实现过,则应该调用派生类的虚函数
,这里是一个例外,下面会详细讲到。 至此,C190的父类的构造函数运行完毕,转而运行C190的构造函数,但是这里C190的构造函数什么都没有。第三行是在main函数中调用obj.foo()得到的,这里直接进入C190运行就可以了。 这里必须注意一点,就是前两行和第三行的虚表是不同的,这是因为前两行的虚表是C180的虚表,而第三行的虚表是C190的虚表。 其实这正是奥秘所在。

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

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

二、为什么构造函数不可以调用虚的函数?

第一,在概念上,构造函数的工作是把对象变成存在物。在任何构造函数中,对象可能只是部分被形成—我们只能知道基类已被初始化了,但不知道哪个类是从这个基类继承来的。然而,虚函数是“向前”和“向外”进行调用。它能调用在派生类中的函数。如果我们在构造函数中也这样做,那么我们所调用的函数可能操作还没有被初始化的成员,这将导致灾难的发生。
 第二,当一个构造函数被调用时,它做的首要的事情之一是初始化它的VPTR。因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码--既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。
所以它使用的VPTR必须是对于这个类的VTABLE。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE。但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的VTABLE,等直到最后的构造函数结束。VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的另一个理由。
    但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置VPTR指向它自己的VTABLE。如果函数调用使用虚机制,它将只产生通过它自己的VTABLE的调用,而不是最后的VTABLE(所有构造函数被调用后才会有最后的VTABLE)。
另外,许多编译器认识到,如果在构造函数中进行虚函数调用,应该使用早捆绑,因为它们知道晚捆绑将只对本地函数产生调用。无论哪种情况,在构造函数中调用虚函数都没有结果。
    所以,构造函数不能是虚的,然而,对于析构函数来说他常常是,而且最好是虚的!这个此处暂时不议..

三、析构函数中调用虚函数

在对象的析构期间,存在与上面同样的逻辑。一旦一个派生类的析构器运行起来,该对象的派生类数据成员就被假设为是未定义的值,这样以来,C++就把它们当做是不存在一样。一旦进入到基类的析构器中,该对象即变为一个基类对象,C++中各个部分(虚函数dynamic_cast运算符等等)都这样处理。dynamic_cast只是有安全检查,不能强制将子类变成父类!


http://www.niftyadmin.cn/n/1078342.html

相关文章

java记事本课程设计报告前言_java课程设计报告(记事本程序).doc

java课程设计报告(记事本程序)课程设计(论文)题 目 名 称 记事本程序课 程 名 称 java 程序设计课程设计学 生 姓 名学 号系 、专 业指 导 教 师2010年 1 月 3 日摘 要本次课程设计的题目是用JAVA语言编写记事本程序&#xff0c;要求是&#xff1a;用图形界面实现&#xff1b;含…

C++ 11右值引用

C 11中引入的一个非常重要的概念就是右值引用。理解右值引用是学习“移动语义”&#xff08;move semantics&#xff09;的基础。而要理解右值引用&#xff0c;就必须先区分左值与右值。 对左值和右值的一个最常见的误解是&#xff1a;等号左边的就是左值&#xff0c;等…

Java对象初始化详解

在Java中&#xff0c;一个对象在可以被使用之前必须要被正确地初始化&#xff0c;这一点是Java规范规定的。本文试图对Java如何执行对象的初始化做一个详细深入地介绍(与对象初始化相同&#xff0c;类在被加载之后也是需要初始化的&#xff0c;本文在最后也会对类的初始化进行介…

cocos python打包教程_cocos2dx-2.x 打包APK(学习笔记 Python + JDK + ADT + NDK )

看了那么多篇文章&#xff0c;自己结合一些前辈的精华写下这个。(因为总是不能在一个教程完成打包的全部工作需要。。。。。。)直接开始第一步&#xff1a;环境配置必须的工具1 Python (我用的是python2.7)2 JDK (我用的是jdk1.7.0_17)3 ADT(包括了eclipse和SDK&#xff0c;用完…

用Irony实现一个计算器附上源码

满足的功能如下&#xff1a;加减乘除四则运算括号优先级 比如&#xff1a;32*4 - 8/4 的结果是92* (2 3) - 2的结果是8Irony是.net平台下实现语言的开发工具箱。作者把C#这种强大的现代语言引入编译器解析领域&#xff0c;语法规则直接通过C#语言来描述。下面看看这个计算器的…

疯狂Java讲义之内部类(二)

大部分时候&#xff0c;我们把类定义成一个独立的程序单元。在某些情况下&#xff0c;我们把一个类在另一个类的内部定义&#xff0c;这个定义在其他类内部的类就被称为内部类&#xff08;有的地方也叫嵌套类&#xff09;&#xff0c;包含内部类的类也被称为外部类&#xff08;…

java 逆序数_【Java】 剑指offer(51)数组中的逆序对

本文参考自《剑指offer》一书&#xff0c;代码采用Java语言。题目在数组中的两个数字如果前面一个数字大于后面的数字&#xff0c;则这两个数字组成一个逆序对。输入一个数组&#xff0c;求出这个数组中的逆序对的总数。思路如果遍历数组&#xff0c;对每个数字都和后面的数字比…

频率响应

一.频率响应的数学推导 参考的学习视频https://www.bilibili.com/video/BV1Qb411W7i2/?spm_id_from333.788.recommend_more_video.-1&vd_source1635a55d1012e0ef6688b3652cefcdfe 对于一个稳定系统来说&#xff0c;的实部应该小于0&#xff0c;因为x(t)中的实数项是衰减…