C++11 新特性详解(一):列表初始化、initializer_list 与右值引用

前言

一、列表初始化

[1. 1C++98 中的 {}](#1. 1C++98 中的 {})

[1.2 C++11:统一的列表初始化](#1.2 C++11:统一的列表初始化)

[1.3 {} 初始化自定义类时到底发生了什么?](#1.3 {} 初始化自定义类时到底发生了什么?)

二,std::initializer_list

[1.1 构造函数的"固有问题"](#1.1 构造函数的“固有问题”)

[1.2 C++98 时代的"曲线救国"方案](#1.2 C++98 时代的“曲线救国”方案)

[方案一:多次 push_back](#方案一:多次 push_back)

方案二:使用数组作为中介

方案三:仅原生数组独享的特权

[2. C++11 的解决方案:std::initializer_list](#2. C++11 的解决方案:std::initializer_list)

[2.1 initializer_list 的底层实现](#2.1 initializer_list 的底层实现)

[2.2 编译器的魔法:花括号列表到 initializer_list 的转换](#2.2 编译器的魔法:花括号列表到 initializer_list 的转换)

[3. 容器如何利用 initializer_list?](#3. 容器如何利用 initializer_list?)

[3.1 新增的构造函数](#3.1 新增的构造函数)

[3.2 完整的调用链](#3.2 完整的调用链)

[3.3 多种使用形式](#3.3 多种使用形式)

[3.4 复杂嵌套:map 的初始化](#3.4 复杂嵌套:map 的初始化)

[4. 内存布局与性能考量](#4. 内存布局与性能考量)

[4.1 内存布局图示](#4.1 内存布局图示)

[4.2 性能特点](#4.2 性能特点)

[4.3 常见问题答疑(FAQ)](#4.3 常见问题答疑(FAQ))

[Q1: initializer_list 会拷贝数据吗?](#Q1: initializer_list 会拷贝数据吗?)

[Q2: 为什么 initializer_list 的元素是 const 的?](#Q2: 为什么 initializer_list 的元素是 const 的?)

[Q3: initializer_list 和原生数组有什么区别?](#Q3: initializer_list 和原生数组有什么区别?)

[Q4: 所有 STL 容器都支持 initializer_list 构造吗?](#Q4: 所有 STL 容器都支持 initializer_list 构造吗?)


bash 复制代码
C++11系列的全部目录

C++11 新特性详解(一)
├── 1. C++11简介
├── 2. 列表初始化
│   ├── C++98中的{}
│   ├── C++11统一初始化
│   └── initializer_list
└── 3. 右值引用基础
    ├── 左值和右值
    ├── 左值引用和右值引用
    ├── std::move
    └── 右值引用变量为什么又是左值

C++11 新特性详解(二)
├── 4. 移动语义
│   ├── 为什么需要移动构造
│   ├── 移动构造与移动赋值
│   ├── 深拷贝类中的资源转移
│   ├── 传值返回
│   └── STL中的移动
├── 5. 右值引用进阶
│   ├── 值类别
│   ├── 引用折叠
│   └── 万能引用

C++11 新特性详解(三)
├── 6. 完美转发
│   ├── 为什么需要forward
│   ├── T&&到底是什么
│   └── std::forward的原理
├── 7. 可变参数模板
│   ├── 参数包
│   ├── sizeof...
│   ├── 包扩展
│   └── 递归展开
└── 8. emplace系列

C++11 新特性详解(四)
├── 9. Lambda表达式
│   ├── Lambda是什么
│   ├── 为什么需要Lambda
│   ├── 仿函数与Lambda
│   ├── operator()
│   ├── auto为什么可以接Lambda
│   ├── 捕捉列表
│   ├── 值捕捉/引用捕捉
│   ├── 隐式捕捉
│   ├── mutable
│   └── Lambda底层原理

C++11 新特性详解(五)
├── 10. std::function
├── 11. bind
├── 12. function + Lambda + bind
├── 13. 逆波兰表达式案例
└── 14. C++11知识体系总结

前言

C++11 是 C++ 发展历史上非常重要的一次标准更新。它并不是简单地增加几个语法,而是对 C++ 的对象管理、泛型编程、STL、函数对象等多个方面进行了系统性的增强。C++11 在 2011 年正式被 ISO 采纳,在 C++98 到 C++11 之间经历了较长的标准空窗期,因此它也是现代 C++ 的一个重要分界点。

本文按照 C++11 的知识脉络,从最容易理解的列表初始化开始,逐步进入 C++11 最重要的核心内容之一------右值引用和移动语义。学习这一部分时不要急着背语法,尤其是右值引用、std::move、移动构造这些概念,它们真正解决的是一个问题:

如何减少不必要的深拷贝,让对象的资源能够被高效地"转移"。


一、列表初始化

1. 1C++98 中的 {}

在 C++98 中,{} 主要用于数组、结构体等聚合类型的初始化。

cpp 复制代码
struct Point
{
    int _x;
    int _y;
};

int main()
{
    int array1[] = { 1, 2, 3, 4, 5 };
    int array2[5] = { 0 };

    Point p = { 1, 2 };

    return 0;
}

这里的 {} 本质上是传统的聚合初始化方式。

但是这种初始化方式存在一个明显的问题:不同类型需要不同的初始化方法

cpp 复制代码
//例如数组可以:

int a[] = {1, 2, 3};

//结构体可以:

Point p = {1, 2};

但是一个普通的自定义类,C++98 并不能简单地把 {} 作为统一的初始化方式使用。

C++11 就开始尝试解决这个问题。


1.2 C++11:统一的列表初始化

C++11 引入了一个重要思想:

尽可能统一对象的初始化方式,让各种类型都可以使用 {} 进行初始化。

因此下面这些代码都成为合法形式:

cpp 复制代码
int x1 = { 2 };

Point p1 { 1, 2 };

Date d1 = { 2025, 1, 1 };

Date d2 { 2024, 7, 25 };

甚至 = 都可以省略:

cpp 复制代码
int x { 2 };

Point p { 1, 2 };

Date d { 2024, 7, 25 };

这里要注意一个非常容易忽略的地方:

cpp 复制代码
Point p {1, 2};

并不是说 {} 本身就是一种特殊对象,而是 C++11 给出了统一的列表初始化语法,再根据目标类型去匹配相应的构造方式。课件中的 Date 示例也专门展示了这一点。


1.3 {} 初始化自定义类时到底发生了什么?

假设我们有:

cpp 复制代码
class Date
{
public:
    Date(int year = 1, int month = 1, int day = 1)
        : _year(year)
        , _month(month)
        , _day(day)
    {}

private:
    int _year;
    int _month;
    int _day;
};
cpp 复制代码
​Date d1 = { 2025, 1, 1 };

可以从语义上理解为:

Date d1(2025, 1, 1);

一个容易让初学者困惑的问题:从语言语义层面可以理解为列表形成临时对象,再进行初始化,但编译器通常会进行拷贝消除等优化,最终直接构造目标对象,因此实际运行时不一定真的发生一次额外的拷贝构造。

所以学习 C++ 时一定要注意:

"语言层面的抽象过程"和"编译器最终生成的机器代码"不是一回事。

我们首先应该理解语言规则,然后再去理解编译器优化。

二,std::initializer_list

1.1 构造函数的"固有问题"

我们知道,vector 是一个类模板,它的构造函数(包括重载)的参数个数和类型在编译时必须是确定的。例如:

cpp 复制代码
cpp

template<class T>
class vector {
public:
    explicit vector(size_t count);                // 1个参数
    vector(size_t count, const T& value);         // 2个参数
    vector(const vector& other);                  // 1个参数(拷贝构造)
    // ... 等等
};

如果要支持 vector<int> v = {1, 2, 3, 4, 5};,那么 vector 必须有一个构造函数能够接受任意数量的 int 参数。但是,构造函数的重载数量是有限的,我们不可能为每一种可能的元素个数都写一个重载:

cpp 复制代码
cpp

vector(int e1);                    // 支持 {1}
vector(int e1, int e2);            // 支持 {1,2}
vector(int e1, int e2, int e3);    // 支持 {1,2,3}
// ... 无穷无尽

这显然是不可行的。

1.2 C++98 时代的"曲线救国"方案

在 C++98 中,只有原生数组 (built-in array)可以享受 {1,2,3,4,5} 这种初始化方式,而像 vector 这样的标准容器却不行。为了达到类似的效果,程序员只能采用一些笨拙的变通方法:

方案一:多次 push_back

cpp

复制代码
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);

缺点 :代码冗长,尤其是当元素很多时;并且会多次调用 push_back,可能引发多次内存分配和拷贝,效率低下。

方案二:使用数组作为中介

cpp

复制代码
int arr[] = {1, 2, 3, 4, 5};
vector<int> v(arr, arr + 5);   // 利用迭代器区间构造函数

缺点:需要额外定义一个数组,而且这种用法不够直观,破坏了代码的简洁性。

方案三:仅原生数组独享的特权

cpp

复制代码
int arr[] = {1, 2, 3, 4, 5};   // 只有数组可以!

原生数组是 C 语言遗留下来的特性,C++ 标准委员会一直希望将这种便利性推广到所有对象,尤其是容器。


2. C++11 的解决方案:std::initializer_list

为了从根本上解决"构造函数参数个数不固定"的问题,C++11 引入了一个全新的类型模板------std::initializer_list<T>。它的核心思想是:编译器遇到花括号列表时,会将其转换为一个 initializer_list 对象,该对象内部保存了指向一个匿名数组的指针,这个数组存储了列表中的所有元素。 这样,容器只需要提供一个以 initializer_list 为参数的构造函数,就能接受任意数量的元素进行初始化。

2.1 initializer_list 的底层实现

std::initializer_list 是一个非常轻量的类模板,其内部实现大致如下(实际标准库实现可能略有不同):

cpp 复制代码
cpp

template<class T>
class initializer_list {
private:
    const T* _begin;   // 指向匿名数组的第一个元素
    const T* _end;     // 指向匿名数组的最后一个元素的下一个位置

public:
    // 构造函数(由编译器隐式调用,用户不能直接构造)
    constexpr initializer_list(const T* b, const T* e) noexcept
        : _begin(b), _end(e) {}

    // 获取元素个数
    constexpr size_t size() const noexcept { return _end - _begin; }

    // 迭代器支持(只读)
    const T* begin() const noexcept { return _begin; }
    const T* end()   const noexcept { return _end; }
};

关键点

  • initializer_list 不拥有数据,它只是一个"视图"(view),指向编译器生成的匿名数组。

  • 该数组的元素是 const 的,所以 initializer_list 只能提供只读访问。

  • initializer_list 的大小固定为两个指针(在 64 位系统上通常为 16 字节),非常轻量。

2.2 编译器的魔法:花括号列表到 initializer_list 的转换

当我们在代码中写下 {10, 20, 30} 时,编译器会执行以下步骤:

  1. 创建一个匿名数组 (通常在栈上):int __hidden_array[] = {10, 20, 30};

  2. 构造一个 std::initializer_list<int> 对象std::initializer_list<int> il(&__hidden_array[0], &__hidden_array[3]);

这个转换过程是编译器自动完成的,对程序员来说是完全透明的。

我们可以通过一个小实验来验证这一点(来自 C++11 课件第 5 页):

cpp 复制代码
cpp

#include <iostream>
#include <initializer_list>

int main() {
    std::initializer_list<int> mylist;
    mylist = {10, 20, 30};

    std::cout << sizeof(mylist) << std::endl;   // 通常输出 16(两个指针)

    int i = 0;
    std::cout << mylist.begin() << std::endl;   // 输出某个栈地址,例如 0x61fdf0
    std::cout << mylist.end()   << std::endl;   // 输出另一个栈地址,例如 0x61fdfc
    std::cout << &i            << std::endl;    // 输出另一个栈地址,例如 0x61fde0

    return 0;
}

观察输出可以发现,begin()end() 返回的地址与局部变量 i 的地址非常接近,这有力地证明了匿名数组存在于栈上。


3. 容器如何利用 initializer_list

3.1 新增的构造函数

既然有了 initializer_list 这个"承载任意多个值"的工具,STL 容器(如 vectorlistmap 等)在 C++11 中都增加了一个新的构造函数重载:

cpp 复制代码
cpp

template<class T>
class vector {
public:
    // 新增的 initializer_list 构造函数
    vector(std::initializer_list<T> il) {
        // 预分配足够的空间
        reserve(il.size());
        // 遍历 initializer_list,将元素拷贝到容器中
        for (const T& elem : il) {
            push_back(elem);
        }
    }
    // ... 其他构造函数
};

3.2 完整的调用链

现在,当我们写下 vector<int> v = {1, 2, 3, 4, 5}; 时,整个流程如下:

  1. 编译器遇到 {1, 2, 3, 4, 5} ,在栈上创建匿名数组 int __tmp[] = {1, 2, 3, 4, 5};

  2. 构造一个 std::initializer_list<int> 临时对象 ,它持有指向 __tmp 的指针。

  3. 调用 vectorinitializer_list 构造函数,将临时对象作为参数传入。

  4. 构造函数内部遍历 initializer_list,将每个元素 push_back 到新构造的 vector 中。

  5. 最终,v 拥有了 {1,2,3,4,5} 这五个元素。

这个过程可能涉及一次拷贝(从匿名数组到 vector 内部存储),但由于 vector 在构造函数中会预先分配空间,所以效率是完全可以接受的。

3.3 多种使用形式

C++11 支持多种形式的列表初始化,它们都依赖 initializer_list

cpp 复制代码
cpp

// 1. 直接构造(显式调用)
vector<int> v1({1, 2, 3, 4, 5});

// 2. 拷贝初始化(最常见的形式)
vector<int> v2 = {1, 2, 3, 4, 5};   // 编译器会优化为直接构造,避免临时对象的拷贝

// 3. 省略等号(最简洁)
vector<int> v3 {1, 2, 3, 4, 5};

// 4. const 引用绑定临时对象(生命周期延长)
const vector<int>& v4 = {1, 2, 3, 4, 5};

// 5. 赋值操作(operator= 也有 initializer_list 重载)
v1 = {10, 20, 30, 40, 50};

3.4 复杂嵌套:map 的初始化

initializer_list 的强大之处还在于它可以嵌套使用,例如初始化 map

cpp 复制代码
cpp

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

解析过程:

  • 外层 { ... } 是一个 initializer_list<pair<string, string>>

  • 内层 {"sort", "排序"} 则是一个 initializer_list 用于构造 pair<string, string> 对象(因为 pair 也支持列表初始化)

  • mapinitializer_list 构造函数接收这些 pair 对象,插入到红黑树中。

这种嵌套机制让 C++ 的初始化语法变得极其统一和强大。


4. 内存布局与性能考量

4.1 内存布局图示

复制代码
栈内存:
    ┌─────────────────────┐
    │  __hidden_array:    │  地址:0x1000
    │  [10]               │  0x1000
    │  [20]               │  0x1004
    │  [30]               │  0x1008
    └─────────────────────┘
    
    ┌─────────────────────┐
    │  mylist (initializer_list): │
    │  _begin = 0x1000    │
    │  _end   = 0x100c    │  (0x1000 + 3*sizeof(int))
    └─────────────────────┘

4.2 性能特点

特性 说明
只读访问 元素是 const 的,不可修改,保证了安全性。
轻量 只包含两个指针,复制成本极低(类似复制两个指针)。
临时性 通常作为临时对象传递,生命周期由编译器管理(绑定到引用或构造容器时会延长)。
容器无关 所有 STL 容器都支持,统一了初始化语法。
拷贝开销 初始化时会将匿名数组的元素拷贝到容器中,但这是必要的;对于移动语义友好的类型,可能进一步优化。

4.3 常见问题答疑(FAQ)

Q1: initializer_list 会拷贝数据吗?

A: 是的,当编译器创建匿名数组时,数据被拷贝到该数组中。但这是合理的,因为字面量本身不是持久存储的。在容器构造过程中,数据还会被再次拷贝到容器的内部存储中。

Q2: 为什么 initializer_list 的元素是 const 的?

A: 因为匿名数组通常位于只读内存区域或栈上,修改它可能导致未定义行为。设计为只读更安全,也符合其作为"视图"的语义。

Q3: initializer_list 和原生数组有什么区别?

A:

  • 原生数组 :大小固定,类型为 T[N],可以取地址,支持修改。

  • initializer_list :大小在运行时确定,类型为 initializer_list<T>,提供迭代器接口,可以像容器一样遍历,但元素只读。

Q4: 所有 STL 容器都支持 initializer_list 构造吗?

A : 是的。vectorlistdequemapsetunordered_map 等所有标准容器都在 C++11 中增加了相应的构造函数和赋值操作符。


相关推荐
XMAIPC_Robot1 小时前
RK3588+STM32:高性能机器人运动控制解决方案,兼顾实时性与AI算力
人工智能·stm32·嵌入式硬件·算法·fpga开发·机器人·arm+fpga
天吾cc1 小时前
RTT-MQTT
网络·单片机·嵌入式硬件·算法
vivo互联网技术1 小时前
MagicBokeh:单步统一生成式框架实现真实感长焦虚化渲染 | CVPR 2026 Oral
人工智能·算法
洋不写bug1 小时前
JavaComparable、Comparator、Cloneable接口解析,深拷贝浅拷贝详细解析
android·java·开发语言
吴声子夜歌1 小时前
正则指引——Golang
开发语言·golang·正则表达式
坐吃山猪1 小时前
WebFlux_学习Subscriber总结
java·开发语言·学习·webflux
白露与泡影1 小时前
Java面试题及答案整理(2026年金九银十最新版,持续更新)
java·开发语言
少控科技1 小时前
农场设备管理代码(1)
开发语言·c#
❀搜不到1 小时前
isat-sam分割导出掩码图
开发语言·python