在仔细阅读了您分享的关于字符串操作的笔记后,我为您整理了一份更系统、更具实用性的 C++ 字符串操作讲义。
在编写这份讲义时,我对您笔记中的一些细节进行了优化和补充,这些细节在实际编程和算法竞赛(如 CSP/NOIP)中非常容易成为“扣分陷阱”:
1. strcmp 的返回值:根据标准库定义,strcmp 只保证返回正数、负数或 0,并不一定会严格返回 1 或 -1。在代码中进行判断时,应使用 strcmp(a, b) > 0 而非 == 1。
2. fgets 读入换行符的问题:fgets 在读取一行时,会将末尾的换行符 \n 也存入字符串中,这在进行字符串匹配或长度计算时极易出错,通常需要手动将其滤掉。
3. string_view 的无零字符结尾(\0)陷阱:string_view 只是对已有字符串的视图,它所指向的区间并不保证以 \0 结尾。如果直接将其内部地址(.data())传给 printf("%s") 或 strlen,会导致越界读取。
4. 延续第 8 章:基于您大纲中的“8”,我顺承第 7 章的内容,为您补充了 “8. C++ 标准字符串类 std::string”,帮助您完成从 C 风格字符数组到现代 C++ 字符串的知识过渡。
以下是为您整理的讲义 Markdown 内容:
C++ 字符串与文本处理讲义
在 C++ 中,字符串的处理主要分为两套体系:
1. C 风格字符串:基于 char 数组和指针,以 \0 结尾。运行效率极高,空间占用紧凑,是算法竞赛(如 CSP/NOIP)中处理大规模输入输出的利器。
2. C++ 风格字符串 (std::string):面向对象的标准库类。安全易用,支持动态扩容,在现代 C++ 开发及日常编程中被广泛推荐。
7. C 风格字符串操作
7.1 核心机制与内存安全
C 风格字符串在内存中是一组连续的字符,且必须以空字符 '\0'(ASCII 码为 0)作为结束标记。
* 如果字符数组中没有 '\0',任何试图读取该字符串的库函数(如 strlen, strcpy, printf)都会继续向后读取相邻的内存,导致越界访问(Undefined Behavior),在竞赛中常表现为 RE(运行错误)或 WA(答案错误)。
7.2 字符串的输入与输出
A. 格式化读写(不含空格的字符串)
char str[100];
// 输入:遇到空格、回车或 TAB 会终止输入
scanf("%s", str);
cin >> str;
// 输出
printf("%s\n", str);
cout << str << endl;
B. 整行读入(包含空格的字符串)
当字符串中包含空格时,需要使用整行读取函数。
在 C++ 中,严禁使用已被废弃且极具安全隐患的 gets(),应使用 fgets()。
// 语法:fgets(字符数组名, 最大读取长度, 输入流)
fgets(str, 100, stdin);
- 注意(陷阱):如果行长度小于最大读取长度,
fgets会将换行符\n一并读入并存储在字符串末尾。 - 去除换行符的标准写法:
cpp #include <cstring> // 将首个出现的 \n 替换为 \0 str[strcspn(str, "\n")] = '\0';
7.3 核心处理函数(头文件 <cstring>)
1. 获取长度:strlen(str)
返回字符串的实际字符个数,不包括末尾的 '\0'。该操作的时间复杂度为 $O(N)$,因此切忌在循环条件中重复调用(如 for(int i=0; i<strlen(str); i++) 会使循环复杂度退化为 $O(N^2)$)。
2. 字符串复制:strcpy 与安全版本 strncpy
// 将 str2 的内容(连同 '\0')复制到 str 中
strcpy(str, str2);
// 安全版本:最多复制 n 个字符,防止 str 空间不足导致溢出
strncpy(str, str2, sizeof(str) - 1);
str[sizeof(str) - 1] = '\0'; // 必须手动确保末尾有 '\0'
3. 字符串拼接:strcat 与安全版本 strncat
将 str2 的内容连接到 str 的末尾(覆盖原 str 末尾的 \0),str 必须有足够的剩余空间。
strcat(str, str2);
strncat(str, str2, n); // 将 str2 的前 n 个字符连接到末尾
4. 字符串比较:strcmp 与 strncmp
逐个字符比较 ASCII 码。
int res = strcmp(str, str2);
- 判断依据:
res > 0:表示str的字典序大于str2(在实际环境中,返回值不一定是1)。res == 0:表示两字符串完全相等。res < 0:表示str的字典序小于str2(返回值不一定是-1)。
5. 字符与子串查找:strchr 与 strstr
- 查找字符:
strchr(str, c)返回指向字符c首次出现位置的指针;若未找到,返回nullptr。 - 查找子串:
strstr(str, str2)返回指向子串str2首次出现位置的指针;若未找到,返回nullptr。
计算具体索引(下标)的方法:利用指针减法。
char* pos = strstr(str, "target");
if (pos != nullptr) {
int index = pos - str; // 利用指针相减获取在数组中的 0 基索引
printf("Found at index: %d\n", index);
}
7.4 格式化与数值转换
在算法竞赛和工程开发中,常常需要在字符串与数值之间进行高效率的转换。
A. 内存格式化读写(sscanf 与 sprintf)
sscanf:从字符串中读取数据(类似于scanf,只是输入源改为了字符串)。sprintf:向字符串中写入格式化内容(常用于数值转字符串、复杂格式拼接)。
char buffer[100];
int age = 20;
double score = 95.5;
// 数值格式化为字符串
sprintf(buffer, "Age:%d Score:%.1f", age, score);
// buffer 变为 "Age:20 Score:95.5"
// 从字符串中解析数据
int parsed_age;
double parsed_score;
sscanf(buffer, "Age:%d Score:%lf", &parsed_age, &parsed_score);
B. 现代 C++ 数值转换(to_string)
头文件 <string>。可以直接将各种数值类型转化为 std::string。
#include <string>
std::string s1 = std::to_string(123); // s1 = "123"
std::string s2 = std::to_string(3.14159); // s2 = "3.141590" (默认保留 6 位小数)
7.5 现代 C++ 零拷贝视图:std::string_view (C++17)
头文件 <string_view>。
* 原理:它是一个轻量级的、非所有权的只读字符串视图(仅包含一个指向字符数据的指针和长度)。
* 优点:在函数传参时,相比 const std::string&,它可以完美兼容 C 风格字符串和 std::string,且完全避免了任何内存拷贝。
#include <string_view>
#include <iostream>
// 使用 string_view 作为参数,既高效又通用
void printWithLength(std::string_view sv) {
std::cout << sv << " (length: " << sv.length() << ")\n";
}
int main() {
printWithLength("Hello World"); // 传入 C 风格字符串,无拷贝
std::string s = "C++";
printWithLength(s); // 传入 std::string,无拷贝
}
⚠️ 重要警告(安全隐患):
std::string_view 不保证其内部字符以 \0 结尾(例如取子串 sv.substr(0, 3) 时,底层并不会被截断填入 \0)。因此,绝不能直接将 sv.data() 传给 printf("%s") 或 strlen(),否则会发生内存越界。
8. C++ 标准字符串类 std::string
作为 C++ 标准库(STL)中最常用的容器之一,std::string(头文件 <string>)彻底解决了 C 风格字符串内存越界和手动管理空间的烦恼。
8.1 基本定义与常规操作
- 声明与初始化:
cpp #include <string> std::string s1 = "Hello"; std::string s2("World"); - 拼接与比较:支持直接使用运算符
+、+=、==、<、>。cpp std::string s3 = s1 + " " + s2; // s3 = "Hello World" if (s1 < s2) { /* 字典序比较 */ } - 长度与空判断:
cpp s1.length(); // 或 s1.size(),时间复杂度为 O(1) s1.empty(); // 返回 bool 值,判断是否为空
8.2 核心成员函数
std::string 提供了极为丰富的成员函数,大大降低了字符串处理的逻辑难度。
1. 子串截取:substr
// 语法:s.substr(起始位置, 截取长度)
std::string s = "Hello C++ World";
std::string sub = s.substr(6, 3); // sub = "C++"
2. 查找子串与字符:find
std::string s = "Hello C++";
size_t pos = s.find("C++"); // 返回找到的第一个字符的索引
if (pos != std::string::npos) {
// 找到了,pos = 6
} else {
// 未找到,返回值为 std::string::npos
}
3. 插入与删除:insert 与 erase
std::string s = "Hello World";
s.insert(5, " C++"); // 在索引 5 处插入,s 变为 "Hello C++ World"
s.erase(5, 4); // 从索引 5 开始,删除 4 个字符,s 变回 "Hello World"
4. 替换子串:replace
std::string s = "I love Python";
s.replace(7, 6, "C++"); // 从第 7 个字符开始,将 6 个字符长度的子串替换为 "C++"
// s 变为 "I love C++"
8.3 字符属性判定函数(头文件 <cctype>)
无论是 C 风格还是 C++ 风格字符串,遍历其中的每一个字符时,通常需要判断其是否为数字、字母等。<cctype> 库提供了非常高效的辅助函数:
| 函数 | 功能描述 |
|---|---|
isdigit(c) |
判断字符 c 是否为十进制数字('0' ~ '9') |
isalpha(c) |
判断字符 c 是否为英文字母('a'~'z', 'A'~'Z') |
isalnum(c) |
判断字符 c 是否为字母或数字 |
islower(c) |
判断字符 c 是否为小写字母 |
isupper(c) |
判断字符 c 是否为大写字母 |
isspace(c) |
判断字符 c 是否为空格、制表符、换行符等空白字符 |
tolower(c) |
将字符 c 转换为小写(非大写字母则保持原样) |
toupper(c) |
将字符 c 转换为大写(非小写字母则保持原样) |
示例应用:统计一个字符串中数字和英文字母的个数:
#include <iostream>
#include <string>
#include <cctype>
int main() {
std::string s = "2026 Happy New Year!";
int digit_cnt = 0, alpha_cnt = 0;
for (char c : s) {
if (std::isdigit(c)) digit_cnt++;
else if (std::isalpha(c)) alpha_cnt++;
}
std::cout << "Digits: " << digit_cnt << "\n"; // 输出 4
std::cout << "Letters: " << alpha_cnt << "\n"; // 输出 12
return 0;
}
—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com