类的成员变量的初始化细节

首先,来看两个问题:

  • 类的构造函数中,成员变量的列表初始化是如何实现的?
  • 为什么列表初始化效率上优于在构造函数中为成员变量赋值?

(后文中,将 “在构造函数中为成员变量赋值” 简称为 “构内赋值”。)

这两个问题从何而来

通常,当你搜索为什么列表初始化优于构内赋值时,基本上所有的博文都会告诉你:“列表初始化使得成员变量在被定义时绑定初值;而在构造函数内赋值,成员变量会先在定义时被初始化为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 列表初始化都做了什么?的更多相关文章

  1. C++11 列表初始化

    在我们实际编程中,我们经常会碰到变量初始化的问题,对于不同的变量初始化的手段多种多样,比如说对于一个数组我们可以使用 int arr[] = {1,2,3}的方式初始化,又比如对于一个简单的结构体: ...

  2. C++11(列表初始化+变量类型推导+类型转换+左右值概念、引用+完美转发和万能应用+定位new+可变参数模板+emplace接口)

    列表初始化 用法 在C++98中,{}只能够对数组元素进行统一的列表初始化,但是对应自定义类型,无法使用{}进行初始化,如下所示: // 数组类型 int arr1[] = { 1,2,3,4 }; ...

  3. c++11——列表初始化

    1. 使用列表初始化 在c++98/03中,对象的初始化方法有很多种,例如 int ar[3] = {1,2,3}; int arr[] = {1,2,3}; //普通数组 struct A{ int ...

  4. C++11的初始化列表

      初始化是一个非常重要的语言特性,最常见的就是对对象进行初始化.在传统 C++ 中,不同的对象有着不同的初始化方法,例如普通数组.POD (plain old data,没有构造.析构和虚函数的类或 ...

  5. c++11之初始化列表

    一.前言     C++的学习中.我想每一个人都被变量定义和申明折磨过,比方我在大学笔试过的几家公司.都考察了const和变量,类型的不同排列组合,让你差别有啥不同.反正在学习C++过程中已经被折磨惯 ...

  6. C++11之列表初始化

    1. 在C++98中,标准允许使用花括号{}来对数组元素进行统一的集合(列表)初始化操作,如:int buf[] = {0};int arr[] = {1,2,3,4,5,6,7,8}; 可是对于自定 ...

  7. [转帖]支撑双11每秒17.5万单事务 阿里巴巴对JVM都做了些什么?

    支撑双11每秒17.5万单事务 阿里巴巴对JVM都做了些什么? https://mp.weixin.qq.com/s?__biz=MzA3OTg5NjcyMg==&mid=2661671930 ...

  8. C++11常用特性介绍——列表初始化

    一.列表初始化 1)C++11以前,定义初始化的几种不同形式,如下: int data = 0;   //赋值初始化 int data = {0};   //花括号初始化 int data(0); / ...

  9. HashMap的初始化,到底都做了什么?

    HashMap的初始化,到底都做了什么? HashMap初始化参数都是什么?默认是多少? 为什么建议初始化设置容量? tableSizeFor方法是做什么的? 如何获取到一个key的hash值?及计算 ...

  10. 大括号之谜:C++的列表初始化语法解析

    有朋友在使用std::array时发现一个奇怪的问题:当元素类型是复合类型时,编译通不过. struct S { int x; int y; }; int main() { int a1[3]{1, ...

随机推荐

  1. 使用Hexo搭建个人博客网站

    参考CSDN上的博客.特此感谢wsmrzx.

  2. 你不知道的 HTTP Referer

    前言 上周突然发现自己的自己站点的图片全都403了,之前还是好好的,图片咋就全都访问不了呢?由于我每次发文章都是先发了掘金,然后再从掘金拷贝到我自己的站点,这样我就不用在自己的站点去上传图片了,非常方 ...

  3. Git练习网址

    爲了方便学习git指令,让新手们更容易地理解,所以推荐一些git练习和博文网址 推荐的网址如下 网址一:Learn Git Branching! https://learngitbranching.j ...

  4. 深入理解Java虚拟机(JVM):原理、结构与性能优化

    1. 介绍 Java虚拟机(JVM)是Java程序的核心执行引擎,负责将Java源代码编译成可执行的字节码,并在运行时负责解释执行字节码或将其编译成本地机器代码.本文将深入探讨JVM的原理.结构以及性 ...

  5. 使用 Go 语言实现二叉搜索树

    原文链接: 使用 Go 语言实现二叉搜索树 二叉树是一种常见并且非常重要的数据结构,在很多项目中都能看到二叉树的身影. 它有很多变种,比如红黑树,常被用作 std::map 和 std::set 的底 ...

  6. 免费拥有自己的 Github 资源加速器

    TurboHub 是一个免费的 Github 资源加速下载站点,可以帮助你快速下载 Github 上的资源.其核心逻辑是通过 Azure Static Web Apps 服务和 Azure Funct ...

  7. 一种基于ChatGPT的高效吃瓜方式的探索和研究。

    你好呀,我是歪歪. 最近掌握了一个新的吃瓜方式,我觉得还行,给大家简单分享一下. 事情说来就话长了,还得从最近的一次"工业革命"开始,也就是从超导材料说起. 8 月 1 日的时候 ...

  8. 开源Word文字替换小工具更新 增加文档页眉和页脚替换功能

    ITGeeker技术奇客发布的开源Word文字替换小工具更新到v1.0.1.0版本啦,现已支持Office Word文档页眉和页脚的替换. 同时ITGeeker技术奇客修复了v1.0.0.0版本因替换 ...

  9. python如何提取浏览器中保存的网站登录用户名密码

    python如何提取Chrome中的保存的网站登录用户名密码? 很多浏览器都贴心地提供了保存用户密码功能,用户一旦开启,就不需要每次都输入用户名.密码,非常方便.作为python脚本,能否拿到用户提前 ...

  10. 《SQLi-Labs》01. Less 1~5

    @ 目录 前言 索引 Less-1 题解 原理 Less-2 题解 Less-3 题解 Less-4 题解 Less-5 题解 原理 sqli.开启新坑. 前言 对于新手,为了更加直观的看到 sql ...