☰
C/C++字符串查找函数全面了解
2026/10/26 11:35:23 网站建设 项目流程

前言


字符串查找是日常代码里出现频率很高的操作,但 C 和 C++ 提供的接口分属两套体系:C 标准库以「返回指针」为主,C++ 的std::string以「返回下标」为主,泛型算法又提供了「返回迭代器」的第三种风格。混用这三套接口,最容易在「没找到怎么表示」这一点上出错。


常见的误解有三个。第一,以为strstr和std::string::find只是参数类型不同——前者返回指针,找不到返回空指针;后者返回下标,找不到返回npos。第二,把std::string::find的返回值存进int,在 64 位平台上可能截断,而且和npos比较时会出现符号问题。第三,以为find_first_of是「查找子串」——它查找的是「任意一个字符首次出现的位置」,和find的语义完全不同。


本文先列 C 风格函数,再讲std::string的六个查找成员,然后补充std::search与std::string_view,最后讨论大小写不敏感查找、嵌入空字符等边界,并给出常见错误对照。


一、C 风格查找函数


C 的字符串是以空字符结尾的字符数组,查找函数都在<string.h>里,参数和返回值都是指针。




函数作用找不到时



strchr(s, c)查找字符 c 第一次出现返回NULL

strrchr(s, c)查找字符 c 最后一次出现返回NULL

strstr(s, sub)查找子串 sub 第一次出现返回NULL

strpbrk(s, accept)查找 s 中第一个属于 accept 集合的字符返回NULL

strspn(s, accept)s 开头连续只由 accept 中字符组成的长度返回长度

strcspn(s, reject)s 开头连续不含 reject 中字符的长度返回长度

memchr(p, c, n)在前 n 个字节中查找字节 c返回NULL



strchr在查找'\0'时会返回指向结尾空字符的指针,因为空字符也算字符串的一部分。strstr的 needle 为空串时返回 haystack 本身,这是标准规定的行为。


/* 适用于 C11 */
#include <stdio.h>
#include <string.h>

void cDemo(void) {
const char* text = "error: file not found";
const char* sub = strstr(text, "not");
if (sub != NULL) {
printf("offset = %ld\n", (long)(sub - text));
}

const char* colon = strchr(text, ':');
if (colon != NULL) {
printf("colon at %ld\n", (long)(colon - text));
}

const char* any = strpbrk(text, ":#");
(void)any;
}

指针相减得到的是元素个数(ptrdiff_t),不是字节数;对char*来说两者相同,换成宽字符就不一样了。printf的%ld配合(long)转换是为了避免ptrdiff_t在不同平台上的格式差异。


这些函数都要求输入以空字符结尾。如果手里是一个「指针 + 长度」的缓冲区,中间可能含有'\0',strstr会在第一个空字符处停下,找不到后面的内容。这种场景要用memchr(按字节查找),或者转成std::string_view。


strtok虽然常用于分割,但它会修改原字符串(把分隔符替换成'\0'),且内部保存静态状态,不可重入、非线程安全。POSIX 提供strtok_r,Windows 提供strtok_s,两者都不是 C 标准的一部分。现代代码更推荐手动遍历或用 C++ 的字符串工具。


二、std::string 的查找成员


std::string提供六个查找函数,语义分两类:查找子串/字符,以及查找字符集合。




成员查找对象方向找不到



find子串或单个字符从 pos 向后npos

rfind子串或单个字符从 pos 向前npos

find_first_of集合中任意一个字符向后npos

find_last_of集合中任意一个字符向前npos

find_first_not_of不在集合中的字符向后npos

find_last_not_of不在集合中的字符向前npos



npos是std::string::npos,类型是size_type,值等于size_type(-1),即无符号类型的最大值。所有比较都必须和npos比,不能和-1比(虽然会隐式转换,但可读性差,也容易引出符号警告)。


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

