指针声明与初始化:野指针的成因、空指针与悬空指针的区别、指针的算术运算

指针这东西,说白了就是一把双刃剑。用好了,你能在内存里自由穿梭;用不好,程序崩溃、数据错乱、内存泄漏,全来了。我刚开始学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 都返回了错误地址。那次排查花了我整整一个下午。

避坑指南: 每次 free/delete 之后,立刻把指针置为 nullptr。这是 C/C++ 程序员的基本素养。我习惯写一个宏:#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 制造者。指针算术运算以类型大小为步长,只在数组范围内安全。

嗯,这些概念说起来简单,但真正在项目里遇到问题的时候,往往就是这些基础没打牢。我每次带新人,都会让他们先把指针这块吃透。因为后面所有的内存管理、数据结构、甚至多线程编程,都离不开对指针的正确理解。

指针声明与初始化知识体系 指针声明与初始化 声明方式 int *p; void *r; 野指针 未初始化 随机地址 未定义行为 空指针 vs 悬空指针 空指针 (nullptr) 悬空指针 (dangling) 指针算术运算 p++ / p-- p + n / p - n 步长 = sizeof(类型)

可可精品资料库