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

字符数组名也是首地址,所以可以用指针走它。最漂亮的一点:因为字符串末尾有 '\0'(7.3),while (*p) 就能自动停在结尾——不需要知道长度。但必须分清两种声明:char s[] = "hello"; 是数组,内容可改;char *s = "hello"; 是指针指向只读常量区,改它会崩。

📝 本节和前面几节的关系

7.3 给了你字符串的四个函数(连接/复制/比较/长度),这一节告诉你它们内部是怎么写出来的——都是"指针 + '\0'"循环。它同时收束了本节群的三条线索:7.5.1 的"数组名是首地址"、7.4.3 的"指针加 1 跳一格"(char* 跳 1 字节)、7.3 的"'\0' 是结束哨兵"。学完它,7.3 那四个函数就从"黑盒"变成了"自己也能写"。

一、字符数组名也是首地址

char s[] = "hello";
char *p = s;              // p 指向 'h'

cout << *p  << endl;      // h
p++;                      // 走一格(char 一格 = 1 字节,7.4.3)
cout << *p  << endl;      // e
cout << p   << endl;      // ello  ← cout 认为 p 是字符串,一路打印到 '\0'

最后一行的行为要特别注意:对 char *,cout << 不是打印地址,而是打印它指向的字符串(一直到 '\0')。这是 char* 的特权(其他类型指针打印的是地址)。

二、'\0' 当哨兵:自动停下

因为 '\0' 的 ASCII 值是 0(2.2.3),而 while (条件) 里 0 就是假(2.4.3 的"非 0 即真"),于是:

while (*p != '\0') { ... }     // 完整写法
while (*p)         { ... }     // 等价简写:走到 '\0' 自动停

完整例子:

char s[] = "hello";
char *p = s;
while (*p) {
    cout << *p << "-";
    p++;
}
// 输出:h-e-l-l-o-

这就是 7.3 那四个函数的内部结构:不论 strlen、strcat 还是 strcmp,核心都是"用指针一路往后走,走到 '\0' 停下"。区别只在"停下之前干什么"。

三、两种声明的天壤之别(本节最大的坑)

char s1[] = "hello";      // ① 数组:内容在"可读可写"的地方
char *s2  = "hello";      // ② 指针:指向"只读常量区"
对比 char s1[] = "hello"; char *s2 = "hello";
是什么 数组(6 格,自己有一块内存) 指针(指向字面量所在只读区)
sizeof 6 4 或 8(指针大小)
能不能改内容 ✅ s1[0] = 'H'; 合法 ❌ s2[0] = 'H'; 运行时崩溃
能不能改指向 ❌ 数组名不能改 ✅ s2 = s1; 合法
用 cout 打印 打印整串 打印整串

为什么 s2[0] = 'H'; 会崩?因为 "hello" 这种字面量被放在程序的只读数据区(想成"刻在石头上的字")。s2 只是指着它,改写就是往石头上刻字——操作系统直接拦住(段错误 / access violation)。

一句口诀:想改内容,用数组;只想读,用指针。

小知识:其实 C++ 更推荐写 const char *s2 = "hello";——把"只读"这件事写进类型里(7.4.5 的 const 安全带),这样 s2[0] = 'H'; 会在编译期就报错,而不是运行时崩溃。

四、实例11:通过指针连接两个字符数组

教材在这一节要求"用指针连接两个字符数组"——也就是亲手实现一遍 strcat:

#include <iostream>
using namespace std;

void myStrcat(char *dest, const char *src) {
    while (*dest) dest++;           // ① 先走到 dest 的末尾('\0' 处)
    while (*src)  *dest++ = *src++; // ② 把 src 逐个搬过来
    *dest = '\0';                   // ③ 收尾:补上结束哨兵
}

int main() {
    char s1[30] = "Hello, ";
    char s2[]   = "World!";

    myStrcat(s1, s2);
    cout << s1 << endl;             // Hello, World!
    cout << "长度 = " << (int)strlen(s1) << endl;   // 13
    return 0;
}

看到什么算成功:Hello, World!、长度 = 13。

三行核心代码,逐行看:

步骤 代码 干什么
① while (*dest) dest++; 把 dest 指针推到自家字符串的末尾(停在 '\0' 那一格)
② while (*src) *dest++ = *src++; 逐个搬字符:取 src 一格 → 放进 dest 一格 → 两个指针各走一格
③ *dest = '\0'; 手动补结束哨兵——不补的话,新字符串没有结尾,后面全乱

② 里那一行值得多念两遍:*dest++ = *src++; 是 C++ 里最经典的"指针搬运",它一口气做了:取值、赋值、两个指针各挪一格(7.5.1 的 *p++ 在这里双份上场)。

30 秒实验:把 ③ 那行 *dest = '\0'; 注释掉,重跑——cout << s1 会打印出额外的一串乱码。因为字符串没有结束哨兵,cout 只能一直往后读,把 s1 后面的内存也当成字符串打印出来了。这就是"漏掉 '\0' 的后果"。

五、常见误区

  1. "char *s = "hello"; 和 char s[] = "hello"; 等价"——完全不同。前者指向只读区(不能改),后者是能改的数组。改前者的内容是运行时崩溃。
  2. "cout << p 会打印指针地址"——对 char * 不成立。它会打印字符串内容。想看地址要转成 (void*)p。
  3. "靠 strlen 才能遍历字符串"——不用。while (*p) 就够了,靠 '\0' 自动停,这也是 7.3 那四个函数的内部写法。
  4. "连接字符串时忘了补 '\0' 没关系"——关系极大。没有结束哨兵的字符数组不是字符串,任何字符串函数都会一路越界读下去。
  5. "char *s 一定能当字符串用"——只有指向以 '\0' 结尾的内容时才是。指向一个没有 '\0' 的普通字符数组,同样会越界。
自测三问(点开对答案) 1. 为什么 `while (*p)` 能自动停在字符串结尾?——`'\0'` 的 ASCII 值是 0,条件为假,循环自动结束。 2. `char *s = "hello"; s[0] = 'H';` 会怎样?——**运行时崩溃**。`s` 指向只读的字符串常量区。 3. 想改字符串内容,应该怎么声明?——`char s[] = "hello";`(数组),或者 `char s[20] = "hello";`

相关笔记