C++11 列表初始化都做了什么?

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

首先,来看两个问题:

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

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

这两个问题从何而来

通常,当你搜索为什么列表初始化优于构内赋值时,基本上所有的博文都会告诉你:"列表初始化使得成员变量在被定义时绑定初值;而在构造函数内赋值,成员变量会先在定义时被初始化为0,然后再被赋值为指定的初值。"总之,意思是,列表初始化相比于构内赋值少了一次对内存的写入。至于这样的说法是否正确?为什么会这样?列表初始化和构内赋值的实现细节是什么样的?很少有人分析。所以,本着"实践出真知"的道理,我们在这本篇博文中做了一些列的实验,并详细的讲述了为什么初始化列表由于构内赋值?

列表初始化 和 构内赋值 的实现细节

首先,先将提出的两个问题回答一下:

  • 类的列表初始化是通过成员变量的拷贝构造函数实现的。
  • 列表初始化相比于构内赋值,减少了一半的函数调用,减少了一半的内存写入。

列表初始化和构内赋值的具体的流程图如下:

构内赋值 的实现细节

构内赋值分为两步实现:

  • 调用各级成员的默认构造函数
  • 调用各级成员的赋值函数

首先,我们来解释一下什么是"各级成员"?如下,类 A 中包含类型为 B 的成员变量 b,而 B 类型还可能包含类型为 C 的成员变量 c,如此递推。直至递推到基础类型(比如 int)。对于类 A 而言,b, c, d ... 就是它的各级成员。

C++ 复制代码
class C{
    D d;
};

class B{
    C c;
};

class A{
    B b;
};

我们通过如下代码来测试构内赋值是如何实现的:

C++ 复制代码
#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
  • 调用了各级成员的赋值函数
  • 向基础类型成员的内存中写入初值

列表初始化的实现细节

我们通过如下代码来观察列表初始化的实现细节:

C++ 复制代码
#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
  • 调用了各级成员的赋值函数
  • 向基础类型成员的内存中写入初值

列表初始化的资源分析如下:

  • 调用了各级成员的拷贝构造函数
  • 向基础类型成员的内存中写入初值

可见,列表初始化比构内赋值减少了一半的资源调用和一半的内存写入。因此列表初始化由于构内赋值。

看来,大多数博文中说的不完全对,他们只说对了内存,却没有分析函数的调用次数。

相关推荐
起名字真南9 分钟前
【OJ题解】C++实现字符串大数相乘:无BigInteger库的字符串乘积解决方案
开发语言·c++·leetcode
少年负剑去9 分钟前
第十五届蓝桥杯C/C++B组题解——数字接龙
c语言·c++·蓝桥杯
cleveryuoyuo10 分钟前
AVL树的旋转
c++
神仙别闹33 分钟前
基于MFC实现的赛车游戏
c++·游戏·mfc
小c君tt40 分钟前
MFC中 error C2440错误分析及解决方法
c++·mfc
木向1 小时前
leetcode92:反转链表||
数据结构·c++·算法·leetcode·链表
阿阿越1 小时前
算法每日练 -- 双指针篇(持续更新中)
数据结构·c++·算法
hunandede1 小时前
FFmpeg存放压缩后的音视频数据的结构体:AVPacket简介,结构体,函数
c++
hunandede2 小时前
FFmpeg 4.3 音视频-多路H265监控录放C++开发十三:将AVFrame转换成AVPacket。视频编码,AVPacket 重要函数,结构体成员学习
c++·ffmpeg·音视频
奋斗的小花生8 小时前
c++ 多态性
开发语言·c++