C++默认构造函数的合成

编译器只在编译期需要的时候合成默认构造函数,而不是在用户需要的时候

文章目录

  • 引入
  • 编译器合成默认构造函数的四种情况
    • 情况一 类中包含带有默认构造函数的类的成员对象
    • 情况二 派生类的基类带有默认构造函数
    • 情况三 类带有一个虚函数
    • 情况四 派生自一个虚基类的类
  • 参考资料


引入

如果有一个类 Foo,其类的定义如下:

class Foo {
public:int val;Foo* next;
};

我们生成一个 Foo 类型的对象

Foo* bar = new Foo;
cout << bar->val << " " << bar->next << endl;

此时编译器会合成默认的构造函数吗,或者说编译会像我们希望的那样,初始化成员变量 valnext 吗,输出如下

可以看到并没有将 valnext 初始化。

编译器合成默认构造函数的四种情况

情况一 类中包含带有默认构造函数的类的成员对象

看下面这个case,类 Foo 的对象是类 Bar 的成员变量,且类 Foo 有默认的构造函数

class Foo {
public:Foo() : val(0), next(nullptr) { cout << "调用Foo的默认构造函数" << endl; }int val;Foo* next;
};class Bar {
public:Foo foo;int x;
};int main()
{Bar* bar = new Bar;cout << bar->x << endl;
}

输出如下:

可以看到调用了 Foo 的默认构造函数,说明此时编译期为 Bar 合成了默认构造函数,虽然这里的 x 也被初始化了,但是这是不能保证的,所以用户想保证 x 被初始化,得用户自己声明定义 Bar 的默认构造函数来完成这个操作,看VS2022下的汇编代码,这里的 Bar::__autoclassinit2 主要是把内存都初始化为 0,Bar::Bar 中调用了 Foo 的默认构造函数。

如果用户自定义了一个默认构造函数,但是只初始化了 x 会是什么结果

class Bar {
public:Bar() {cout << "调用Bar的默认构造函数" << endl;x = 0;}Foo foo;int x;
};

其结果如下,可以看到先调用了 Foo 的默认构造函数,说明编译器会在 Bar 的默认构造函数的代码前,先调用 Foo 的默认构造函数

可以看作编译器对 Bar 的默认构造函数进行了扩张

Bar::Bar()
{foo.Foo::Foo();// 省略输出x = 0;
}

如果有多个类成员对象都要构造器的初始化操作,那么会成员对象在类中的声明次序来调用各个类的默认构造器,比如我们有以下三个类:

class Dopey { public: Dopey() { cout << "调用Dopey的默认构造函数" << endl; } };
class Sneezy { public: Sneezy() { cout << "调用Sneezy的默认构造函数" << endl; } Sneezy(int x) { cout << "调用Sneezy的默认构造函数" << endl; m_x = x; } private: int m_x; };class Bashful { public: Bashful() {cout << "调用Bashful的默认构造函数" << endl; } };

以及包含上面三个类对象为成员变量的类 Snow_White

class Snow_White
{
public:Dopey dopey;Sneezy sneezy;Bashful bashful;private:int mumble;
};

如果 Snow_White 没有定义默认构造函数,那么会有一个默认构造函数被编译期合成出来,依次调用 DopeySneezyBashful 的默认构造函数。

如果 Snow_White 定义了下面的默认构造函数

Snow_White::Snow_White() : sneezy(1024) { cout << "调用Snow_White的构造函数" << endl; mumble = 2048; };

它会被扩张为

Snow_White::Snow_White() : sneezy(1024)
{dopey.Dopey::Dopey();sneezy.Sneezy::Sneezy(1024);bashful.Bashful::Bashful();mumble = 2048;
}

代码输出结果


情况二 派生类的基类带有默认构造函数

如果一个没有任何构造器的类派生自一个带有默认构造器的基类,那么这个派生类的默认构造器需要被编译器和出来,它将调用上一层基类的默认构造器。

如果派生类还带有有默认构造函数的类对象作为成员变量,那么在所有基类构造函数被调用后,会调用这些成员变量的默认构造函数。

还是上面的例子,只不过让 Snow_White 派生自一个基类 BaseBase 有一个默认的构造函数。

