字符数组就是"装字符的一排柜子";而 C++ 里的字符串,本质上是末尾带一个 '\0' 的字符数组——那个看不见的 '\0' 是字符串的"结束哨兵"。因为字符串只是个数组,所以四个常用操作都不能用运算符:不能用 + 连接、不能用 = 复制、不能用 == 比较,得去找 <cstring> 里的四个函数——strcat、strcpy、strcmp、strlen。
这一节把前面三处的老账一起结了:2.2.4 讲过"字符串常量末尾自动补 '\0'"、2.3.5 讲过"字符变量存的是 ASCII 码"、3.1.7 讲过"strlen 数的是存量、sizeof 量的是容量"。现在把它们装进 7.1 学的一维数组里,就得到了"字符串"。这也是本章唯一"四件事合成一节"的地方(教材在第 7.3 节下并列讲了连接·复制·比较·长度,编号 7.3.1~7.3.4 教材未标,故合为一篇)。
一、字符数组与字符串:差一个 '\0'
写法 1:用字符串字面量初始化(最常用)
char s[6] = "hello"; // 5 个字符 + 1 个 '\0',正好 6 格
char t[] = "hello"; // 长度自动算出 6(7.1.3)
必须留出 '\0' 的位置:"hello" 有 5 个可见字符,但数组至少要 6 格。写 char s[5] = "hello"; 会装不下(Compiler 通常报 initializer-string for array of chars is too long)。
写法 2:逐字符摆
char u[6] = {'h', 'e', 'l', 'l', 'o', '\0'}; // 手动补 '\0',才是字符串
char v[5] = {'h', 'e', 'l', 'l', 'o'}; // 没有 '\0':只是字符数组,不是字符串
两者天差地别:v 里没有结束哨兵,一旦把它当字符串用(比如 cout << v),程序会一路往后读,直到偶然碰到某个 '\0' 才停——读到什么内容就打印什么,这叫越界读取。
'\0' 的三个作用
| 作用 | 说明 |
|---|---|
| 标记结尾 | 所有字符串函数(strlen、strcat…)都靠它判断"到哪儿算完" |
| 解释两把尺子 | sizeof(s) 量的是整排柜子容量(6);strlen(s) 数的是哨兵之前的字符数(5) |
| 遍历的终止条件 | 指针遍历字符串可以写成 while (*p),靠 '\0' 的 ASCII 值 0 自动停下(7.5.3) |
二、字符串的输入输出:不用循环
这是字符数组最舒服的一点——它自带"整进整出"的能力:
char name[20];
cin >> name; // 敲 hello,整串读进去(自动补 '\0')
cout << name; // 整串打印,不用一格一格循环
但有两个坑:
cin >> name遇空白就停——敲Li Ming只会读到Li;- 想读一整行(含空格),得用
cin.getline(name, 20);(第二个参数是最多读多少格)。
对比 7.1 的数值数组:int a[5] 只能 cin >> a[i] 一格一格来,数组本身不能整体输入;而 char 数组因为有了 '\0',"整体"这件事才有意义。
三、四大操作总览
C++ 没有给字符串配运算符,四个常用操作都在 <cstring> 里:
| 要做的事 | 函数 | 用法 | 一句话 |
|---|---|---|---|
| 连接 | strcat |
strcat(目标, 源) |
把"源"接到"目标"末尾 |
| 复制 | strcpy |
strcpy(目标, 源) |
把"源"拷进目标(覆盖原有内容) |
| 比较 | strcmp |
strcmp(s1, s2) |
相等返回 0;s1 大返回正数、小返回负数 |
| 求长度 | strlen |
strlen(s) |
数到 '\0' 为止的字符个数 |
四个函数共同的隐含前提:都要求参数字符数组以 '\0' 结尾;四个函数都不检查目标空间够不够——空间是你自己的责任。
四、字符串连接:strcat(实例01、实例02、实例03)
#include <cstring> // 四个函数都在这儿
strcat(目标数组, 源字符串);
把"源"接到"目标"的末尾,结果留在"目标"里。记住两条硬规矩:目标必须够大、目标必须是可写的数组(不能是字符串常量)。
实例01:连接两个字符串(基本用法)
#include <iostream>
#include <cstring>
using namespace std;
int main() {
char s1[30] = "Hello, "; // 留足空间
char s2[] = "World!";
strcat(s1, s2); // 把 s2 接到 s1 末尾
cout << s1 << endl; // Hello, World!
cout << "s1 长度 = " << strlen(s1) << endl; // 13
cout << "s1 容量 = " << sizeof(s1) << endl; // 30
return 0;
}
看到什么算成功:Hello, World!、13、30。
重点看最后两行:strlen 数出 13 个字符(连接后的实际内容),sizeof 仍是 30(柜子容量没变)。"存量"和"容量"分家——这就是 3.1.7 那把尺子在这里的现场演示。
实例02:连续拼接,凑成一句话
#include <iostream>
#include <cstring>
using namespace std;
int main() {
char sentence[100] = ""; // 空串起步(空花括号写法,7.1.3)
char name[] = "小明";
char verb[] = " 正在学 ";
char lang[] = "C++";
strcat(sentence, name); // 一次次往后接
strcat(sentence, verb);
strcat(sentence, lang);
cout << sentence << endl;
return 0;
}
看到什么算成功:小明 正在学 C++。
读代码:strcat 是"就地往后接",所以可以像搭积木一样一段段拼。出发点必须是已经以 '\0' 结尾的字符串——char sentence[100] = ""; 是最常用的"空起点"写法。
实例03:先检查空间再连接(安全版)
#include <iostream>
#include <cstring>
using namespace std;
int main() {
char target[12] = "Hello";
char src[] = ", World!";
int used = strlen(target); // 已用 5 格
int need = strlen(src); // 还要 8 格
int capacity = sizeof(target); // 总共 12 格
if (used + need + 1 <= capacity) { // 4.2.1 的单分支 if
strcat(target, src);
cout << "连接成功:" << target << endl;
} else {
cout << "空间不足!需要 " << used + need + 1
<< " 格,只有 " << capacity << " 格" << endl; // 走这一支
}
return 0;
}
看到什么算成功:打印 空间不足!需要 14 格,只有 12 格。
算式:5(已用)+ 8(, World! 有 8 个字符)+ 1('\0')= 14 格,比容量 12 大——于是拒绝连接、把危险挡在门外。假如直接 strcat(target, src),那 14 格内容就会硬写进 12 格的柜子,越界两格,正是 7.1.2 那个"不报错的坑"。
为什么要加 +1?因为字符串还得多留一格放 '\0'。忘了这个 +1,就是 strcat 最经典的越界事故。
五、字符串复制:strcpy
strcpy(目标数组, 源字符串);
为什么不能直接 s2 = s1;?因为数组名不是可改的变量(7.5.1),而且数组不能整体赋值(7.1.3)。所以复制必须靠 strcpy:
#include <iostream>
#include <cstring>
using namespace std;
int main() {
char src[] = "C++";
char dst[10];
strcpy(dst, src); // 把 src 拷进 dst
cout << "dst = " << dst << endl; // C++
strcpy(dst, "换一个"); // 也可直接拷字面量(会覆盖原有内容)
cout << "dst = " << dst << endl; // 换一个
// dst = src; // ❌ 编译不过:数组不能整体赋值
return 0;
}
两个要点:① strcpy 是覆盖式的——目标原有内容被整段替换(和 strcat 的"往后接"正相反);② 目标空间必须装得下源串 + '\0',strcpy 同样不做检查。
六、字符串比较:strcmp
int r = strcmp(s1, s2);
返回值只有三种含义:
| 返回值 | 含义 |
|---|---|
| 0 | s1 和 s2 完全相同 |
| 负数 | s1 小于 s2 |
| 正数 | s1 大于 s2 |
比的是字符的 ASCII 码,从左往右逐个比,第一个不同的字符决定大小(2.2.3 的 ASCII 表在这里上岗)。
#include <iostream>
#include <cstring>
using namespace std;
int main() {
char a[] = "apple";
char b[] = "apple";
char c[] = "banana";
cout << strcmp(a, b) << endl; // 0 —— 完全一样
cout << (strcmp(a, c) < 0) << endl; // 1 —— a 在 c 前面('a' < 'b')
cout << (strcmp(c, a) > 0) << endl; // 1 —— c 在 a 后面
return 0;
}
为什么不能写 if (a == b)?因为 a 和 b 是数组名,== 比的是两个数组的地址(7.5.1 会讲清),不是内容。两个内容相同的字符串,地址不同,a == b 得到 false——这是最坑的一类"不报错错误"。
正确写法:if (strcmp(a, b) == 0) 才算"内容相同"。
七、获取字符串长度:strlen(实例04)
unsigned int n = strlen(s);
数到 '\0' 为止的字符个数,不含 '\0'。
实例04:量一量字符串有多长
#include <iostream>
#include <cstring>
using namespace std;
int main() {
char s1[] = "hello";
char s2[20] = "hi"; // 容量 20,内容 2
char s3[20] = ""; // 空串
cout << "s1: strlen=" << strlen(s1) << " sizeof=" << sizeof(s1) << endl;
cout << "s2: strlen=" << strlen(s2) << " sizeof=" << sizeof(s2) << endl;
cout << "s3: strlen=" << strlen(s3) << " sizeof=" << sizeof(s3) << endl;
// 手写一个"数长度"循环,看 '\0' 是怎么当哨兵的
int count = 0;
while (s1[count] != '\0') count++; // 3.1.7 说 strlen 就是这么数出来的
cout << "手数 s1 = " << count << endl;
return 0;
}
看到什么算成功:
s1: strlen=5 sizeof=6
s2: strlen=2 sizeof=20
s3: strlen=0 sizeof=20
手数 s1 = 5
三行读数就够把两个概念钉死:
| 字符串 | strlen(存量) |
sizeof(容量) |
差别原因 |
|---|---|---|---|
s1[] = "hello" |
5 | 6 | 容量由初始化算出(5 + '\0') |
s2[20] = "hi" |
2 | 20 | 容量是声明的 20,内容只有 2 |
s3[20] = "" |
0 | 20 | 空串 = 只有一个 '\0' |
最后那段手写循环是本节最好的注脚:strlen 没有魔法,它就是"从第一格往后数,碰到 '\0' 停下"。理解了这五行,就理解了"为什么字符串必须有结束哨兵"。
八、常见误区
- "
char s[5] = "hello";正好装满 5 格"——装不下。字符串要 6 格(5 个字符 + 1 个'\0')。这类错误会让编译器报too long,算幸运。 - "可以用
=复制、用+连接、用==比较"——三个都不行。字符串是数组,运算符不管它。必须用strcpy/strcat/strcmp。 - "
strcmp返回 1 和 -1"——不保证。标准只保证"相等 = 0、谁大谁正数"。判断大小写strcmp(a,b) > 0,永远别写== 1。 - "
strcat会自动扩容"——不会,而且不检查。目标空间不够就越界写内存(7.1.2 的老坑)。牢记"目标容量 ≥ 已有长度 + 新增长度 + 1"。 - "
strlen(s)和sizeof(s)差不多"——差别是本节的重点:存量 vs 容量。传进函数的数组上sizeof会失效(7.8.3 详述),而strlen不受影响。 - "
cin >> s能读一整行"——遇空白就停。要读整行用cin.getline(s, 长度);
自测四问(点开对答案)
1. `char s[5]; strcpy(s, "hello");` 有什么问题?——**越界**:装 `"hello"` 至少要 6 格(含 `'\0'`),`strcpy` 不会检查空间。 2. 怎么判断两个字符串内容相同?——`if (strcmp(a, b) == 0)`。**不能写 `a == b`**(那比的是地址)。 3. `strlen("C++")` 和 `sizeof("C++")` 分别是几?——`3` 和 `4`(存量 vs 容量,容量含 `'\0'`)。 4. `strcat` 和 `strcpy` 的区别?——`strcat` 是**往后接**(保留原有内容),`strcpy` 是**整段覆盖**。- ☐ 7.4 指针(下一节)