跳到主要内容

C++ 11基于范围的for循环

引言

在C++11标准之前,要遍历数组或容器需要使用传统的for循环或迭代器,这种方式虽然灵活但代码冗长且容易出错。C++11引入的基于范围的for循环(Range-based for loop)大大简化了集合遍历的语法,使代码更加简洁、易读,并减少了可能的错误。

这个新语法允许我们更自然地表达"对容器中的每个元素执行操作"的意图,无需关心索引管理或迭代器操作的细节。

基本语法

基于范围的for循环的基本语法如下:

cpp
for (声明变量 : 表达式) {
// 循环体
}

这里:

  • 声明变量 是要在每次迭代中使用的变量
  • 表达式 是要遍历的范围(可以是数组、容器或其他可迭代对象)
  • 循环体中可以使用声明的变量对每个元素进行操作

基础示例

遍历数组

让我们先看一个简单的示例,使用基于范围的for循环遍历一个整数数组:

cpp
#include <iostream>

int main() {
int numbers[] = {1, 2, 3, 4, 5};

// 使用基于范围的for循环
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

return 0;
}

输出:

1 2 3 4 5

与传统for循环对比

为了理解基于范围的for循环带来的简化,让我们比较一下传统的for循环写法:

cpp
// 传统for循环
for (int i = 0; i < 5; ++i) {
std::cout << numbers[i] << " ";
}

// 使用迭代器的for循环
for (int* it = std::begin(numbers); it != std::end(numbers); ++it) {
std::cout << *it << " ";
}

// 基于范围的for循环
for (int num : numbers) {
std::cout << num << " ";
}

基于范围的for循环明显更加简洁,而且不容易因为索引错误或迭代器边界问题导致bug。

使用方式详解

遍历STL容器

基于范围的for循环可以与任何标准容器一起使用,如vectorlistmap等:

cpp
#include <iostream>
#include <vector>
#include <map>
#include <string>

int main() {
// 遍历vector
std::vector<int> vec = {10, 20, 30, 40, 50};
for (int value : vec) {
std::cout << value << " ";
}
std::cout << std::endl;

// 遍历map
std::map<std::string, int> ages = {
{"Alice", 25},
{"Bob", 30},
{"Charlie", 35}
};
for (const auto& pair : ages) {
std::cout << pair.first << " is " << pair.second << " years old." << std::endl;
}

return 0;
}

输出:

10 20 30 40 50
Alice is 25 years old.
Bob is 30 years old.
Charlie is 35 years old.
提示

当遍历关联容器如mapunordered_map时,迭代变量是键值对(pair类型)。

引用与值传递

基于范围的for循环默认是通过值传递的,这意味着循环内部处理的是原始元素的副本。如果要修改原始元素,需要使用引用:

cpp
#include <iostream>
#include <vector>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};

// 通过值传递 - 不修改原始元素
for (int num : numbers) {
num *= 2; // 只修改了副本,不影响原始数据
}

// 输出原始数据(未修改)
std::cout << "After value passing: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

// 通过引用传递 - 修改原始元素
for (int& num : numbers) {
num *= 2; // 直接修改原始数据
}

// 输出修改后的数据
std::cout << "After reference passing: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

return 0;
}

输出:

After value passing: 1 2 3 4 5
After reference passing: 2 4 6 8 10

常量引用

当不需要修改元素但又想避免复制开销时,特别是对于大型对象,可以使用常量引用:

cpp
#include <iostream>
#include <vector>
#include <string>

class User {
private:
std::string name;
int id;

public:
User(std::string n, int i) : name(n), id(i) {}

void print() const {
std::cout << "User(" << name << ", " << id << ")" << std::endl;
}
};

int main() {
std::vector<User> users = {
User("Alice", 1),
User("Bob", 2),
User("Charlie", 3)
};

// 使用常量引用避免复制对象
for (const User& user : users) {
user.print();
}

return 0;
}

输出:

User(Alice, 1)
User(Bob, 2)
User(Charlie, 3)

auto关键字结合使用

结合auto关键字可以进一步简化代码,特别是当处理复杂类型时:

cpp
#include <iostream>
#include <map>
#include <string>

int main() {
std::map<std::string, std::vector<int>> data = {
{"Alice", {90, 85, 94}},
{"Bob", {80, 75, 85}}
};

for (const auto& entry : data) {
std::cout << entry.first << "'s scores: ";
for (const auto& score : entry.second) {
std::cout << score << " ";
}
std::cout << std::endl;
}

return 0;
}

输出:

Alice's scores: 90 85 94
Bob's scores: 80 75 85
备注

使用auto可以让编译器自动推导类型,避免手动指定复杂的类型名称,提高代码可读性和维护性。

结构化绑定(C++17)

虽然这超出了C++11的范围,但值得一提的是,C++17引入的结构化绑定可以与基于范围的for循环结合,使代码更加简洁:

cpp
// C++17 结构化绑定与基于范围的for循环结合
std::map<std::string, int> ages = {{"Alice", 25}, {"Bob", 30}};

for (const auto& [name, age] : ages) {
std::cout << name << " is " << age << " years old." << std::endl;
}

实际应用场景

数据处理

基于范围的for循环在数据处理场景中特别有用:

