Appearance
🧵09. String 字符串
0. char 的内存模型
在学 string 之前,我们先搞清楚一个问题:计算机里到底怎么存一段字符串?
0.1 内存就是一排带编号的柜子
可以把内存想象成一排带编号的格子,每个格子可以放一个字节(8 bit)的数据:
内存地址: 1000 1001 1002 1003 1004 1005 1006
数据: [ ][ ][ ][ ][ ][ ][ ]我们之前学的 char 类型占 1 个字节,所以一个 char 变量就是占了其中一个格子。
0.2 char 数组就是连续占了一排格子
cpp
char s[4] = {'A', 'B', 'C', 'D'};它在内存中长这样:
地址: 1000 1001 1002 1003
┌──────┬──────┬──────┬──────┐
数据: │ 'A' │ 'B' │ 'C' │ 'D' │
└──────┴──────┴──────┴──────┘
s[0] s[1] s[2] s[3]每个格子存一个字符,连续排列,通过下标访问。
0.3 核心问题:字符串到哪结束?
现在假设我们有一个 char 数组,只存了 3 个有效字符:
cpp
char s[5] = {'H', 'e', 'y'}; // 只填了前 3 个内存里实际是这样:
地址: 2000 2001 2002 2003 2004
┌──────┬──────┬──────┬──────┬──────┐
数据: │ 'H' │ 'e' │ 'y' │ ??? │ ??? │
└──────┴──────┴──────┴──────┴──────┘像这样,如果我有一个函数要遍历这个字符串(比如打印它),它怎么知道该在第几个格子停下?
- 它不知道数组长度(数组传参时会退化成指针,长度信息丢失)
- 它不能看格子有没有值——因为没被初始化的格子里可能是随机垃圾数据
0.4 \0 就是字符串的"句号"
为了解决这个问题,C 语言约定了一个规则:所有字符串末尾必须加一个 \0(空字符,ASCII 码为 0),作为结束标记。
cpp
char s[6] = {'H', 'e', 'l', 'l', 'o', '\0'};内存长这样:
地址: 3000 3001 3002 3003 3004 3005
┌──────┬──────┬──────┬──────┬──────┬──────┐
数据: │ 'H' │ 'e' │ 'l' │ 'l' │ 'o' │ '\0' │
└──────┴──────┴──────┴──────┴──────┴──────┘
↑ 结束标记!当 cout 或 strlen() 这类函数遍历字符串时,它们会一直走,直到碰到 \0 才停下来。
cpp
cout << s; // 从 s[0] 开始逐个输出,遇到 s[5] 的 '\0' 停下 → "Hello"0.5 忘记 \0 会怎样?
cpp
char bad[5] = {'H', 'e', 'l', 'l', 'o'}; // 忘记加 '\0'
cout << bad; // 危险!会一直读下去,直到在内存中碰巧遇到一个 0内存: 4000 4001 4002 4003 4004 4005 4006 4007 ...
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│ 'H' │ 'e' │ 'l' │ 'l' │ 'o' │ ?? │ ?? │ '\0' │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
↑ 越界了!读到垃圾数据这就是所谓的缓冲区溢出——程序读了不该读的内存,轻则打印乱码,重则崩溃。
所以写 C 风格的字符串,永远记得给
\0留一个位置。char s[6] = "Hello";虽然只写了 5 个字母,但实际占了 6 个格子,最后一个就是编译器自动加的\0。
0.6 char 数组的日常痛点
了解内存模型后,你就能理解为什么用 char[] 处理字符串这么麻烦了:
| 操作 | char[] 的写法 | 槽点 |
|---|---|---|
| 复制 | strcpy(dest, src) | 要确保 dest 空间够 |
| 拼接 | strcat(dest, src) | 同样要担心空间,还得先算长度 |
| 比较 | strcmp(a, b) == 0 | 不能用==,否则比的不是内容而是地址 |
| 获取长度 | strlen(s) | 每次遍历一遍,O(n) |
| 越界风险 | 没有保护机制 | 忘加\0 就崩 |
正因如此,C++ 的设计师才给我们准备了一个更好用的东西——string 类型。
1. 什么是 string
string 是 C++ 标准库提供的一种字符串类型,它位于 <string> 头文件中。你可以把它理解成一个"升级版的字符串",比 C 风格的 char[]使用起来更加方便 。
cpp
#include <string> // 使用 string 需要包含这个头文件string 到底是什么?
细心的你可能已经发现了——string 和我们之前学的 int、float 不太一样。
cpp
int a = 5;
// a. ← 点开后什么也没有
string s = "Hi";
// s. ← 点开后会看到一大堆功能因为 string 不是基本数据类型,而是一个"对象"。
对象可以简单理解为一个"有智慧的变量",它里面不仅存了数据,还自带了操作这些数据的方法。
比如
s.length()就是在调用 string 内部自带的"计算长度"的函数。
那为什么 + 可以直接拼接字符串呢?因为 string 内部重新定义了 + 的含义——这在 C++ 里叫运算符重载。简单说就是告诉编译器:"当两个 string 用 + 连接时,把内容拼在一起"。
NOTE
这些概念(类、对象、成员函数、运算符重载)后面学到"类和对象"时会详细讲解。现在你只需要记住:string 是一种"高级类型",它自带了很多好用的功能。 这也是 C++ 比 C 更"现代化"的地方之一。
1.1 定义和初始化
cpp
string s1; // 空字符串
string s2 = "Hello"; // 直接用字符串字面量初始化
string s3 = s2; // 用另一个 string 初始化
string s4(5, 'A'); // 结果为 "AAAAA"对比
char[]:定义string不需要指定长度,不用担心空间不够。
1.2 输入输出
cpp
string name;
cin >> name; // 输入:Arin,遇到空格会停
getline(cin, name); // 输入一整行,包括空格
cout << name << endl; // 输出NOTE
̀getline(cin, s) 可以读取包含空格的整行文本,而 cin >> s 遇到空格或换行就会停下。
2. string 的优点
2.1 不用管 \0
还记得字符数组必须用 \0 结尾吗?string 完全不需要,它内部自动管理结束标志。
cpp
char old[4] = {'A', 'B', 'C', '\0'}; // 忘了 \0 就出 bug
string s = "ABC"; // 简单、安全2.2 直接用 + 拼接
cpp
string a = "Hello";
string b = " World";
string c = a + b; // c = "Hello World"
c += "!"; // c = "Hello World!"用
char[]拼接?得先算长度、调strcat、还得保证空间够……想想就头痛。
2.3 直接比较
cpp
string a = "apple", b = "banana";
if (a < b) cout << "apple 排在前面"; // 按字典序比较,没问题char[] 不能直接用 < > 比较(会比较地址而不是内容),得调 strcmp。
2.4 自动管理内存
string 会自动扩容,不用像 char[] 那样担心数组越界。
cpp
string s = "Hi";
s = "This is a much longer string..."; // 自动扩容,没问题!2.5 丰富的成员函数
获取长度
cpp
string s = "Hello";
cout << s.length(); // 5
cout << s.size(); // 也是 5,和 length() 一样查找子串
cpp
string s = "I love C++";
int pos = s.find("love"); // pos = 2(从 0 开始计数)
if (pos != string::npos) { // string::npos 表示"没找到"
cout << "找到了,位置在:" << pos;
}提取子串
cpp
string s = "Hello World";
string sub = s.substr(6, 5); // 从下标 6 开始取 5 个字符 → "World"转换为 C 风格字符串
cpp
string s = "Hello";
const char* old = s.c_str(); // 转成 const char*,给需要 char[] 的函数用3. 常用操作速查表
| 操作 | 写法 | 说明 |
|---|---|---|
| 定义 | string s; | 空字符串 |
| 拼接 | s1 + s2 或 s += "xxx" | 直接相加 |
| 长度 | s.length() 或 s.size() | 返回字符数 |
| 访问字符 | s[i] | 像数组一样用下标,从 0 开始 |
| 查找 | s.find("xx") | 返回位置,没找到返回string::npos |
| 取子串 | s.substr(pos, len) | 从 pos 取 len 个字符 |
| 转 C 风格 | s.c_str() | 用于兼容需要char* 的场景 |
| 输入一行 | getline(cin, s) | 读一整行,包括空格 |
| 判空 | s.empty() | 空返回 true,否则 false |
TIP
string 的下标和数组一样从 0 开始:s[0] 是第一个字符。
4. 例题:答题卡填涂
这是CPP二级考级的一道真题,如果用
char[]来处理会非常麻烦,但用string就轻松很多。
题目描述
新浪微博上有网友发文称:"朋友买了本玻尔×海森堡的物理大佬同人本,送了300道高数题。更绝的是,要做完题目按照答案涂答题卡,涂出一个二维码,扫描二维码才能看到特典……"
现在请你根据答案帮助读者填写答题卡。
规则:
- 二维码是一个
n × n的网格,左下角坐标(1, 1),右上角坐标(n, n) - 每个答案对应一个格子位置
- 答案可能是以下三种格式之一:
- 整数(不超过 4 位):补足 4 位后,前两位 = 横坐标,后两位 = 纵坐标
- 两小问
答案1;答案2:第一问答案 = 横坐标,第二问答案 = 纵坐标 - 分数
分子/分母:分子 = 横坐标,分母 = 纵坐标
输入格式:
n m
答案1
答案2
...
答案m输出格式: 输出一个 n × n 的网格,空格用 . 表示,涂了答案的黑格用 # 表示。
样例输入:
5 3
3
2;3
1/4样例输出:
.....
..#..
..#..
.....
.#...思路分析
这个题的关键在于解析不同格式的答案字符串,提取出坐标。用 string 来处理简直不要太合适:
- 读入一行答案 → 用
s.find(";")判断是否包含;(两小问格式) - 用
s.find("/")判断是否包含/(分数格式) - 都不包含 → 整数格式,补零到 4 位
- 用
s.substr()提取各部分 - 用
stoi()把字符串转为整数
如果用 C 风格的
char[]和strtok来做……代码会复杂得多,而且容易出错。
参考代码
cpp
#include <iostream>
#include <string>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
// 初始化网格,全部为 '.'
string grid[100]; // 因为 n ≤ 90,开 100 够用
for (int i = 0; i < n; i++) {
grid[i] = string(n, '.'); // 用 string 构造 n 个 '.'
}
string ans;
getline(cin, ans); // 吃掉上一行的换行
for (int i = 0; i < m; i++) {
getline(cin, ans); // 读入一整行答案
int x, y;
// 判断是不是两小问格式(包含 ;)
if (ans.find(';') != string::npos) {
int pos = ans.find(';');
string sx = ans.substr(0, pos); // 分号前的部分
string sy = ans.substr(pos + 1); // 分号后的部分
x = stoi(sx);
y = stoi(sy);
}
// 判断是不是分数格式(包含 /)
else if (ans.find('/') != string::npos) {
int pos = ans.find('/');
string sx = ans.substr(0, pos); // 分子
string sy = ans.substr(pos + 1); // 分母
x = stoi(sx);
y = stoi(sy);
}
// 否则是整数格式
else {
// 补足 4 位:前面补 0
while (ans.length() < 4) {
ans = "0" + ans; // string 可以直接用 + 拼接!
}
string sx = ans.substr(0, 2); // 前两位 = 横坐标
string sy = ans.substr(2, 2); // 后两位 = 纵坐标
x = stoi(sx);
y = stoi(sy);
}
// 填涂格子(注意坐标系转换:左下角是 (1,1))
grid[n - y][x - 1] = '#';
}
// 输出网格
for (int i = 0; i < n; i++) {
cout << grid[i] << endl;
}
return 0;
}代码亮点
看看这段代码里用了哪些 string 的绝活:
| 用法 | 作用 |
|---|---|
string(n, '.') | 快速构造 n 个. 的字符串 |
ans.find(';') | 查找分隔符位置 |
ans.substr(0, pos) | 截取分号前的部分 |
"0" + ans | 用+ 直接在字符串前面补零 |
stoi(s) | 把字符串转为整数 |
grid[i] = ... | string 可以直接赋值,不用 strcpy |
TIP
stoi() 是 "string to int" 的缩写,它能把 "123" 这样的字符串变成整数 123。使用时需要包含 <string> 头文件。
5. 总结
| 对比项 | char[](C 风格) | string(C++ 风格) |
|---|---|---|
需要\0 | ✅ 是的 | ❌ 不需要 |
| 拼接 | strcat(),麻烦 | + 或 +=,直接 |
| 比较 | strcmp() | < > == 直接比 |
| 赋值 | strcpy() | = 直接赋值 |
| 获取长度 | strlen() | .length() |
| 自动扩容 | ❌ | ✅ |
| 安全性 | 容易越界 | 安全 |
NOTE
string 用起来这么方便,其实是因为它底层是一个类(class),封装了很多功能。学了"类和对象"之后,你就能理解它为什么这么好用了——到时候你也可以自己写一个像 string 一样好用的类型!
从这节课开始,我们写 C++ 程序处理字符串,就优先用 string 吧!🚀