class Base {
public:Base() {cout << "调用Base的默认构造函数" << endl;}
};
class Snow_White : public Base
{
public:Snow_White() : sneezy(1024) { cout << "调用Snow_White的构造函数" << endl; mumble = 2048; };Dopey dopey;Sneezy sneezy;Bashful bashful;private:int mumble;
};

代码输出如下


情况三 类带有一个虚函数

因为虚函数涉及到要给该类的对象一个合适的指向其虚函数表的指针,所以如果没有默认的构造函数,则编译期会合成默认的构造函数,用以指定合适的虚函数指针。

比如有如下类 Widget,其有一个虚函数 flip,类 BellWhistle 都派生自 Widget

class Widget {
public:virtual void flip() const = 0;
};class Bell : public Widget
{
public:void flip() const override{cout << "Bell::flip()" << endl;}
};class Whistle : public Widget
{
public:
void flip() const override{cout << "Whistle::flip()" << endl;}
};;void flip(const Widget& widget)
{widget.flip();
}int main()
{Bell b;Whistle w;flip(b);flip(w);
}

下面两个扩张操作会在编译期间发生:

  1. 一个虚函数的表 vtbl 会被编译器产生出来(放在.rdata只读数据区),内放类的虚函数地址
  2. 在每一个类的对象中,一个额外的指针成员(虚函数表指针 vptr),会被编译器合成出来,指向类相关的虚函数表的地址

此外,widget.flip() 的虚拟引发操作(virtual invocation) 会被重新改写,以使用 widget 的vptr 和 vtbl 中的 flip() 条目

( *widget.vptr[ 1 ] )( &widget )
  1. 1 表示 flip() 在虚函数表中的固定索引
  2. &widget 代表要交给被调用的某个 flip() 函数实体的 this 指针

为了让上述机制发挥功效,编译器必须为每一个 Widget (或其派生类)的对象的 vptr 设定初值,放置适当的 vtbl 地址。对于类所定义的每一个构造器,编译器会安插一些代码来做这样的事情。对于未声明任何构造器的类,编译器会为它们合成一个默认的构造器,以便正确地初始化每一个类对象的 vptr。


情况四 派生自一个虚基类的类

Virtual base class 的实现法在不同的编译器之间有极大的差异。然而,每一种实现法的共通点在于必须使虚基类,在其每一个派生类对象中的位置,能够于执行期准备妥当。例如下面这段程序代码:

class X { public: int i; };
class A : public virtual X { public: int j; };
class B : public virtual X { public: double d; };
class C : public A, public B { public: int k; };void foo(A* const pa) { pa->i = 1024; }int main()
{foo(new A);foo(new C);
}

编译器无法固定住 foo() 之中经由 pa 而存取的 X::i 的实际偏移位置,因为 pa 的真正类型可以改变,编译器必须改变”执行存取操作“的那些码,使 X::i 可以延迟至执行期才决定下来。

原先 cfront 的做法是靠 “在派生类对象的每一个虚基类中安插一个指针” 来完成。所有 “经由引用或指针来存取一个虚基类” 的操作都可以通过相关指针来完成。在上面的例子中,foo() 可以被改写如下,以符合这样的实现策略:

void foo(A* const pa ) { pa->__vbcX->i = 1024; } 

其中,__vbcX 表示编译器所产生的指针,指向虚基类 X ,而 __vbcX 是在对象构建期间被完成的。对于类所定义的每一个构造函数,编译器会安插那些 ”允许每一个虚基类执行期存取操作“ 的代码。如果类没有声明任何构造函数,编译器必须为它合成一个默认构造函数。

在VS2022可以看到类的内存布局:
A 的内存布局如下

B 的内存布局如下

C 的内存布局如下

我猜测这里中间空出来的字节就是为了存放 __vbcX 或其它类似的实现,这里的实现是通过vbtable,vbtable里存放着基类成员的实际偏移位置。

我们可以看到A中构造函数的汇编代码如下,可以看到将 A::vbtable 的偏移地址赋给了 this 指针指向地址的首位置

监视 this 指针和两个成员变量的地址,可以看到有个 0x00de2140 的位置是被用来存放 vbtable 的偏移地址的,剩下用来存放变量


参考资料

