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 << " ";
}
小技巧: 遍历 map 的时候,记得用 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()

注意: 不要在范围 for 循环中修改容器的大小(插入或删除元素)。迭代器会失效,程序直接崩。我曾经在代码审查时抓到过这种 bug,排查了半天才发现是遍历时 push_back 了。

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 让自定义类型也能享受花括号初始化的便利。两者配合使用,代码写起来特别舒服。

两者结合:遍历初始化列表

范围 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 构造函数。这个坑我踩过一次,调试了半天才发现是括号用错了。

建议: 写代码时统一风格。我个人习惯用花括号做初始化,圆括号做函数调用。这样不容易混淆。

知识结构图

下面这张图梳理了本章的核心内容:

C++11 新特性:范围for与初始化列表 范围 for 循环 语法糖:编译器展开为迭代器循环 支持数组和所有STL容器 用引用避免拷贝,用const保护数据 ⚠ 遍历时不要修改容器大小 std::initializer_list 轻量代理对象,内部是数组 支持 begin() / end() / size() 自定义类可添加initializer_list构造函数 ⚠ 花括号优先匹配initializer_list 配合使用 让代码更简洁、更安全、更现代

范围 for 和 initializer_list 是 C++11 里性价比最高的两个特性。学起来不费劲,用起来效果立竿见影。我建议你从现在开始,所有遍历都改成范围 for,所有需要初始化的容器都用花括号。习惯之后,你会觉得以前的写法太啰嗦了。

公众号:蓝海资料掘金营,微信deep3321