C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++ 列表初始化与右值引用

C++11 新特性之列表初始化、initializer_list 与右值引用

作者:薛定谔のbug

列表初始化允许直接在对象创建时初始化变量,这种方式可以避免一些隐式类型转换带来的问题,同时使得代码更加清晰和安全,本文给大家介绍C++11 新特性之列表初始化、initializer_list 与右值引用,感兴趣的朋友一起看看吧

C++11引入了多项重要新特性,其中包括列表初始化、std::initializer_list和右值引用(Rvalue references),这些特性极大地增强了C++的灵活性和表达能力。下面,我们将详细解释这些新特性。

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 中,{} 主要用于数组、结构体等聚合类型的初始化。

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;
}

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

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

//例如数组可以:
int a[] = {1, 2, 3};
//结构体可以:
Point p = {1, 2};

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

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

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

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

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

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

int x1 = { 2 };
Point p1 { 1, 2 };
Date d1 = { 2025, 1, 1 };
Date d2 { 2024, 7, 25 };

甚至 = 都可以省略:

int x { 2 };
Point p { 1, 2 };
Date d { 2024, 7, 25 };

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

Point p {1, 2};

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

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

假设我们有:

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;
};
​Date d1 = { 2025, 1, 1 };
可以从语义上理解为:
Date d1(2025, 1, 1);

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

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

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

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

二,std::initializer_list

1.1 构造函数的“固有问题”

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

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
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

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,可能引发多次内存分配和拷贝,效率低下。

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

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

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

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

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.1initializer_list的底层实现

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

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; }
};

关键点:

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

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

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

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

#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 容器(如 vector、list、map 等)在 C++11 中都增加了一个新的构造函数重载:

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. 调用 vector 的 initializer_list 构造函数,将临时对象作为参数传入。
  4. 构造函数内部遍历 initializer_list,将每个元素 push_back 到新构造的 vector 中。
  5. 最终,v 拥有了 {1,2,3,4,5} 这五个元素。

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

3.3 多种使用形式

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

// 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:

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

解析过程:

这种嵌套机制让 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:

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

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

到此这篇关于C++11 新特性之列表初始化、initializer_list 与右值引用的文章就介绍到这了,更多相关C++ 列表初始化与右值引用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文