C 没有「字符串类型」,这个事实是本章所有故事的起点。所谓字符串,就是一段以 '\0' 结尾的 char 数组;而「指针与字符串」的组合,贡献了 C 语言史上最多的「能编译、能运行、会崩」三连。本章从一个我带新生时每年都要救一次的事故开始:改一个字符串字面量。原文小节编号错成 5.x,一并修正。
char *s = "hello";
s[0] = 'H'; // 编译通过,运行段错误
学生最懵的点:编译器一个警告都不给(老版本),运行就崩。真相:"hello" 是字符串字面量,编译器把它放进只读段(.rodata,第 2 章的五段地图),s 只是个指向它的指针;写只读段,MMU 拦下,SIGSEGV(第 2 章亲手触发过)。修法两步走:语义上用 const 把「别改」写进类型,让编译器当场拦截:
const char *s = "hello"; // s[0] = 'H' 直接编译报错
这个坑的姐妹版是 6.2 的对子,每年面试必考。
char *s = "hello"; // s:指针,指向只读段里的字面量
char t[] = "hello"; // t:数组,把字面量拷贝进栈上 6 字节(含 '\0')
char *s = "hello" | char t[] = "hello" | |
|---|---|---|
| 内存 | s(栈上 8 字节)→ .rodata 里的 6 字节 | 栈上实打实 6 字节副本 |
| 能改吗 | 不能(写只读段,UB) | 能,随便改 |
| sizeof | 8(指针大小) | 6(整个数组) |
| 生命周期 | 字面量全程活着 | 跟随所在函数 |
一行分辨法:右边有方括号是「复制一份给自己」,没有是「只记个地址」。要修改就用 char t[];只读引用(传参、存常量)用 const char *。两个都合法,场景不同,混用的代价就是 6.1 的段错误。
<string.h> 全家桶没有长度参数,全靠扫到 '\0' 才知道结束:
strlen(s) // 数到 '\0' 为止,O(n) 而非 O(1)!长度信息不存在,每次都现数
strcpy(d, s) // 连 '\0' 一起拷;前提:d 足够大,它不检查
strcat(d, s) // 从 d 的 '\0' 处接着写;同样的前提
strcmp(a, b) // 字典序,返回负/零/正(判断相等用 == 0,别拿返回值当布尔)
危险集中在 strcpy/strcat:不检查目标大小,源一长就写穿缓冲区。这不是纯学术话题,是史上最著名的安全漏洞家族(缓冲区溢出,第 14 章有专门一节,当年 Morris 蠕虫、无数 CVE 的主角)。安全姿势:
char buf[16];
snprintf(buf, sizeof buf, "%s-%s", a, b); // 永远按 sizeof 写,截断但不越界
(strncpy 也在名单上但有个自己的坑:源太长时不补 '\0',后续 strlen 找不到终点照样跑飞,不如 snprintf 干净。)
按分隔符切串,行为有两个反直觉点,都值得单独说:
char buf[] = "a,b,c"; // 必须是可写数组(理由见陷阱一)
char *tok = strtok(buf, ","); // 第一次传原串
while (tok) {
puts(tok);
tok = strtok(NULL, ","); // 后续传 NULL:继续上次的进度
}
// 输出三行:a b c
陷阱一:它改写原串。找到分隔符时,直接把那字节替换成 '\0'(就地切分)。所以传 char *p = "a,b,c" 进去等于往只读段写,当场崩。陷阱二:不可重入。上次切到哪,靠函数内部一个 static 变量记着(所以第二次调用传 NULL),嵌套解析两个串、或者多线程同时用,位置互相踩。需要时可重入版 strtok_r(状态由调用者自己保管)。这个函数是「static 变量的方便与代价」的活教材。
每个 C 学习者都该写一次自己的 strlen,它是「指针遍历」的最小完备样本:
size_t my_strlen(const char *s) {
const char *p = s;
while (*p) p++; // *p 非 0 就前进;'\0' 停
return (size_t)(p - s); // 指针相减 = 元素个数(第 3 章)
}
四行代码覆盖了本章三个知识点:*p 解引用判断、指针自增(步长 1)、指针相减。参数用 const char * 承诺「我只读」,这个习惯能让你免于 6.1 那类事故,也让调用者放心传字面量进来。
if (strcmp(a, b)) 是「不相等」(返回非零),写成「相等才执行」逻辑就反了;明确写 == 0。strlen(s) - 5 在短串时不会得负数,而是回绕成天文数字,循环直接越界。减法前先比大小。char t[] 版再跑,验证能改;最后打印 sizeof(s)、sizeof(t)、strlen(s) 三个值,对齐 6.2 的表。my_strcat(char *d, const char *s)(先把 d 走到 '\0',再逐字拷),再写一个带长度上限的安全版 my_strncat(d, s, n),保证不写穿。char buf[8] = "hello" 里 sizeof(buf) 和 strlen(buf) 各是多少?若执行 strcat(buf, "world") 会发生什么(数一数需要几个字节)?