第 6 章 · 指针与字符串

C 没有字符串类型,字符串 = 以 '\0' 结尾的 char 数组;指针 + 字符串有一堆「能编译但会崩」的坑。

5.1 字符串字面量在只读段

char *s = "hello";

"hello" 是字符串字面量,编译器放在只读段(.rodata);s 只是指向它的指针。改 *s 是 UB(现代系统直接 SIGSEGV):

s[0] = 'H';    // 未定义行为,通常段错误

正确的只读用法:用 const 把「别改」写进类型里,让编译器帮你拦住:

const char *s = "hello";   // s[0] = 'H' 会直接编译报错

5.2 char* vs char[]:一个最经典的坑

char *s = "hello";       // s 指向只读段里的字面量,6 字节在 .rodata
char  t[] = "hello";     // t 是栈上/全局的可写数组,6 字节拷贝到这里
char *s = "hello"char t[] = "hello"
内存位置只读段(字面量)可写(栈或全局)
能否修改不能(UB)合法
sizeof8(指针)6(含 '\0'
本质指针指向字面量数组,内容拷自字面量
t[0] = 'H';   // 合法
s[0] = 'H';   // UB

5.3 字符串库函数:全靠 '\0' 找终点

<string.h> 里的函数没有长度参数,靠末尾的 '\0' 判断结束——这既是便捷,也是危险的根源:

strlen(s)     // 返回 '\0' 之前的字符数(不含 '\0'),O(n)
strcpy(d,s)   // 把 s 拷到 d,包括 '\0';d 必须足够大
strcat(d,s)   // 把 s 拼到 d 末尾;d 必须足够大
strcmp(a,b)   // 逐字符比字典序,返回负数/0/正数
  • strcpy/strcat 不检查目标缓冲区大小,源太长就缓冲区溢出——这是历史上无数安全漏洞的根源(见 13 章)。
  • 安全的替代品:strncpysnprintf(但 strncpy 不保证补 '\0',有它自己的坑)。

5.4 strtok 的两个陷阱

strtok 按分隔符切字符串,但有两个反直觉的行为:

  1. 会改写原串:它把找到的分隔符替换成 '\0',所以不能传只读的字符串字面量。
  2. 不可重入:内部用 static 保存位置,不能嵌套解析,也不能在多线程里用。
char buf[] = "a,b,c";
char *tok = strtok(buf, ",");   // 第一次传原串
while (tok) {
    puts(tok);
    tok = strtok(NULL, ",");    // 后续传 NULL
}
  • char *p = "a,b,c"strtok 会崩(它要写 '\0')。
  • 多线程/嵌套用 strtok_r(可重入,需自备状态指针)。

5.5 字符指针 vs 字符数组作为参数

函数里 char*char[] 都退化成 char*,分不清「只读字面量」还是「可写数组」;只读的函数参数要写 const char*

size_t my_strlen(const char *s) {   // 承诺不修改
    size_t n = 0;
    while (*s++) n++;
    return n;
}

5.6 与汇编 / 其他语言对照

  • 汇编strlen 常被编译成 repne scasb(重复扫描找 0)或 SIMD 批量比较——因为要扫到 '\0',所以字符串长度是 O(n) 而非 O(1)。
  • JavaString 是不可变对象,长度字段缓存 O(1);「字符串」和「字符数组」是两个类型,编译期就隔离了 C 的坑。
  • C++std::string 管理堆内存、自带长度、自动扩容,c_str() 返回的 const char* 仍要求你不改。
  • Pythonstr 不可变、内部统一 Unicode,bytes 才对应 C 的字节串。

① 字面量在只读段,想改就存进数组;② char*/char[] 内存位置不同;③ 字符串函数靠 '\0' 找终点,缓冲区溢出是头号风险。