C++中为什么构造函数和析构函数不允许调用虚函数?

devtools/2024/11/13 9:58:45/

目录

1.引言

2.不要在构造函数中调用虚函数的原因

2.1.对象不完全构造问题

2.2.虚函数表(vtable)尚未初始化

3.不要在析构函数中调用虚函数的原因

4.构造函数中调用虚函数的实际效果

4.1.静态绑定而非动态绑定

4.2.运行时行为分析

5.替代方案

5.1.成员初始化列表

5.2.两阶段构造

5.3.工厂模式

6.总结


1.引言

        在C++编程中,构造函数初始化对象的状态,确保对象在使用前被正确构建。而虚函数则提供了多态性,允许程序在运行时根据实际对象类型调用合适的函数。然而,C++标准明确禁止在构造函数中调用虚函数。这一规定背后有着深刻的原因和逻辑。本文将详细探讨这一问题,解析其背后的机制,并通过代码示例加深理解。

        构造函数

        构造函数是类的成员函数,其名称与类名相同,且在对象创建时自动调用。它的主要任务是初始化对象的成员变量,确保对象处于有效状态。

class Base {
public:Base() {// 构造函数的实现}
};

        虚函数

        虚函数是通过virtual关键字声明的成员函数,允许派生类重写该函数,实现多态性。在运行时,根据对象的实际类型调用合适的函数。

class Base {
public:virtual void show() {// 基类的实现}
};class Derived : public Base {
public:void show() override {// 派生类的实现}
};

2.不要在构造函数中调用虚函数的原因

2.1.对象不完全构造问题

        在概念上,构造函数的工作是为对象进行初始化。在构造函数完成之前,被构造的对象被认为“未完全生成”。在C++中,对象的构造是一个逐步的过程,从基类到派生类,每个类的构造函数都会按顺序被调用。在这个过程中,对象的完整类型(包括其所有派生类)尚未完全形成。因此,如果构造函数中调用了虚函数,那么由于对象的完整类型尚未确定,编译器无法确定应该调用哪个版本的虚函数(即哪个类的实现)。

2.2.虚函数表(vtable)尚未初始化

        在C++中,多态性通常通过虚函数表(vtable)来实现。每个包含虚函数的类都有一个vtable,它存储了类中所有虚函数的地址。然而,在对象的构造函数执行期间,vtable可能还没有完全初始化。这意味着,如果构造函数尝试调用虚函数,它可能会调用到一个尚未正确设置或尚未指向正确函数实现的地址,从而导致未定义行为。

3.不要在析构函数中调用虚函数的原因

        同样的,在析构函数中调用虚函数,函数的入口地址也是在编译时静态决定的。也就是说,实现的是实调用而非虚调用。

#include <iostream>
using namespace std;class A{
public:virtual void show(){cout<<"in A"<<endl;}virtual ~A(){show();}
};class B:public A{
public:void show(){cout<<"in B"<<endl;}
};int main(){A a;B b;
}

程序输出结果是:

 in A in A

        在类B的对象b退出作用域时,会先调用类B的析构函数,然后调用类A的析构函数,在析构函数~A()中,调用了虚函数show()。从输出结果来看,类A的析构函数对show()调用并没有发生虚调用。

        从概念上说,析构函数是用来销毁一个对象的,在销毁一个对象时,先调用该对象所属类的析构函数,然后再调用其基类的析构函数,所以,在调用基类的析构函数时,派生类对象的“善后”工作已经完成了,这个时候再调用在派生类中定义的函数版本已经没有意义了。

        因此,一般情况下,应该避免在构造函数和析构函数中调用虚函数,如果一定要这样做,程序猿必须清楚,这是对虚函数的调用其实是实调用

4.构造函数中调用虚函数的实际效果

4.1.静态绑定而非动态绑定

        即使在构造函数中调用了虚函数,编译器也会进行静态绑定,调用的是基类的函数实现,而不是动态绑定到派生类的实现。例如: 

4.2.运行时行为分析

        通过反汇编或调试工具,我们可以观察到在构造函数中调用虚函数时,编译器实际上直接调用了基类的函数,没有进行虚函数表的查找和动态绑定。

5.替代方案

5.1.成员初始化列表

        在构造函数中,尽量使用成员初始化列表来初始化成员变量,避免在构造函数体中执行复杂的逻辑。

class Base {
public:Base(int value) : member(value) {// 构造函数体尽量简洁}virtual void show() = 0;  // 纯虚函数private:int member;
};class Derived : public Base {
public:Derived(int value) : Base(value) {// Derived特定的初始化}void show() override {std::cout << "Derived show with member: " << member << std::endl;}private:int member;  // 假设Derived也有一个同名成员
};

5.2.两阶段构造

        从设计原则的角度来看,构造函数应该专注于初始化对象的状态,而不是执行依赖于对象类型的复杂逻辑。如果需要在对象构造后根据对象类型执行不同的操作,应该将这些操作放在构造函数之外的其他成员函数(如初始化函数或设置函数)中,并通过虚函数来实现多态性。

class Base {
public:Base() {// 基础初始化}virtual void initialize() {show();}virtual void show() {std::cout << "Base show" << std::endl;}
};class Derived : public Base {
public:Derived() {// Derived特定的初始化可以放在这里,但最好还是在initialize中}void initialize() override {show();}void show() override {std::cout << "Derived show" << std::endl;}
};int main() {Base* b = new Derived();b->initialize();  // 调用initialize实现多态delete b;return 0;
}

5.3.工厂模式

        使用工厂模式创建对象,并在工厂函数中进行必要的初始化,确保对象在使用前已经完全构造和初始化。

class Base {
public:virtual void show() = 0;static Base* create();  // 工厂函数
};class Derived : public Base {
public:void show() override {std::cout << "Derived show" << std::endl;}
};Base* Base::create() {Base* b = new Derived();// 可以在这里进行额外的初始化return b;
}int main() {Base* b = Base::create();b->show();delete b;return 0;
}

6.总结

        在C++中,构造函数不允许调用虚函数,这一规定是基于对象构造过程的安全性和一致性考虑。在对象未完全构造时调用虚函数,可能引发未定义行为,破坏程序的正确性。因此,我们应遵循这一规定,通过成员初始化列表、两阶段构造、工厂模式等替代方案,确保对象的正确初始化和多态行为的实现。


http://www.ppmy.cn/devtools/114018.html

相关文章

客户端/服务器的简易实现

目录 一,网络编程套接字 二,UDP/TCP的区别(​编辑) 三,UDP API使用 四,TCP API使用 一,网络编程套接字 socket socket(操作系统给应用程序的API,起了一个名字,就成为socket API) socket API提供了两套API分别为UDP和TCP: 二,UDP/TCP的区别() TCP有链接,可靠传输,面向字…

Qt中的延时

单次触发延时 单次触发延时是指定时器在指定的延时后触发一次&#xff0c;然后自动停止。这种方式非常适合只需要延时执行一次操作的场景。 #include <QTimer> #include <QObject>class MyClass : public QObject {Q_OBJECT public:MyClass() {QTimer::singleSho…

华为9月新品预告,「玄玑」之韵,引领科技潮流

随着科技的不断发展&#xff0c;智能手机已经成为我们日常生活中不可或缺的一部分。 作为全球领先的科技企业&#xff0c;华为一直致力于为消费者带来创新且具有竞争力的产品。 近日&#xff0c;有关华为9月份即将发布的新品消息引起了广泛关注。 据悉&#xff0c;这次新品将…

基于51单片机的矿井安全检测系统

基于51单片机的矿井安全检测系统使用51单片机作为系统主控&#xff0c;LCD1602进行显示同时系统集成了ADC0808和烟雾传感器、甲烷传感器&#xff0c;二者结合测量环境烟雾值&#xff0c;同时使用DHT11温湿度传感器获取环境温湿度值&#xff0c;使用L298N驱动风扇&#xff0c;利…

【Python机器学习】循环神经网络(RNN)——传递数据并训练

与其他Keras模型一样&#xff0c;我们需要向.fit()方法传递数据&#xff0c;并告诉它我们希望训练多少个训练周期&#xff08;epoch&#xff09;&#xff1a; model.fit(X_train,y_train,batch_sizebatch_size,epochsepochs,validation_data(X_test,y_test)) 因为个人小电脑内…

iPhone 16系列:熟悉的味道,全新的体验

来看看iPhone 16和Plus这两个新成员&#xff0c;实话说&#xff0c;它们和之前曝光的样子几乎完全一致。下面我们就一起来细数一下这次的几大变化吧。 外观设计&#xff1a;焕然一新 首先&#xff0c;最显眼的变化就是后置镜头模组的布局调整为了垂直排列。这一改变使得整个背…

初写MySQL四张表:(1/4)

今天我们的任务主线&#xff0c;便是完成创建该表&#xff1a; 表&#xff1a; 何为表&#xff1a; 表&#xff0c;Table也。 我在这里只是简单谈谈&#xff1a; 数据库的对象之一&#xff1a;数据库的对象有四大类&#xff0c;表是其中最根本的存在对象。其特点就是&…

管道缺陷检测系统源码分享

管道缺陷检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…