第1章:字符串越界——那些年我们踩过的坑
大家好,我是你们的老朋友。今天咱们聊聊字符串越界。
说实话,字符串越界是我在C/C++开发中见过最多、也最容易翻车的问题之一。你想想看,一个简单的strcpy,一个不小心,程序就崩了。更可怕的是,有时候它不崩,只是静悄悄地破坏数据,等到你发现时,已经是一地鸡毛。
我个人习惯把字符串越界分为三类:字面量与字符数组的“亲密接触”、strcpy/strcat等函数的“暴力操作”、以及C++ std::string的“隐藏陷阱”。咱们一个一个来看。
1.1 字符串字面量与字符数组
先看一个经典例子:
char str[10];
strcpy(str, "Hello, World!"); // 越界!"Hello, World!" 有13个字符+'\0',共14字节
这段代码,说白了就是往一个只能装10个字符的盒子里塞了14个字符。结果?要么程序崩溃,要么数据被破坏。我在项目中遇到过类似的情况,当时是一个日志模块,字符串长度没控制好,结果把相邻的配置数据给覆盖了,排查了整整两天才找到原因。
为什么会这样?因为字符串字面量是存储在只读数据区的,而字符数组是在栈上分配的。当你用strcpy把字面量拷贝到数组时,如果数组空间不够,就会发生缓冲区溢出。
我曾经见过一个新手,他这样写:
char name[5] = "Alice"; // 看似没问题,但"Alice"是5个字符+'\0',共6字节
嗯,这里要注意,C语言中字符串字面量会自动在末尾添加'\0'。所以"Alice"实际占用6个字节。数组只有5个字节,'\0'就写到了数组外面。这种错误特别隐蔽,因为程序可能暂时不崩溃,但数据已经被污染了。
1.2 strcpy/strcat等函数的越界风险
strcpy和strcat是C语言中最常用的字符串操作函数,也是最容易出问题的。它们不会检查目标缓冲区的大小,只会一股脑地拷贝数据。
看这个例子:
char dest[20];
char src[] = "This is a very long string that will overflow the buffer";
strcpy(dest, src); // 越界!src的长度远超20字节
strcat也一样:
char buf[10] = "Hello";
strcat(buf, " World!"); // 越界!" World!" 有7个字符,加上"Hello"的5个,共12个,buf只有10字节
我个人习惯,只要看到strcpy和strcat,就会下意识地检查目标缓冲区的大小。如果无法确定,我会改用strncpy和strncat,并指定最大拷贝长度。
我曾经在一个嵌入式项目中,因为strcpy导致了一个严重的bug。当时是一个通信协议解析模块,接收到的数据包长度超过了预期,strcpy直接把数据写到了栈上的返回地址区域。结果程序跳转到了一个随机地址,系统直接崩溃。嗯,从那以后,我再也不敢在不确定长度的情况下使用strcpy了。
1.3 C++ std::string的越界风险
很多C++开发者觉得用了std::string就万事大吉了。其实不然,std::string虽然会自动管理内存,但依然有越界的风险。
最常见的场景是使用c_str()获取C风格字符串后,再对返回的指针进行操作:
std::string s = "Hello";
const char* p = s.c_str();
p[10] = 'A'; // 越界!p指向的缓冲区只有6个字节(包括'\0')
还有一种是使用at()和operator[]的区别:
std::string s = "Hello";
s[10] = 'A'; // 未定义行为!越界访问
s.at(10) = 'A'; // 会抛出std::out_of_range异常
我个人建议,在需要随机访问字符串元素时,优先使用at()而不是operator[]。虽然at()有性能开销,但安全第一。你想想看,一个越界访问导致的崩溃,可能比那点性能开销要严重得多。
- 字符数组的大小必须 >= 字符串长度 + 1
- strcpy/strcat不会检查缓冲区大小,尽量用strncpy/strncat替代
- std::string的c_str()返回的指针是只读的,不要修改
- 优先使用std::string::at()而不是operator[]
知识体系图
下面这张图展示了字符串越界的核心知识结构,我把它画成了SVG,方便你理解:
避坑指南
我曾经在代码审查中看到过这样的写法:
char buffer[256];
sprintf(buffer, "User: %s, Age: %d", username, age);
嗯,这里要注意,如果username的长度超过240个字符(加上其他固定字符串),buffer就会溢出。我建议改用snprintf:
char buffer[256];
snprintf(buffer, sizeof(buffer), "User: %s, Age: %d", username, age);
snprintf会限制写入的字符数,不会超过缓冲区大小。这是C99标准引入的函数,大多数编译器都支持。
最后,我想说一句:字符串越界的问题,说白了就是“你以为的”和“实际上的”之间的差距。你以为缓冲区够大,实际上不够;你以为字符串不会变长,实际上变了。所以,养成检查长度的习惯,使用安全的函数,你的代码会少很多坑。
好了,这一章的内容就到这里。记住,字符串越界不是小事,它可能导致数据损坏、程序崩溃,甚至安全漏洞。下一章我们会继续深入探讨指针越界的其他场景。