第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把字面量拷贝到数组时,如果数组空间不够,就会发生缓冲区溢出。

⚠️ 警告: 字符数组的大小必须 >= 字符串长度 + 1(结尾的'\0')。少一个字节都不行!

我曾经见过一个新手,他这样写:

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,并指定最大拷贝长度。

💡 提示: 使用strncpy时,记得手动添加'\0'。因为strncpy在源字符串长度 >= n时,不会自动添加结尾的'\0'。

我曾经在一个嵌入式项目中,因为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,方便你理解:

字符串越界知识体系 字符串字面量与字符数组 strcpy/strcat 函数 C++ std::string 数组大小必须 ≥ 字符串长度 + 1 '\0' 结尾容易被忽略 栈上分配,空间有限 不检查缓冲区大小 strncpy/strncat 更安全 记得手动添加 '\0' c_str() 返回只读指针 at() 比 operator[] 安全 自动管理内存,但非万能 核心原则:永远不要假设缓冲区足够大 检查长度、使用安全函数、优先使用std::string

避坑指南

我曾经在代码审查中看到过这样的写法:

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标准引入的函数,大多数编译器都支持。

⚠️ 警告: snprintf的返回值是“如果缓冲区足够大,应该写入的字符数”,而不是实际写入的字符数。如果返回值 >= sizeof(buffer),说明数据被截断了。

最后,我想说一句:字符串越界的问题,说白了就是“你以为的”和“实际上的”之间的差距。你以为缓冲区够大,实际上不够;你以为字符串不会变长,实际上变了。所以,养成检查长度的习惯,使用安全的函数,你的代码会少很多坑。

好了,这一章的内容就到这里。记住,字符串越界不是小事,它可能导致数据损坏、程序崩溃,甚至安全漏洞。下一章我们会继续深入探讨指针越界的其他场景。

可可精品资料库