当前位置:   article > 正文

C语言指针部分1(指针基本概念和野指针)_c语言程序设计部分指针(一)

c语言程序设计部分指针(一)

4.3.1.指针到底是什么?

4.3.1.1、指针变量和普通变量的区别
指针的实质就是个变量,它跟普通变量没有任何本质区别。指针完整的名字应该叫指针变量,简称为指针。
4.3.1.2、为什么需要指针?
(1)指针的出现是为了实现间接访问。在汇编中都有间接访问,其实就是CPU的寻址方式中的间接寻址。
(2)间接访问(CPU的间接寻址)是CPU设计时决定的,这个决定了汇编语言必须能够实现间接寻址,又决定了汇编之上的C语言也必须实现间接寻址。
(3)高级语言如Java、C#等没有指针,那他们怎么实现间接访问?答案是语言本身帮我们封装了。
4.3.1.3、指针使用三部曲:定义指针变量、关联指针变量(指针绑定)、解引用
(1)当我们int *p定义一个指针变量p时,因为p是局部变量,所以也遵循C语言局部变量的一般规律(定义局部变量并且未初始化,则值是随机的),所以此时p变量中存储的是一个随机的数字。
(2)此时如果我们解引用p,则相当于我们访问了这个随机数字为地址的内存空间。那这个空间到底能不能访问不知道(也许行也许不行),所以如果直接定义指针变量未绑定有效地址就去解引用几乎必死无疑。
(3)定义一个指针变量,不经绑定有效地址就去解引用,就好象拿一个上了镗的枪随意转了几圈然后开了一枪。
(4)指针绑定的意义就在于:让指针指向一个可以访问、应该访问的地方(就好象拿着枪瞄准目标的过程一样),指针的解引用是为了间接访问目标变量(就好象开枪是为了打中目标一样)

实验1:演示指针的标准使用方式,指针使用分3步:定义指针变量、给指针变量赋值(绑定指针)、解引用

#include <stdio.h>


int main(void)
{
	// 演示指针的标准使用方式
	// 指针使用分3步:定义指针变量、给指针变量赋值(绑定指针)、解引用
	int a = 23;
	// 第一步,定义指针变量
	int *p;
	printf("p = %p.\n", p);		// %p打印指针和%x打印指针,打印出的值是一样的
	printf("p = 0x%x.\n", p);
	
	
	// 第二步,绑定指针,其实就是给指针变量赋值,也就是让这个指针指向另外一个变量
	// 当我们没有绑定指针变量之前,这个指针不能被解引用。
	p = &a;				// 实现指针绑定,让p指向变量a
	p = (int *)4;		// 实现指针绑定,让p指向内存地址为4的那个变量
	
	// 第三步,解引用。
	// 如果没有绑定指针到某个变量就去解引用,几乎一定会出错。
	*p = 555;			// 把555放入p指向的变量中
	
	
	
/*
	// a的实质其实就是一个编译器中的符号,在编译器中a和一个内存空间联系起来
	// 这个内存空间就是a所代表的那个变量。
	int a;			// 定义了int型变量,名字叫a
	int *p;			// 定义了一个指针变量,名字叫p,p指向一个int型变量
	
	a = 4;			// 可以操作
	p = 4;			// 编译器不允许,因为指针变量虽然实质上也是普通变量,但是它的
					// 用途和普通变量不同。指针变量存储的应该是另外一个变量的地址
					// 而不是用来随意存一些int类型的数。
	
	p = (int *)4;	// 我们明知道其实就是数字4,但是我强制类型转换成int *类型的4
					// 相当于我告诉编译器,这个4其实是个地址(而且是个int类型变量
					// 的地址),那么(int *)4就和p类型相匹配了,编译器就过了。
*/	
	
	return 0;
}
  • 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
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

4.3.2.指针带来的一些符号的理解

我们写的代码是给编译器看的,代码要想达到你想象的结果,就必须编译器对你的代码的理解和你自己对代码的理解一样。编译器理解代码就是理解的符号,所以我们要正确理解C语言中的符号,才能像编译器一样思考程序、理解代码。
4.3.2.1、星号*
(1)C语言中*可以表示乘号,也可以表示指针符号。这两个用法是毫无关联的,只是恰好用了同一个符号而已。
(2)星号在用于指针相关功能的时候有2种用法:第一种是指针定义时,结合前面的类型用于表明要定义的指针的类型;第二种功能是指针解引用,解引用时p表示p指向的变量本身

