字符数组名也是首地址,所以可以用指针走它。最漂亮的一点:因为字符串末尾有 '\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' 的后果"。
五、常见误区
- "
char *s = "hello";和char s[] = "hello";等价"——完全不同。前者指向只读区(不能改),后者是能改的数组。改前者的内容是运行时崩溃。 - "
cout << p会打印指针地址"——对char *不成立。它会打印字符串内容。想看地址要转成(void*)p。 - "靠
strlen才能遍历字符串"——不用。while (*p)就够了,靠'\0'自动停,这也是 7.3 那四个函数的内部写法。 - "连接字符串时忘了补
'\0'没关系"——关系极大。没有结束哨兵的字符数组不是字符串,任何字符串函数都会一路越界读下去。 - "
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";`- ☐ 7.6 指针在函数中的应用(下一节)
相关笔记
- 字符数组 —— 四个字符串函数的手写原理
- 指针与一维数组 ——
*p++的出处 - 指向常量的指针与指针常量 ——
const char *s - 布尔类型 —— "非 0 即真"是
while (*p)的根据 - 🔷 C++基础 · 索引 —— 全课路线图