欢迎光临
我们一直在努力

【C++11篇(一)】发展历程、列表初始化详解

在这里插入图片描述

大家好,欢迎来到 huangjin007_ 的博客

个人主页:huangjin007_
🔥
文章收录专栏:零基础入门C++

总会有一些坚持能从冰封的土地里培育出十万朵怒放的蔷薇


C++11篇(一) —— 发展历程、列表初始化详解

  从本篇内容开始,正式开启 C++11 深度学习之旅。接下来我会掰开揉碎拆解每一处语法细节,全程干货,坐稳发车~ ദ്ദി˶ー̀֊ー́ )✧

文章目录

  • C++11篇(一) —— 发展历程、列表初始化详解
    • 1. C++11 的发展历史:一个时代的里程碑
    • 2. 列表初始化的演进
      • 2.1 C++98 里的 `{}` —— 只能用于特定场景
      • 2.2 C++11 的目标:一切对象皆可 `{}`
    • 3. 代码详解:从内置类型到自定义类型
      • 3.1 内置类型也可用 `{}`
      • 3.2 自定义类型的列表初始化:临时对象 + 优化
      • 3.3 常量引用绑定临时对象
      • 3.4 单参数的隐式转换
      • 3.5 省略 `=`
      • 3.6 容器操作中的大杀器
    • 4. std::initializer_list
      • 4.1 `initializer_list` 是什么?
      • 4.2 验证 `initializer_list` 的实现
      • 4.3 容器如何支持多元素初始化
      • 4.4 同样适用于赋值操作
    • 5. 深入本质:类型转换与优化
      • 5.1 窄化转换的防御
      • 5.2 `explicit` 与列表初始化
    • 结语:

1. C++11 的发展历史:一个时代的里程碑

  在 C++ 的进化史上,C++11 绝对是一个划时代的存在。它是自 C++98 以来的第二个主要标准版本,同时也是迄今为止改动最大、影响最深远的一次更新。

  原本这个版本被社区称作 C++0x,因为大家满怀期待地认为它能在 2010 年之前发布。但由于标准委员会希望引入的特性实在太多,讨论和设计过程旷日持久,最终它被正式批准并发布的时间是 2011 年 8 月 12 日。从 2003 年的 C++03 到 2011 年的 C++11,中间整整隔了 8 年——这也是 C++ 历史上最长的一次版本更新间隔。从那以后,C++ 进入了稳定的节奏,每隔 3 年就会推出一个新的标准版本(例如 C++14、C++17、C++20 等)。

  C++11 的使命是巨大的:它引入了大量全新的语言特性和标准库组件,比如自动类型推导 auto、范围 for 循环、智能指针、lambda 表达式、右值引用和移动语义,以及我们今天要重点聊的——统一的初始化方式(列表初始化)。这些特性极大提升了代码的抽象能力和编写效率。

在这里插入图片描述


2. 列表初始化的演进

2.1 C++98 里的 {} —— 只能用于特定场景

  在 C++98 中,花括号 {} 并不是一个通用的初始化工具,它主要用于两种场合:

  • 数组初始化
  • 结构体/聚合体的成员初始化

例如:

// C++98 支持
int array1[] = { 1, 2, 3, 4, 5 }; // 数组
int array2[5] = { 0 }; // 数组,其余元素自动置零

struct Point
{
int _x;
int _y;
};
Point p = { 1, 2 }; // 聚合体初始化

  除此之外,花括号几乎无用武之地。如果你想用类似的方式初始化一个自定义类,或者直接初始化一个标准库容器,C++98 就无能为力了。

2.2 C++11 的目标:一切对象皆可 {}

  C++11 的设计者希望提供一种 统一的初始化方式,让所有对象(不管是内置类型还是用户自定义类型)都能使用 {} 来初始化。这就是 列表初始化。

  这个想法一经实现,带来了几个重要的特性:

  • 内置类型支持
  • 自定义类型支持(背后隐含了类型转换和临时对象的优化)
  • 可以省略等号
  • 在多参数构造时极为便利

3. 代码详解:从内置类型到自定义类型

  为了让讲解更加清晰,我们先把需要用到的 Date 类摆出来:

class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year), _month(month), _day(day)
{
cout << "Date(int year,int month,int day)" << endl;
}

Date(const Date& d)
:_year(d._year), _month(d._month), _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}

private:
int _year;
int _month;
int _day;
};

  这个类很简单:一个三参数构造函数(每个参数都有默认值),一个拷贝构造函数,并且它们都会在调用时输出信息,方便我们追踪构造过程。

3.1 内置类型也可用 {}

int x1 = { 2 };
int x2 = 2;

  int x1 = { 2 }; 这行代码在 C++11 中是完全合法的。它用列表初始化的方式将 x1 初始化为 2。本质上,这里 {2} 会被解释成一个只有一个元素的列表,然后用来初始化 x1。它和 int x2 = 2; 在效果上完全一样。你可以把 {} 看成一种更严格、更统一的初始化符号。

