ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++for 语句的要点与注意事项小结

C++for 语句的要点与注意事项小结 前言for是 C 里使用频率最高的控制语句也是最容易被「凭手感」写的那一个。它表面上有两种形态——传统三段式for和 C11 引入的范围 forrange-based for但真正会出问题的细节散落在语法、作用域、对象生命周期、迭代器失效和性能这几层里。几个常见的误解值得先摆出来「范围 for 会把容器拷贝一份」——不对。标准规定的展开形式里for-range-initializer是绑定到auto上的临时对象的生命周期会被延长到整个循环「for (int i 0; i v.size(); i)是安全的」——当v为空且用无符号类型时v.size() - 1会回绕成一个极大值配合下标访问就是 UB「循环条件每次都会重新求值所以要把size()提出来」——对内置类型和无副作用的内联调用现代编译器会自己优化真正该提的是有副作用的函数调用比如循环条件里写strlen。本文按「三段式的语法细节 → 范围 for 的展开规则 → 循环中改容器 → 性能与类型」四部分把要点过一遍最后汇总 8 个真会踩的坑。代码基准C17涉及 C20 / C23 的地方会单独标注并给出 C17 写法。一、三段式 for 的语法细节for的语法形式是for ( init-statement condition_opt ; expression_opt ) statement注意两个分号是必须的condition和expression都可以省略所以for (;;)是合法的无限循环。三个部分各自的性质部分求值时机说明init-statement只执行一次可以是表达式语句也可以是简单声明simple-declarationcondition每次迭代开始前含第一次按语境转换成bool省略则视为trueexpression每次迭代体结束后continue之后也会执行这是continue与while语义的关键差别statement循环体可以是一条语句或一个复合语句init-statement声明的名字其作用域覆盖整个for语句包括条件、迭代表达式和循环体循环结束后名字不再可见。这正是for (int i 0; ...)比「在循环外声明i」更安全的原因不会污染外层作用域。一个常被忽略的细节init-statement如果是简单声明只能声明同一种类型的多个变量。下面两种写法只有一种合法for (int i 0, j 10; i j; i, --j) { /* 合法都是 int */ } // for (int i 0, double d 0.0; ...) // 非法一条声明里混了两种类型第三部分用的是逗号运算符不是逗号分隔的语句列表所以里面只能放表达式int i 0, j 10; for (; i j; i, --j) { } // 合法 // for (; i j; i, if (x) {} ) // 非法if 是语句不是表达式条件部分也可以放声明这在while里更常见例如用std::optional或指针int* p nullptr; for (int arr[] {1, 2, 3}, *it arr; (p it) ! arr 3; it) { /* ... */ }这类写法可读性差工程上不建议但知道语法允许能帮你读懂别人写的代码。二、范围 for 的展开规则C11 起的范围 for 是语法糖标准给出了等价的展开形式C17 的措辞大致如下{ auto __range for-range-initializer; // 绑定到引用临时对象生命周期延长 auto __begin begin-expr; // 通常是 __range.begin() 或 ADL 找到的 begin auto __end end-expr; // 同理 for (; __begin ! __end; __begin) { for-range-declaration *__begin; // 用 *__begin 初始化循环变量 statement } }从这段展开能直接推出四条重要结论for-range-initializer只求值一次不会每轮重新求值。所以for (auto x : computeVector())里的computeVector()只调用一次。临时对象的生命周期被延长auto __range绑定右值后临时对象的生命期延到作用域结束也就是整个循环。所以在 C17 里写for (auto x : makeVector())是安全的。循环变量是*__begin的拷贝或引用取决于for-range-declaration怎么写auto x是拷贝auto x是引用auto x是转发引用。begin与end通过成员查找或 ADL 找到优先用__range.begin()找不到就用实参依赖查找argument-dependent lookupADL找到的自由函数begin(__range)。这也是自定义容器能参与范围 for 的机制。关于生命周期还有一处需要提醒标准在 C23 里进一步收紧了这块规则把range-initializer内部产生的临时对象的生命期一并延长到循环结束此前的措辞在少数复杂表达式上存在悬垂风险。如果你的项目固定在 C17遇到形如「函数返回一个内部持有临时对象的聚合再对它的成员做范围 for」的复杂表达式时最稳妥的做法是先把结果存进一个具名变量再循环。具体措辞以 cppreference 的 range-based for 页面为准。C20 还给范围 for 加了初始化语句init-statement形如for (int i 0; auto row : matrix) { /* 用 i 记录行号需要 C20 及以上 */ }C17 下的替代写法是把i声明到循环外面int i 0; for (auto row : matrix) { /* 用 i */ i; }但要注意后一种写法会让i在循环结束后仍然可见这点和 C20 的语义不同。三、循环体内修改容器什么时候会出事这是范围 for 最大的雷区。三条规则必须记牢循环写法在循环体内修改容器会怎样for (auto x : v)且修改v的结构增删元素迭代器可能失效之后__begin是UBfor (auto x : v)且修改v的结构拷贝已经完成本次迭代安全但下一轮的__begin仍是 UBfor (auto x : v)只改元素的值不增删安全v大小不变时vector的引用不会失效「修改元素值」和「修改容器结构」是完全不同的两件事。前者只要不涉及vectorbool这类代理类型就没问题后者一旦触发扩容或元素搬移正在使用的迭代器就悬垂了。需要「边遍历边删除」时正确姿势是用迭代器配合erase的返回值#include iostream #include vector int main() { std::vectorint v {1, 2, 3, 4, 5, 6}; // 删除所有偶数erase 返回下一个有效迭代器 for (auto it v.begin(); it ! v.end(); ) { if (*it % 2 0) it v.erase(it); // 不要再 it else it; } for (const int x : v) std::cout x ; // 1 3 5 std::cout \n; // C20 起可以用 std::erase_if需要 vectorC20 // std::erase_if(v, [](int x) { return x 0; }); return 0; }同样的模式在std::list/std::map上也成立它们也是「erase返回下一个有效迭代器」。而std::vector的erase会让删除点之后的迭代器全部失效所以必须先接收返回值绝不能在erase之后继续用旧迭代器。四、类型与性能上的细节有符号与无符号的混用是循环里最常见的编译警告来源。v.size()的类型是std::size_t无符号与int i比较会触发符号比较警告GCC/Clang 的-Wsign-compareMSVC 的 C4018。三种处理方式写法说明for (std::size_t i 0; i v.size(); i)最直接注意下标表达式的类型也就统一了for (std::size_t i 0, n v.size(); i n; i)把 size 提到变量里适合循环体可能改变v的场景for (const auto x : v)不需要下标时优先用它for (int i 0, n static_castint(v.size()); i n; i)需要int语义时C20 起有std::ssize更直接条件里不要放昂贵调用。下面这段是典型的 O(n²)#include cstring #include iostream int main() { const char* s hello, world; std::size_t n 0; for (std::size_t i 0; i std::strlen(s); i) n; // 每次迭代都重新求长度 std::cout n \n; const std::size_t len std::strlen(s); n 0; for (std::size_t i 0; i len; i) n; // 只求一次 std::cout n \n; return 0; }编译器能否把strlen提出循环取决于它能否证明s指向的内容在循环里不变。对于const char*指向只读数据、且循环体不写内存的情况优化器常常能提取但只要循环体调用了任何可能写内存的函数就必须每次重新求值。不要指望优化器替你解决这个问题把不变量手动提出来永远是对的。循环变量的拷贝代价。for (auto x : v)每轮都拷贝一个元素。对int无所谓对std::string、std::vector这类类型就是实打实的分配与拷贝。规则很简单需求写法只读且元素小内置类型、指针for (auto x : v)或for (const auto x : v)都行只读元素可能很大for (const auto x : v)需要修改容器里的元素for (auto x : v)泛型代码不确定左值右值for (auto x : v)常见坑点1. 无符号变量做倒序循环❌for (std::size_t i v.size() - 1; i 0; --i) { /* 用 v[i] */ }i 0对无符号类型恒真而且当v为空时v.size() - 1回绕成极大值v[i]立即越界——这是UB标准不保证任何行为。✅for (std::size_t i v.size(); i-- 0; ) { /* 用 v[i] */ }2. 在范围 for 里增删元素❌for (const auto x : v) { if (x 0) v.push_back(-x); // 可能触发扩容迭代器失效UB }✅ 先用一个副本或新容器收集结果循环结束后再合并或者改用下标迭代。3. erase 之后继续用旧迭代器❌for (auto it v.begin(); it ! v.end(); it) { if (*it % 2 0) v.erase(it); // it 已失效it 是 UB }✅it v.erase(it);并且在 erase 分支里不要再it。4. 对 bool 特化版本用 auto 取元素❌for (auto b : vb) { b true; }——std::vectorbool的operator[]返回的是代理对象proxy reference的右值不能绑定到非const左值引用编译失败错误信息通常很难懂。✅for (auto b : vb) { b true; }或者明确地用下标for (std::size_t i 0; i vb.size(); i) vb[i] true;。补充一句std::vectorbool是标准里唯一被特化的容器它的元素不满足「容器元素必须是对象」的常规假设很多泛型代码对它会失败。5. for 后面多写了分号❌#include iostream int main() { for (int i 0; i 10; i); // 循环体是那一个分号什么也没做 std::cout done\n; // 只执行一次而不是十次 return 0; }上面这段能编译、能运行只是行为不是你想要的——这类「静默出错」比编译错误难查得多。✅ 需要循环体就写花括号for (int i 0; i 10; i) { std::cout i \n; }。6. 在循环条件里调用有副作用的函数❌std::vectorint v {1, 2, 3}; int idx 0; for (auto it v.begin(); it ! v.end() next(idx); it) { } // idx 被条件悄悄改了✅ 循环条件只做判断副作用放到循环体或第三部分。7. 循环变量遮蔽shadowing外层同名变量❌int i 100; for (int i 0; i 3; i) { } // 内层 i 遮蔽外层 std::cout i \n; // 仍是 100不是 3✅ 换个名字或者让编译器帮你抓GCC/Clang 用-WshadowMSVC 用/W4时也会提示。8. 循环里对 string 迭代时修改其长度❌std::string s abc; for (char c : s) { if (c b) s !; // 可能重新分配c 变成悬垂引用UB }✅ 先把要追加的内容收集到另一个字符串循环结束后再s ...。总结要点结论三段式语法两个分号必须写init-statement只能是表达式或简单声明第三部分是表达式continue之后仍会执行范围 for 展开初始化表达式只求值一次临时对象绑定到auto上生命期延长循环变量auto拷贝、auto引用、auto转发引用按需选择改容器改元素值安全vectorbool除外改结构导致迭代器失效删除元素用it v.erase(it)且不要在同一轮再it无符号倒序用for (std::size_t i n; i-- 0; )性能把有副作用的表达式提出循环条件大对象用constfor的坑几乎都集中在两处引用/拷贝的选择和容器结构在循环中被改动。前者影响正确性和性能后者直接决定程序是不是 UB。写循环时多问一句「我这轮里改了什么、谁可能失效」比记住任何语法细节都管用。
返回列表