void cppDemo() {
const std::string text = "error: file not found";

const std::size_t pos = text.find("not");
if (pos != std::string::npos) {
std::cout << "found at " << pos << '\n';
}

const std::size_t lastSpace = text.rfind(' ');
if (lastSpace != std::string::npos) {
std::cout << "last word: " << text.substr(lastSpace + 1) << '\n';
}

const std::size_t vowel = text.find_first_of("aeiou");
const std::size_t nonSpace = text.find_first_not_of(" ");
(void)vowel;
(void)nonSpace;
}

find和rfind的第二个参数是起始位置:find(str, pos)从pos开始向后找,rfind(str, pos)从pos开始向前找(包含pos)。默认pos是 0 或npos,具体取决于重载。


几个边界行为:



  • s.find("")返回pos(默认 0),空串在任何位置都匹配;

  • s.rfind("")返回s.size();

  • s.find("x", pos)中pos > s.size()时返回npos,不抛异常;

  • s.at(pos)越界才抛std::out_of_range,find不抛。


find还有接受const char*、char、std::string_view(C++17 模板重载)的版本。传 C 字符串时,长度由strlen决定,所以无法查找包含'\0'的模式;要查找二进制内容,先把模式构造成std::string并带上长度。


find_first_of和find的区别必须记牢:


// 适用于 C++17
const std::string s = "hello world";
s.find("lo"); // 3:查找子串 "lo"
s.find_first_of("lo"); // 2:查找 'l' 或 'o' 中任意一个字符首次出现

find_first_of("lo")在s[2]处找到'l',而find("lo")在s[3]处找到连续的子串。名字里的of提示了「字符集合」的语义。


三、泛型算法与 string_view


<algorithm>里的std::search在任意迭代器区间上查找子序列,返回迭代器:


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

void searchDemo() {
const std::string haystack = "error: file not found";
const std::string needle = "file";

const auto it = std::search(haystack.begin(), haystack.end(),
needle.begin(), needle.end());
if (it != haystack.end()) {
const std::size_t pos =
static_cast<std::size_t>(it - haystack.begin());
(void)pos;
}
}

std::search返回的是迭代器,不是下标,需要自己做减法。它的优点是不依赖具体容器,数组、std::vector<char>、std::string都能用;缺点是写法比成员函数长。C++17 还提供了带执行策略的重载,以及std::boyer_moore_searcher等搜索器,适合在长文本中反复查找同一模式,具体接口和可用性以标准库文档为准。


std::string_view(C++17)提供与std::string相同的find系列成员,但不拥有字符,适合只读的切片:


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

void viewDemo() {
const std::string_view text = "error: file not found";
const std::size_t pos = text.find("file", 0);
const std::string_view word = text.substr(pos, 4); // "file"
(void)word;
}

string_view的substr返回的是视图,不复制字符;原字符串销毁后视图悬垂。用它做查找时,要保证底层缓冲区在视图使用期间有效。


std::string::find与std::string_view::find的返回类型分别是各自的size_type,但npos的值都是无符号最大值。混用时不要拿std::string::npos去和一个std::string_view的返回值做类型敏感的比较,统一用auto接收更省心。


四、大小写不敏感查找


标准库没有直接提供大小写不敏感的查找。可以用std::search加自定义比较:


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

bool icharEqual(char a, char b) {
const unsigned char ua = static_cast<unsigned char>(a);
const unsigned char ub = static_cast<unsigned char>(b);
return std::tolower(ua) == std::tolower(ub);
}

std::size_t findCaseInsensitive(const std::string& haystack,
const std::string& needle) {
const auto it = std::search(haystack.begin(), haystack.end(),
needle.begin(), needle.end(), icharEqual);
if (it == haystack.end()) {
return std::string::npos;
}
return static_cast<std::size_t>(it - haystack.begin());
}

std::tolower的参数必须是unsigned char能表示的值或EOF。直接把负的char传进去是未定义行为,所以要先转unsigned char。std::tolower的行为还受当前 C 区域设置(locale)影响,对非 ASCII 字符不保证按预期折叠大小写;真正的 Unicode 大小写匹配需要专门的库。