3.2 自定义类型的列表初始化:临时对象 + 优化

接下来是重点:

Date d1 = { 2026, 6, 19 };

  这行代码是如何工作的?编译器看到 { 2026, 6, 19 },会尝试用这三个值去构造一个 Date 对象。具体流程如下:

  • { 2026, 6, 19 } 先被用来构造一个 Date 类型的临时对象,也就是调用 Date(int year, int month, int day) 构造函数。
  • 这个临时对象再作为参数去拷贝构造 d1,即调用 Date(const Date& d)。
  •   按照语言规则,这是一个“构造临时对象 + 拷贝构造”的两步过程。但是 C++ 标准允许一种重要的优化:拷贝省略,编译器可以把这个过程合二为一,直接用 { 2026, 6, 19 } 去构造 d1,而无需生成中间的临时对象。你在运行程序时只会看到一次构造函数调用输出,就是这个优化的结果。

    在这里插入图片描述

    注意:即使没有优化,语义上也是先构造临时对象再拷贝。但在现代编译器中,哪怕不开启优化选项,也常常会执行这种“合二为一”的优化。

    3.3 常量引用绑定临时对象

    const Date& d2 = { 2026, 6, 19 };

      同样,这里 { 2026, 6, 19 } 先构造出一个临时 Date 对象,然后 d2 这个常量引用绑定到该临时对象上。C++ 规定,常量引用可以延长临时对象的生命周期,所以 d2 成为该临时对象的别名,你可以安全地使用它,直到 d2 离开作用域。

      这在函数传参时非常有用,例如某个函数接受 const Date&,你可以直接传入 {2026,6,19} 作为实参。

    3.4 单参数的隐式转换

    Date d3 = { 2026 }; // C++11 列表初始化,只传一个值
    Date d4 = 2026; // C++98 也可以,因为构造函数没有 explicit

      Date 的构造函数所有参数都有默认值,所以它同时也是一个转换构造函数(只有单个实参,且无 explicit 修饰的构造函数)。当只提供一个参数时,既可以写成 d3 = {2026},也可以直接写成 d4 = 2026。后者是 C++98 就支持的隐式类型转换,而前者是 C++11 统一初始化下的等价写法。

    3.5 省略 =

    C++11 允许在列表初始化时省略等号,形式更简洁:

    Point p1{ 1, 2 }; // 直接列表初始化
    int x3{ 2 }; // 内置类型也可以
    Date d6{ 2026, 6, 19 }; // 直接构造,无拷贝
    const Date& d7{ 2026, 6, 19 }; // 引用绑定

      这种不写 = 的写法叫做直接列表初始化。它和带了 = 的拷贝列表初始化在语义上有时有细微差别,但对于我们目前遇到的例子,它们的效果是等价的。

      需要注意的是:只有 {} 语法才能省略 =,如果你完全不写 {},是不能省略 = 的。比如 Date d8 2026; 是错误的,编译器无法理解。

    3.6 容器操作中的大杀器

      在实际编程中,列表初始化最大的便利之一体现在 STL 容器 的 push_back 和 insert 这类操作上:

    vector<Date> v;
    v.push_back(d1); // 1. 传一个已存在的对象
    v.push_back(Date(2026, 6, 19)); // 2. 显式构造匿名对象传进去

    v.push_back({ 2026, 6, 19 }); // 3. 直接用列表初始化,更简洁

      对于方法 2,你需要写 Date(2026, 6, 19) 来显式构造一个匿名对象。而方法 3,你只需提供 { 2026, 6, 19 },编译器就会根据 push_back 的参数类型自动构造出一个 Date 临时对象,然后添加到容器中。这种方式显然更直观、更方便。

      再看一个更明显的例子——map 的 insert:

    map<string, string> dict;
    dict.insert({ "xxx", "yyy" }); // 直接用花括号初始化一个 pair

      map<string, string>::insert 接受的参数类型是 pair<const string, string>,我们直接用 { "xxx", "yyy" } 就能构造出这个 pair 对象,不用写成 make_pair("xxx","yyy") 或 pair<string,string>("xxx","yyy")。


    4. std::initializer_list

      上面的例子中,我们始终是在构造单个对象。但如果我想这样写呢?

    vector<int> v = { 1, 2, 3 };

      或者再大胆一点:

    map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };

      这里的花括号里包含了多个元素。为了让这种语法能够工作,C++11 标准库引入了一个全新的类型:std::initializer_list。

    4.1 initializer_list 是什么?

      简单来说,initializer_list 是一个轻量级的、只读的“数组视图”。当你写下 { 10, 20, 30 } 这样的列表时,编译器会做以下事情:

  • 在栈上创建一个匿名的、元素类型为 const int 的临时数组,存放 10, 20, 30。
  • 用这个数组的首地址和尾后地址(或首地址加长度)构造一个 initializer_list<int> 对象。
  •   因此,initializer_list 内部通常只包含两个指针(分别指向数组的起始和末尾,或者一个指针加一个长度)。

    4.2 验证 initializer_list 的实现

    initializer_list<int> mylist;
    mylist = { 10, 20, 30 };

    cout << sizeof(mylist) << endl;

    int i = 0;
    cout << mylist.begin() << endl; // 指向数组起始地址
    cout << mylist.end() << endl; // 指向数组尾后地址
    cout << &i << endl; // 栈上变量 i 的地址

    在这里插入图片描述

      initializer_list 只是一个轻量外壳,仅存起始、结束指针,64位系统一个指针占8字节,2个指针合计16字节,所以 sizeof(mylist)=16 。实际 {10,20,30} 数组是编译器在栈上临时创建,列表只是引用这片内存,所以自身大小元素多少无关。

      当你打印 mylist.begin() 和 mylist.end() 时,会发现这两个地址与变量 i 的地址很接近(都在栈空间内),这说明底层数组确实分配在栈上,而不是堆上。这也是为什么 initializer_list 的拷贝是浅拷贝,多个 initializer_list 可能指向同一块数组,使用时要确保底层数组的生命周期问题。

      initializer_list 提供了 begin()、end() 等接口,因此可以像普通容器一样用范围 for 遍历:

    for (auto e : mylist)
    {
    cout << e << " ";
    }

    4.3 容器如何支持多元素初始化

      STL 容器(如 vector、list、map)都增加了一个接受 std::initializer_list 的构造函数,这才使得我们可以这样写:

    vector<int> v1({ 1, 2, 3, 4, 5 }); // 直接传递 initializer_list
    vector<int> v2 = { 1, 2, 3, 4, 5 }; // 拷贝列表初始化
    const vector<int>& v3 = { 1, 2, 3, 4, 5 }; // 引用绑定到临时 vector

      这三个写法在最终效果上都是把 1,2,3,4,5 放进 vector 中,但它们背后发生的细节略有不同:

    • v1({…}):用 {…} 生成一个 initializer_list<int>,直接传给 vector 的构造函数,构造出 v1。
    • v2 = {…}:先生成 initializer_list 构造临时 vector,再用这个临时对象拷贝构造 v2,然后编译器通常会优化为直接构造(同前面 Date 的优化)。
    • v3:临时 vector 被常量引用绑定,生命周期延长至 v3 作用域结束。

      map 的多元素初始化更是 initializer_list 与列表初始化的完美结合:

    map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };

      这个花括号的整体是一个 initializer_list<pair<const string, string>>。里层的每个 {"sort", "排序"} 又会各自去调用 pair 的构造函数,形成一个个 pair 元素。所有元素被遍历并插入到 map 中。一行代码完成了一个字典的初始化。

    4.4 同样适用于赋值操作

      initializer_list 不仅用于构造,也用于赋值。标准库容器同样提供了接受 initializer_list 的赋值运算符重载:

    vector<int> v1 = { 1, 2, 3, 4, 5 };
    v1 = { 10, 20, 30, 40, 50 }; // 用 initializer_list 替换所有内容


    5. 深入本质:类型转换与优化

      我们来回顾一下列表初始化背后发生的事情,并把它归纳成一条清晰的规则:

      当使用 {} 初始化一个对象时,编译器会尝试找到一个可以通过这些参数调用的构造函数。 如果目标类型是类类型,{} 里的值会被视为构造函数参数,并触发构造。这一过程中可能产生临时对象,但编译器有权(而且几乎总是会)通过拷贝省略优化掉不必要的拷贝。

      对于容器而言,多元素列表会隐式生成 std::initializer_list,然后调用专有的构造函数或赋值函数。

    5.1 窄化转换的防御

      列表初始化还有一个额外的好处:禁止窄化转换。

    int x = 3.14; // 允许,但会丢失精度,x 变成 3
    int y = { 3.14 }; // 错误!窄化转换在列表初始化中不被允许

      这种防御性使得代码更加安全,当你需要显式进行窄化转换时,必须使用 static_cast<int>(3.14) ,强制写明意图,避免无意丢数据。

    5.2 explicit 与列表初始化

    当构造函数被声明为 explicit 时:

    class Date
    {
    public:
    explicit Date(int year, int month = 1, int day = 1)
    { ... }
    };

      此时,Date d = {2026,6,19}; 等号+花括号 = 拷贝式初始化,禁止调用 explicit 构造函数,编译报错,不允许隐式转换。   而 Date d{ 2026, 6, 19 }; 是直接列表初始化,可以调用 explicit 构造函数。


    结语:

      今天的内容到这里就结束了,希望你能有所收获~

    干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _

    赞(0)
    未经允许不得转载:171主机测评 » 【C++11篇(一)】发展历程、列表初始化详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址