在 C++ 中,如果我们想检查某个字符或单词在字符串中是否存在,或者获取它具体出现在哪个下标位置,可以使用 std::string 提供的 find() 和 rfind() 成员函数。
从左往右查找:find()
对于 std::string 来说,find() 是最常用的字符串查找函数,既可以从头查找目标第一次出现的位置,也可以从指定下标继续向后查找。
1. 查找首次出现的位置
默认情况下,find() 会从字符串的开头(下标 0)开始从左往右搜索目标内容,并返回目标第一次出现时的起始下标。
语法:
str.find(target)说明:
参数 target 既可以是一段子字符串,比如 "C++",也可以是单个 char 字符,比如 '='。调用 find() 后的返回值主要有以下 2 种情况:
- 查找成功:返回目标内容第一次出现时的起始下标,下标从 0 开始计算,返回值类型为无符号整数
std::size_t。 - 查找失败:返回特殊常量
std::string::npos,表示没有找到目标。
可能有些小伙伴会问:为什么没找到时不返回 -1 呢?因为跟前面求长度的 size() 函数一样,find() 返回的 std::size_t 是无符号整数,只能保存 0 和正整数,无法直接表示负数。
因此,标准库专门定义了常量 std::string::npos 来代表查找失败。其中 npos 是 “no position(没有位置)” 的缩写,我们平时直接拿它作为 “没找到” 的判断标志即可。
示例 1:查找子字符串的位置
#include <iostream>
#include <string>
int main() {
std::string text = "Learn C++ Programming";
std::size_t pos = text.find("C++");
std::cout << pos << '\n';
return 0;
}运行结果如下。
6分析:
在 "Learn C++ Programming" 中,开头的 "Learn " 连同空格共占 6 个字符,字符 'C' 位于下标 6,所以 text.find("C++") 返回 6。如果目标在字符串里出现了多次,find() 默认只返回第一次出现的位置。
在实际写代码时,我们应该怎么判断目标到底有没有找到呢?标准做法是配合 if 语句,把返回值与 std::string::npos 进行比较,请看下面例子。
示例 2:判断是否查找成功
#include <iostream>
#include <string>
int main() {
std::string text = "lvyenet";
std::size_t first = text.find("lvye");
std::size_t second = text.find("Python");
if (first != std::string::npos) {
std::cout << "找到 lvye,位置为:" << first << '\n';
}
if (second == std::string::npos) {
std::cout << "未找到 Python" << '\n';
}
return 0;
}运行结果如下。
找到 lvye,位置为:0
未找到 Python分析:
在这个例子中,"lvye" 就在字符串最开头,因此 first 的值为 0,满足 first != std::string::npos。而 "Python" 并不存在,second 等于 std::string::npos。
这里有一个初学者极易踩的坑:大家千万不要把 find() 的返回值直接放进 if 条件括号里当成布尔值判断:
std::size_t pos = text.find("lvye");
// 正确方式
if (pos != std::string::npos) {
std::cout << "找到了" << '\n';
}
// 错误方式
if (pos) {
std::cout << "找到了" << '\n';
}为什么 if (pos) 是错的呢?因为在 C++ 中,整数 0 当作条件判断时会被视为 false,而非 0 整数全都会被视为 true。
当要找的词恰好位于字符串最开头的下标 0 处时,if (0) 会误判为 “没找到”。反过来,当真正没找到时,std::string::npos 在底层是一个非 0 的极大正整数,if (pos) 反而会误判为 “找到了”,结果完全反过来了。
2. 从指定位置开始查找
如果目标内容在字符串中出现了多次,我们还可以给 find() 传入第 2 个参数 pos,跳过前面已经找到的部分,从指定下标继续往后查找。
语法:
str.find(target, pos)说明:
第 2 个参数 pos 用来指定从哪个下标开始往后找。需要注意,如果最终找到了,返回的位置依然是从字符串开头 0 算起的真实下标,并不会从 pos 重新编号。
示例 3:从指定位置向后查找
#include <iostream>
#include <string>
int main() {
std::string text = "cat-dog-cat-dog";
std::size_t first = text.find("dog");
if (first != std::string::npos) {
std::size_t second = text.find("dog", first + 1);
std::cout << "第一次出现位置:" << first << '\n';
std::cout << "第二次出现位置:" << second << '\n';
}
return 0;
}运行结果如下。
第一次出现位置:4
第二次出现位置:12分析:
上面代码中,第一次调用 text.find("dog") 从头查找,得到下标 4。第二次调用 text.find("dog", first + 1) 从下标 5 继续往后找,跳过了前一个 "dog",拿到后一个 "dog" 的下标 12。
如果目标出现了不止两次,我们还可以把 find(target, pos) 放进循环里反复推进查找起点,从而找出目标出现的所有位置。
从右往左查找:rfind()
find() 是从左往右查找,返回目标第一次出现的位置。如果我们需要查找目标 “最后一次” 出现的位置,比如获取文件名 "index.min.js" 中最后一个点号 '.' 的位置,就可以使用 rfind() 函数。
语法:
str.rfind(target)说明:
其中,rfind 是 “reverse find(反向查找)” 的缩写。rfind() 会从字符串的末尾开始从右往左寻找目标内容:如果找到了,返回目标最后一次出现时的起始下标;如果没找到,同样返回 std::string::npos。
示例 4:对比 find() 与 rfind()
#include <iostream>
#include <string>
int main() {
std::string filename = "index.min.js";
std::size_t firstDot = filename.find('.');
std::size_t lastDot = filename.rfind('.');
std::cout << "第一个点号位置:" << firstDot << '\n';
std::cout << "最后一个点号位置:" << lastDot << '\n';
return 0;
}运行结果如下。
第一个点号位置:5
最后一个点号位置:9分析:
在这个例子中,"index.min.js" 一共有两个点号。filename.find('.') 从左往右找,返回第一个点号的下标 5。而 filename.rfind('.') 从右往左找,直接定位到最后一个点号的下标 9。
这里小伙伴们要注意,虽然 rfind() 的寻找方向是从右往左,但它返回的下标依然是从字符串左端按 0、1、2 依次编号的,并不是从末尾倒着数。
结合 find() 和 substr() 截取
find() 和 rfind() 本身只负责告诉我们目标在哪里,并不会自动把内容提取出来。在实际开发中,最常见的配合方式是:先调用 find() 找到分隔符号的下标,再传给 substr() 函数截取出所需内容。
示例 5:拆分配置项的键值对
#include <iostream>
#include <string>
int main() {
std::string config = "site=lvyenet";
std::size_t pos = config.find('=');
if (pos != std::string::npos) {
std::string key = config.substr(0, pos);
std::string value = config.substr(pos + 1);
std::cout << "key = " << key << '\n';
std::cout << "value = " << value << '\n';
}
return 0;
}运行结果如下。
key = site
value = lvyenet分析:
在这个例子中,config.find('=') 先找到等号的下标 pos 为 4。等号左边的字符个数正好等于等号的下标值,所以调用 config.substr(0, pos) 就得到了 "site"。
而等号右边的部分从 pos + 1 开始一直延伸到末尾,直接调用 config.substr(pos + 1) 就拿到了 "lvyenet"。
同理,在前面的示例 4 中找到最后一个点号的位置 lastDot 之后,只需要调用 filename.substr(lastDot),就能把文件后缀名 ".js" 提取出来。
starts_with() 与 ends_with()
如果我们不需要知道子串的具体下标位置,只是想简单判断字符串是不是以某段前缀开头,或者是不是以某段后缀结尾,从 C++20 标准开始,可以直接调用 starts_with() 和 ends_with() 函数:
starts_with(前缀):判断字符串是否以指定前缀开头,比如 url.starts_with("https://"),返回 true 或 false。
ends_with(后缀):判断字符串是否以指定后缀结尾,比如 file.ends_with(".png"),返回 true 或 false。