4.3.2.2、取地址符&
取地址符使用时直接加在一个变量的前面,然后取地址符和变量加起来构成一个新的符号,这个符号表示这个变量的地址。

4.3.2.3、指针定义并初始化、与指针定义然后赋值的区别

(1)指针定义时可以初始化,指针的初始化其实就是给指针变量初值(跟普通变量的初始化没有任何本质区别)。
(2)指针变量定义同时初始化的格式是:int a = 32; int *p = &a;
(2)不初始化时指针变量先定义再赋值:int a = 32; int *p; 	p = &a;		正确的
									*p = &a;	错误的
  • 1
  • 2
  • 3
  • 4

4.3.2.4、左值与右值
(1)放在赋值运算符左边的就叫左值,右边的就叫右值。所以赋值操作其实就是:左值 = 右值;
(2)当一个变量做左值时,编译器认为这个变量符号的真实含义是这个变量所对应的那个内存空间;当一个变量做右值时,编译器认为这个变量符号的真实含义是这个变量的值,也就是这个变量所对应的内存空间中存储的那个数。
(3)左值与右值的区别,就好象现实生活中“家”这个字的含义。譬如“我回家了”,这里面的家指的是你家的房子(类似于左值);但是说“家比事业重要”,这时候的家指的是家人(家人就是住在家所对应的那个房子里面的人,类似于右值)
练习题目:指针的使用

#include <stdio.h>


int main(void)
{
	int a = 3, b = 5;
	
	a = b;		// 当a做左值时,我们关心的是a所对应的内存空间,而不是其中存储的3
	b = a;		// 当a做右值时,我们关心的是a所对应空间中存储的数,也就是5
	
	
	
/*
	int a;		// &a就表示a的地址。
	int *p;
	p = &a;		// 编译器一看到&a,就知道我们是要把变量a的地址赋值给指针变量p
				// 因为变量a的地址是编译器分配的,所以只有编译器才知道a的地址
				// 所以我们没法直接把a的地址的数字赋值给p,只有用符号&a来替代。
	
	// 理解&a,*p这样的符号,关键在于要明白当&和*和后面的变量结合起来后,就共同构成
	// 了一个新的符号,这个新的符号具有一定的意义。
*/
	
/*
	int a = 23;
	int b = 0;
	// 演示指针变量解引用
	int *p;							// *p就是我们说的星号的第一种用法
	p = &a;
	b = *p;				            // *p就是我们说的星号的第二种用法
	printf("b = %d.\n", b);         // b = 23	
*/	
	
/*
	// 演示指针变量定义

	// 把*和指针变量放在一起,而不是和int挨着,是为了一行定义多个变量时好理解
	int *p5, *p6;		// 这样才是定义了2个int *指针变量p5、p6
	int *p5, p6;		// p5是int *指针,p6是int的普通变量
	int* p5, p6;		// p5是int *指针,p6是int的普通变量
	
	// 实际编译测试,p1到p4都没有警告,说明4种写法编译器认为是一样的,都是定义了
	// int *类型的指针p
	int a = 4;
	int *p1;			// *和int结果,表明p的类型是int *,也就是
						// p是指向int类型变量的指针	
					
	int* p2;
	int*p3;
	int * p4;
	
	p1 = &a;
	p2 = &a;
	p3 = &a;
	p4 = &a;
*/
	
	return 0;
}

  • 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
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

4.3.3.野指针问题

