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

文章分析了在C++中,由于构造函数和析构函数执行时对象的状态,调用的虚函数实际上是基类版本,而非派生类版本,因此在这些阶段无法实现多态效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先说结论

从语法上讲,调用完全没有问题。但是不能达到多态的效果。

 原因

首先摘取一段来自《Effective C++》的解释:

派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。 同样,进入基类析构函数时,对象也是基类类型。

对于构造函数的情况,构造的顺序是先基类后子类,所以在调用基类构造函数的时候这个子类压根还没构造,所以虚函数不会生效。

类似的,对于析构函数,先析构子类后基类,当调用基类析构函数的时候,子类已经析构掉了,所以虚函数也不会生效。

对象的虚函数表地址在对象的构造和析构过程中会随着部分类的构造和析构而发生变化。

构造函数调用虚函数

        当实例化一个派生类对象时,首先进行基类部分的构造,然后再进行派生类部分的构造。即创建Derive对象时,会先调用Base的构造函数,再调用Derive的构造函数。

  当在构造基类部分时,派生类还没被完全创建,从某种意义上讲此时它只是个基类对象。即当Base::Base()执行时Derive对象还没被完全创建,此时它被当成一个Base对象,而不是Derive对象,因此Foo绑定的是Base的Foo。

        子类在创建的时候是先调用父类的构造器,父类的构造器先设置好父类的虚表地址,然后再执行构造器中的代码,然后在回到子类构造器,先将虚表地址替换为子类的虚表地址,然后执行子类构造器的代码。因此如果在父类构造器中调用虚函数,那么此时是从父类的虚表中查找函数地址,查找到的还是父类的虚函数地址。因此会调用父类的虚函数,而不会调用子类的虚函数。

          对象的虚函数表在对象被构造的过程中是在不断变化的,构造基类部分(Base)时被绑定一次,构造派生类部分(Derive)时,又重新绑定一次。基类构造函数中的虚函数调用,按正常的虚函数调用规则去调用函数,自然而然地就调用到了基类版本的虚函数,因为此时对象绑定的是基类的虚函数表。

class Base {
public:
  Base() {
    std::cout << "Base::ctor()" << std::endl;
    Foo();
  }
  virtual ~Base() {}

  virtual void Foo() { std::cout << "Base::Foo()" << std::endl; }
};

class Derive : public Base {
public:
  Derive() {
    std::cout << "Derive::ctor()" << std::endl;
    Foo();
  }
  ~Derive() {}

  virtual void Foo() { std::cout << "Derive::Foo()" << std::endl; }
};

运行结果:

Base::ctor()
Base::Foo()
Derive::ctor()
Derive::Foo()

所以构造函数中调用虚拟函数就是调用跟调用普通函数效果一样,达不到多态效果。

析构函数中调用虚函数

class Base {
public:
  Base() {}
  virtual ~Base() {
    std::cout << "Base::dtor()" << std::endl;
    Bar();
  }

  virtual void Bar() { std::cout << "Base::Bar()" << std::endl; }
};

class Derive : public Base {
public:
  Derive() {}
  ~Derive() {
    std::cout << "Derive::dtor()" << std::endl;
    Bar();
  }

  virtual void Bar() { std::cout << "Derive::Bar()" << std::endl; }
};

int main() {
  Base *p = new Derive;
  delete p;

  return 0;
}

运行结果

Derive::dtor()
Derive::Bar()
Base::dtor()
Base::Bar()

       而同样,我们可以认为在析构的过程中,虚函数表也是在不断变化的。 因为析构的时候先执行子类的析构函数,再执行父类的析构函数,在执行当前类的析构函数的时候会先将虚表地址设置为当前类的虚表地址,然后再从虚表里查找函数地址,因此找到的只能是当前的虚函数的地址。    

        析构函数的调用跟构造函数的调用顺序是相反的,它从最派生类的析构函数开始的。也就是说当基类的析构函数执行时,派生类的析构函数已经执行过,派生类中的成员数据被认为已经无效。假设基类中虚函数调用能调用得到派生类的虚函数,那么派生类的虚函数将访问一些已经“无效”的数据,所带来的问题和访问一些未初始化的数据一样。

所以,析构函数是调用虚拟函数跟调用普通函数没啥区别,同样也达不到多态效果。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值