资讯详情

【C++】iterator(迭代器):遍历与失效规则

📅 2026/10/6 2:47:59 | 华诺云谱 👁 阅读
【C++】iterator(迭代器):遍历与失效规则
目录一、迭代器遍历1.begin() / end()2. rbegin() / rend()3. cbegin() / cend() crbegin() / crend()4. 范围for auto4.1 auto4.2 范围for5. 遍历方式总结二、迭代器失效规则1. 扩容导致的失效2. insert 之后的失效3. erase导致后续失效总结三、reverse以 string 和 vector 为例可以将迭代器理解为指针不一定是指针它模仿指针的行为支持随机访问。遍历方式有很多种但关键是要理解迭代器失效规则。一、迭代器遍历1.begin() / end()begin()获取第一个元素的迭代器。end()获取最后一个元素的下一个位置的迭代器end指向的是\0之后的位置不是\0本身解引用 end() 是未定义行为。begin() 和 end() 构成左闭右开区间[ begin,end )使用如下int main() { std::string s hello; // 非 const 对象调用 iterator begin() std::string::iterator it1 s.begin(); *it1 H; // 可以修改s 变成 Hello // const 对象调用 const_iterator begin() const std::string cs world; std::string::const_iterator it2 cs.begin(); // *it2 W; // 错误const_iterator 不能修改 std::cout s cs \n; }2. rbegin() / rend()rbegin和rend是反向迭代器倒着遍历rbegin()指向最后一个元素。rend()指向第一个的前一个。它们配合使用可以让迭代器从后往前移动变成了向左走。int main() { string s1(12345); string::reverse_iterator rit1 s1.rbegin(); // rbegin指向尾rend指向头 while (rit1 ! s1.rend()) { (*rit1); cout *rit1 ; // 5 4 3 2 1 rit1; // 依然用 } cout endl; // const版本 const string s2(67890); string::const_reverse_iterator rit2 s2.rbegin(); while (rit2 ! s2.rend()) { cout *rit2 ; rit2; } cout endl; return 0; }3. cbegin() / cend() crbegin() / crend()返回只读迭代器不能通过它修改字符。cbeingstring::cbegin - C Referencecend()string::cend - C Referencecrbegin()string::crbegin - C Referencecrend()string::crend - C Reference4. 范围for auto4.1 auto语法auto是 C11 引入的关键字作用是让编译器在编译期自动推导变量的类型。它不是一个类型而是一个“类型占位符”。比如auto x 10; // x 是 int auto y 3.14; // y 是 double auto c a; // c 是 char auto s hello; // s 是 const char* (常量字符串)std::vectorstd::string v {apple, banana}; // 传统写法it的类型又臭又长 std::vectorstd::string::iterator it v.begin(); for (; it ! v.end(); it) { std::cout *it std::endl; } // auto 写法编译器自己推导 it 的类型 for (auto it v.begin(); it ! v.end(); it) { std::cout *it std::endl; }有了auto后可以省去手写复杂类型代码更简洁而且不会因为手误写错类型。注意事项1.根据等号右边的初始值推导出左边的类型。所以auto左边必须要初始化auto x; // 错误 auto y 10; // 正确2. 会忽略 const 和 引用const int a 10; auto b a; // b 是 int不是 const int如果想保留 引用或 const要自己写const int a 10; auto b a; // b 是 const int const auto c a; // c 是 const int3. auto类型不能出现在数组中int main(){ auto arr[] { 1,2,3,4,5 }; // err, auto类型不能出现在数组中 return 0; }4. auto 初始化指针时推导为指针类型而auto* 必须初始化指针const int a 10; auto p1 a; // ok auto* p2 a; //ok auto* p3 a; // 错误auto* 必须初始化指针5. auto也有弊端会降低代码的可读性4.2 范围for范围for底层依赖迭代器 begin() / end() 语法简单。for ( 声明变量 : 要遍历的容器 ) { // 循环体 }容器可以是std::string、std::vector、数组、初始化列表甚至是自己写的提供了begin/end的类。变量每次循环会把容器里的当前元素赋给这个变量。代码举例string s(111111); // 范围for语法糖 for (char ch : s){ cout ch ; } cout endl; // 也可以 for (auto ch : s) // auto会自动识别类型 { cout ch 3; } cout endl;std::vectorint v {1, 2, 3}; // 1. 值拷贝改的是副本v 不变 for (auto x : v) { x * 10; } // 2. 引用改的是本体v 变了 for (auto x : v) { x * 10; } // 3. 常量引用只读且不拷贝效率最高 for (const auto x : v) { std::cout x ; }底层原理编译器看到 范围for 会自动把它展开成 迭代器版本比如auto x : v 实际上是 auto x *begin()所以范围 for 只是帮助省去了写迭代器的麻烦本质上还是在用迭代器遍历。所以范围for不适用于没有 begin / end 的类型。5. 遍历方式总结int main() { // 下标[] string s( ); for (int i 0;i s.size();i) { cout s[i]1; } cout endl; // 迭代器 string::iterator it s.begin(); while (it ! s.end()) // 这里的判断条件最好不要用 ,因为有的容器不能用比较 { cout *it 2; it; } cout endl; // 范围for for (auto ch : s) { cout ch 3; } cout endl; return 0; }二、迭代器失效规则只要容器可能重新分配内存或者元素位置发生移动都可能会使旧的迭代器、指针、引用失效。1. 扩容导致的失效void vectorT::insert(iterator pos, const T x) { if (_finish _end_of_storage) { size_t len pos - _start; size_t newCapacity capacity() 0 ? 4 : capacity() * 2; reserve(newCapacity); // 扩容pos迭代器失效 pos _start len; } // 指向了新空间但pos还指向旧空间 // 挪动数据,pos指向旧空间err memmove(pos 1, pos, sizeof(T) * (_finish - pos)); *pos x; _finish; }正确做法扩容后必须重新获取迭代器reserve(newCapacity); pos _start len; // len 要提前算好2. insert 之后的失效int main() { std::vectorint v; v.reserve(10); v {1, 2, 3, 4}; auto it v.begin() 2; // it 指向 3下标为2 std::cout 插入前 it 指向: *it std::endl; // 输出 3 // 在下标1处插入 99未触发扩容 v.insert(v.begin() 1, 99); // 此时 v {1, 99, 2, 3, 4} // 旧内存没变但 it 依然指向下标2的位置 // 此时下标2的值变成了 2而不是原来的 3 std::cout 插入后 it 指向: *it std::endl; // 输出 2逻辑已失效 return 0; }统一认为 insert 之后的迭代器是失效的3. erase导致后续失效#include iostream #include vector int main() { std::vectorint v {10, 20, 30, 40, 50}; // it 指向第 3 个元素值为 30 auto it v.begin() 2; std::cout 删除前it 指向: *it std::endl; // 输出 30 // 删除第 2 个元素20 v.erase(v.begin() 1); // 此时 v 变成了 {10, 30, 40, 50}且底层指针 _finish相当于_size 向前移了 1 格 // it 底层的地址没变仍然指向原来的第 3 格但那里的值已经变成了 40 std::cout 删除后it 指向: *it std::endl; // 输出 40逻辑错误 return 0; }erase 删除点及其之后的所有迭代器全部失效。正确的做法用 erase 的返回值来更新迭代器erase会返回被删除元素的下一个元素的有效迭代器。std::vectorint v {10, 20, 30, 40}; // 正确示范删除容器中所有的 20 for (auto it v.begin(); it ! v.end(); ) { if (*it 20) { // 关键用 erase 的返回值覆盖旧的 it it v.erase(it); // 此时 it 自动指向了原来 20 后面的元素也就是 30且是安全的 } else { it; } }总结操作是否扩容失效范围原因insert未扩容否插入点及之后元素向后移动数据错位insert扩容是全部失效内存搬家旧内存被释放野指针erase否删除点及之后元素向前移动数据错位_finish前移三、reversereverse 通常是标准库算法 std::reverse用来颠倒容器里的元素顺序注意string和vector中本身没有 reverse 成员函数必须用标准库算法。而 list 里有 reverse成员函数std::reverse头文件algorithm参数要反转的区间 [ begin,end )#include algorithm #include string #include vector std::string s hello; std::reverse(s.begin(), s.end()); // s 变成 olleh std::vectorint v {1, 2, 3, 4}; std::reverse(v.begin(), v.end()); // v 变成 {4, 3, 2, 1}
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