字符串的暗礁:字面量不可修改、缺少空字符终止、缓冲区溢出

大家好,我是你们的老朋友。今天咱们聊聊字符串——C语言里最常用、也最容易翻车的地方。

字符串这东西,看着简单,其实暗礁密布。我做了十几年嵌入式开发,见过太多因为字符串出问题的bug。有的让设备死机,有的让数据错乱,有的甚至让整个系统崩溃。说白了,C语言的字符串就是一块充满陷阱的沼泽地。

嗯,咱们今天就把这些暗礁一个一个挖出来,看看它们到底藏在哪儿。

暗礁一:字符串字面量不可修改

先看一个最常见的坑。

char *p = "hello";
p[0] = 'H';  // 运行时崩溃!

为什么会这样?

因为 "hello" 是字符串字面量,它存储在只读数据段。你试图修改它,就是往只读内存里写数据——操作系统会直接给你一个段错误。

我在项目中遇到过类似的情况。有一次调试一个通信协议解析模块,发现只要收到特定报文,系统就重启。查了两天才发现,是有人把配置字符串直接当字面量传进去,然后在函数里改了它。

警告:字符串字面量是 const char* 类型,不是 char*。虽然编译器不一定报错,但运行时一定会出问题。

正确的做法是什么?

char str[] = "hello";  // 可修改的数组
str[0] = 'H';          // 没问题

或者用 const 明确声明:

const char *p = "hello";  // 明确告诉编译器:这是只读的
个人习惯:我写代码时,只要是指向字符串字面量的指针,一律加 const。这样编译器能帮我检查,避免不小心修改。

暗礁二:字符串缺少空字符终止

C语言的字符串,本质上就是一个以 '\0' 结尾的字符数组。没有这个终止符,字符串就不是字符串,而是一堆乱码。

看这个例子:

char buf[5] = {'H', 'e', 'l', 'l', 'o'};
printf("%s", buf);  // 输出:Hello???... 后面跟着一堆垃圾

为什么?因为 buf 没有 '\0' 结尾。printf 会一直读下去,直到在内存中碰到一个 '\0' 为止。你永远不知道它会读到什么。

我曾经调试过一个串口打印乱码的问题。排查到最后,发现是协议栈里一个字符串缓冲区初始化时忘了清零,导致字符串末尾没有终止符。打印出来的数据后面总是跟着一些奇怪的字节。

正确的做法:

char buf[6] = {'H', 'e', 'l', 'l', 'o', '\0'};  // 手动加终止符
// 或者更简单:
char buf[] = "hello";  // 编译器自动加 '\0'
关键点:使用字符串函数(strlen、strcpy、strcat 等)时,一定要确保字符串以 '\0' 结尾。否则这些函数会越界读取,结果不可预测。

暗礁三:strcpy 和 strcat 的缓冲区溢出

这是C语言字符串操作里最臭名昭著的陷阱。没有之一。

char dest[10];
char *src = "This is a very long string that will overflow the buffer";
strcpy(dest, src);  // 缓冲区溢出!

strcpy 不会检查目标缓冲区的大小。它只管把源字符串一个一个字节地复制过去,直到遇到 '\0'。如果源字符串比目标缓冲区长,它就会写到缓冲区之外的内存里。

后果是什么?

  • 覆盖其他变量的值
  • 破坏函数返回地址(经典的栈溢出攻击)
  • 程序崩溃
  • 安全漏洞

strcat 也一样:

char buf[10] = "Hello";
strcat(buf, " World!!!");  // 溢出!

它会在 buf 的末尾追加字符串,同样不检查空间够不够。

警告:strcpy 和 strcat 是C语言标准库中公认的危险函数。很多安全编码规范都明确禁止使用它们。

那怎么办?用带长度限制的版本:

char dest[10];
char *src = "This is a very long string";
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';  // 确保终止

strncat 也一样:

char buf[10] = "Hello";
strncat(buf, " World!!!", sizeof(buf) - strlen(buf) - 1);
避坑指南:我曾经在一个嵌入式项目里,因为 strcpy 溢出导致整个系统的任务调度表被破坏。从那以后,我写代码时只要涉及字符串复制,一律用 snprintf 替代 strcpy/strcat。snprintf 会保证不溢出,而且用法更灵活。
char dest[10];
snprintf(dest, sizeof(dest), "%s", src);  // 安全,不会溢出

知识体系总览

下面这张图总结了字符串操作的三大陷阱和对应的规避方法:

字符串操作三大陷阱与规避方法 字符串陷阱 字面量不可修改 用数组存储可修改字符串 使用 const 声明只读指针 缺少 '\0' 终止 手动添加 '\0' 终止符 用字符串字面量初始化 缓冲区溢出 使用 strncpy/strncat 用 snprintf 替代 核心原则:明确所有权,检查边界,使用安全函数

总结一下

字符串操作看似简单,但三个陷阱个个致命:

陷阱 表现 规避方法
字面量不可修改 运行时崩溃 用数组存储,或用 const 声明
缺少 '\0' 终止 乱码、越界读取 确保缓冲区以 '\0' 结尾
缓冲区溢出 数据破坏、崩溃、安全漏洞 使用带长度限制的函数

说实话,这些坑我几乎都踩过。每次踩完都告诉自己:下次一定注意。但人嘛,总有疏忽的时候。所以最好的办法,就是养成好习惯——写代码时多问自己一句:这个字符串从哪里来?到哪里去?缓冲区够不够大?

嗯,今天就聊到这儿。记住一句话:字符串操作,安全第一。


公众号:蓝海资料掘金营,微信deep3321