4.3.3.1、什么是野指针?哪里来的?有什么危害?
(1)野指针,就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
(2)野指针很可能触发运行时段错误(Sgmentation fault)
(3)因为指针变量在定义时如果未初始化,值也是随机的。指针变量的值其实就是别的变量(指针所指向的那个变量)的地址,所以意味着这个指针指向了一个地址是不确定的变量,这时候去解引用就是去访问这个地址不确定的变量,所以结果是不可知的。
(4)野指针因为指向地址是不可预知的,所以有3种情况:
第一种是指向不可访问(操作系统不允许访问的敏感地址,譬如内核空间)的地址,结果是触发段错误,这种算是最好的情况了;
第二种是指向一个可用的、而且没什么特别意义的空间(譬如我们曾经使用过但是已经不用的栈空间或堆空间),这时候程序运行不会出错,也不会对当前程序造成损害,这种情况下会掩盖你的程序错误,让你以为程序没问题,其实是有问题的;
第三种就是指向了一个可用的空间,而且这个空间其实在程序中正在被使用(譬如说是程序的一个变量x),那么野指针的解引用就会刚好修改这个变量x的值,导致这个变量莫名其妙的被改变,程序出现离奇的错误。一般最终都会导致程序崩溃,或者数据被损害。这种危害是最大的。
(5)指针变量如果是局部变量,则分配在栈上,本身遵从栈的规律(反复使用,使用完不擦除,所以是脏的,本次在栈上分配到的变量的默认值是上次这个栈空间被使用时余留下来的值),就决定了栈的使用多少会影响这个默认值。因此野指针的值是有一定规律不是完全随机,但是这个值的规律对我们没意义。因为不管落在上面野指针3种情况的哪一种,都不是我们想看到的。

4.3.3.2、怎么避免野指针?
(1)野指针的错误来源就是指针定义了以后没有初始化,也没有赋值(总之就是指针没有明确的指向一个可用的内存空间),然后去解引用。
(2)知道了野指针产生的原因,避免方法就出来了:在指针的解引用之前,一定确保指针指向一个绝对可用的空间。
(3)常规的做法是:
第一点:定义指针时,同时初始化为NULL
第二点:在指针解引用之前,先去判断这个指针是不是NULL
第三点:指针使用完之后,将其赋值为NULL
第四点:在指针使用之前,将其赋值绑定给一个可用地址空间
(4)野指针的防治方案4点绝对可行,但是略显麻烦。很多人懒得这么做,那实践中怎么处理?在中小型程序中,自己水平可以把握的情况下,不必严格参照这个标准;但是在大型程序,或者自己水平感觉不好把握时,建议严格参照这个方法。

4.3.3.3、NULL到底是什么?
(1)NULL在C/C++中定义为:

#ifdef _cplusplus			// 定义这个符号就表示当前是C++环境
#define NULL 0				// 在C++中NULL就是0
#else
#define NULL (void *)0		// 在C中NULL是强制类型转换为void *的0
#endif

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

(2)在C语言中,int *p;你可以p = (int *)0;但是不可以p = 0;因为类型不相同。
(3)所以NULL的实质其实就是0,然后我们给指针赋初值为NULL,其实就是让指针指向0地址处。为什么指向0地址处?2个原因。第一层原因是0地址处作为一个特殊地址(我们认为指针指向这里就表示指针没有被初始化,就表示是野指针);第二层原因是这个地址0地址在一般的操作系统中都是不可被访问的,如果C语言程序员不按规矩(不检查是否等于NULL就去解引用)写代码直接去解引用就会触发段错误,这种已经是最好的结果了。
(4)一般在判断指针是否野指针时,都写成
if (NULL != p)
而不是写成 if (p != NULL)
原因是:如果NULL写在后面,当中间是==号的时候,有时候容易忘记写成了=,这时候其实程序已经错误,但是编译器不会报错。这个错误(对新手)很难检查出来;如果习惯了把NULL写在前面,当错误的把==写成了=时,编译器会报错,程序员会发现这个错误。
练习题目:野指针的操作实例

#include <stdio.h>

int main(void)
{
	int a;
	int *p = NULL;
	// 中间省略400行代码······
	//p = (int *)4;			// 4地址不是你确定可以访问的,就不要用指针去解引用
	
	p = &a;			// 正确的使用指针的方式,是解引用指针前跟一个绝对可用的地址绑定

	//if (p != NULL)
	if (NULL != p)
	{
		*p = 4;
	}
	p = NULL;		// 使用完指针变量后,记得将其重新赋值为NULL
	
/*
	int *p;		// 局部变量,分配在栈上,栈反复被使用,所以值是随机的
	
	//printf("p = %p.\n",p);
	*p = 4;		// Segmentation fault (core dumped)运行时段错误,原因为野指针
*/
	
	return 0;
}

  • 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
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/程序安全守护者/article/detail/63236
推荐阅读
  

闽ICP备14008679号