C 野指针

什么是野指针

指向一块无效内存、或指向已不再归你使用的内存的指针,且程序无法察觉这一点。

关键在于"无法察觉"。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)
Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 Stack 由 Jimmy 设计