野指针

  无情的搬砖机器= =

一、野指针由来

1.1 野指针概念(简述)

概述:“野指针”不是NULL指针,是指向“垃圾”内存的指针。(内存能不能用另外一回事)

  1. (局部)指针变量没有初始化
  2. 使用已经释放后的指针
  3. 指针所指向的变量在指针之前被销毁

1.2 野指针概念(专业描述)

  • 指针未初始化
      指针变量在定义时不会自动初始化成空指针,而是随机的一个值,可能指向任意空间,这就使得该指针成为野指针。因此指针在初始化时要么指向一个合理的地址,要么初始化为NULL即使不初始化,调用指针前,一定要赋值!

  • 指针指向的变量被freedelete后没有置为NULL
      在调用free或delete释放空间后,指针指向的内容被销毁,空间被释放,但是指针的值并未改变,仍然指向这块内存,这就使得该指针成为野指针。因此在调用freedelete之后,应将该指针置为NULL

  • 指针操作超过所指向变量的生存期
      当指针指向的变量的声明周期已经结束时,如果指针仍然指向这块空间,就会使得该指针成为野指针。这种错误很难防范,只有养成良好的编程习惯,才能避免这类情况发生。

1.3 野指针的要点(简单描述)

  1. 野指针通常是因为指针变量中保存的值不是一个合法的内存地址而造成的
  2. 野指针不是NULL,是一个指向不可用内存的指针
  3. C语言中没有方法可以判断是否为野指针(可替换成NULL指针,NULL指针不容易弄错,可以通过if来判断是否为NULL指针)

1.4 野指针的要点(深度描述)

野指针只能避免而无法判断

  无法判断一个指针是否为野指针,因为野指针本身有值,指向某个内存空间,只是这个值是随机的或错误的。

Ps:空指针并非野指针,它具有特殊性和确定性,可以进行判断;因此要避免在程序中出现野指针,可以做完操作及时将指针指向NULL

野指针并非立马让系统出事

  指针也是数据,首先如果是局部的,不置空也没关系反正用不到了;如果是全局的,得用的时候释放了可能也会立马再次新赋值,如果不是那肯定需要重置为null,这也是方便你后面的判断是否需要赋值,如果你再次用不到,那么(不重置)就完全不影响程序的健壮性。但是,如果 再调该(野)指针就会可能出现问题! 有的可能比较复杂不一定开始就初始化,那你在某个地方用的时候会判断是否为空,然后给它赋值。就像很多做逻辑判断的bool,初始也会有值,如果没值,那你就看系统给的初始值,区别就是指针如果没初始化,然后对指针进行操作(调用),可能会导致崩溃。也就是说,野指针并不是直接让系统出事,而是自己无意识产生野指针但还调用的操作才是让系统出事的真正原因!

野指针的错误是严重的

  1. 指向不可访问的地址
      危害:触发段错误。

  1. 指向一个可用的,但是没有明确意义的空间
      危害:程序可以正确运行,但通常这种情况下,我们就会认为我们的程序是正确的没有问题的,然而事实上就是有问题存在,所以这样就掩盖了我们程序上的错误。

  1. 指向一个可用的,而且正在被使用的空间
      危害:如果我们对这样一个指针进行解引用,对其所指向的空间内容进行了修改,但是实际上这块空间正在被使用,那么这个时候变量的内容突然被改变,当然就会对程序的运行产生影响,因为我们所使用的变量已经不是我们所想要使用的那个值了。通常这样的程序都会崩溃,或者数据被损坏。

二、未初始化指针的神奇操作

  指针未初始化,系统一般会自动分配内存给未初始化的指针,但也有时候指向NULL由于太过玄学,建议直接初始化置或一个有用的内存。

2.1 非字符串指针未初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h> //分配内存时用到的头文件
int main()
{
int a=20,*p; //这里定义了一个整型指针,但没赋初值,这时我们叫这个指针为野指针
printf("%d->%p\n", a, p); //观察%p是输出一个地址数据
p=NULL;
printf("%d->%p\n", a, p);
p=&a;
printf("%d->%p:%d\n", a, p, *p);
p=(int *)malloc(sizeof(int));
printf("%d->%p:%d\n", a, p, *p);
*p=30;
printf("%d->%p:%d\n", a, p, *p);

return 0;
}

程序执行的结果如下:

