☰
C ++迭代器iterator在string中使用方法介绍
2026/10/31 5:30:19 网站建设 项目流程

前言


迭代器(iterator)是 C++ 容器与算法之间的桥梁:它用一套统一的接口表示「容器中的某个位置」,让std::find、std::sort、std::copy这类算法不必知道容器具体是std::string、std::vector还是std::list。std::string的迭代器是随机访问迭代器(random access iterator),支持+n、-n、下标式跳跃和前后比较,用起来接近指针。


常见的误解有四个。第一,认为std::string::iterator就是char*。在很多实现的标准库配置下它确实被实现为指针或指针的薄包装,但标准只规定它的行为,不规定它的类型;依赖「它一定是指针」的代码不可移植。第二,认为修改字符串后旧迭代器仍然可用。插入、删除、扩容都可能使迭代器失效,继续解引用是未定义行为(undefined behavior, UB)。第三,在erase之后又对同一个迭代器执行++,跳过一个元素甚至越过end()。第四,用迭代器逐字节处理 UTF-8 文本,把多字节字符当成独立字节修改,导致乱码。


本文讲std::string迭代器的类型、基本操作、修改时的失效规则,以及与标准算法配合的注意事项。示例以 C++17 为基准。


一、迭代器类型与基本操作


std::string提供以下几组迭代器接口:




接口返回类型用途



begin()/end()iterator可读写遍历

cbegin()/cend()const_iterator只读遍历,C++11 起

rbegin()/rend()reverse_iterator从尾到头

crbegin()/crend()const_reverse_iterator只读反向,C++11 起



begin()指向第一个字符,end()指向「最后一个字符之后」的位置,这是一个合法的哨兵位置,可以比较,但不能解引用。空字符串的begin() == end()。反向迭代器把顺序颠倒:rbegin()指向最后一个字符,rend()指向第一个字符之前。


// 适用于 C++17
#include <iostream>
#include <string>

int main() {
std::string s = "abcdef";

for (auto it = s.begin(); it != s.end(); ++it) {
std::cout << *it;
}
std::cout << '\n';

for (auto it = s.cbegin(); it != s.cend(); ++it) {
std::cout << *it; // 只读,不能赋值
}
std::cout << '\n';

for (auto it = s.rbegin(); it != s.rend(); ++it) {
std::cout << *it;
}
std::cout << '\n';

std::cout << *(s.begin() + 2) << '\n'; // 随机访问:'c'
return 0;
}

随机访问迭代器支持it + n、it - n、it[n]、it1 - it2。两个迭代器相减得到difference_type,对std::string通常是有符号整数类型,表示距离而不是下标;把end() - begin()转成int之前要确认范围。std::distance(begin, end)是更通用的写法,对随机访问迭代器是常数时间,对链表则是线性时间。


范围 for 循环本质上就是迭代器遍历的语法糖,for (char c : s)等价于从begin()到end()逐个解引用。只读遍历时写const auto&可以避免拷贝,也防止误改。


二、修改操作与迭代器失效


std::string的修改接口大多以迭代器为位置参数,并在 C++11 起返回有用的迭代器:


// 适用于 C++17
#include <string>

void edit(std::string& s) {
auto it = s.begin() + 1;
it = s.insert(it, 'X'); // 返回指向插入字符的迭代器
it = s.erase(it); // 删除该字符,返回下一个位置
s.insert(s.end(), 2, '!'); // 在末尾插入两个 '!'
}

insert返回指向第一个插入字符的迭代器;erase返回被删元素之后的迭代器。利用返回值可以在循环中安全地连续修改,而不必每次重新begin()。但前提是每次修改后都使用返回的新迭代器,不能继续用旧的那个。


失效规则需要保守理解:



  • 任何可能重新分配缓冲区的操作(reserve、append、insert、replace、resize等)都可能使所有迭代器、指针、引用失效。

  • erase至少会使被删位置及其之后的迭代器失效;具体范围与实现有关。

  • operator[]、at、begin、end等不修改内容的操作不会使迭代器失效,但通过operator[]修改字符不会改变长度,也不会触发重新分配。

  • 一旦迭代器失效,解引用、递增、比较都是 UB,不能靠「看起来还能读到旧值」来判断安全。


标准对basic_string的失效规定相当保守:除了operator[]、at、data、front、back、begin、end等少数不修改内容的接口,任何非 const 成员函数都可能使引用、指针、迭代器失效。具体实现是否因为短字符串优化而「碰巧没动堆」不能作为依据。工程上的稳妥做法是:修改之后重新获取迭代器,或者用下标/find重新定位,不要缓存跨修改的迭代器。


