OI-Wiki 字符串标准库函数全解析:C 字符数组函数与 C++ std::string 常用操作速查
【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. (某大型游戏线上攻略,内含炫酷算术魔法)项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki
导读
在 OI / ICPC 竞赛与日常算法开发中,字符串处理是最基础也最高频的操作之一。本篇文章以 OI-Wiki 的 lib-func.md 为主体,系统梳理 C 标准库(<string.h>/<cstring>)中操作字符数组的核心函数,以及 C++ 标准库std::string的常用成员函数,并结合仓库内 string 类文档、字符串基础定义 与 哈希示例源码 进行原理级扩充。读完本文,你将能够:快速查阅每个字符串函数的签名、行为边界与复杂度;避开strlen进循环、size_t与负数比较、strcmp返回值误解等经典坑;在竞赛代码中正确选择字符数组或std::string完成读入、拼接、查找、截取、替换与删除。
一、两种字符串表示:字符数组与 string 类
在进入函数细节之前,先明确本文操作的两类对象。OI-Wiki 的 字符串基础篇 给出了规范定义:
- 字符串是将 $n$ 个字符顺次排列形成的序列,$n$ 称为字符串长度;
- 字符串的存储方式有三种:用
char数组存储并以空字符'\0'结尾(C 风格字符串)、使用 C++ 标准库std::string类、以及用双引号括起来的字符串字面量。
C 标准库函数面向的是字符数组char[]/const char*;C++ 标准库std::string面向的是字符串对象,同时向下兼容字符数组。两者互补,竞赛中经常混用(例如用s.c_str()把std::string交给printf输出)。
二、C 标准库:字符数组操作函数详解
C 标准库操作字符数组char[]/const char*,对应头文件为 C 语言的<string.h>或 C++ 中的<cstring>。以下是 OI-Wiki 列出的全部常用函数。
2.1 格式化输入输出:printf / scanf / sprintf / sscanf
printf("%s", s):用%s输出一个字符串(字符数组)。注意传入的是数组首地址,输出到第一个'\0'为止。scanf("%s", &s):用%s读入一个字符串(字符数组)。scanf以空白字符(空格、换行、制表符)为分隔符,因此它无法读入含空格的字符串;若需要读入一行含空格的文本,应改用gets或 C++ 的std::getline。仓库中大量示例代码采用std::cin读入后再处理,例如 prefix-sum_1.cpp 中std::cin >> a[i]的常规用法。sscanf(const char *__source, const char *__format, ...):从字符串__source中按格式读取变量。例如sscanf(str, "%d", &a)会把字符串str解析出的整数写入a。这在解析"数字以字符串形式给出"的题目时非常实用。sprintf(char *__stream, const char *__format, ...):将格式化内容输出到字符数组__stream中。例如sprintf(str, "%d", i)把整数i转成字符串存入str。使用时要确保目标数组容量足够,否则会产生缓冲区溢出;C11 提供了更安全的snprintf可指定写入上限。
2.2 长度与比较:strlen / strcmp
strlen(const char *str):返回从str[0]开始直到'\0'为止的字符数。关键注意点:未开启 O2 优化时,如果把它写在循环条件中(如for (int i = 0; i < strlen(s); ++i)),每次迭代都会重新调用strlen扫描整串,使整体复杂度退化为 $\Theta(N^2)$;而strlen本身的复杂度是 $\Theta(N)$,因此应提前将长度存入变量。OI-Wiki 在 hash_1.cpp 中正是先int len = strlen(str);再进入循环,就是这一原则的实践。strcmp(const char *str1, const char *str2):按照字典序比较两个字符串:若str1字典序更小返回负值,两者相同返回0,str1更大则返回正值。常见误区:不要认为返回值只有0、1、-1三种——不同平台下的实现返回值都遵循"正负零"语义,但具体数值未必是1或-1。因此正确用法是只判断< 0、== 0、> 0,而不是与1、-1直接比较。
字典序的定义参见 basic.md:以第 $i$ 个字符作为第 $i$ 关键字进行大小比较,且空字符小于字符集内任何字符(即
a < aa)。strcmp正是按此规则比较的。
2.3 复制:strcpy / strncpy
strcpy(char *str, const char *src):把src中的字符(含结尾的空终止符'\0')复制到str中,str与src均为字符数组头指针,返回值为str。安全提示:strcpy不检查目标数组容量,目标空间不足时会越界写;竞赛数据规模大或字符串长时尤其要注意预分配足够大的数组。strncpy(char *str, const char *src, int cnt):至多复制cnt个字符到str中。若src提前终止而数量未达cnt,则继续向str写入空字符,直到总共写入cnt个字符。注意:当src长度恰好不小于cnt时,strncpy不会自动补'\0',此时str可能不是以空字符结尾的合法 C 字符串,使用printf("%s")前需手动在str[cnt]处置'\0'。
2.4 拼接与查找:strcat / strstr / strchr / strrchr
strcat(char *str1, const char *str2):把str2接到str1的结尾,用str2的首字符替换掉str1末尾的'\0',返回str1。同样存在缓冲区溢出风险,且要求str1有足够剩余空间。strstr(char *str1, const char *str2):若str2是str1的子串,返回str2在str1中首次出现位置的地址;否则返回NULL。常用于判断包含关系与定位子串起点。strchr(const char *str, int c):返回字符c在字符串str中第一次出现的位置地址;找不到则返回NULL。strrchr(const char *str, int c):返回字符c在字符串str中最后一次出现的位置地址;找不到则返回NULL。
实现提示:从源码结构看,这些函数都依赖
'\0'作为终止标记,因此传入的字符数组必须保证以空字符结尾,否则会越界扫描。
2.5 C 函数速查总表
| 函数 | 功能 | 返回值 | 关键注意点 |
|---|---|---|---|
printf("%s", s) | 输出字符串 | 输出字符数 | 以'\0'为结尾 |
scanf("%s", &s) | 读入字符串 | 成功读入项数 | 遇空白停止,不能读含空格串 |
sscanf(src, fmt, ...) | 从字符串解析变量 | 成功解析项数 | 与fscanf行为一致,数据源为内存字符串 |
sprintf(buf, fmt, ...) | 格式化写入字符数组 | 写入字符数 | 注意缓冲区容量 |
strlen(s) | 求长度 | size_t | 循环条件中反复调用会退化为 $\Theta(N^2)$ |
strcmp(a, b) | 字典序比较 | 负/零/正 | 返回值不一定是±1,只判断符号 |
strcpy(dst, src) | 复制(含'\0') | dst | 目标数组需足够大 |
strncpy(dst, src, cnt) | 至多复制cnt个 | dst | src不够长时补'\0';够长时不自动补 |
strcat(a, b) | 把b接到a尾 | a | 空间需足够 |
strstr(a, b) | 子串首次出现地址 | 地址或NULL | 判断子串包含关系 |
strchr(s, c) | 字符首次出现地址 | 地址或NULL | 按单字符查找 |
strrchr(s, c) | 字符最后出现地址 | 地址或NULL | 从尾部向前找 |
三、C++ 标准库:std::string 对象操作详解
C++ 标准库操作字符串对象std::string(定义于<string>头文件,本质是std::basic_string<char>的别名),同时也提供对字符数组的兼容。本节结合 string 类文档 中的示例与输出逐项展开。
3.1 拼接、赋值与访问
- 重载加法运算符
+:当+两边是string/char/char[]/const char*类型时,将两个变量连接,返回拼接后的新字符串。这是std::string相比字符数组最大的便利——C 风格拼接必须手动保证空间并调用strcat。 - 赋值运算符
=:右侧可以是const string/string/const char*/char*。string支持动态分配空间,因此无需像字符数组那样预知字符串长度;对应代价是std::cin输入string的速度比scanf慢,可用cin.tie(nullptr)->sync_with_stdio(false)加速(见 hash_1.cpp)。 - 访问运算符
[cur]:返回cur位置的引用,因此可以读写,如s[0] = 'A'。 - 访问函数
data()/c_str():返回一个指向相同内容的const char*指针。C++11 之前c_str()保证末尾有空字符而data()不保证;C++11 起两者行为一致。典型用法是交给printf输出:
printf("%s", s); // 不能保证编译通过,行为未定义 printf("%s", s.data()); // C++11 前行为未定义,C++11 后正确输出 printf("%s", s.c_str()); // 一定能够正确输出3.2 容量与长度:size / length
- 容量函数
size():返回字符串的字符个数;同义的还有length()。
printf("s 的长度为 %zu", s.size()); printf("s 的长度为 %zu", s.length()); printf("s 的长度为 %zu", strlen(s.c_str()));- 复杂度:
strlen(s.c_str())一定是与字符串长度线性相关的;size()和length()的复杂度在 C++98 标准中未指定,在C++11 中被指定为常数复杂度,且在常见编译器上即使是 C++98 也是常数。 - 类型警告:这三个函数(以及后文
find)的返回值类型都是size_t(unsigned long)。size_t是无符号类型,不支持直接与负数比较或运算,需要时请显式强制转换(如(int)s.find('a'))。
3.3 查找:find / rfind
find(ch, start = 0):查找并返回从start开始(含start)第一次出现字符/字符串ch的位置(从0计数);找不到时返回string::npos(定义为-1,但类型仍是size_t)。注意原文档指出"返回-1"指的是npos的取值,直接以size_t打印会得到巨大的无符号数。rfind(ch):从末尾向前查找,返回第一个找到的位置(从0计数);找不到同样返回string::npos。
示例与输出(来自 string.md):
string s = "OI Wiki", t = "OI", u = "i"; int pos = 5; printf("字符 I 在 s 的 %lu 位置第一次出现\n", s.find('I')); printf("字符 a 在 s 的 %lu 位置第一次出现\n", s.find('a')); printf("字符 a 在 s 的 %d 位置第一次出现\n", s.find('a')); printf("字符串 t 在 s 的 %lu 位置第一次出现\n", s.find(t)); printf("在 s 中自 pos 位置起字符串 u 第一次出现在 %lu 位置", s.find(u, pos));字符 I 在 s 的 1 位置第一次出现 字符 a 在 s 的 18446744073709551615 位置第一次出现 // 即为 size_t(-1),具体数值与平台有关 字符 a 在 s 的 -1 位置第一次出现 // 强制转换为 int 类型则正常输出 -1 字符串 t 在 s 的 0 位置第一次出现 在 s 中自 pos 位置起字符串 u 第一次出现在 6 位置实战建议:判断是否存在时写
if (s.find(x) != string::npos),而不是if (s.find(x) != -1),后者在无符号比较下会产生与平台相关的怪异行为。
3.4 截取子串:substr
substr(start, len):从start(从0计数)开始截取长度为len的子串;缺省len时截取到字符串末尾;若从start开始的后缀长度不足len,则截取整个后缀(不会越界报错)。返回类型是新的string,可以用c_str()交给printf。
示例与输出:
string s = "OI Wiki", t = "OI"; printf("从字符串 s 的第四位开始的最多三个字符构成的子串是 %s\n", s.substr(3, 3).c_str()); printf("从字符串 t 的第二位开始的最多三个字符构成的子串是 %s", t.substr(1, 3).c_str());从字符串 s 的第四位开始的最多三个字符构成的子串是 Wik 从字符串 t 的第二位开始的最多三个字符构成的子串是 I3.5 追加:append
append(s):将字符串s整体添加到当前字符串末尾。append(s, pos, n):将字符串s中从pos开始的n个字符连接到当前字符串末尾。适用于只取源串一部分追加的场景。
3.6 替换:replace
replace(pos, n, s):删除从pos开始的n个字符,然后在pos处插入串s(替换总长度不受限制,结果可能变长或变短)。
示例与输出:
string s = "OI Wiki"; s.replace(2, 5, ""); printf("将字符串 s 的第 3~7 位替换为空串后得到的字符串是 %s\n", s.c_str()); s.replace(s.begin(), s.begin() + 2, "NOI"); printf("将字符串 s 的前两位替换为 NOI 后得到的字符串是 %s", s.c_str());将字符串 s 的第 3~7 位替换为空串后得到的字符串是 OI 将字符串 s 的前两位替换为 NOI 后得到的字符串是 NOI除
replace(pos, count, str)外,std::string还提供迭代器版本replace(first, last, str),用[first, last)区间(first含、last不含)指定被替换范围,上例第二句即使用了迭代器形式。迭代器版本配合begin() + k可以精确表达"删除前 k 个字符"这类需求。
3.7 删除:erase
erase(pos, n):删除从pos开始的n个字符;缺省n时删除pos位置及其之后的所有字符(等价于"截断")。
示例与输出:
string s = "OI Wiki", t = " Wiki"; char u = '!'; s.erase(2); printf("从字符串 s 的第三位开始删去所有字符后得到的字符串是 %s\n", s.c_str()); s.insert(2, t); printf("在字符串 s 的第三位处插入字符串 t 后得到的字符串是 %s\n", s.c_str()); s.insert(7, 3, u); printf("在字符串 s 的第八位处连续插入 3 次字符串 u 后得到的字符串是 %s", s.c_str());从字符串 s 的第三位开始删去所有字符后得到的字符串是 OI 在字符串 s 的第三位处插入字符串 t 后得到的字符串是 OI Wiki 在字符串 s 的第八位处连续插入 3 次字符串 u 后得到的字符串是 OI Wiki!!!3.8 插入:insert
insert(pos, s):在pos位置插入字符串s。- 更通用的形式
insert(index, count, ch):在index处连续插入count次字符ch(如上例的s.insert(7, 3, u)即在第 8 位连续插入 3 个'!')。注意index从0计数,插入位置即"下标为 index 的字符之前"。
3.9 比较运算:重载的关系运算符
std::string重载了全部比较逻辑运算符(<、>、<=、>=、==、!=),按字典序比较,复杂度为 $\Theta(N)$。这使得string可以直接交给std::sort对若干字符串按字典序排序,也可以直接用于std::map/std::set的键。
四、std::string 成员速查总表
| 操作 | 形式 | 行为 | 注意点 |
|---|---|---|---|
| 拼接 | s1 + s2 | 连接后返回新string | 两侧可为string/char/char[]/const char* |
| 赋值 | s = x | 右侧可为string/const char*/char* | 动态扩容 |
| 下标访问 | s[i] | 返回i位置引用 | 从0计数,可读写 |
| 转 C 指针 | s.c_str()/s.data() | 返回const char* | C++11 后两者一致 |
| 长度 | s.size()/s.length() | 字符个数,size_t | C++11 起常数复杂度 |
| 查找 | s.find(x, start = 0) | 从start起首次出现位置 | 找不到返回npos(即-1,size_t) |
| 反向查找 | s.rfind(x) | 从末尾起首次出现位置 | 找不到返回npos |
| 截取 | s.substr(start, len) | 返回子串 | 缺省len截到末尾;超长自动截断 |
| 追加 | s.append(t)/s.append(t, pos, n) | 末尾追加整串或片段 | 返回*this,可链式调用 |
| 替换 | s.replace(pos, n, t) | 删除[pos, pos+n)再插入t | 结果长度可变 |
| 删除 | s.erase(pos, n) | 删除[pos, pos+n) | 缺省n删到末尾 |
| 插入 | s.insert(pos, t)/s.insert(pos, cnt, ch) | 在pos处插入串或重复字符 | 从0计数 |
| 比较 | <>==等 | 字典序比较 | 复杂度 $\Theta(N)$,可直接排序 |
五、竞赛实战要点与常见误区
5.1 字符数组 vs std::string 的选择
- 追求极致速度、需要自行管理内存的底层场景,用
char[]+ C 库函数; - 追求开发效率、字符串长度不确定或需要频繁拼接/查找的场景,用
std::string; - 两者可以无缝衔接:
s.c_str()把string交给 C 函数,string(str)把字符数组包装成string。仓库示例 hash_1.cpp 同时使用了两者——用char s[L]存原始串、strlen取长度,同时把哈希值存进vector<int>,正是"C 速度 + C++ 便利"的典型组合。
5.2 六个最容易踩的坑
strlen写进循环条件:未开 O2 时每次迭代 $\Theta(N)$ 扫描,整体退化为 $\Theta(N^2)$。务必int len = strlen(s);先存变量。strcmp返回值当±1用:标准只保证"负/零/正"三态,不同平台返回的具体数值不同,只与0比较符号。size_t与负数比较:find、size返回size_t,与-1比较或用%d打印都会出错。判空用npos,打印需强转int。strcpy/strcat越界:C 函数不检查容量,目标数组需预先分配足够空间。strncpy不自动补'\0':当源串长度不小于cnt时,结果串可能没有空字符结尾。scanf("%s")读不了空格:遇空白即停止;需要读整行时改用std::getline或逐字符读入。
5.3 复杂度速记
strlen、strcmp、strcat、strstr朴素实现均为线性或线性相关($O(N)$ 或 $O(NM)$,随实现而异);std::string的size()/length():C++11 起常数复杂度;find/rfind:实现为线性扫描,$O(N)$;substr、replace、erase、insert:需复制受影响区间,线性复杂度,频繁操作长串时注意常数开销;std::string的关系运算符比较:$\Theta(N)$。
六、延伸阅读
- 更完整的字符串术语体系(字符集、子串、后缀、前缀、字典序、回文串、汉明距离)见 docs/string/basic.md;
std::string的设计动机、动态扩容与data()/c_str()在 C++11 前后的差异详解见 docs/lang/csl/string.md;- 基于字符数组与哈希实现的完整竞赛示例见 docs/string/code/hash/hash_1.cpp;
- 后续进阶算法(KMP、AC 自动机、后缀数组、后缀自动机等)可继续阅读 docs/string 目录下的对应文档。
【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. (某大型游戏线上攻略,内含炫酷算术魔法)项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考