47、C++11新特性(范围for与初始化列表)
C++11 带来的好东西太多了。但要说日常写代码最常用、最顺手的两样,我肯定会投「范围 for 循环」和「std::initializer_list」一票。这俩特性看着简单,用好了能让代码干净一大截。
我记得刚接触 C++11 那会儿,第一反应就是:终于不用再手写迭代器循环了!你想想看,以前遍历一个 vector,得写 for (auto it = vec.begin(); it != vec.end(); ++it),又长又容易写错。现在一行搞定。
范围 for 循环:遍历就该这么写
范围 for 循环的语法特别直白:
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 传统写法
for (size_t i = 0; i < numbers.size(); ++i) {
std::cout << numbers[i] << " ";
}
// 范围 for 写法
for (int n : numbers) {
std::cout << n << " ";
}
说白了,就是把「怎么遍历」这件事交给编译器,你只管说「我要遍历什么」就行。
我个人习惯在需要修改元素时用引用:
for (int& n : numbers) {
n *= 2; // 直接修改原容器
}
不需要修改时就用 const 引用,避免拷贝开销:
for (const int& n : numbers) {
std::cout << n << " ";
}
const auto&。我以前见过有人用 auto 遍历 map,每次迭代都拷贝一个 pair,性能直接崩了。
范围 for 的底层原理
范围 for 循环其实是个语法糖。编译器会把它展开成基于迭代器的循环。嗯,这里要注意:它要求容器有 begin() 和 end() 方法,或者有对应的全局 begin()/end() 函数。
所以原生数组也能用:
int arr[] = {10, 20, 30};
for (int x : arr) {
std::cout << x << " ";
}
但动态分配的数组不行——因为它没有 begin()/end()。
std::initializer_list:让初始化更自然
这个特性我太喜欢了。它让你可以用花括号初始化列表来构造对象:
std::vector<int> v = {1, 2, 3, 4, 5};
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
为什么能做到?因为 STL 容器都提供了接受 std::initializer_list 的构造函数。
你也可以给自己的类加上这个能力:
class MyContainer {
public:
MyContainer(std::initializer_list<int> list) {
for (int val : list) {
data_.push_back(val);
}
}
private:
std::vector<int> data_;
};
// 使用
MyContainer c = {10, 20, 30};
我个人习惯在写工具类时,只要涉及「一组初始值」,就加上 initializer_list 构造函数。用起来特别顺手。
initializer_list 的细节
它是个轻量级代理对象,内部其实是个数组。你可以遍历它,但不能修改它里面的元素。它有三个核心方法:
| 方法 | 说明 |
|---|---|
begin() |
返回指向第一个元素的指针 |
end() |
返回指向末尾后一个元素的指针 |
size() |
返回元素个数 |
注意:initializer_list 的生命周期由编译器管理。不要试图保存它的指针或引用,离开花括号作用域就失效了。
两者结合:遍历初始化列表
范围 for 和 initializer_list 经常一起出现。比如写一个接受任意数量参数的函数:
void printAll(std::initializer_list<std::string> items) {
for (const auto& item : items) {
std::cout << item << " ";
}
std::cout << std::endl;
}
// 调用
printAll({"apple", "banana", "cherry"});
你看,这里范围 for 遍历 initializer_list,两个新特性完美配合。
避坑指南
我遇到过一个问题:构造函数重载时,initializer_list 的优先级比普通构造函数高。比如:
std::vector<int> v(10, 5); // 10个5
std::vector<int> v{10, 5}; // 2个元素:10和5
圆括号和花括号的结果完全不同。花括号会优先匹配 initializer_list 构造函数。这个坑我踩过一次,调试了半天才发现是括号用错了。
知识结构图
下面这张图梳理了本章的核心内容:
范围 for 和 initializer_list 是 C++11 里性价比最高的两个特性。学起来不费劲,用起来效果立竿见影。我建议你从现在开始,所有遍历都改成范围 for,所有需要初始化的容器都用花括号。习惯之后,你会觉得以前的写法太啰嗦了。