C++11 列表初始化都做了什么?
类的成员变量的初始化细节
首先,来看两个问题:
- 类的构造函数中,成员变量的列表初始化是如何实现的?
- 为什么列表初始化效率上优于在构造函数中为成员变量赋值?
(后文中,将 “在构造函数中为成员变量赋值” 简称为 “构内赋值”。)
这两个问题从何而来
通常,当你搜索为什么列表初始化优于构内赋值时,基本上所有的博文都会告诉你:“列表初始化使得成员变量在被定义时绑定初值;而在构造函数内赋值,成员变量会先在定义时被初始化为0,然后再被赋值为指定的初值。”总之,意思是,列表初始化相比于构内赋值少了一次对内存的写入。至于这样的说法是否正确?为什么会这样?列表初始化和构内赋值的实现细节是什么样的?很少有人分析。所以,本着“实践出真知”的道理,我们在这本篇博文中做了一些列的实验,并详细的讲述了为什么初始化列表由于构内赋值?。
列表初始化 和 构内赋值 的实现细节
首先,先将提出的两个问题回答一下:
- 类的列表初始化是通过成员变量的拷贝构造函数实现的。
- 列表初始化相比于构内赋值,减少了一半的函数调用,减少了一半的内存写入。
列表初始化和构内赋值的具体的流程图如下:

