)
前言这是字符串函数的第二篇聚焦四类日常使用频率最高的操作查找、比较与截取、数值转换、分割与拼接。第一篇讲的是构造、赋值、容量与访问本篇不再重复。需要先说明C 里有两套字符串工具一套是cstring里的 C 风格函数strlen、strcpy、strstr一套是std::string的成员函数和string_view。新代码应优先用后者前者只在对接 C 接口时使用。典型误解有三个。第一把std::string::find的返回值当成int和-1比较——它返回的是无符号的size_type找不到时返回npos写成if (s.find(x) 0)永远为假。第二以为strncpy是「安全版strcpy」——当源串长度大于等于n时它不会补\0后续按字符串使用就是越界读。第三以为std::stoi会告诉你「解析到哪个位置为止」——它确实有pos参数但不检查尾部多余字符12abc会被解析成 12。本文的示例基准为 C17涉及 C20 的starts_with/ends_with和 C23 的contains会单独标注。一、查找族find 的六个变体1.1 基本查找std::string提供六个查找函数全部返回size_type失败时返回std::string::npos函数查找内容方向find子串或字符从前往后rfind子串或字符从后往前find_first_of字符集合中任意一个从前往后find_last_of字符集合中任意一个从后往前find_first_not_of第一个不在集合中的字符从前往后find_last_not_of最后一个不在集合中的字符从后往前// 适用于 C17#include stringstd::string s namevalue;path/tmp;const auto eq s.find(); // 4const auto semi s.find(;); // 10const auto missing s.find(absent); // std::string::nposif (missing std::string::npos) {// 没找到}find的第二个参数是起始位置可以用来查找所有出现// 适用于 C17std::size_t pos 0;while ((pos s.find(, pos)) ! std::string::npos) {// 处理 pos 处的 pos; // 必须前进否则死循环}忘记pos是最常见的死循环来源。1.2 npos 是无符号的npos的定义等价于size_type(-1)即该无符号类型的最大值。所以下面两种写法都是错的// 适用于 C17 —— 错误if (s.find(x) 0) { } // 永远为假int idx static_castint(s.find(x)); // npos 截断后是 -1但不可移植正确写法只有 std::string::npos或! std::string::npos。如果一定要转成有符号下标先判断npos再转换。1.3 string_view 上的查找std::string_viewC17提供同名的一组find函数语义一致但不分配内存、不拷贝字符// 适用于 C17#include string_viewstd::string_view sv keyvalue;const auto pos sv.find();if (pos ! std::string_view::npos) {std::string_view key sv.substr(0, pos);std::string_view val sv.substr(pos 1);}string_view不拥有字符它的生命周期必须短于被观察的字符串。把string_view存进容器、返回给调用方而底层字符串已经销毁就是悬垂。需要长期保存时转成std::string。二、比较、截取与前缀后缀2.1 compare 的三值语义compare返回负数、零、正数分别表示小于、等于、大于。注意标准只规定符号不保证是-1和1// 适用于 C17std::string a apple;std::string b banana;if (a.compare(b) 0) {// a 字典序小于 b}compare还有带位置的版本a.compare(pos, len, other)比较a从pos开始的len个字符与other。位置越界会抛std::out_of_range。日常判断相等直接用判断大小用可读性更好compare适合需要三值结果的场景比如排序比较器。2.2 substr// 适用于 C17std::string s 2026-10-18;std::string year s.substr(0, 4); // 2026std::string month s.substr(5, 2); // 10std::string rest s.substr(8); // 18substr(pos, count)从pos开始取最多count个字符count超出剩余长度时取到末尾。pos size()会抛std::out_of_rangepos size()返回空串。第二参数省略表示取到末尾。substr每次调用都构造一个新std::string在循环里对长字符串反复截取会产生大量分配。只读场景改用std::string_view::substr它返回视图不复制。2.3 前缀与后缀C20 起std::string和std::string_view都有starts_with/ends_with// 需要 C20#include stringstd::string path /usr/local/bin;bool abs path.starts_with(/);bool bin path.ends_with(bin);C17 及以前要自己写// 适用于 C17bool starts_with(const std::string s, const std::string prefix) {return s.size() prefix.size() s.compare(0, prefix.size(), prefix) 0;}bool ends_with(const std::string s, const std::string suffix) {return s.size() suffix.size() s.compare(s.size() - suffix.size(), suffix.size(), suffix) 0;}先比较长度再compare避免size() - suffix.size()在无符号运算下回绕成巨大值。这是手写版本最容易漏的一步。C23 还增加了contains等价于find(...) ! npos同样需要确认工具链支持。三、数值转换3.1 stoi 家族函数目标类型异常std::stoiintinvalid_argument、out_of_rangestd::stollong同上std::stolllong long同上std::stoul/std::stoull无符号同上std::stof/std::stod/std::stold浮点同上// 适用于 C17#include stringstd::size_t pos 0;try {int v std::stoi(123abc, pos); // v 123, pos 3} catch (const std::invalid_argument) {// 完全无法解析} catch (const std::out_of_range) {// 超出 int 范围}stoi的第三个参数是进制默认 10传 0 表示按前缀自动判断。它不要求整个字符串都被消耗所以判断「是否是合法整数」必须同时检查pos s.size()// 适用于 C17bool is_integer(const std::string s) {if (s.empty()) return false;std::size_t pos 0;try {std::stoi(s, pos);} catch (...) {return false;}return pos s.size();}3.2 to_stringstd::to_string对整数和浮点都有重载返回std::string// 适用于 C17std::string a std::to_string(42); // 42std::string b std::to_string(3.14); // 小数位数由实现决定浮点版本的小数位数标准没有规定通常等价于%f的 6 位但不保证。需要精确格式就用std::ostringstream配std::setprecision或 C20 的std::format。3.3 from_chars不抛异常的解析C17 的charconv提供std::from_chars它不分配、不抛异常、不要求空字符结尾// 适用于 C17#include charconv#include system_errorconst std::string text 123;int value 0;const char* first text.data();const char* last first text.size();auto [ptr, ec] std::from_chars(first, last, value);if (ec std::errc{} ptr last) {// 整个字符串都是合法整数}ec为std::errc::invalid_argument表示没有任何数字std::errc::result_out_of_range表示溢出。整数版本在 C17 已普遍可用浮点版本虽然也是 C17 标准的一部分但各标准库实现时间差别较大使用前要查自己工具链的支持情况。方式失败表示是否分配是否要求\0结尾std::stoi抛异常可能是接收std::stringstd::from_chars返回错误码否否std::sscanf返回成功项数否是std::atoi返回 0无法区分非法否是四、C 风格函数与分割4.1 常见 C 函数函数用途风险std::strlen求长度要求\0结尾否则越界std::strcmp比较返回值的符号有意义具体值不保证std::strstr查找子串返回指针找不到为nullptrstd::strcpy复制不检查目标容量缓冲区溢出std::strncpy限长复制源串过长时不补\0std::snprintf格式化返回值可能大于缓冲区大小strncpy的行为值得单独说它最多复制n个字符如果源串长度小于n剩余部分用\0填满如果源串长度大于等于n它不写终止符。所以正确用法是// 适用于 C17char buf[16];std::snprintf(buf, sizeof(buf), %s, src); // 更推荐或者用strncpy后手动buf[sizeof(buf) - 1] \0;。新代码没有理由再用strcpy。4.2 strtok 的问题strtok用静态内部状态保存「下一次从哪里开始」因此不可重入两个线程同时用会互相破坏不能嵌套分割两个字符串会修改输入串把分隔符改成\0。C 里更安全的做法是用std::stringfind或者用std::getline配分隔符// 适用于 C17#include sstream#include string#include vectorstd::vectorstd::string split(const std::string line, char delim) {std::vectorstd::string out;std::istringstream iss(line);std::string item;while (std::getline(iss, item, delim)) {out.push_back(item);}return out;}std::getline在遇到连续分隔符时会产生空字段这一点和strtok会跳过连续分隔符不同按需求选择。4.3 手写 split 与 trim不需要istringstream时用find手写更直接// 适用于 C17std::vectorstd::string split(const std::string s, char delim) {std::vectorstd::string out;std::size_t start 0;while (true) {const std::size_t pos s.find(delim, start);if (pos std::string::npos) {out.push_back(s.substr(start));break;}out.push_back(s.substr(start, pos - start));start pos 1;}return out;}trim 用find_first_not_of和find_last_not_of组合注意全部是空白字符时要返回空串不能对npos做减法。五、完整示例解析一行配置// 适用于 C17#include charconv#include iostream#include string#include string_view#include system_error#include vectorstd::string_view trim(std::string_view s) {const auto first s.find_first_not_of( \t\r\n);if (first std::string_view::npos) return {};const auto last s.find_last_not_of( \t\r\n);return s.substr(first, last - first 1);}std::vectorstd::string_view split(std::string_view s, char delim) {std::vectorstd::string_view out;std::size_t start 0;while (true) {const std::size_t pos s.find(delim, start);if (pos std::string_view::npos) {out.push_back(s.substr(start));break;}out.push_back(s.substr(start, pos - start));start pos 1;}return out;}bool parse_int(std::string_view s, int value) {const char* first s.data();const char* last first s.size();const auto result std::from_chars(first, last, value);return result.ec std::errc{} result.ptr last;}int main() {const std::string line 42, 7 ,1000,oops ;const auto fields split(line, ,);long long sum 0;for (std::string_view field : fields) {const std::string_view token trim(field);int value 0;if (parse_int(token, value)) {sum value;} else {std::cout skip: token \n;}}std::cout sum sum \n;return 0;}这份示例把本篇文章的几类函数串了起来find定位分隔符substr切片find_first_not_of/find_last_not_of去空白from_chars做无异常解析。全程没有堆分配字符串split返回的是string_view指向line的存储前提是line的生命周期覆盖整个使用过程。常见坑点❌if (s.find(x) 0)✅if (s.find(x) std::string::npos)find返回无符号的size_type与 0 比较永远不成立。❌ 找到后不推进位置while ((pos s.find(c, pos)) ! npos) { ... }✅ 循环体里pos或pos 1 起始位置不变会反复找到同一个字符造成死循环。❌strncpy(dst, src, n);之后直接当 C 字符串用✅ 确保最后一位是\0或改用snprintf源串长度大于等于n时不会写入终止符。❌int v std::stoi(12abc);认为输入非法会报错✅ 检查pos s.size()或用from_chars比较ptrstoi只解析前缀尾部多余字符不会触发异常。❌substr(pos, len)不检查pos是否越界✅ 先判断pos s.size()pos size()会抛std::out_of_range。❌ 用strtok分割同时另一个线程也在用✅ 用std::getlineistringstream或手写find循环strtok依赖静态内部状态不可重入。❌ends_with手写时直接s.compare(s.size() - suffix.size(), ...)✅ 先判断s.size() suffix.size()无符号减法在s较短时会回绕成巨大值。❌ 把std::string_view存起来指向的std::string已销毁✅ 需要长期保存时转成std::stringstring_view不拥有字符生命周期由外部保证。总结需求推荐函数注意事项查找子串/字符find/rfind失败返回npos不要与 0 比较查找字符集合find_first_of等四个变体区分「在集合中」与「不在集合中」截取substrpos越界会抛异常比较、、comparecompare只保证返回值的符号前缀/后缀C20starts_with/ends_with旧标准手写时先比长度字符串转数字from_chars推荐或stoistoi不检查尾部字符数字转字符串to_string/to_chars浮点精度格式需显式控制分割getlineistringstream或findstrtok不可重入字符串处理的难点不在函数数量而在边界空串、全是空白、分隔符在开头或结尾、位置越界、无符号回绕。把npos的判断、长度的预检查、解析后的尾部校验这三件事固定成习惯绝大多数「偶尔崩溃」的字符串 bug 都能提前消掉。下一篇会讲格式化输出、正则表达式和编码转换这些内容依赖的标准版本更新使用前需要先确认工具链的支持情况。