C++11详解(一):统一初始化——列表初始化与 initializer_list

C++11详解(一):统一初始化------列表初始化与 initializer_list


🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《指针合集》 《C语言基础》 《数据结构》 《机器学习导论》 《前端基础》 《python基础》 《C++从入门到入土》 《Linux的学习之旅》 ✨ 数据即知识,压缩即智能

前言:C++11为什么要重新设计初始化?

1. C++98 中初始化方式比较零散

在 C++98 中,不同类型往往使用不同方式初始化。

数组:

cpp 复制代码
int a[] = {1, 2, 3, 4, 5};

结构体:

cpp 复制代码
struct Point
{
    int x;
    int y;
};

Point p = {1, 2};

普通变量:

cpp 复制代码
int x = 10;

对象:

cpp 复制代码
Date d(2025, 1, 1);

容器:

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

可以看到:

text 复制代码
数组       → {}
普通变量   → =
对象       → ()
容器       → 一个一个插入

初始化语法并不统一。

C++11 的一个重要目标,就是尽可能提供一套统一的初始化方式:

text 复制代码
{}

这就是:

text 复制代码
列表初始化
List Initialization

一、C++11 的列表初始化

1. 内置类型也可以使用 {}

C++11 中可以写:

cpp 复制代码
int x{10};

double d{3.14};

char ch{'A'};

也可以保留等号:

cpp 复制代码
int x = {10};

因此:

cpp 复制代码
int x1 = {10};
int x2{10};

都是合法的。

第二种:

cpp 复制代码
int x2{10};

通常称为:

text 复制代码
直接列表初始化

二、自定义类型也支持列表初始化

1. 普通类

定义:

cpp 复制代码
#include <iostream>
using namespace std;

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

C++11 可以:

cpp 复制代码
Date d2{2025, 1, 1};

甚至:

cpp 复制代码
Date d3 = {2025, 1, 1};

编译器会根据:

text 复制代码
2025
1
1

寻找能够匹配的构造函数。


三、列表初始化并不是"简单换了一对括号"

这是一个很容易产生误解的地方。

cpp 复制代码
Date d{2025, 1, 1};

虽然表面看起来只是:

text 复制代码
() → {}

但:

text 复制代码
列表初始化

在 C++ 语言中有自己完整的一套重载决议与类型检查规则。

尤其需要注意:

text 复制代码
initializer_list
窄化转换
构造函数选择

这些问题。


四、列表初始化禁止很多窄化转换

这是 {} 相比传统初始化非常重要的特点。

所谓窄化转换,可以简单理解为:

把一个值转换成表达能力更弱、可能损失信息的类型。

例如:

cpp 复制代码
double d = 3.14;

int x = d;

传统写法通常允许:

text 复制代码
3.14
↓
3

但是:

cpp 复制代码
int x{d};

编译器通常会直接报错。


1. 一个简单例子

cpp 复制代码
double d = 3.14;

int x1 = d;   // 可以,可能丢失小数
// int x2{d}; // 错误:窄化转换

因此 {} 能够帮助我们在编译阶段发现一部分潜在的数据丢失问题。


五、为什么现代 C++ 经常推荐 {}

例如:

cpp 复制代码
int x{10};
string s{"hello"};
vector<int> v{1, 2, 3};

这样有几个明显优点:

text 复制代码
初始化语法更加统一

能够发现一部分窄化转换

方便初始化容器

方便构造临时对象

可以直接调用对象构造函数

但这并不意味着:

任何时候 {} 都一定比 () 更好。

因为一旦类提供:

cpp 复制代码
initializer_list

构造函数,{} 的重载选择可能和 () 不一样。

后面就会看到这一点。


六、容器初始化为什么仍然是一个问题?

假设我们希望创建:

cpp 复制代码
vector<int>

并直接放进去:

text 复制代码
1 2 3 4 5

如果没有专门机制,理论上可能需要:

cpp 复制代码
vector<int> v;

v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);

那么能不能:

cpp 复制代码
vector<int> v{1, 2, 3, 4, 5};

当然可以。

支持它的重要机制之一,就是:

cpp 复制代码
std::initializer_list

七、什么是 std::initializer_list

需要:

cpp 复制代码
#include <initializer_list>

它是 C++11 引入的一个标准库类模板:

cpp 复制代码
std::initializer_list<T>

可以把它理解成:

一个轻量级对象,用来访问编译器为 {x1,x2,x3...} 创建的一组只读元素。

例如:

cpp 复制代码
std::initializer_list<int> il
    = {10, 20, 30, 40};

此时:

text 复制代码
il
↓
10 20 30 40

八、initializer_list 可以遍历

例如:

cpp 复制代码
#include <iostream>
#include <initializer_list>
using namespace std;