cpp
#include <iostream>
#include <vector>
#include <numeric>

int main() {
std::vector<double> temperatures = {22.5, 23.2, 21.8, 24.0, 22.7};

// 计算平均温度
double sum = 0.0;
for (double temp : temperatures) {
sum += temp;
}
double average = sum / temperatures.size();

// 找出低于平均值的温度
std::cout << "Average temperature: " << average << std::endl;
std::cout << "Below average temperatures: ";
for (double temp : temperatures) {
if (temp < average) {
std::cout << temp << " ";
}
}
std::cout << std::endl;

return 0;
}

输出:

Average temperature: 22.84
Below average temperatures: 22.5 21.8 22.7

游戏开发

在游戏开发中,基于范围的for循环可以用于处理游戏对象集合:

cpp
#include <iostream>
#include <vector>
#include <string>

class GameObject {
public:
std::string name;
bool active;

GameObject(const std::string& n, bool a) : name(n), active(a) {}

void update() {
if (active) {
std::cout << "Updating " << name << std::endl;
}
}
};

int main() {
std::vector<GameObject> gameObjects = {
GameObject("Player", true),
GameObject("Enemy1", true),
GameObject("Enemy2", false),
GameObject("Item", true)
};

// 游戏主循环中更新所有活动对象
for (GameObject& obj : gameObjects) {
if (obj.active) {
obj.update();
}
}

return 0;
}

输出:

Updating Player
Updating Enemy1
Updating Item

文件处理

使用基于范围的for循环处理文件数据:

cpp
#include <iostream>
#include <fstream>
#include <vector>
#include <string>

int main() {
// 假设我们已经从文件读取了这些数据
std::vector<std::string> lines = {
"Hello world",
"C++11 is great",
"Range-based for loop",
"Makes code cleaner"
};

// 处理每一行
int lineNumber = 1;
for (const std::string& line : lines) {
std::cout << lineNumber++ << ": " << line << std::endl;
}

return 0;
}

输出:

1: Hello world
2: C++11 is great
3: Range-based for loop
4: Makes code cleaner

工作原理

基于范围的for循环在编译时会被转换为等效的传统for循环。它依赖于begin()end()函数来获取迭代器:

cpp
for (for-range-declaration : expression) {
statement
}

会被编译器转换为类似以下形式:

cpp
{
auto&& __range = expression;
auto __begin = begin(__range);
auto __end = end(__range);
for (; __begin != __end; ++__begin) {
for-range-declaration = *__begin;
statement
}
}
警告

基于范围的for循环在遍历过程中不应修改容器的大小(如添加或删除元素),否则可能导致未定义行为。

限制与注意事项

  1. 无法获取索引:基于范围的for循环无法直接获取当前元素的索引。如果需要索引,可以使用计数器或传统for循环。

  2. 不能修改容器结构:在遍历过程中,不应该添加或删除容器元素,因为这可能会使迭代器失效。

  3. 没有步长控制:不能像传统for循环那样控制步长,每次都会遍历所有元素。

  4. 必须支持迭代:被遍历的对象必须支持begin()end()函数(或同名全局函数)。

自定义类型支持

要让自定义类支持基于范围的for循环,需要实现begin()end()方法,或者相应的全局函数:

cpp
#include <iostream>

class NumberRange {
private:
int start;
int end;

public:
NumberRange(int s, int e) : start(s), end(e) {}

class Iterator {
private:
int current;
public:
Iterator(int val) : current(val) {}

bool operator!=(const Iterator& other) const {
return current != other.current;
}

int operator*() const {
return current;
}

Iterator& operator++() {
++current;
return *this;
}
};

Iterator begin() const {
return Iterator(start);
}

Iterator end() const {
return Iterator(end);
}
};

int main() {
NumberRange range(1, 5);

for (int num : range) {
std::cout << num << " ";
}
std::cout << std::endl;

return 0;
}

输出:

1 2 3 4

总结

基于范围的for循环是C++11引入的一个重要语法特性,它让代码更加简洁、易读,并减少了潜在的错误。这种循环特别适用于:

  • 遍历数组或STL容器
  • 对集合中的每个元素执行相同操作
  • 减少索引错误和迭代器边界问题

通过合理使用引用和auto关键字,基于范围的for循环可以进一步提高代码的效率和可读性。虽然有一些限制,但在大多数遍历场景下,基于范围的for循环是更优的选择。

练习

为了巩固所学内容,尝试以下练习:

  1. 使用基于范围的for循环计算一个整数向量的总和和平均值。
  2. 创建一个程序,使用基于范围的for循环找出字符串向量中最长的字符串。
  3. 实现一个函数,使用基于范围的for循环将一个容器中的所有元素复制到另一个容器,但只复制满足特定条件的元素。
  4. 创建一个自定义类,实现必要的方法使其支持基于范围的for循环。

扩展资源

  • C++11标准文档中关于基于范围的for循环的详细说明。
  • C++标准库中的算法,如std::for_each,提供了另一种遍历集合的方法。
  • 探索C++17和C++20中引入的更多循环相关特性。

通过充分理解和灵活运用基于范围的for循环,你可以编写更加简洁、易读且不易出错的C++代码。