什么是野指针
指向一块无效内存、或指向已不再归你使用的内存的指针,且程序无法察觉这一点。
关键在于"无法察觉"。NULL 不是野指针——对 NULL 解引用会立刻段错误,这是响亮的失败;野指针是沉默的失败:它可能指向已释放的堆、指向从未初始化的栈槽、指向已经退出的函数的栈帧……解引用它时程序不报错,只是悄悄地读写了不该碰的内存。
三者对比:
| 指针状态 |
解引用后果 |
可检测性 |
NULL |
立刻 SIGSEGV |
极易定位 |
| 野指针 |
可能正常、可能踩内存、可能崩溃 |
难 |
| 有效指针 |
符合预期 |
— |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
高地址
┌─────────────────────────────────────┐
│ │
│ 内核空间 (kernel) │
│ │
├─────────────────────────────────────┤ 0x7fff... (栈顶附近)
│ │
│ 栈 (stack) │ ← 局部变量、函数参数
│ ↓ 向下增长 ↓ │ 自动存储期 (auto)
│ │
│ ... 空闲区 ... │
│ │
│ ↑ 向上增长 ↑ │
│ 堆 (heap) │ ← malloc / new 分配的内存
│ │
├─────────────────────────────────────┤
│ .bss (未初始化数据段) │ ← 初值为0/无初值的全局、静态变量
│ │ 启动时清零
├─────────────────────────────────────┤
│ .data (已初始化数据段) │ ← 有非零初值的全局、静态变量
├─────────────────────────────────────┤
│ .rodata (只读数据段) │ ← 字符串字面量 "abc"、const 常量
├─────────────────────────────────────┤
│ .text (代码段) │ ← 机器指令 (程序代码)
│ │ 只读、可共享
└─────────────────────────────────────┘
低地址
|
1
2
3
4
5
|
int *p = NULL;
printf("%p\n", (void*)p); // 打印(nil),打印指针的值,空指针指向的地址也就是0
printf("%p\n", (void*)&p); // 打印 0x7ffe940cc0b0,这是这个指针变量自己的地址
free(p); //free 空指针没问题,free()中会判断空指针后return
printf("%d\n",*p);//对空指针解引用->Segmentation fault
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
void mmmm(){
int a = 0;
int b = 0;
int c = 0;
printf("%p\n",&a);
printf("%p\n",&b);
printf("%p\n",&c);
}
void foo(){
int *f;
printf("野指针f的地址:%p",&f);
printf("野指针f的值:%p",f);
}
int main(void) {
mmmm();
foo();
return 0;
}
:~$ ./a.out
0x7ffe3649c69c
0x7ffe3649c6a0
0x7ffe3649c6a4
野指针f的地址:0x7ffe3649c6a0
野指针f的值:(nil)
//把abc 的值改为0xffffffff;
0x7ffd32c32dfc
0x7ffd32c32e00
0x7ffd32c32e04
野指针f的地址:0x7ffd32c32e00
野指针f的值:0xffffffffffffffff
|
如上,未初始化的指针,栈上给其分配内存时,没有初始化的话,它存的值,也就是它指向的地址的值,取决于栈上那块位置的值;如上,前面的函数mmmm()调用完后,栈上了还留有脏数据,新的函数f00() 创建的野指针的值,变成了栈上的脏值,它是不确定的,也就导致野指针指向了不确定的地址,解引用这个指针,极有可能访问到非法地址,导致程序崩溃。
野指针的成因
未初始化的指针
指针变量声明后若不初始化,它的值是栈上的垃圾值——上一个函数留在那里的位模式。编译器不会报错(C 允许"读未初始化变量"只是未定义行为),运行时也不会报错,只有解引用的那一刻才摊牌。
1
2
3
4
5
6
7
|
int main(void)
{
int *p; /* 栈上残留的位模式,纯属随机 */
if (condition())
*p = 42; /* 写向一个随机地址 —— 野指针 */
return 0;
}
|
悬垂指针(dangling pointer)
指针曾经有效,但它指向的内存已经失效。失效有三种典型途径:
free 之后继续用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = malloc(sizeof(int)); // 申请堆内存
*p = 42;
printf("释放前: *p = %d, p = %p\n", *p, (void *)p);
free(p); // 释放,但 p 还留着旧地址
// p 现在是悬垂指针!
printf("释放后: p = %p(地址没变,但已失效)\n", (void *)p);
printf("*p = %d\n", *p); // UB:读已释放内存
p = NULL; // ← 正确做法:释放后置空,避免悬垂
return 0;
}
:~$ ./a.out
释放前: *p = 42, p = 0x6180834242a0
释放后: p = 0x6180834242a0(地址没变,但已失效)
*p = 403190820
|
free(p) 只释放了内存,没有改变 p 里存的地址值。所以 p 还指着那块旧地方,但那地方已经不属于你了。
函数返回了栈地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
#include <stdio.h>
int *get_ptr(void) {
int x = 42;
printf("局部变量x的地址:%p\n",&x);
return &x;// 返回局部变量地址
} // ← 函数返回,x 的栈空间被"释放"
int main(void) {
int *p = get_ptr(); // p 拿到的是已经失效的栈地址 → 悬垂指针
printf("p 的值(旧地址): %p\n", (void *)p);
// 此时 x 的栈空间可能已被覆盖
printf("*p = %d\n", *p); // UB:可能读到 42,也可能读到垃圾,也可能崩
return 0;
}
:~$ gcc x.c
x.c: In function ‘get_ptr’:
x.c:6:12: warning: function returns address of local variable [-Wreturn-local-addr]
:~$ ./a.out
局部变量x的地址:0x7ffcc0cd36b4
p 的值(旧地址): (nil)
Segmentation fault (core dumped)
|