1
2
3
4
5
20->(nil)
20->(nil)
20->0x7ffe4b25dc0420
20->0xfaa0100
20->0xfaa01030

2.2 字符串指针未初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h> //分配内存时用到的头文件int
int main()
{
char *a="ShaTang";
char *p; //这里定义了一个字符串指针,但没赋初值,这时我们叫这个指针为野指针
printf("%s->%p\n", a, p); //观察%p是输出一个地址数据
p=NULL;
printf("%s->%p\n", a, p);
p=a;
printf("%s->%p:%s\n", a, p, p);
p=(char *)malloc(sizeof(char));
printf("%s->%p:%s\n", a, p, p);
p="Zhu";
printf("%s->%p:%s\n", a, p, p);

return 0;
}

程序执行的结果如下:

1
2
3
4
5
ShaTang->(nil)
ShaTang->(nil)
ShaTang->0x4006d4:ShaTang
ShaTang->0x134a010
ShaTang->0x4006f1:Zhu

Ps:观察2.1和2.2,就会发现字符(数组)类型,引用数据和查看地址都是用指针。
  编译器此时帮我们把 未初始化指针 指向 NULL。我们对野指针的定义:指针指向垃圾(未知)的内存;在这里,我们就不能称它为野指针。

2.3 例1变形的玄学

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h> //分配内存时用到的头文件
int main()
{
int a=20;
int *d1,*p;//这里定义了一个整型指针,但没赋初值,这时我们叫这个指针为野指针

printf("%d->%p\n", a, p); //观察%p是输出一个地址数据
printf("%d->%p\n", a, d1); //观察%p是输出一个地址数据
p=NULL;
printf("%d->%p\n", a, p);
printf("%d->%p\n", a,d1);

return 0;
}

编译器1,程序执行的结果如下:

1
2
3
4
20->0x7ffde8b16b80
20->0x4004f0
20->(nil)
20->0x4004f0

编译器2,程序执行的结果如下:

1
2
3
4
->0x7fff890b4780
->(nil)
->(nil)
->(nil)

  编译器此时分配内存给 未初始化指针,还是玄学分配,有时候置NULL(虚_野指针),有时候又分配一块内存(真_野指针)。因此需要注意 要指针的初始化,或者调用时一定要(检查)赋值,带来不可估量的Bug。
  编译器1和编译器2对 例2.1 编译的结果都是一样的,但是对 例2.3 的编译结果却各不相同。但是反过来,只是简单变动,编译器就能玄学分配,这是很恐怖的事情。

三、野指针概念案例

  按照野指针的概念,举如下例子

例1:指针变量没有初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
char *s1; //不初始化,此时指向NULL
char *s2="Zhu";

// s1=(char *) malloc(sizeof(char)); //重新分配一块内存给指针
// strcpy(s1 , s2);

printf("%s\n", s1);
printf("%p\n", s1);

return 0;
}

编译器1,程序执行的结果如下:

1
2

0x7fffea4889f0

编译器2,程序执行的结果如下:

1
11 Segmentation fault

  这里不采用strcpy举例说明 指针变量没有初始化 的问题,后面单独一节再讲解原因。
  这里很明显,在编译器2上,printf打印 未初始化的指针,出现段错误(实际指针 指向NULL)。而编译器1,则是通过了,并得知编译器1给 未初始化的指针 赋了一块随机内存。你的代码在不同的编译器上,有的报错,有的通过,这也是野指针带来的危害。

例2:使用已经释放后的指针(释放后没改指向NULL)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <malloc.h>
#include <string.h>
void func(char* p)
{
printf("%s\n", p);
free(p);
}
int main()
{
char* s = (char*)malloc(sizeof(char));
printf("%p\n", s);
strcpy(s, "Delphi Tang");
func(s);
printf("%s\n", s); //OOPS!
printf("%p\n", s);
return 0;
}

程序执行的结果如下:

1
2
3
4
0x1f0e010
Delphi Tang

0x1f0e010

  程序是可以正常执行的。但是在执行结果第三行:打印野指针指向的内存为空白。首先我们先重新明确上面的概念:“野指针”不是NULL指针,是指向“垃圾”内存的指针。内存的申请释放和指针没有太大关系,内存释放后,printf能正常打印出指针指向的地址,但是地址所在的内存内容就有问题了(为下次调用埋雷)。

