Skip to content

range-for:索引变量的手滑 bug,咱不要了 ​

写传统的 for 循环遍历数组,咱们总得管好那个索引变量。for (int i = 0; i < n; ++i) 这行代码写过无数遍,也写错过无数遍:< 写成 <= 越界访问,i 忘了自增死循环,数组长度改了但循环条件忘了跟着改。这类 bug 烦人的地方在于:逻辑明明是对的,败在纯手滑的体力活上。

C++11 的 range-for 循环就是冲这个来的:索引不用咱们管,直接告诉编译器"把这个集合里的每个元素给我过一遍"。

基本语法 ​

range-for 的语法长这样:

C++
for (类型 变量名 : 集合) {
    // 使用变量
}

咱们拿一个最简单的例子对比。假设有一个数组,想把每个元素打印出来:

C++
#include <iostream>

int main()
{
    int scores[] = {90, 85, 78, 92, 88};

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

    // range-for 循环
    for (int score : scores) {
        std::cout << score << " ";
    }
    std::cout << std::endl;

    return 0;
}

运行结果:

text
90 85 78 92 88
90 85 78 92 88

咱们看两种写法的输出完全一样,但 range-for 版本少了索引变量 i,少了数组长度 5,少了 scores[i] 的下标访问。

于是手滑的机会跟着一起少掉,起止位置编译器全替咱们算好了。它能遍历的东西也多:C 风格数组、std::array、std::vector、std::string、花括号初始化列表,凡是能从头到尾走一遍的集合都支持。

C 风格数组的一个陷阱:退化成指针 ​

range-for 对 C 风格数组是原生支持的,但有一个限制咱们得提前知道:数组作为函数参数传递时会退化成指针,这时 range-for 就用不了了。

C++
void print_array(int arr[])  // arr 在这里其实是指针
{
    // 编译错误!编译器不知道 arr 指向多少个元素
    // for (int x : arr) { ... }
}

原因在于 range-for 需要知道集合的起点和终点。数组退化成指针后,编译器手里只剩一个地址,"元素个数"这个信息丢了,终点在哪它就无从知道了——咱们也帮不上忙。

range-for 不能用于裸指针。咱们手里如果只握着 int* 加一个长度 size_t n,那就只能回到传统 for 循环;等学到 std::span(C++20),这个问题会有更好的解法。

咱们推荐用 std::array 替代 C 风格数组:性能和 C 数组一样,又自带标准的 begin()/end() 接口,range-for 直接就能用:

C++
std::array<int, 5> scores = {90, 85, 78, 92, 88};
for (int s : scores) {
    std::cout << s << " ";
}

auto 的三种写法:值、引用、const 引用 ​

auto 能帮咱们省掉手写类型的麻烦。它在 range-for 里有三种写法,行为截然不同,这一篇就把它们分清楚。

按值访问写 for (auto x : arr):每次迭代复制一份元素给 x,咱们在循环体里改 x,改的是副本,原集合纹丝不动。对 int 这种小类型无所谓,遍历大对象就是白白多复制一次。

咱们想改原元素,就用按引用访问 for (auto& x : arr),x 是原元素的引用,没有复制开销,改它就是改原件。只想读不打算改,就用按 const 引用访问 for (const auto& x : arr):只读引用,复制免了,手滑想改编译器直接拦下。这是遍历大对象时的首选,也是泛型代码里推荐的默认写法。

来用简短的例子感受一下三者差异吧!

C++
int nums[] = {1, 2, 3};

// 按值:改副本,原数组不变
for (auto x : nums) { x *= 2; }
// nums 仍是 {1, 2, 3}

// 按引用:直接改原数组
for (auto& x : nums) { x *= 2; }
// nums 变成 {2, 4, 6}

// const 引用:只读遍历,编译器会阻止修改
for (const auto& x : nums) {
    std::cout << x << " ";  // 2 4 6
    // x *= 2; // <- 不可以!clangd就会给你漂红线啦!
}

千万别在需要修改元素的时候用 for (auto x : arr):改的只是一份副本,原数组纹丝不动。这种 bug 的特点是编译通过、运行不报错、但结果不对,属于最难排查的那一类。要改元素就用 auto&。& 是引用的标记,引用要到第 4 章才正式讲,咱们先记住用法:auto& 拿到的是原件。

遍历字符串 ​

std::string 也能用 range-for 遍历,咱们每次迭代拿到一个字符。比如统计一段文本里的元音字母:

C++
std::string text = "Hello C++ World";
int vowel_count = 0;
for (char c : text) {
    char lower = (c >= 'A' && c <= 'Z') ? (c - 'A' + 'a') : c;
    if (lower == 'a' || lower == 'e' || lower == 'i'
        || lower == 'o' || lower == 'u') {
        ++vowel_count;
    }
}
std::cout << "元音字母个数: " << vowel_count << std::endl;
// 输出: 元音字母个数: 3

引用版本还能让咱们原地修改字符串,比如转大写:

C++
for (auto& c : text) {
    c = static_cast<char>(
        std::toupper(static_cast<unsigned char>(c)));
}

这里的 static_cast<unsigned char> 不是多此一举。std::toupper 的参数是 int,而 char 在 C++ 里可能是 signed 的——负值直接传进去就是未定义行为,碰到扩展 ASCII 或中文字符时结果可能完全不对,编译器还不会警告咱们。所以先转 unsigned char 再提升为 int,这是调用字符函数时的标准写法,养成习惯就好。

C++17 前瞻:结构化绑定 ​

C++17 引入的结构化绑定和 range-for 配合得很好。完整讲解要等到容器那一章,咱们可以先看个眼熟:

C++
// C++17:遍历键值对容器时直接拆开 key 和 value
for (const auto& [key, value] : my_map) {
    std::cout << key << " -> " << value << std::endl;
}

