1. 指针越界初探:什么是指针越界?
指针越界,说白了就是你的程序访问了不该访问的内存地址。
我刚开始做Qt开发那会儿,总觉得指针这东西挺简单的——不就是个地址嘛。直到有一次,程序跑着跑着突然崩了,而且崩得毫无规律。有时候运行十分钟才崩,有时候一启动就崩。排查了整整两天,最后发现是一个数组越界导致的。嗯,从那以后我再也不敢小看指针越界了。
指针越界的本质
每个指针都指向一块内存区域。这块区域是有边界的。你只能在这块区域内读写数据。一旦跨过了这个边界,就是越界。
举个例子:
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
// 合法访问:p[0] 到 p[4]
// 越界访问:p[5] 或 p[-1]
arr[5] 这个位置,内存里确实有数据。但那不是你的数据。可能是别的变量的值,也可能是系统关键数据。你强行去读写,后果不可预测。
常见场景一:数组越界
数组越界是最经典的指针越界场景。我遇到过不少新手,写循环时边界条件搞错了,多访问了一个元素。
// 错误示例
int data[10];
for (int i = 0; i <= 10; i++) { // 注意:i <= 10 多了一次
data[i] = i * 2; // 当 i=10 时越界
}
// 正确写法
for (int i = 0; i < 10; i++) {
data[i] = i * 2;
}
你看,就多了一个等号,程序就出问题了。这种错误在代码审查时很容易漏掉。
我曾经接手过一个项目,里面有个数组越界bug,症状是程序运行一段时间后,某个按钮的文本突然变成了乱码。查了半天才发现,是另一个模块的数组越界写,恰好覆盖了按钮对象的成员变量。
常见场景二:字符串越界
C风格的字符串以 '\0' 结尾。很多人会忘记这个结束符占一个位置。
// 错误示例
char name[5] = "Hello"; // "Hello" 实际需要6个字符(含'\0')
// 这里已经越界了,只是编译器没报错
// 正确写法
char name[6] = "Hello"; // 或者直接用 QString
在Qt开发中,我强烈建议你直接用 QString。它内部管理内存,基本不会出现字符串越界的问题。但如果你要跟C库交互,或者处理底层数据,那就得小心了。
常见场景三:容器越界
Qt 的容器类(QList、QVector、QMap 等)提供了两种访问方式:
- operator[]:不检查边界,越界时行为未定义
- at():检查边界,越界时抛出异常
QVector<int> vec = {10, 20, 30};
// 危险写法
int val = vec[5]; // 越界!不会报错,但 val 的值是垃圾数据
// 安全写法
int val = vec.at(5); // 越界!会抛出 std::out_of_range 异常
// 更安全的做法:先检查
if (5 < vec.size()) {
int val = vec[5];
}
我个人习惯在调试阶段用 at(),这样能尽早发现越界问题。发布版本再换成 operator[] 提升性能。
指针越界的后果
越界访问的后果,取决于你踩到了什么内存:
| 越界类型 | 可能后果 |
|---|---|
| 读越界 | 读到垃圾数据,程序逻辑出错 |
| 写越界(踩到未使用内存) | 暂时没问题,但可能破坏内存池结构 |
| 写越界(踩到其他变量) | 其他变量值被篡改,出现诡异bug |
| 写越界(踩到堆管理结构) | free() 或 delete 时崩溃 |
| 写越界(踩到虚函数表) | 调用虚函数时崩溃,极难排查 |
本章知识体系
下面这张图帮你理清指针越界的整体脉络:
避坑指南
结合我多年的调试经验,给你几个实用建议:
- 写循环时多看一眼边界条件——尤其是 < 和 <= 的区别,还有 size() 返回的是元素个数,不是最大索引
- 能用 Qt 容器就别用原生数组——QVector 的 at() 方法能帮你捕获越界
- 调试阶段打开 AddressSanitizer——Qt Creator 里配置一下,越界立刻就能发现
- 别迷信「多分配几个字节就没事」——我曾经见过有人给数组多分配了100个字节来「防越界」,结果越界更严重了
QMAKE_CXXFLAGS += -fsanitize=address -fno-omit-frame-pointer,然后链接 LIBS += -fsanitize=address。这样越界时程序会直接崩溃并打印出调用栈,省去你大量排查时间。
指针越界这个话题,说深了可以写一本书。但今天我们先打好基础,搞清楚它是什么、常见在哪儿。后面的章节,我会带你一步步深入,从调试到修复,把这块硬骨头啃下来。