反向迭代器有一个容易混淆的点:rit.base()返回的是「与rit相邻的下一个正向位置」,而不是rit本身指向的位置。例如s.rbegin().base() == s.end()。在erase反向迭代器指向的元素时,通常要写s.erase(std::next(rit).base())或先转成正向迭代器,直接传rit.base()会删错位置。


三、与标准算法配合


迭代器的价值在与算法配合时才充分体现。std::find、std::count、std::reverse、std::sort都只要求迭代器满足相应的类别,std::string的随机访问迭代器全部满足。


// 适用于 C++17
#include <algorithm>
#include <cctype>
#include <iostream>
#include <iterator>
#include <string>

int main() {
std::string s = "Hello, World";

// 转小写:必须先把 char 转成 unsigned char,避免负值传给 tolower
for (auto it = s.begin(); it != s.end(); ++it) {
const unsigned char uc = static_cast<unsigned char>(*it);
*it = static_cast<char>(std::tolower(uc));
}

auto comma = std::find(s.begin(), s.end(), ',');
if (comma != s.end()) {
std::cout << "index: "
<< std::distance(s.begin(), comma) << '\n';
s.erase(comma); // comma 之后不可再用
}

std::reverse(s.begin(), s.end());
std::cout << s << '\n';
return 0;
}

几个细节:



  • std::tolower的参数是int,如果直接传char,在char有符号且字符值大于 127 时会得到负值,行为未定义;先转unsigned char再传,是标准推荐写法。

  • std::distance返回difference_type,不要假设它是int;输出或与size_t比较时注意符号。

  • std::reverse原地反转字节序列。对纯 ASCII 没问题;对 UTF-8 中文,反转后字节顺序被打乱,产生非法序列。字符级反转必须先按码点切分。

  • std::sort(s.begin(), s.end())会按字节排序,同样只适合单字节字符集;对 UTF-8 文本没有「按字符排序」的语义。


如果只是只读遍历,C++17 的std::string_view可以避免构造std::string,但它同样按字节提供迭代器,不会自动处理编码。


常见坑点


坑点一:erase之后又对旧迭代器递增。


❌it = s.erase(it); ++it;跳过一个元素,循环可能越界。


✅ 直接用erase的返回值作为下一轮的位置:it = s.erase(it);,不要再++。


坑点二:缓存end()后修改字符串。


❌auto e = s.end(); s += "x"; for (auto it = s.begin(); it != e; ++it),扩容后e失效。


✅ 修改后重新获取begin()/end(),或使用基于范围的操作。


坑点三:把iterator与const_iterator混用。


❌ 函数返回const_iterator,调用方却用iterator接收,编译失败或需要转换。


✅ 只读接口用cbegin()/cend(),需要修改时统一用iterator;const 对象调用begin()得到的是const_iterator。


坑点四:解引用end()。


❌char c = *s.end();哨兵位置不可解引用,UB。


✅ 先判断it != s.end();空字符串要单独处理。


坑点五:用int保存size()或distance。


❌int n = s.size();超长字符串截断;int d = it2 - it1;大距离溢出。


✅ 用std::string::size_type或std::ptrdiff_t,必要时用auto。


坑点六:std::tolower(*it)直接传char。


❌ 负值字符传给tolower,行为未定义。


✅ 先转unsigned char,再转回char写回。


坑点七:对 UTF-8 字符串按字节反转或排序。


❌std::reverse(s.begin(), s.end())把三字节汉字拆散,产生非法序列。


✅ 按码点切分后再反转;或明确只处理 ASCII。


坑点八:反向迭代器base()直接用于erase。


❌s.erase(rit.base());删除的是rit的下一个位置,不是它指向的字符。


✅ 用s.erase(std::next(rit).base()),或先转成正向迭代器再删。


总结




操作接口注意点



正向遍历begin()/end()end()不可解引用

只读遍历cbegin()/cend()避免误改

反向遍历rbegin()/rend()base()有偏移

随机访问it + n、it[n]下标不检查边界

插入insert(pos, ...)返回新位置,旧迭代器失效

删除erase(pos)返回下一位置,不要重复递增

距离std::distance返回difference_type

算法find/reverse/sort按字节处理,注意编码



std::string的迭代器把字符串接入了标准算法体系,用起来像指针,但失效规则比指针严格得多。写代码时守住三条:不跨修改缓存迭代器、erase只用返回值、按字节处理时先确认字符集。这样既能享受算法的便利,也不会掉进悬垂迭代器与越界解引用的坑。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询