C++ 构造/析构函数中调用虚函数的问题

测试用例

#include "stdafx.h"

using namespace std; 

class ClassA
{
public:
ClassA(){
cout<<"ClassA::ClassA() begin"<<endl;
Print();
cout<<"ClassA::ClassA() end"<<endl;;
}
virtual void Print(){
cout<<"ClassA::Print()"<<endl;
}
virtual ~ClassA(){
cout<<"ClassA::~ClassA() begin"<<endl;
Print();
cout<<"ClassA::~ClassA() end"<<endl;;
}
};


class ClassB : public ClassA
{
public:
ClassB(){
cout<<"ClassB::ClassB() begin"<<endl;
Print();
cout<<"ClassB::ClassB() end"<<endl;;
}

void Print(){
cout<<"ClassB::Print()"<<endl;
}

~ClassB(){
cout<<"ClassB::~ClassB() begin"<<endl;
Print();
cout<<"ClassB::~ClassB() end"<<endl;;
}
};


int _tmain(int argc, _TCHAR* argv[])
{

ClassB* pClassB = new ClassB;


cout<<"------------"<<endl;
ClassA* pClassA = pClassB;
pClassA->Print();

cout<<"------------"<<endl;
delete pClassB;

return 0;
}




在vs2010运行结果如下


ClassA::ClassA() begin
ClassA::Print()
ClassA::ClassA() end
ClassB::ClassB() begin
ClassB::Print()
ClassB::ClassB() end
------------
ClassB::Print()
------------
ClassB::~ClassB() begin
ClassB::Print()
ClassB::~ClassB() end
ClassA::~ClassA() begin
ClassA::Print()
ClassA::~ClassA() end


(对代码稍作修改,在gcc中的编译结果也是如此)


        可以看到,在new ClassB时,虽然在父类ClassA的构造函数调了的是被ClassB覆盖的虚函数Print(),但是实际上还是调用的ClassA的Print。这是因为


        继承类在构造的时候总是首先调用其基类的构造函数来对属于其基类的部分进行构造,在这个时候,整个类被当作基类来处理,继承类的部分对整个类来说好像不存在一样,直到基类的构造函数退出并进入继承类的构造函数,该类才被当作继承类来出来处理。对析构也一样,只是析构的顺序正好相反。


       进一步分析,如果在析构函数中调用纯虚函数呢?将ClassA中的Print()改为纯虚函数


测试代码2:
#include "stdafx.h" 
using namespace std; 

class ClassA
{
public:
ClassA(){
cout<<"ClassA::ClassA() begin"<<endl;
Print();
cout<<"ClassA::ClassA() end"<<endl;;
}
virtual void Print() = 0;
virtual ~ClassA(){
cout<<"ClassA::~ClassA() begin"<<endl;
Print();
cout<<"ClassA::~ClassA() end"<<endl;;
}
};


class ClassB : public ClassA
{
public:
ClassB(){
cout<<"ClassB::ClassB() begin"<<endl;
Print();
cout<<"ClassB::ClassB() end"<<endl;;
}

void Print(){
cout<<"ClassB::Print()"<<endl;
}

~ClassB(){
cout<<"ClassB::~ClassB() begin"<<endl;
Print();
cout<<"ClassB::~ClassB() end"<<endl;;
}
};

int _tmain(int argc, _TCHAR* argv[])
{
ClassB* pClassB = new ClassB;

cout<<"------------"<<endl;
ClassA* pClassA = pClassB;
pClassA->Print();

cout<<"------------"<<endl;
delete pClassB;

return 0;
}
        编译出错:
Test error LNK2019: 无法解析的外部符号 "public: virtual void __thiscall ClassA::Print(void)" (?Print@ClassA@@UAEXXZ) ,该符号在函数 "public: __thiscall ClassA::ClassA(void)" (??0ClassA@@QAE@XZ) 中被引用

 稍加改动,继续测试:
测试代码3:

#include "stdafx.h"
using namespace std; 
class ClassA
{
public:
ClassA(){
cout<<"ClassA::ClassA() begin"<<endl;
Print();
cout<<"ClassA::ClassA() end"<<endl;;
}
virtual void Print(){
Print2();
}
virtual void Print2() = 0;
virtual ~ClassA(){
cout<<"ClassA::~ClassA() begin"<<endl;
Print();
cout<<"ClassA::~ClassA() end"<<endl;;
}
};


class ClassB : public ClassA
{
public:
ClassB(){
cout<<"ClassB::ClassB() begin"<<endl;
Print();
cout<<"ClassB::ClassB() end"<<endl;;
}

void Print(){
cout<<"ClassB::Print()"<<endl;
}
void Print2(){
cout<<"ClassB::Print2()"<<endl;
}
~ClassB(){
cout<<"ClassB::~ClassB() begin"<<endl;
Print();
cout<<"ClassB::~ClassB() end"<<endl;;
}
};


int _tmain(int argc, _TCHAR* argv[])
{
ClassB* pClassB = new ClassB;

cout<<"------------"<<endl;
ClassA* pClassA = pClassB;
pClassA->Print();

cout<<"------------"<<endl;
delete pClassB;

return 0;
}
成功编译,但是运行时出现runtime error的错误。编译器不会绕弯啊。

 最后,建议大家在构造函数中别做太复杂的事,最好只是对成员变量的初始化工作。复杂点的操作另写一个初始化函数。


 构造函数为什么不能是虚函数
 1 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。
2 虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行。


1,从存储空间角度
    虚函数对应一个vtable,这大家都知道,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,无法找到vtable,所以构造函数不能是虚函数。
2,从使用角度
        虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。
虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
3、构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它。但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
4、从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数  
  从实际含义上看,在调用构造函数时还不能确定对象的真实类型(因为子类会调父类的构造函数);而且构造函数的作用是提供初始化,在对象生命期只执行一次,不是对象的动态行为,也没有太大的必要成为虚函数
5、当一个构造函数被调用时,它做的首要的事情之一是初始化它的V P T R。因此,它只能知道它是“当前”类的,而完全忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码- -既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。
        所以它使用的V P T R必须是对于这个类的V TA B L E。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内, V P T R将保持被初始化为指向这个V TA B L E, 但如果接着还有一个更晚派生的构造函数被调用,这个构造函数又将设置V P T R指向它的 V TA B L E,等.直到最后的构造函数结束。V P T R的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生 类顺序的另一个理由。
        但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置V P T R指向它自己的 V TA B L E。如果函数调用使用虚机制,它将只产生通过它自己的V TA B L E的调用,而不是最后的V TA B L E(所有构造函数被调用后才会有最后的V TA B L E)。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值