《深度探索C++对象模型》—— Stanley B.Lippman著,侯捷译

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/4227.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Day53|动态规划part14: 1143.最长公共子序列、1035. 不相交的线、53. 最大子序和

1143. 最长公共子序列 这题有点像递增子序列和公共子数组的组合&#xff0c; 要求公共子序列不一定非要是连续的。 确定dp数组下标及其含义 dp[i][j]表示text1[i - 1]与text2[j - 1]结尾的最高公共子序列。 长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的…

Redis 服务等过期策略和内存淘汰策略解析

redis服务是基于内存运行的&#xff0c;所以很多数据都存放在内存中&#xff0c;但是内存又不是无限的&#xff0c;所以redis就引出了key的过期和淘汰策略。 一、Redis的过期策略&#xff1a; 我们在set key的时候&#xff0c;可以给它设置一个过期时间&#xff0c;比如expire …

【神经网络结构可视化】PlotNeuralNet的安装、测试及创建自己的神经网络结构可视化图形

文章目录 前提准备1、下载MikTeX2、下载Git bash3、下载PlotNeuralNet 进行测试1、解压PlotNeuralNet-master.zip2、打开Git bash3、 在my_project中查看生成的pdf文件 创建自己的神经网络结构可视化图形 前提准备 1、下载MikTeX 下载链接&#xff1a; MikTeX ( https://mikt…

【图解计算机网络】TCP协议三次握手与四次挥手

TCP协议三次握手与四次挥手 三次握手流程为什么是三次握手&#xff0c;而不是两次或四次四次挥手流程TIME_WAIT 为什么要等待 2MSL为什么握手是三次&#xff0c;挥手是四次&#xff1f; 三次握手流程 首先是客户端&#xff08;也就是我们的浏览器&#xff09;发送一个SYN标志位…

C++11 数据结构5 队列的概念,队列的顺序存储,实现,测试

一&#xff0c;队列的概念 队列是一种特殊的受限制的线性表。 队列&#xff08;queue&#xff09;是只允许在一端进行插入操作&#xff0c;而在另一端进行删除操作的线性表。 队列是一种先进先出的t&#xff08;First In First Out&#xff09;的线性表&#xff0c;简称FIF…

请编写函数fun,其功能是:将所有大于1小于整数m的非素数存入xx所指数组中,非素数的个数通过k传回。

本文收录于专栏:算法之翼 https://blog.csdn.net/weixin_52908342/category_10943144.html 订阅后本专栏全部文章可见。 本文含有题目的题干、解题思路、解题思路、解题代码、代码解析。本文分别包含C语言、C++、Java、Python四种语言的解法完整代码和详细的解析。 题干 请编…

NDK 基础(五)—— C++ 高级特性2

1、左值右值 在 C 中&#xff0c;左值&#xff08;lvalue&#xff09;和右值&#xff08;rvalue&#xff09;是用于描述表达式的术语&#xff0c;它们与赋值操作和内存中对象的生命周期有关。 **左值&#xff08;lvalue&#xff09;**是指可以出现在赋值操作符左侧的表达式&a…

商店数据(九)