如果只是比较 ASCII,也可以手写(c >= 'A' && c <= 'Z') ? c + 32 : c,避免 locale 依赖。无论哪种写法,都要明确适用范围,不要把它当成通用的 Unicode 方案。


五、完整示例


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

std::size_t findCaseInsensitive(const std::string& haystack,
const std::string& needle) {
const auto it = std::search(
haystack.begin(), haystack.end(),
needle.begin(), needle.end(),
[](char a, char b) {
return std::tolower(static_cast<unsigned char>(a)) ==
std::tolower(static_cast<unsigned char>(b));
});
if (it == haystack.end()) {
return std::string::npos;
}
return static_cast<std::size_t>(it - haystack.begin());
}

int main() {
const std::string text = "Error: File Not Found";

const std::size_t exact = text.find("File");
const std::size_t loose = findCaseInsensitive(text, "file");
const std::size_t firstColon = text.find(':');
const std::size_t anyVowel = text.find_first_of("aeiou");
const std::size_t lastWord = text.rfind(' ');

std::cout << exact << '\n'; // 按成员函数语义应为 7
std::cout << loose << '\n'; // 大小写不敏感,应为 7
std::cout << firstColon << '\n'; // 应为 5
std::cout << anyVowel << '\n'; // 第一个元音在 "Error" 的 E? 注意大写不算
std::cout << lastWord << '\n';
return 0;
}

最后一行注释里提到一个细节:find_first_of("aeiou")只匹配小写元音,"Error"的E是大写,不会命中;第一个小写元音出现在"Error"的o(下标 4)。这正是大小写敏感查找的体现,不是 bug。


常见坑点


1. 把 find 的结果存进 int❌int pos = s.find("x");—— 超长字符串时截断,与npos比较也可能出问题。 ✅const auto pos = s.find("x");或std::string::size_type pos。


2. 用 >= 0 判断是否找到❌if (s.find("x") >= 0)—— 无符号返回值恒为真。 ✅if (s.find("x") != std::string::npos)。


3. 混淆 find 与 find_first_of❌ 用find_first_of("abc")查找子串"abc"。 ✅ 查找连续子串用find("abc");查找字符集合用find_first_of。


4. 对非空结尾缓冲区用 strstr❌ 对「指针 + 长度」且中间含'\0'的数据调用strstr。 ✅ 用memchr或std::string_view,按长度处理。


5. 以为 string::find 不能处理嵌入空字符❌ 用s.find("b")查找含'\0'的模式,长度被strlen截断。 ✅ 把模式构造成带长度的std::string或string_view再查找。


6. 用 strtok 分割后继续使用原字符串❌strtok把分隔符改成'\0',之后拿原串做其他操作用到的是被改过的内容。 ✅ 先复制一份,或改用不修改输入的分割方式。


7. 直接对 char 调用 tolower❌std::tolower(c),c 为负值时 UB。 ✅ 先转static_cast<unsigned char>(c)。


8. 忽略 rfind 的起始位置❌s.rfind("x", 0)以为从末尾找。 ✅ 第二个参数是「从哪个下标向前找」,默认是npos;从末尾找不要传 0。


总结




场景CC++



查找单个字符strchr/strrchrfind/rfind

查找子串strstrfind/rfind

查找字符集合strpbrkfind_first_of/find_last_of

查找不在集合中的字符组合strspn/strcspnfind_first_not_of

通用区间查找无std::search

找不到的表示NULLnpos

返回值指针下标或迭代器



查找函数的语义不复杂,出错几乎都集中在三类:找不到的表示(NULL还是npos)、下标的类型(有符号还是无符号)、以及「子串」和「字符集合」的混淆。把这三件事分清,再注意空字符结尾与大小写问题,C 和 C++ 两套接口都能用得比较稳。




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

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

立即咨询