例3:指针所指向的变量在指针之前被销毁

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
char* func()
{
char p[] = "Delphi Tang";
return p;
}
int main()
{
char* s = func();
printf("%s\n", s); //OOPS!
return 0;
}

  在我测试用的多个编译器,结果都不相同;局部变量在函数执行完,内存是已经释放的,还调用指向该内存的指针,即野指针调用;造成的结果在每个编译器都不太相同。

四、strcpy引发的段错误

  很多人讲解 未初始化的指针 导致的 野指针的时候,很多实例代码都是用到strcpy来讲解 指针未初始化 的问题。这是很不严谨的,从上面的案例分析,稍微改下代码,系统就能分配一块内存 或者 置NULL

4.1 错误的例子1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
char *s1; //不初始化,此时指向NULL
char *s2="Shatang";

// s1=(char *) malloc(sizeof(char)); //重新分配一块内存给指针

strcpy(s1 , s2);
printf("%s\n", s1);
printf("%p\n", s1);

return 0;
}

编译器1,程序执行的结果如下:

1
2
Shatang
0x7ffc79832540

编译器2,程序执行的结果如下:

1
11 Segmentation fault

在编译器2环境下,注释strcpy(s1 , s2);printf("%s\n", s1);,程序执行的结果如下:

1
(nil)

  也就说,用strcpy来举例 未初始化指针 的问题是有问题的。当编译器给 未初始化指针 置NULL时,这时候已经不算是野指针了,反而会出现段错误;当随机分配内存,编译却通过了,说明这个问题跟野指针无关,这是由strcpy引发的段错误。

4.2 strcpy语法

strcpy的语法如下:

1
strcpy(char* dest, const char *src);

被覆盖的 dest首先是个变量,变量就必须有内存存放;而一个指向NULL的指针没有指向任何内存。没有内存,怎么存储覆盖过来的值?

  或者来个更直接的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
char *s1=NULL; //不初始化,此时指向NULL
char *s2="Shatang";

// s1=(char *) malloc(sizeof(char)); //重新分配一块内存给指针

strcpy(s1 , s2);
printf("%s\n", s1);
printf("%p\n", s1);

return 0;
}

所有的编译器程序执行的结果如下:

1
12 Segmentation fault

4.3 错误的例子2

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>

int main()
{
char *s1="Shatang";
char *s2="Zhu";
strcpy(s1 , s2); // OOPS!
printf("%s\n", s1);
printf("%p\n", s1);

return 0;
}

  注意:这里我们将字符串指针初始化,指向一块明确内存(不是野指针了);但程序执行还会报 理所应当的 Segmentation fault的错误。strcpy函数的dest是一个变量,不能指向字符串常量;字符串常量存放在内存位置的字符常量区(详情看内存知识), 字符串指针指向这个区域,而且这个区域是一个const 属性的不可修改的;因此 再进行拷贝覆盖的时候会出现段错误。

4.4 正确的例子1

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>

int main()
{
char s1[10]="Shatang";
char *s2="Zhu";
strcpy(s1 , s2);
printf("%s\n", s1);
printf("%p\n", s1);

return 0;
}

程序执行的结果如下:

1
2
Zhu
0x7ffff41e089e

4.5 正确的例子2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
char *s1; //不初始化,此时指向NULL
char *s2="Zhu";

s1=(char *) malloc(sizeof(char)); //重新分配一块内存给指针

strcpy(s1 , s2);
printf("%s\n", s1);
printf("%p\n", s1);

return 0;
}

程序执行的结果如下:

1
2
Zhu
0x144e010

  由此可见strcpy引发的段错误,并非是野指针引起的;而是编程人对strcpy的用法不了解导致的。

4.6 得出来的结论

  只要调用该指针前,先把指针赋值指向对应的内存,就不会影响到系统的健壮性。如果严谨一点,还是把对指针进行 初始化赋值 或 置为NULL吧! (抽风编译器牛逼!)

五、经典野指针错误

  野指针犯错方式是花式的、神奇的。随着编译器的不同,野指针造成的结果也不相同;甚至在同一个编译器下,你定义变量的数量不同,多一个或少一个,系统就可能让 未初始化的指针 置NULL或者是分配随机内存。野指针,强就强在,随机神秘翻车!不仅如此,还是编译通过的后续翻车(关键点)!!!

  因此,大家也不要对野指针编译的结果不同太诧异。具体的错误例子可以看 内存分配方式 一节内容。

-------------本文结束感谢您的阅读-------------