C++ 字符串查找

在 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。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号