std::string 查找、截取与转换:find 返回 npos 不处理,线上直接崩
摘要
std::string 的 find 返回 npos 忘记判断、substr 越界抛异常、stoi 遇到非数字输入直接崩溃——这三个问题在生产环境里都出过事故。本文用真实场景拆解 find/rfind/substr/compare 的正确用法,讲清 stoi/to_string 的异常边界,并给出 trim/split/replace_all 的可复用实现。每个 API 配可运行代码和输出,看完就能用。
一、为什么这三个 API 最容易出事?
先看一段真实出事的代码:
// 从配置行中提取 key 和 value,格式 "key=value"std::pair<std::string,std::string>parseConfig(conststd::string&line){size_t pos=line.find('=');std::string key=line.substr(0,pos);// pos == npos 时?std::string val=line.substr(pos+1);// npos + 1 溢出?return{key,val};}如果传入的line里没有=,find返回std::string::npos。此时:
line.substr(0, npos)会截取到末尾,不报错line.substr(npos + 1)中npos + 1变成 0(无符号整数回绕),实际从开头截取
第二个 bug 极隐蔽。程序不崩,但逻辑完全错了。如果换成line.at(pos),就会直接抛std::out_of_range。
一句话记住:find的返回值必须判断 npos,不能直接参与下标运算。
二、find 家族:六个函数,一张表说清
std::string提供了六个查找函数,很多人只用过find,遇到复杂场景就手写循环。
| 函数 | 查找内容 | 搜索方向 | 未找到返回 |
|---|---|---|---|
find | 子串/字符 | 从头到尾 | npos |
rfind | 子串/字符 | 从尾到头 | npos |
find_first_of | 字符集合中任意一个 | 从头到尾 | npos |
find_last_of | 字符集合中任意一个 | 从尾到头 | npos |
find_first_not_of | 不在字符集合中的字符 | 从头到尾 | npos |
find_last_not_of | 不在字符集合中的字符 | 从尾到头 | npos |
#include<string>#include<iostream>intmain(){std::string url="https://example.com:8080/path?q=1";size_t scheme_end=url.find("://");// 5size_t host_end=url.find(':',8);// 19(从 8 开始找)size_t path_start=url.find('/');// 7(注意:找的是 :// 里的 /)size_t path_start2=url.find('/',8);// 24(从 8 之后找)size_t last_dot=url.rfind('.');// 15(最后一个 '.' 在 example.com 里)// find_first_of:找第一个出现的任意分隔符std::string nums="one,two;three four";size_t sep=nums.find_first_of(",; ");// 3(逗号)std::cout<<"scheme_end="<<scheme_end<<", host_end="<<host_end<<", last_dot="<<last_dot<<", sep="<<sep<<std::endl;// 输出:scheme_end=5, host_end=19, last_dot=15, sep=3return0;}find_first_of vs find 的关键区别:
std::string s="hello world";std::cout<<s.find("world")<<std::endl;// 6(找整个子串 "world")std::cout<<s.find_first_of("world")<<std::endl;// 1(找 'o',因为 "world" 中第一个在 s 里出现的字符是 'o')find_first_of("world")找的是字符集合{'w','o','r','l','d'}中任意一个字符在s中首次出现的位置。s[1]是'e',不在集合里;s[2]是'l',在集合里,所以返回 2?不,s[0]='h'不在,s[1]='e'不在,s[2]='l'在——返回2。实际运行后你会发现输出是2,不是 1。'o'在s[4],'l'在s[2]先出现。
一句话记住:find找子串,find_first_of找“字符集合里的任意一个”。
三、substr:截取子串,越界会抛异常
substr(pos, len)从pos开始截取len个字符,返回新的std::string:
std::string s="hello world";std::cout<<s.substr(0,5)<<std::endl;// "hello"std::cout<<s.substr(6)<<std::endl;// "world"(len 省略,取到末尾)std::cout<<s.substr(6,100)<<std::endl;// "world"(len 超长,自动截到末尾)关键陷阱:substr的pos参数如果> size(),会抛出std::out_of_range:
std::string s="hello";try{s.substr(10);// pos = 10 > size() = 5}catch(conststd::out_of_range&e){std::cout<<"异常:"<<e.what()<<std::endl;}但pos == size()是合法的,返回空字符串:
std::cout<<s.substr(s.size())<<std::endl;// "",不抛异常安全截取模式:
std::stringsafeSubstr(conststd::string&s,size_t pos,size_t len=std::string::npos){if(pos>s.size())return"";returns.substr(pos,len);}一句话记住:substr的pos可以等于size()(返回空串),但不能大于size()(抛异常)。
四、compare:返回值不是“是否相等”
compare返回一个整数,语义是字典序比较:
| 返回值 | 含义 |
|---|---|
< 0 | *this在str之前 |
0 | 两者等价 |
> 0 | *this在str之后 |
std::string a="apple";std::string b="banana";if(a.compare(b)<0){std::cout<<a<<" 排在 "<<b<<" 前面"<<std::endl;}最常见的误用:用compare的返回值判断相等:
// ❌ 危险:compare 返回的是非零值,不是 1if(s.compare("hello")==1){...}// 错!"hello" 相等时返回 0,不相等时可能返回任意负数/正数// ✅ 正确if(s.compare("hello")==0){...}// 或者直接用 ==if(s=="hello"){...}Clang-Tidy 有一个专门的检查项misc-string-compare,就是为了抓这种误用。
什么时候该用compare?当需要排序或判断字典序先后时。日常判等直接用==。
C++20 新增的便捷函数:
std::string s="hello world";// C++20if(s.starts_with("hello")){...}// 前缀检查if(s.ends_with("world")){...}// 后缀检查// C++23if(s.contains("lo wo")){...}// 子串存在性在 C++20 之前,前缀/后缀检查需要手写:
// C++17 写法boolstartsWith(conststd::string&s,conststd::string&prefix){returns.size()>=prefix.size()&&s.compare(0,prefix.size(),prefix)==0;}一句话记住:compare == 0才是相等,判断前缀/后缀优先用 C++20 的starts_with/ends_with。
五、stoi / to_string:异常边界必须处理
5.1 to_string:数值转字符串
std::string s1=std::to_string(42);// "42"std::string s2=std::to_string(3.14);// "3.140000"std::string s3=std::to_string(true);// "1"to_string对浮点数的默认精度是 6 位,不够灵活。需要控制精度时用std::ostringstream:
#include<sstream>#include<iomanip>std::ostringstream oss;oss<<std::fixed<<std::setprecision(2)<<3.14159;std::string s=oss.str();// "3.14"5.2 stoi:字符串转整数,三个异常
std::stoi(str, &pos, base)有三个参数,但日常最常忽略的是异常处理:
#include<string>#include<iostream>intmain(){// 正常情况intv1=std::stoi("42");// 42intv2=std::stoi("1010",nullptr,2);// 10(二进制)// 三种异常try{std::stoi("abc");// std::invalid_argument}catch(conststd::invalid_argument&e){std::cout<<"非数字输入"<<std::endl;}try{std::stoi("99999999999999999999");// std::out_of_range}catch(conststd::out_of_range&e){std::cout<<"超出 int 范围"<<std::endl;}// 部分解析:pos 告诉你解析到哪了size_t pos=0;intv3=std::stoi("42abc",&pos);// v3 = 42, pos = 2std::cout<<"解析了 "<<pos<<" 个字符"<<std::endl;// "2"return0;}关键理解:stoi遇到非数字字符时不一定抛异常。"42abc"会成功解析出42,pos设为 2。只有第一个非空白字符就不是数字时,才抛invalid_argument。
std::stoi(" 42");// 42(跳过前导空白)std::stoi("42abc");// 42, pos=2(部分解析)std::stoi("abc42");// 抛 invalid_argument安全封装:
std::optional<int>safeStoi(conststd::string&s){try{size_t pos=0;intv=std::stoi(s,&pos);if(pos!=s.size())returnstd::nullopt;// 要求完整解析returnv;}catch(...){returnstd::nullopt;}}一句话记住:stoi对"42abc"返回 42 不报错,要完整解析必须检查pos == s.size()。
六、实战:trim / split / replace_all
标准库没有提供这三个最常用的字符串工具,自己写一遍:
6.1 trim
std::stringtrim(conststd::string&s){constchar*ws=" \t\n\r\f\v";size_t start=s.find_first_not_of(ws);if(start==std::string::npos)return"";size_t end=s.find_last_not_of(ws);returns.substr(start,end-start+1);}6.2 split
std::vector<std::string>split(conststd::string&s,chardelim){std::vector<std::string>result;std::stringstreamss(s);std::string item;while(std::getline(ss,item,delim)){result.push_back(item);}returnresult;}6.3 replace_all
std::stringreplace_all(std::string s,conststd::string&from,conststd::string&to){if(from.empty())returns;size_t pos=0;while((pos=s.find(from,pos))!=std::string::npos){s.replace(pos,from.size(),to);pos+=to.size();}returns;}一个易忽略的点:replace_all的pos += to.size()不能写成pos += from.size()。如果to比from短,会出现死循环。
七、高频面试题速答
Q1:find没找到返回什么?
std::string::npos,一个size_t类型的最大值常量。必须判断,不能直接参与下标运算。
Q2:substr(pos)中pos > size()会怎样?
抛出std::out_of_range。pos == size()合法,返回空字符串。
Q3:stoi("42abc")会抛异常吗?
不会。返回42,pos参数设为2。只有第一个非空白字符不是数字时才抛invalid_argument。
Q4:compare返回 1 表示什么?
表示*this字典序在参数之后,不是“相等”。判等用compare(...) == 0或==。
Q5:C++20 有什么新函数替代手写前缀检查?
starts_with()和ends_with()。C++23 增加了contains()。
八、总结
| API | 核心要点 | 常见坑 |
|---|---|---|
find | 返回npos表示未找到 | 不判断 npos 直接用 |
find_first_of | 找字符集合任意一个 | 误以为找子串 |
substr | pos > size()抛异常 | npos + 1回绕 |
compare | 返回<0 / 0 / >0 | 用== 1判等 |
stoi | 部分解析不抛异常 | 不检查pos |
to_string | 浮点默认 6 位精度 | 精度不可控 |
下一篇进入本系列的重头戏:SSO、COW 与 std::string 的真实内存布局。会用代码打印地址,验证 libstdc++、libc++、MSVC 三家的 SSO 容量差异,并解释为什么 C++11 之后 COW 实现被“逼退”了。
系列导航:
- 上一篇:《std::string 访问与修改:operator[]、at、data 与迭代器失效》
- 下一篇:《std::string 底层实现:SSO、COW 与内存布局》
评论区互动:你被find返回npos坑过吗?是在哪个场景下踩的?评论区说说,我看看能不能出一个“npos 避坑合集”。