方括号里的 [key, value] 把一个包含多个字段的对象"解构"成独立变量,比手动写 pair.first、pair.second 直观得多。您暂时看不懂没关系,知道有这个能力就好。

幕后机制——range-for 到底做了什么 ​

为什么 range-for 既能用于数组,又能用于 std::vector、std::string 这些完全不同的类型?答案很直接:编译器把 range-for 翻译成了一个等价的传统循环,咱们来看这个翻译结果。

C++
// for (auto x : coll) 大致等价于:
{
    auto&& __range = coll;
    for (auto __it = __range.begin(); __it != __range.end(); ++__it) {
        auto x = *__it;
        // 循环体
    }
}

咱们看编译器做的事情:调用 begin() 拿到起点,调用 end() 拿到终点,然后一步步走过去。对于 C 风格数组,编译器自己知道长度,拿首元素指针加长度充当起止位置。这也就意味着,任何提供了 begin() 和 end() 的类型都能用 range-for,std::array 比 C 风格数组好用的道理也在这。

实战演练——range_for.cpp ​

咱们把前面的用法整合进一个完整的程序,演示求和、计数、原地修改:

展开代码收起代码共 47 行
C++
// range_for.cpp
// Platform: host
// Standard: C++17

#include <array>
#include <cctype>
#include <iostream>
#include <string>

int main()
{
    // 求和
    std::array<int, 6> data = {3, 7, 1, 9, 4, 6};
    int sum = 0;
    for (const auto& x : data) {
        sum += x;
    }
    std::cout << "总和: " << sum << std::endl;

    // 计数
    int target = 6;
    int count = 0;
    for (const auto& x : data) {
        if (x == target) { ++count; }
    }
    std::cout << "值 " << target << " 出现了 " << count
              << " 次" << std::endl;

    // 原地修改:每个元素翻倍
    std::array<int, 6> doubled = data;
    for (auto& x : doubled) { x *= 2; }
    std::cout << "翻倍后: ";
    for (const auto& x : doubled) {
        std::cout << x << " ";
    }
    std::cout << std::endl;

    // 字符串转大写
    std::string message = "range-for is elegant";
    for (auto& c : message) {
        c = static_cast<char>(
            std::toupper(static_cast<unsigned char>(c)));
    }
    std::cout << "转大写: " << message << std::endl;

    return 0;
}

编译运行:

bash
g++ -std=c++17 -Wall -Wextra -o range_for range_for.cpp
./range_for

运行结果:

text
总和: 30
值 6 出现了 1 次
翻倍后: 6 14 2 18 8 12
转大写: RANGE-FOR IS ELEGANT

在线运行 ​

您也可以在线运行这份综合示例,观察求和、计数、原地修改和字符串操作:

Compiler Explorer

range-for 综合演练:求和、计数、修改、字符串

在线运行并观察 range-for 的四种典型用法。试着修改数组内容或 target 值。

code/examples/vol1/07_range_for.cpp

动手试试 ​

练习一:找最大值 ​

给定一个 std::array<int, 8>,请您用 range-for 找出最大值并打印。提示:声明 max_val 初始化为首元素,遍历比较即可。

text
数组: 12 3 45 7 23 56 8 19
最大值: 56
参考答案
展开代码收起代码共 21 行
C++
#include <iostream>
#include <array>

int main()
{
    std::array<int, 8> value = {12, 3, 45, 7, 23, 56, 8, 19};
    int max_val = value[0];
    std::cout << "数组: ";
    for (const auto& x : value)
    {
        if (max_val < x)
        {
            max_val = x;
        }
        std::cout << x << " ";
    }

    std::cout << std::endl
              << "最大值: " << max_val << std::endl;
    return 0;
}

编译运行:

bash
g++ -std=c++17 -Wall -Wextra main.cpp -o main && ./main

运行结果:

text
数组: 12 3 45 7 23 56 8 19
最大值: 56

练习二:统计元音 ​

请您用 range-for 统计 std::string 中元音字母(a/e/i/o/u,不区分大小写)的个数。

text
字符串: "Beautiful C++"
元音个数: 5
参考答案
C++
#include <iostream>
#include <string>

int main()
{
    std::string text = "Beautiful C++";
    int vowel_count = 0;
    for (char c : text)
    {
        char lower = (c >= 'A' && c <= 'Z') ? (c - 'A' + 'a') : c;
        if (lower == 'a' || lower == 'e' || lower == 'i' || lower == 'o' || lower == 'u')
        {
            ++vowel_count;
        }
    }
    std::cout << "元音个数: " << vowel_count << std::endl;

    return 0;
}

编译运行:

bash
g++ -std=c++17 -Wall -Wextra main.cpp -o main && ./main

运行结果:

text
元音个数: 5

练习三:原地修改 ​

请您用 range-for 的引用版本,把数组中所有负数取绝对值。

text
修改前: 3 -7 1 -9 4 -6
修改后: 3 7 1 9 4 6
参考答案
展开代码收起代码共 24 行
C++
#include <iostream>
#include <array>

int main()
{
    std::array<int, 6> value = {3, -7, 1, -9, 4, -6};
    std::cout << "修改前: ";
    for (auto& x : value)
    {
        std::cout << x << " ";
        if (x < 0)
        {
            x *= -1;
        }
    }
    std::cout << std::endl
              << "修改后: ";
    for (const auto& x : value)
    {
        std::cout << x << " ";
    }
    std::cout << std::endl;
    return 0;
}

编译运行:

bash
g++ -std=c++17 -Wall -Wextra main.cpp -o main && ./main

运行结果:

text
修改前: 3 -7 1 -9 4 -6
修改后: 3 7 1 9 4 6

pdf-latest-4-g85128cc · 85128cc · 2026-10-05