字符串的「陷阱」:字符串操作中的越界风险
字符串操作,看起来简单,实际上是最容易出问题的地方之一。我见过太多项目,就因为一个 strcpy 或者 gets,线上服务直接崩溃。说白了,C 语言的字符串就是个字符数组,没有边界检查,全靠程序员自己盯着。
今天咱们就聊聊这几个「老朋友」——strcpy、sprintf、gets,看看它们到底怎么坑人的,以及怎么避开这些坑。
1. strcpy:最经典的越界元凶
strcpy 这个函数,我估计每个 C 程序员都用过。它的作用是把一个字符串复制到另一个缓冲区里。但问题是,它根本不检查目标缓冲区够不够大。
char dest[10];
char *src = "Hello, this is a very long string!";
strcpy(dest, src); // 缓冲区只有10字节,源字符串远超10字节
这段代码一跑,dest 后面的内存就被覆盖了。轻则程序崩溃,重则被黑客利用执行恶意代码。我在项目中遇到过类似的情况,一个同事用 strcpy 处理用户输入,结果线上服务频繁段错误,查了半天才发现是这里出了问题。
strcpy 处理不可信数据。如果非要用,请确保目标缓冲区足够大。
那怎么办?我个人的习惯是:
- 用
strncpy代替strcpy,指定最大复制长度 - 或者直接用
snprintf,更安全
char dest[10];
char *src = "Hello, this is a very long string!";
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0'; // 手动加结束符
嗯,这里要注意:strncpy 不会自动加 \0,如果源字符串长度大于等于目标缓冲区大小,它就不加结束符了。所以一定要手动补上。
2. sprintf:格式化输出的隐藏炸弹
sprintf 也是个老熟人。它把格式化后的字符串写入缓冲区,同样不检查长度。你想想看,如果格式化后的内容比缓冲区大,那后果跟 strcpy 一样。
char buf[50];
int id = 12345;
char *name = "ThisIsAVeryLongUserNameThatExceedsBufferSize";
sprintf(buf, "User: %s, ID: %d", name, id); // 格式化后远超50字节
我曾经在日志模块里用过 sprintf,结果日志内容太长,直接把栈上的返回地址给覆盖了。程序崩溃后,调试器都定位不到问题在哪。后来我学乖了,一律用 snprintf。
snprintf 代替 sprintf,指定缓冲区大小。它会自动截断,不会越界。
char buf[50];
int id = 12345;
char *name = "ThisIsAVeryLongUserNameThatExceedsBufferSize";
snprintf(buf, sizeof(buf), "User: %s, ID: %d", name, id);
这样写,就算内容超长,也只会截断,不会破坏内存。安全第一嘛。
3. gets:标准库里的「毒瘤」
gets 这个函数,我建议你直接忘掉它。它从标准输入读取一行,存到缓冲区里,但完全不限制长度。输入多长它就存多长,缓冲区溢出是必然的。
char buf[100];
gets(buf); // 如果用户输入超过100字节,直接溢出
为什么标准库会留这么个函数?说实话,历史原因。早期 C 标准里就有它,后来发现太危险,C11 标准直接把它删了。但很多老代码还在用,或者编译器为了兼容性还保留着。
gets!永远不要!如果你在代码审查中看到 gets,直接打回。
替代方案很简单:用 fgets。
char buf[100];
if (fgets(buf, sizeof(buf), stdin) != NULL) {
// 处理输入
buf[strcspn(buf, "\n")] = '\0'; // 去掉换行符
}
fgets 会限制读取长度,不会越界。而且它会把换行符也读进来,所以需要手动去掉。这个细节很多人会忽略。
4. 知识体系总览
下面这张图,是我整理的字符串操作越界风险的核心逻辑。你看一眼,基本就明白怎么回事了。
5. 避坑指南
我曾经在代码审查中看到过各种花式越界。总结下来,有这么几条铁律:
- 不用
gets——这个没得商量,直接 ban 掉 - 用
snprintf代替sprintf——多写几个参数,换来安全 - 用
strncpy或strlcpy代替strcpy——记得手动加\0 - 所有字符串操作,先算好缓冲区大小——别偷懒,多算一步
- 用静态分析工具扫描——比如
cppcheck、clang-tidy,能自动发现这类问题
核心原则:永远假设输入数据是不可信的、长度不可控的。写代码时多问自己一句:「如果这里输入超长,会怎么样?」
好了,字符串的陷阱就聊到这儿。记住,安全不是靠运气,是靠习惯。养成好习惯,越界问题就能少一大半。
可可精品资料库