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
会逐渐把"对象怎么构造、怎么传递"这件事继续向前推进。