C 没有字符串类型,字符串 = 以 '\0' 结尾的 char 数组;指针 + 字符串有一堆「能编译但会崩」的坑。
char *s = "hello";
"hello" 是字符串字面量,编译器放在只读段(.rodata);s 只是指向它的指针。改 *s 是 UB(现代系统直接 SIGSEGV):
s[0] = 'H'; // 未定义行为,通常段错误
正确的只读用法:用 const 把「别改」写进类型里,让编译器帮你拦住:
const char *s = "hello"; // s[0] = 'H' 会直接编译报错
char* vs char[]:一个最经典的坑char *s = "hello"; // s 指向只读段里的字面量,6 字节在 .rodata
char t[] = "hello"; // t 是栈上/全局的可写数组,6 字节拷贝到这里
char *s = "hello" | char t[] = "hello" | |
|---|---|---|
| 内存位置 | 只读段(字面量) | 可写(栈或全局) |
| 能否修改 | 不能(UB) | 合法 |
sizeof | 8(指针) | 6(含 '\0') |
| 本质 | 指针指向字面量 | 数组,内容拷自字面量 |
t[0] = 'H'; // 合法
s[0] = 'H'; // UB
'\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 章)。strncpy、snprintf(但 strncpy 不保证补 '\0',有它自己的坑)。strtok 的两个陷阱strtok 按分隔符切字符串,但有两个反直觉的行为:
'\0',所以不能传只读的字符串字面量。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(可重入,需自备状态指针)。函数里 char* 和 char[] 都退化成 char*,分不清「只读字面量」还是「可写数组」;只读的函数参数要写 const char*:
size_t my_strlen(const char *s) { // 承诺不修改
size_t n = 0;
while (*s++) n++;
return n;
}
strlen 常被编译成 repne scasb(重复扫描找 0)或 SIMD 批量比较——因为要扫到 '\0',所以字符串长度是 O(n) 而非 O(1)。String 是不可变对象,长度字段缓存 O(1);「字符串」和「字符数组」是两个类型,编译期就隔离了 C 的坑。std::string 管理堆内存、自带长度、自动扩容,c_str() 返回的 const char* 仍要求你不改。str 不可变、内部统一 Unicode,bytes 才对应 C 的字节串。① 字面量在只读段,想改就存进数组;②
char*/char[]内存位置不同;③ 字符串函数靠'\0'找终点,缓冲区溢出是头号风险。