构内赋值 的实现细节
构内赋值分为两步实现:
- 调用各级成员的默认构造函数
- 调用各级成员的赋值函数
首先,我们来解释一下什么是“各级成员”?如下,类 A 中包含类型为 B 的成员变量 b,而 B 类型还可能包含类型为 C 的成员变量 c,如此递推。直至递推到基础类型(比如 int)。对于类 A 而言,b, c, d ... 就是它的各级成员。
class C{
D d;
};
class B{
C c;
};
class A{
B b;
};
我们通过如下代码来测试构内赋值是如何实现的:
#include<iostream>
class Test0{
int a;
public:
Test0():a(0){
std::cout<< "0默认构造\n";
}
Test0(const Test0& t1):a(t1.a){
std::cout<< "0拷贝构造\n";
}
Test0(Test0&& t1):a(t1.a){
std::cout<< "0移动构造\n";
}
void operator= (const Test0& t1){
std::cout<< "0赋值函数\n";
a = t1.a;
}
};
class Test1{
Test0 t;
public:
Test1(){
std::cout<< "1默认构造\n";
}
Test1(const Test1& t1):t(t1.t){
std::cout<< "1拷贝构造\n";
}
Test1(Test1&& t1):t(t1.t){
std::cout<< "1移动构造\n";
}
void operator= (const Test1& t1){
std::cout<< "1赋值函数\n";
t = t1.t;
}
};
class Test2{
private:
Test1 t1;
public:
Test2(const Test1& t1){
std::cout<< "2构造\n";
this->t1 = t1;
}
};
int main(){
Test1 t1; \\ main 函数第一行代码
std::cout<< "---------------\n";
Test2 t2(t1); \\ main 函数第三行代码
}
在上面的代码中,我们定义了三个类,并依次包含,最底层的类 Test0 包含了一个基础类型 int。在 main 函数中,我们首先默认构造了一个 Test1 类型的对象 t1,而后将 t1 传入到 Test2 的构造函数中。Test2 的构造函数,是通过构内赋值实现的。我们运行上述代码,结果如下:
0默认构造
1默认构造
---------------
0默认构造
1默认构造
2构造
1赋值函数
0赋值函数
可以看到,main 函数的第一行代码通过默认构造函数构建对象 t1,从输出的第 1、2 行可以看出,t1 及其各级成员的构造函数自下而上的运行,即:int 的默认构造 -> Test0 的默认构造 -> Test1 的默认构造。默认构造函数,会定义变量,并初始化为 0。
main 函数的第三行我们定义变量 t2,将 main 函数第一行定义的 t1 传入其构造函数。从输出的第 4、5、6 行可以看出,t2 的成员变量 t1 首先经过了默认构造,然后才进入到 t2 的构造函数中。而后在 t2 的构造函数中,我们将 main 函数第一行定义的 t1 赋值给 t2 的成员变量 t1。从输出的 7、8 行可以看出,这个赋值操作自上而下的调用了成员的赋值函数,即: Test1 的赋值函数 -> Test0 的赋值函数 -> int 的赋值函数。
至此完成了构内赋值。整个过程的资源分析如下:
- 调用了各级成员的默认构造
- 向基础类型成员的内存中写入 0
- 调用了各级成员的赋值函数
- 向基础类型成员的内存中写入初值
列表初始化的实现细节
我们通过如下代码来观察列表初始化的实现细节:
#include<iostream>
class Test0{
int a;
public:
Test0():a(0){
std::cout<< "0默认构造\n";
}
Test0(const Test0& t1):a(t1.a){
std::cout<< "0拷贝构造\n";
}
Test0(Test0&& t1):a(t1.a){
std::cout<< "0移动构造\n";
}
void operator= (const Test0& t1){
std::cout<< "0赋值函数\n";
a = t1.a;
}
};
class Test1{
Test0 t;
public:
Test1(){
std::cout<< "1默认构造\n";
}
Test1(const Test1& t1):t(t1.t){
std::cout<< "1拷贝构造\n";
}
Test1(Test1&& t1):t(t1.t){
std::cout<< "1移动构造\n";
}
void operator= (const Test1& t1){
std::cout<< "1赋值函数\n";
t = t1.t;
}
};
class Test2{
private:
Test1 t1;
public:
Test2(const Test1& t1):t1(t1){
std::cout<< "2构造\n";
}
};
int main(){
Test1 t1;
std::cout<< "---------------\n";
Test2 t2(t1);
}
相比于构内赋值的实现细节中的代码,我们将 Test2 的构造函数改为列表初始化。代码的运行结果如下:
0默认构造
1默认构造
---------------
0拷贝构造
1拷贝构造
2构造
观察输出的 4,5 行,列表初始化通过调用各级成员的拷贝构造函数来完成。这种调用是自下而上的,即:int 的拷贝构造 -> Test0 的拷贝构造 -> Test1 的拷贝构造。基础类型的成员经历了一次内存写入。
观察输出的 4, 5, 6 行,列表初始化在进入构造函数的函数体之前完成。
列表初始化的资源分析如下:
- 调用了各级成员的拷贝构造函数
- 向基础类型成员的内存中写入初值
列表初始化 与 构内赋值 所用资源比较
构内赋值的资源分析如下:
- 调用了各级成员的默认构造
- 向基础类型成员的内存中写入 0
- 调用了各级成员的赋值函数
- 向基础类型成员的内存中写入初值
列表初始化的资源分析如下:
- 调用了各级成员的拷贝构造函数
- 向基础类型成员的内存中写入初值
可见,列表初始化比构内赋值减少了一半的资源调用和一半的内存写入。因此列表初始化由于构内赋值。
看来,大多数博文中说的不完全对,他们只说对了内存,却没有分析函数的调用次数。
C++11 列表初始化都做了什么?的更多相关文章
- C++11 列表初始化
在我们实际编程中,我们经常会碰到变量初始化的问题,对于不同的变量初始化的手段多种多样,比如说对于一个数组我们可以使用 int arr[] = {1,2,3}的方式初始化,又比如对于一个简单的结构体: ...
- C++11(列表初始化+变量类型推导+类型转换+左右值概念、引用+完美转发和万能应用+定位new+可变参数模板+emplace接口)
列表初始化 用法 在C++98中,{}只能够对数组元素进行统一的列表初始化,但是对应自定义类型,无法使用{}进行初始化,如下所示: // 数组类型 int arr1[] = { 1,2,3,4 }; ...
- c++11——列表初始化
1. 使用列表初始化 在c++98/03中,对象的初始化方法有很多种,例如 int ar[3] = {1,2,3}; int arr[] = {1,2,3}; //普通数组 struct A{ int ...
- C++11的初始化列表
初始化是一个非常重要的语言特性,最常见的就是对对象进行初始化.在传统 C++ 中,不同的对象有着不同的初始化方法,例如普通数组.POD (plain old data,没有构造.析构和虚函数的类或 ...
- c++11之初始化列表
一.前言 C++的学习中.我想每一个人都被变量定义和申明折磨过,比方我在大学笔试过的几家公司.都考察了const和变量,类型的不同排列组合,让你差别有啥不同.反正在学习C++过程中已经被折磨惯 ...
- C++11之列表初始化
1. 在C++98中,标准允许使用花括号{}来对数组元素进行统一的集合(列表)初始化操作,如:int buf[] = {0};int arr[] = {1,2,3,4,5,6,7,8}; 可是对于自定 ...
- [转帖]支撑双11每秒17.5万单事务 阿里巴巴对JVM都做了些什么?
支撑双11每秒17.5万单事务 阿里巴巴对JVM都做了些什么? https://mp.weixin.qq.com/s?__biz=MzA3OTg5NjcyMg==&mid=2661671930 ...
- C++11常用特性介绍——列表初始化
一.列表初始化 1)C++11以前,定义初始化的几种不同形式,如下: int data = 0; //赋值初始化 int data = {0}; //花括号初始化 int data(0); / ...
- HashMap的初始化,到底都做了什么?
HashMap的初始化,到底都做了什么? HashMap初始化参数都是什么?默认是多少? 为什么建议初始化设置容量? tableSizeFor方法是做什么的? 如何获取到一个key的hash值?及计算 ...
- 大括号之谜:C++的列表初始化语法解析
有朋友在使用std::array时发现一个奇怪的问题:当元素类型是复合类型时,编译通不过. struct S { int x; int y; }; int main() { int a1[3]{1, ...
随机推荐
- sql中当关联查询主表很大影响查询速度时怎么办?
sql中当关联查询主表很大时,直接关联,查询速度会较慢,这时可以先利用子查询经筛选条件筛除一部数据,这样主连接表体量减少,这样能一定程度加快速度. (1)常规join -- 最慢7.558s sele ...
- 如何编写难以维护的React代码?——滥用useEffect
如何编写难以维护的React代码?--滥用useEffect 在许多项目中,我们经常会遇到一些难以维护的React代码.其中一种常见的情况是滥用useEffect钩子,特别是在处理衍生状态时.让我们来 ...
- Linux 命令:ps
ps -ef ps -e f # 树形显示
- 2021-3-9 保存csv格式文件
public void SaveCSV(DataTable dt, string fullPath) { FileInfo fi = new FileInfo(fullPath); if (!fi.D ...
- 【pandas小技巧】--按类型选择列
本篇介绍的是pandas选择列数据的一个小技巧.之前已经介绍了很多选择列数据的方式,比如loc,iloc函数,按列名称选择,按条件选择等等. 这次介绍的是按照列的数据类型来选择列,按类型选择列可以帮助 ...
- react18 hooks自定义移动端Popup弹窗组件RcPop
基于React18 Hooks实现手机端弹框组件RcPop react-popup 基于react18+hook自定义多功能弹框组件.整合了msg/alert/dialog/toast及android ...
- 论文解读(APCA)《Adaptive prototype and consistency alignment for semi-supervised domain adaptation》
[ Wechat:Y466551 | 付费咨询,非诚勿扰 ] 论文信息 论文标题:Adaptive prototype and consistency alignment for semi-super ...
- 从零玩转系列之微信支付实战PC端支付微信取消接口搭建 | 技术创作特训营第一期
一.前言 从零玩转系列之微信支付实战PC端支付微信取消接口搭建 | 技术创作特训营第一期 halo各位大佬很久没更新了最近在搞微信支付,因商户号审核了我半个月和小程序认证也找了资料并且将商户号和小程序 ...
- Go 如何正确关闭通道
序言 Go 在通道这一块,没有内置函数判断通道是否已经关闭,也没有可以直接获取当前通道数量的方法.所以对于通道,Go 显示的不是那么优雅.另外,如果对通道进行了错误的使用,将会直接引发系统 panic ...
- 分布式测试插件 pytest-xdist 使用详解
使用背景: 大型测试套件:当你的测试套件非常庞大,包含了大量的测试用例时,pytest-xdist可以通过并行执行来加速整体的测试过程.它利用多个进程或计算机的计算资源,可以显著减少测试执行的时间. ...