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

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

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

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

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

相关推荐
努力实干10 小时前
面向对象设计原则——C++
开发语言·c++·数码相机
syagain_zsx13 小时前
jsoncpp 使用指南:编译链接、Json::Value 取值与高频坑
c++·json·库·http格式
布莱克60514 小时前
理解 = delete 与 = default:两种赋值语义的差异
c++
小小、码农15 小时前
[特殊字符]【C++11】类的新功能全面解析:移动构造、移动赋值、default、delete、final 与 override
linux·开发语言·网络·c++
库玛西15 小时前
外部排序全解析:败者树、置换-选择排序与最佳归并树
c语言·数据结构·c++·笔记·考研·算法
General_G17 小时前
创建型模式—原型(Prototype)模式
c++·设计模式·原型模式
syagain_zsx17 小时前
protobuf 实战:从 .proto 到 C++ 序列化的完整链路
c++·学习·程序人生·谷歌·protobuf
welfare96218 小时前
新号别搞:C++_list
开发语言·c++
xiaoye-duck18 小时前
《C++ 项目:手写大模型接入 SDK》云端 API 调用基础与 Apifox 调试实战
网络·c++·语言模型
坤坤子吖18 小时前
Linux 线程同步与互斥——条件变量与线程同步
linux·开发语言·c++·笔记·学习