目录 65.店铺入驻字段表 66.店铺分类表 67.店铺配置表 68.店铺快递公司关联表 69.店铺资料附加表 70.店铺入驻流程表 71.店铺运费模板表 72.消息类型表 65.店铺入驻字段表 CREATE TABLE wst_bases (id int(11) NOT NULL AUTO_INCREMENT COMMENT 自增id,flowld int(11)…

如何安全进行速卖通自养号测评操作?

对于新加入的卖家而言&#xff0c;进行销量测评显得尤为关键。速卖通平台上的新店往往难以获得活动的扶持&#xff0c;且初始流量相当有限。因此&#xff0c;开店的首要任务便是积极展开测评工作&#xff0c;努力积累初始的评论和销售记录。测评的益处颇为显著&#xff0c;它不…

SpringBoot项目启动,传参有哪些方式?

SpringBoot项目启动&#xff0c;传参有哪些方式&#xff1f; 1.Spring级别的参数 直接在启动 Spring Boot 应用的命令行中使用 -- 后跟参数名和值的方式来传递参数。 记住&#xff1a;一般是对于Spring Boot应用特有的配置参数&#xff0c;确保它们遵循Spring Boot的配置属性命…

【视频打架行为数据集】打斗场景视频数据集简要介绍

一、UBI-Fight&#xff08;异常事件检测数据集&#xff09; 介绍 UBI-Fights 数据集是一个独特的全新大型数据集&#xff0c;涉及特定的异常检测并仍然在打斗场景中提供广泛的多样性&#xff0c;该数据集包含 80 小时的视频&#xff0c;在帧级别进行了完全注释。由 1000 个视…

# 从浅入深 学习 SpringCloud 微服务架构(五)Consul(2)

从浅入深 学习 SpringCloud 微服务架构&#xff08;五&#xff09;Consul&#xff08;2&#xff09; 段子手168 一、consul 集群&#xff1a;consul 集群的基础知识 1、启动 sonsul 服务命令&#xff1a; 以开发者模式快速启动&#xff1a; consul agent -dev -client0.0.0…

13.JAVAEE之HTTP协议

HTTP 最新的版本应该是 HTTP/3.0 目前大规模使用的版本 HTTP/1.1 使用 HTTP 协议的场景 1.浏览器打开网站 (基本上) 2.手机 APP 访问对应的服务器 (大概率) 学习 HTTP 协议, 重点学习 HTTP 的报文格式 前面的 TCP/IP/UDP 和这些不同, HTTP 的报文格式,要分两个部分来看待.请求…

移动端日志采集与分析最佳实践

前言 做为一名移动端开发者&#xff0c;深刻体会日志采集对工程师来说具有重要意义&#xff0c;遇到问题除了 debug 调试就是看日志了&#xff0c;通过看日志可以帮助我们了解应用程序运行状况、优化用户体验、保障数据安全依据&#xff0c;本文将介绍日志采集的重要性、移动端…

高级防爬还得是公众号

平时一天也就1K的流量&#xff0c;最近流量暴涨&#xff0c;已经用自研的WAF防火墙阻挡了很多恶意攻击和爬虫&#xff0c;已经过滤掉很多低级攻击和爬取了。 多出的流量&#xff0c;也仅仅多了一个导航 dh.yu7s.com 用户&#xff0c;多时没有用的机器人爬虫&#xff0c;不封掉浪…

EXCEL表格中的数字,为什么每次打开会自动变成日期?

一、典型现象 在工作中&#xff0c;有时会发现公司里的报表&#xff0c;经过多人多次的重复的使用和修改后&#xff0c;会出现这种情况&#xff1a; 1.在表格里按照需要输入数字&#xff0c;保存工作簿。 2.然而&#xff0c;再次打开工作簿&#xff0c;里面的数字变成日期&a…

c#学习入门2

十、运算符 1&#xff09;算术运算符是用于数值类型变量计算的运算符&#xff0c;它返回的结果是数值 1.赋值符号 2.算数运算符 加 减- 乘* 除/ 取余% 3.算数运算符的优先级 4.算术运算符的复合运算 5.算术运算符的自增减 2&#xff09;字符串拼接 1.字符串拼接方式1 注意&…

自定义View-旋转变色圆角三角形的绘制

本文字数&#xff1a;3151字 预计阅读时间&#xff1a;20分钟 在现代设计中&#xff0c;动效图在APP的UI界面中所起到的作用无疑是显著的。相比于静态的界面&#xff0c;动效更符合人类的自然认知体系&#xff0c;它有效地降低了用户的认知负载&#xff0c;UI动效俨然已经成为了…

错误代码126:加载d3dcompiler_43.dll失败,分享多种解决方法

在正常使用电脑的过程中&#xff0c;当我尝试启动并运行一款心仪的游戏时&#xff0c;系统却突然弹出一个令人困扰的错误提示“错误代码126:加载d3dcompiler_43.dll失败”&#xff0c;它会导致游戏无法正常运行。为了解决这个问题&#xff0c;我经过多次尝试和总结&#xff0c;…

VMware ESXi虚拟机备份的方法和步骤

关于虚拟机备份 VMware ESXi 是 VMware vSphere 企业虚拟化套件的核心组件。在版本4.1之前&#xff0c;它一直被称为ESX。ESXi是一种裸机管理程序&#xff0c;直接安装在物理服务器上&#xff0c;它提供对底层资源的直接访问和控制&#xff0c;允许您在单个物理主机上创建和运…