指针声明与初始化:野指针的成因、空指针与悬空指针的区别、指针的算术运算
指针这东西,说白了就是一把双刃剑。用好了,你能在内存里自由穿梭;用不好,程序崩溃、数据错乱、内存泄漏,全来了。我刚开始学C/C++那会儿,没少被指针坑过。今天咱们就把指针声明、初始化,以及那些让人头疼的野指针、空指针、悬空指针,一次性讲清楚。
指针声明:你声明的是地址,不是值
先看最基本的声明方式:
int *p; // p 是一个指向 int 的指针
char *q; // q 是一个指向 char 的指针
void *r; // r 是一个无类型指针,可以指向任何类型
这里有个细节,我当年就犯过迷糊:int* p, q; 你以为 p 和 q 都是指针?错!只有 p 是指针,q 是普通 int 变量。正确的写法是 int *p, *q;。嗯,星号要紧挨着变量名,或者每个变量前都加星号,别偷懒。
野指针的成因:没初始化就敢用?
野指针,就是指向了「不知道哪里」的指针。它的值是一个随机地址,你解引用它,等于在乱闯内存。
最常见的成因就一个:声明了指针,但没有给它赋值。
int *p; // 野指针诞生了!p 的值是随机的
*p = 42; // 危险!你正在往一个未知地址写入数据
我在项目中遇到过这样的场景:一个同事写了个函数,里面声明了一个指针,没初始化就直接用了。结果程序在开发机上跑得好好的,一上生产环境就随机崩溃。查了两天才发现,开发机的内存布局恰好让那个随机地址是可写的,生产环境就没那么幸运了。
怎么避免?简单:声明指针的同时就初始化。如果暂时不知道指向哪里,就初始化为 nullptr(C++11 以后)或 NULL(C 语言)。
int *p = nullptr; // 安全,明确指向空
int *q = NULL; // C 风格,等价于 0
空指针与悬空指针:一字之差,天壤之别
这两个概念,初学者特别容易搞混。我当年也花了点时间才理清楚。
| 类型 | 定义 | 成因 | 解引用后果 |
|---|---|---|---|
| 空指针 | 指向地址 0(或 nullptr)的指针 | 主动初始化为 NULL/nullptr | 通常立即崩溃(段错误),容易定位 |
| 悬空指针 | 指向已释放/无效内存的指针 | free/delete 后未置空,或返回局部变量地址 | 可能崩溃,也可能静默产生错误数据,极难排查 |
空指针其实不可怕。你想想看,它明确告诉你「我这里没有有效数据」。你只要在解引用前检查一下:
if (p != nullptr) {
*p = 42; // 安全
}
悬空指针才叫阴险。它指向的内存曾经有效,但现在已经被回收了。那块内存可能已经被其他变量占用,也可能被操作系统收回。你写数据进去,可能把别人的数据覆盖了;你读数据出来,可能读到完全不相干的东西。
举个例子:
int *p = (int*)malloc(sizeof(int));
*p = 100;
free(p); // 内存释放了,但 p 仍然指向那块地址
// 此时 p 就是悬空指针
*p = 200; // 危险!写入已释放的内存
我曾经就吃过这个亏。一个模块里释放了内存,但忘了把指针置空。后来另一个模块拿到这个指针,以为还能用,直接往里写数据。结果数据没写进去,还把堆管理器的元数据给破坏了,导致后续所有 malloc 都返回了错误地址。那次排查花了我整整一个下午。
#define SAFE_FREE(p) do { free(p); p = nullptr; } while(0)
指针的算术运算:别把它当普通整数
指针加减整数,结果不是简单的地址加减。它是以「指向类型的大小」为步长来移动的。
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // p 指向 arr[0]
p++; // p 现在指向 arr[1],地址增加了 sizeof(int) 字节
p += 2; // p 现在指向 arr[3],地址又增加了 2 * sizeof(int) 字节
printf("%d\n", *p); // 输出 40
为什么不是直接加 1 个字节?因为指针的类型告诉编译器,它指向的数据占多大空间。你想想看,如果 p 是 char*,p++ 只移动 1 个字节;如果是 int*,移动 4 个字节(32位系统)。这样设计才合理,不然你每次移动后还得手动计算偏移量。
指针减法也是类似的:
int *q = &arr[4]; // q 指向 arr[4]
int diff = q - p; // diff 等于 1,因为 q 和 p 之间隔了 1 个 int 元素
这里有个容易踩的坑:指针减法得到的是元素个数,不是字节数。如果你需要字节数,要乘以 sizeof(类型)。
我记得有一次,一个同事在循环里用指针遍历数组,结果循环条件写错了,指针多走了两步。程序没崩溃,但输出的结果里混进了栈上其他变量的值。那个 bug 藏了三天才被发现,因为每次运行结果都不一样——栈上的数据会随着函数调用而变化。
所以我的建议是:能用下标访问就别用指针算术。下标访问更直观,也更容易检查边界。只有在性能敏感的场景(比如图像处理、音视频编解码)才考虑用指针算术来优化。
小结一下
指针声明时一定要初始化,要么指向有效对象,要么指向 nullptr。野指针是未定义行为的根源,空指针是安全的「无效状态」,悬空指针是最阴险的 bug 制造者。指针算术运算以类型大小为步长,只在数组范围内安全。
嗯,这些概念说起来简单,但真正在项目里遇到问题的时候,往往就是这些基础没打牢。我每次带新人,都会让他们先把指针这块吃透。因为后面所有的内存管理、数据结构、甚至多线程编程,都离不开对指针的正确理解。
可可精品资料库