🔷 第七章 数组、指针和引用
📄 一句话概括

字符数组就是"装字符的一排柜子";而 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;         // 整串打印,不用一格一格循环

但有两个坑:

对比 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' 停下"。理解了这五行,就理解了"为什么字符串必须有结束哨兵"。

八、常见误区

  1. "char s[5] = "hello"; 正好装满 5 格"——装不下。字符串要 6 格(5 个字符 + 1 个 '\0')。这类错误会让编译器报 too long,算幸运。
  2. "可以用 = 复制、用 + 连接、用 == 比较"——三个都不行。字符串是数组,运算符不管它。必须用 strcpy / strcat / strcmp。
  3. "strcmp 返回 1 和 -1"——不保证。标准只保证"相等 = 0、谁大谁正数"。判断大小写 strcmp(a,b) > 0,永远别写 == 1。
  4. "strcat 会自动扩容"——不会,而且不检查。目标空间不够就越界写内存(7.1.2 的老坑)。牢记"目标容量 ≥ 已有长度 + 新增长度 + 1"。
  5. "strlen(s) 和 sizeof(s) 差不多"——差别是本节的重点:存量 vs 容量。传进函数的数组上 sizeof 会失效(7.8.3 详述),而 strlen 不受影响。
  6. "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` 是**整段覆盖**。

相关笔记