int main()
{
    initializer_list<int> il
        = {10, 20, 30, 40};

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

    cout << endl;

    return 0;
}

输出:

text 复制代码
10 20 30 40

也可以:

cpp 复制代码
for (auto it = il.begin();
     it != il.end();
     ++it)
{
    cout << *it << " ";
}

九、initializer_list 中的元素为什么不能修改?

initializer_list<T> 访问的是:

text 复制代码
const T

因此:

cpp 复制代码
initializer_list<int> il
    = {1, 2, 3};

不能:

cpp 复制代码
//*il.begin() = 100;

可以把它理解为:

text 复制代码
initializer_list
负责"描述这一组初始化元素"

而不是一个真正可修改的 vector

它本身也不会拥有类似:

text 复制代码
capacity
push_back
erase

这种容器能力。


十、initializer_list 并不是 vector

两者很容易混淆。

1. vector

cpp 复制代码
vector<int> v{1, 2, 3};

vector 真正拥有:

text 复制代码
动态内存
size
capacity
insert
erase
push_back

2. initializer_list

cpp 复制代码
initializer_list<int> il{1, 2, 3};

更接近一个:

text 复制代码
只读视图 / 轻量代理

它只是帮助函数或构造函数:

text 复制代码
接收一组数量可变的同类型初始化元素

十一、vector 为什么能使用 {1,2,3,4,5}

标准容器提供了类似:

cpp 复制代码
vector(
    initializer_list<value_type> il
);

这样的构造函数。

可以简化理解为:

cpp 复制代码
template<class T>
class vector
{
public:
    vector(initializer_list<T> il)
    {
        for (const auto& e : il)
        {
            push_back(e);
        }
    }

    // ...
};

所以:

cpp 复制代码
vector<int> v{1, 2, 3};

本质过程可以理解为:

text 复制代码
{1,2,3}
   ↓
initializer_list<int>
   ↓
vector 的 initializer_list 构造函数
   ↓
构造 vector

十二、list、map 等容器同样支持

例如:

cpp 复制代码
list<int> lt{1, 2, 3, 4};

以及:

cpp 复制代码
map<string, int> m
{
    {"apple", 3},
    {"banana", 5},
    {"orange", 2}
};

这里实际上组合了两层列表初始化:

text 复制代码
外层 {}
↓
初始化 map

内层 {}
↓
初始化 pair<const string,int>

十三、map 初始化过程怎么理解?

例如:

cpp 复制代码
map<string, int> m
{
    {"apple", 3},
    {"banana", 5}
};

可以抽象理解成:

text 复制代码
"apple", 3
      ↓
pair

"banana", 5
      ↓
pair

两个 pair
      ↓
initializer_list<value_type>
      ↓
map 构造函数

这也是 C++11 列表初始化非常方便的地方。


十四、initializer_list 也可以支持赋值

例如:

cpp 复制代码
vector<int> v{1, 2, 3};

v = {10, 20, 30, 40};

对应容器提供:

cpp 复制代码
operator=(initializer_list<T>);

因此不只是构造,重新赋值也可以使用 {}


十五、自己给类增加 initializer_list 构造

例如设计一个简单容器:

cpp 复制代码
#include <initializer_list>
#include <iostream>
using namespace std;

class Numbers
{
public:
    Numbers(initializer_list<int> il)
    {
        for (auto e : il)
        {
            cout << e << " ";
        }

        cout << endl;
    }
};

int main()
{
    Numbers n{1, 2, 3, 4, 5};

    return 0;
}

输出:

text 复制代码
1 2 3 4 5

也就是说:

text 复制代码
initializer_list

并不是 STL 容器专属机制。

我们自己的类型也可以使用。


十六、一个非常重要的坑:initializer_list 构造优先级

考虑:

cpp 复制代码
#include <initializer_list>
#include <iostream>
using namespace std;

class Test
{
public:
    Test(int x, int y)
    {
        cout << "Test(int,int)" << endl;
    }

    Test(initializer_list<int> il)
    {
        cout << "initializer_list" << endl;
    }
};

执行:

cpp 复制代码
Test t1(1, 2);

会更自然地匹配:

cpp 复制代码
Test(int, int)

但是:

cpp 复制代码
Test t2{1, 2};

如果 initializer_list 构造函数可匹配,列表初始化的重载决议会非常偏向:

text 复制代码
initializer_list 构造函数

所以:

text 复制代码
{}
和
()

不能机械地认为完全等价。


十七、vector 中最经典的例子

观察:

cpp 复制代码
vector<int> v1(5, 1);

它表示:

text 复制代码
5 个元素

每个元素都是 1

即:

text 复制代码
1 1 1 1 1

而:

cpp 复制代码
vector<int> v2{5, 1};

表示:

text 复制代码
两个元素:

5
1

所以:

