好的,我们来对 C++ 中的字符串操作函数进行一次全面而详细的讲解。
我们将分为三个主要部分:
- C 风格字符串的操作函数 (
<cstring>
头文件):这是从 C 语言继承来的,理解它们有助于了解 C++ 的历史和与 C 库的交互,但在现代 C++ 中应谨慎使用。 std::string
类的成员函数 (<string>
头文件):这是现代 C++ 中处理字符串的首选方式,安全、方便、功能强大。- 作用于
std::string
的 STL 算法 (<algorithm>
头文件):展示了std::string
作为标准容器的强大之处。
Part 1: C 风格字符串的操作函数 (头文件 <cstring>
)
这些函数操作以 \0
(空字符) 结尾的字符数组。它们大多不进行边界检查,是导致缓冲区溢出等安全问题的重灾区,应极力避免在 C++ 新代码中使用。
函数原型 (简化) | 描述 | 风险与注意 |
size_t strlen(s) | 计算字符串 s 的长度,不包括 \0 。 | 效率低 (O(n)),每次都需从头遍历。 |
strcpy(dest, src) | 将字符串 src 复制到 dest 。 | 极度危险!不检查 dest 空间,极易导致缓冲区溢出。 |
strncpy(dest, src, n) | 最多复制 src 的 n 个字符到 dest 。 | 如果 src 长度>=n ,不会自动添加\0 ,需要手动处理。 |
strcat(dest, src) | 将字符串 src 追加到 dest 的末尾。 | 极度危险!不检查 dest 剩余空间,极易导致缓冲区溢出。 |
strncat(dest, src, n) | 最多追加 src 的 n 个字符到 dest ,并总是在最后添加一个\0 。 | 相对安全,但仍需程序员自己计算好空间。 |
int strcmp(s1, s2) | 比较 s1 和 s2 。返回 0 (相等), <0 (s1 <s2 ), >0 (s1 >s2 )。 | 注意不能用 == 比较 C 风格字符串内容,那是在比较指针地址。 |
char* strchr(s, c) | 在 s 中查找字符 c 首次出现的位置,返回指向该位置的指针,否则返回 nullptr 。 | |
char* strstr(s1, s2) | 在 s1 中查找子串 s2 首次出现的位置,返回指向该位置的指针,否则返回 nullptr 。 |
示例(仅为演示,不推荐使用):
#include <cstring>
#include <iostream>
int main() {
char dest[20] = "Hello";
const char* src = ", World!";
std::cout << "Length: " << strlen(dest) << std::endl; // 输出 5
strcat(dest, src); // 危险操作,但这里空间足够
std::cout << dest << std::endl; // 输出 "Hello, World!"
if (strcmp(dest, "Hello, World!") == 0) {
std::cout << "They are equal." << std::endl;
}
}
Part 2: std::string
类的成员函数 (头文件 <string>
)
这是现代 C++ 的标准做法。std::string
对象会自动管理内存,并提供了丰富的成员函数。
1. 修改与操作 (Modification)
函数 (简化) | 描述 | 示例 |
s.append(str) | 在字符串 s 的末尾追加 str 。(+= 运算符更常用) | s.append("tail"); |
s.push_back(c) | 在末尾追加一个字符 c 。 | s.push_back('!'); |
s.assign(str) | 将 s 的内容替换为 str 。 (= 运算符更常用) | s.assign("new content"); |
s.insert(pos, str) | 在索引 pos 处插入字符串 str 。 | s.insert(5, " C++"); |
s.erase(pos, len) | 从索引 pos 处开始,删除 len 个字符。 | s.erase(0, 6); |
s.replace(pos, len, str) | 从索引 pos 处开始,将 len 个字符替换为 str 。 | s.replace(0, 4, "Hi"); |
s.substr(pos, len) | 返回一个从 pos 开始,长度为 len 的新子字符串。 | std::string sub = s.substr(6, 5); |
s.clear() | 清空字符串,使其变为空。 | s.clear(); |
s.resize(n, c) | 改变字符串大小为 n 。若变长,用字符 c 填充新空间。 | s.resize(10, ' '); |
示例:
#include <string>
#include <iostream>
int main() {
std::string s = "Hello C++";
std::cout << "Original: " << s << std::endl;
s.insert(6, "Awesome ");
std::cout << "After insert: " << s << std::endl; // "Hello Awesome C++"
s.replace(s.find("C++"), 3, "World");
std::cout << "After replace: " << s << std::endl; // "Hello Awesome World"
std::string sub = s.substr(6, 7);
std::cout << "Substring: " << sub << std::endl; // "Awesome"
s.erase(5, 8);
std::cout << "After erase: " << s << std::endl; // "Hello World"
}
2. 查找 (Searching)
所有查找函数在找不到时,都会返回一个特殊静态成员 std::string::npos
。
函数 (简化) | 描述 |
s.find(str, pos=0) | 从索引 pos 开始,查找 str 首次出现的位置。 |
s.rfind(str, pos=npos) | 从索引 pos 开始(或末尾),反向查找 str 首次出现的位置。 |
s.find_first_of(chars, pos=0) | 从 pos 开始,查找 chars 中任意一个字符首次出现的位置。 |
s.find_last_of(chars, pos=npos) | 从 pos 开始,反向查找 chars 中任意一个字符首次出现的位置。 |
s.find_first_not_of(chars, pos=0) | 从 pos 开始,查找第一个不包含在 chars 中的字符。 |
s.find_last_not_of(chars, pos=npos) | 从 pos 开始,反向查找第一个不包含在 chars 中的字符。 |
示例:
std::string text = "file.cpp - a C++ source file.";
const std::string vowels = "aeiou";
size_t dot_pos = text.find('.');
if (dot_pos != std::string::npos) {
std::string extension = text.substr(dot_pos + 1);
std::cout << "Extension: " << extension << std::endl; // cpp - a C++ source file.
}
size_t first_vowel = text.find_first_of(vowels);
std::cout << "First vowel at: " << first_vowel << std::endl; // 1 ('i')
3. 转换 (Conversion)
函数 | 描述 |
s.c_str() | 返回一个指向 s 内部数据的 const char* C 风格字符串。用于与 C API 交互。 |
std::to_string(n) | (全局函数) 将数字 n 转换为 std::string 。 (C++11) |
std::stoi(s) | (全局函数) 将 std::string s 转换为 int 。 (C++11) |
std::stod(s) | (全局函数) 将 std::string s 转换为 double 。 (C++11) |
Part 3: std::string
与 STL 算法 (头文件 <algorithm>
)
std::string
是一个标准的序列容器,因此几乎所有的 STL 算法都可以作用于它。这极大地扩展了它的能力。
你需要提供迭代器 s.begin()
和 s.end()
作为算法的操作范围。
算法示例 | 描述 | 示例代码 |
std::reverse(begin, end) | 反转范围内的元素。 | std::reverse(s.begin(), s.end()); |
std::sort(begin, end) | 对范围内的元素进行排序。 | std::sort(s.begin(), s.end()); |
std::transform(b1, e1, b2, op) | 对范围1的每个元素应用操作op ,结果存入范围2。 | std::transform(s.begin(), s.end(), s.begin(), ::toupper); |
std::remove(begin, end, val) | 移除范围内所有等于 val 的元素(不改变大小)。 | s.erase(std::remove(s.begin(), s.end(), ' '), s.end()); 1 |
std::find(begin, end, val) | 在范围内查找 val ,返回迭代器或 end() 。 | auto it = std::find(s.begin(), s.end(), 'x'); |
std::count(begin, end, val) | 统计 val 在范围内出现的次数。 | int count = std::count(s.begin(), s.end(), 'a'); |
示例:
#include <string>
#include <algorithm>
#include <iostream>
#include <cctype>
int main() {
std::string s = "Hello World";
// 1. 全部转为大写
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return std::toupper(c); });
std::cout << "Uppercase: " << s << std::endl; // "HELLO WORLD"
// 2. 反转字符串
std::reverse(s.begin(), s.end());
std::cout << "Reversed: " << s << std::endl; // "DLROW OLLEH"
// 3. 移除所有空格
s.erase(std::remove(s.begin(), s.end(), ' '), s.end());
std::cout << "No spaces: " << s << std::endl; // "DLROWOLLEH"
}
总结与最佳实践
- 首选
std::string
:在所有 C++ 项目中,都应默认使用std::string
来处理字符串。它安全、高效且功能全面。 - 避免
<cstring>
:除非你必须与一个只接受char*
的 C 风格旧 API 交互,否则不要使用<cstring>
中的函数。 - 善用运算符:
std::string
重载的+
,+=
,==
,<
,>
等运算符让代码更直观、简洁。 - 掌握查找函数和
npos
:find
系列函数是字符串处理的核心,理解并正确使用std::string::npos
进行结果判断是必须的。 - 发挥 STL 算法的威力:对于复杂的、非标准的字符串操作(如大小写转换、条件删除、字符排序等),第一时间想到使用
<algorithm>
中的通用算法,而不是自己手写循环。这能让你的代码更标准、更健壮、也更简洁。
Footnotes
-
这是著名的 Erase-Remove Idiom,
std::remove
只会将不被移除的元素向前移动,并返回一个新的逻辑终点,需要配合s.erase()
才能真正缩短字符串。 ↩