text 复制代码
()
和
{}

在这里具有完全不同的语义。


十八、auto 和列表初始化

C++11 还经常看到:

cpp 复制代码
auto il = {10, 20, 30};

这里 il 会被推导成类似:

cpp 复制代码
initializer_list<int>

因此可以:

cpp 复制代码
for (auto e : il)
{
    cout << e << endl;
}

但是不要因此形成:

text 复制代码
所有使用 {} 的 auto 都一定推导为 initializer_list

这种过度简化。

实际推导规则与具体语法形式有关。


十九、列表初始化在函数调用中的应用

假设:

cpp 复制代码
class Date
{
public:
    Date(int year,
         int month,
         int day)
    {}
};

void PrintDate(const Date& d)
{
}

调用可以直接:

cpp 复制代码
PrintDate({2025, 1, 1});

编译器利用:

text 复制代码
{2025,1,1}

构造 Date 临时对象,再绑定到参数。


二十、在容器插入中的应用

例如:

cpp 复制代码
vector<Date> dates;

dates.push_back(
    {2025, 1, 1}
);

相比:

cpp 复制代码
Date d(2025, 1, 1);

dates.push_back(d);

前者更加紧凑。

后面学习移动语义和:

cpp 复制代码
emplace_back

之后,还会继续解决:

text 复制代码
如何减少临时对象和拷贝

的问题。


二十一、列表初始化真正统一了什么?

可以把 C++11 之前的:

text 复制代码
数组初始化
对象构造
结构体初始化
容器初始化
临时对象构造

统一成非常相似的:

cpp 复制代码
Type obj{...};

例如:

cpp 复制代码
int x{10};

Point p{1, 2};

Date d{2025, 1, 1};

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

map<string, int> m
{
    {"apple", 1},
    {"banana", 2}
};

代码在形式上更加一致。


二十二、常见错误

1. 认为 {} 与 () 完全一样

错误。

cpp 复制代码
vector<int> v1(5, 1);
vector<int> v2{5, 1};

语义完全不同。


2. 认为 initializer_list 是 vector

不是。

它没有:

text 复制代码
push_back
erase
capacity

等能力。


3. 认为 initializer_list 中元素可以修改

元素视为:

text 复制代码
const T

不能直接修改。


4. 忽略窄化转换

例如:

cpp 复制代码
double d = 3.14;

// int x{d};

列表初始化会阻止这种潜在信息损失。


5. 认为 initializer_list 的底层布局是标准固定的

不要依赖类似:

text 复制代码
"一定两个指针"
"一定在栈上"

这样的实现细节。

程序应该只使用:

cpp 复制代码
begin()
end()
size()

等标准接口。


总结

C++11 列表初始化解决的核心问题是:

text 复制代码
让不同类型拥有更加统一的初始化语法

最重要的语法:

cpp 复制代码
Type obj{args...};

而:

cpp 复制代码
std::initializer_list<T>

则让函数和类能够方便地接收:

text 复制代码
{x1,x2,x3,...}

这一组同类型初始化元素。

需要重点记住四件事情:

text 复制代码
1. {} 可以初始化内置类型和自定义类型

2. 列表初始化会阻止很多窄化转换

3. STL 容器通过 initializer_list 支持
   vector<int>{1,2,3}

4. 如果类存在 initializer_list 构造函数,
   {} 与 () 的构造函数选择可能不同

理解这一部分以后,C++11 后面的:

text 复制代码
右值引用
移动构造
emplace

会逐渐把"对象怎么构造、怎么传递"这件事继续向前推进。


相关推荐
jimy122 分钟前
右值大类rvalue的xvalue,和 左值大类glvalue的xvalue的区别
开发语言·c++
金立基包装胶水24 分钟前
纸袋热封胶除了粘不牢,还有哪些隐性问题?
大数据·笔记·其他
我爱cope25 分钟前
【计算机网络 | 网络层补充:内部网关协议:RIP 与 OSPF 如何在自治系统内选择路由?】
网络·学习·计算机网络·智能路由器
影视飓风TIM27 分钟前
C++哈希表:unordered_set / unordered_map底层原理
c++·算法·哈希算法·散列表
是隼人32 分钟前
buuctf-pwn ciscn_2019_n_5题解(学习过程持续 更新)
c语言·学习·安全·pwn入门·ctf入门
疯狂打码的少年1 小时前
【数据库技术】SQL高级查询(分组/排序/聚合函数)
java·数据库·笔记·sql
旖旎夜光1 小时前
【AI入门】大模型介绍全解析:从模型、LLM 到提示词与嵌入
人工智能·笔记·python·学习·ai编程
OPEN-F1 小时前
C++工程化:编译链接、调试与性能优化
开发语言·c++
王的宝库1 小时前
【无标题】
笔记·restful