深入理解指针【2】--(通俗易懂!!!)

const修饰指针-2

前面,咱们已经讲过了const修饰普通变量(非指针变量)的特点。那么,今天我们来讲讲const修饰指针变量的特点吧。const修饰指针变量很特别,有3种情况要进行解读。

  • const放在* 左边:咱们就用整形指针变量来进行举例。比如,const int *p=&a或者int const *p=&a这两种写法都是在*左边的情况。直接说结论——const 放在 *的左边时,*p是无法执行的(p解操作后,所对应的值是无法修改的),但是p存放的地址可以改变。进行代码展示:
#define  _CRT_SECURE_NO_WARNINGS	1
#include <stdio.h>
int main()
{
	int a = 10;
	const int* p = &a;   //或者 int const *p=&a
	*p = 30;			//const放在*的左边,*p是无法执行的
	return 0;
}
#define  _CRT_SECURE_NO_WARNINGS	1
#include <stdio.h>
int main()
{
	int a = 10;
	const int* p = &a;   //或者 int const *p=&a
	*p = 30;
	return 0;
}

结果运行图:在这里插入图片描述

  • const放在*右边:放在左边,咱们已经知道了,放在右边,自然而然就能推理出来。比如,int * const p=&a。放在右边就只有这一种写法,可不能这样写int * p const=&a它所限制的是——指针变量所存放的地址是无法修改的,但是*p(所指的变量内容是可以改变的)可以执行。进行代码展示:
#define  _CRT_SECURE_NO_WARNINGS	1
#include <stdio.h>
int main()
{
	int a = 10;
	int c = 20;
	int* const p = &a;    //不可以写成这样:	int* p const = &a;  
	p = &c;           //无法执行
	printf("%p\n", p);
	return 0;
}

#define  _CRT_SECURE_NO_WARNINGS	1
#include <stdio.h>
int main()
{
	int a = 10;
	int c = 20;
	int* const p = &a;   
	p = &c;
	printf("%p\n", p);
	return 0;
}

结果运行图:在这里插入图片描述

  • const放在*的左边和右边:放在 *的左边是限制指针变量的解引用操作,放在 *的右边是限制指针变量的地址改变,那么放在*左右边呢?——即限制指针变量的解引用操作,又限制指针变量的地址改变。进行代码展示:
#define  _CRT_SECURE_NO_WARNINGS	1
#include <stdio.h>
int main()
{
	int a = 10;
	int c = 20;
	int const * const p = &a;   
	*p = 30;				//无法执行
	p = &c;					//无法执行
	printf("%d\n", a);
	printf("%p\n", p);
	return 0;
}

结果运行图:在这里插入图片描述

指针运算

指针的基本运算有三种,如下。

  • 指针+ - 整数:就是地址+ - 整数,咱们前面也讲过了 ,地址(指针)+ - 整数,能跨过多少字节取决于指针变量类型。以前我们写过访问数组的代码,比如,printf("%d ",arr[i])
    今天我们用指针进行访问,进行代码展示:
#define  _CRT_SECURE_NO_WARNINGS	1
#include <stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;		//我们讲过,数组名就是首元素地址,也可以这样写 int* p = &arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p+i));		
	}
	return 0;
}

结果运行图:在这里插入图片描述
代码还可以这样写:

#define  _CRT_SECURE_NO_WARNINGS	1
#include<stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;	
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p++));
	}
	return 0;
}

结果运行图:在这里插入图片描述
这里我们稍微讲一下p+ip++的区别。这俩的本质上没什么区别——都是对数组进行访问。它们的访问方式不同,p+i进行遍历访问,因为解引用操作是对p+i整体进行的,不会改变原有的p的值,每加一个 i 就相当于一个新的p+i的整体分身。但是,p++就不同了,它每次++后,p的值就改变了,就会进行跨越访问——向后移动。如图所示:在这里插入图片描述

  • 指针 - 指针:我们直接说结论:指针-指针(地址-地址)的绝对值就是两个指针之间的元素个数这里有个大前提:两个指针所指的空间必须是同一块且连续的空间。这很好理解,只有同一个空间,且必须是连续的,咱们才能得到它们之间的元素个数。进行代码展示:
#define  _CRT_SECURE_NO_WARNINGS	1
#include<stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int* pb = &arr[8];
	int c = (int)(pb - p);   //咱们最好强制转换一下类型,省的警告。
	printf("%d\n", c);		//不强制转换类型也可以。
	return 0;
}

结果运行图:在这里插入图片描述
当我们对数组进行指针- 指针运算的时候,可以直接用下标数字直接相减就OK.指针也是有大小指针的——大指针(大地址),小指针(小地址)。当小指针 - 大指针的时候,结果就是负值,但是它的绝对值也是元素个数——所以我们前面讲过了,它们运算的绝对值就是元素个数。进行代码展示:

#define  _CRT_SECURE_NO_WARNINGS	1
#include<stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int* pb = &arr[8];
	int c = (int)(p - pb);	//小指针-大指针
	printf("%d\n", c);
	return 0;
}

结果运行图:在这里插入图片描述

  • 指针的关系运算:我们前面也讲过了关系运算符,比如,> ,< ,= ,!=。指针的关系运算无非就是这些关系运算符的操作数变成了指针而已。进行代码展示:
#define  _CRT_SECURE_NO_WARNINGS	1
#include<stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int* pb = &arr[8];
	if (pb > p)   //大指针 > 小指针,所以条件成立
	{
		printf("hhhh");
	}
	return 0;
}

结果运行图:在这里插入图片描述
我们已经学了指针的+ - 运算,指针 - 指针运算和指针关系运算。那么,我们来写个程序运用一下:

// 	写一个程序,用来模仿 sizeof 来计算数组的元素个数

进行代码展示:

#define  _CRT_SECURE_NO_WARNINGS	1
#include<stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int* pb = &arr[9];
	int count = 0;
	while (p <= pb)
	{
		count++;
		p++;
	}
	printf("%d\n", count);
	int b = sizeof(arr) / sizeof(arr[0]);
	printf("这是sizeof计算的结果:%d\n", b);
	return 0;
}

结果运行图:在这里插入图片描述
还可以模拟 strlen这个函数,大家可以自行尝试一下。

野指针

概念就是指针所指的位置是不可知的,或者所指的位置是不属于自己的,就称为野指针。(随机的,不可知的,没有限制的)。接下来,我们讲解野指针的成因。

  • 指针未初始化指针变量也是变量,咱们以前在变量的那个章节中讲过——局部变量未初始化时,会被系统随机分配数值,全局变量不初始化时,会被默认分配为0当我们指针变量为全局变量时,会被分配一个地址是0地址),而且这个地址不会随程序每次调用而改变。指针变量当作局部变量的场景是最多的,所以我们后面所讲指针变量时,就默认都是局部变量当我们不给指针变量初始化时,就会被系统随机分配地址。这是很危险的,我们知道可以通过指针来更改内存里的数据,这样的话,我们使用指针变量(未初始化)进行更改时,就有可能更改对于我们来讲很重要的数据(事与愿违)。进行代码展示:
    当为全局变量指针时:
#define  _CRT_SECURE_NO_WARNINGS	1
#include<stdio.h>
int* p ;		//全局变量指针
int main()
{
	printf("%p\n", p);
	return 0;
}

结果运行图:在这里插入图片描述
当为局部变量时:

#define  _CRT_SECURE_NO_WARNINGS	1
#include<stdio.h>
//int a;
//int* p;
int main()
{
	int* p;			//局部变量
	printf("%p\n", p);
	return 0;
}

结果运行图:在这里插入图片描述

  • 指针越界访问当我们确定好空间后,就只能在这个空间内进行数据的访问,不能访问本空间以外的空间。这个很好理解,我们有多少钱就花多少钱,不能去抢别人的钱。进行代码展示:
#define  _CRT_SECURE_NO_WARNINGS	1
#include<stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i <= sz; i++)
	{
		printf("%d ", *(p + i));   //当访问到下标为10时,就是越界访问
	}
	return 0;
}

结果运行图:在这里插入图片描述

  • 指针访问的空间被释放:先进行代码展示:
#define  _CRT_SECURE_NO_WARNINGS	1
#include<stdio.h>
int* test()
{
	int a = 10;
	return &a;
}
int main()
{
	int* p = test();
	printf("%d\n",*p);
	return 0;
}

结果运行图:在这里插入图片描述
当我们在调用函数test()时,操作系统就会给a分配好内存,当我们调用完后,这个内存就后被释放,还给操作系统。 这个知识,咱们在栈帧的创建与销毁中讲过。当testa的地址传给p时,p就是野指针了。因为,当内存释放的时候,那块内存已经不属于a的内存了,但是这个内存还存在,不会被销毁的因为内存的地址是硬件设计),我们就没有权限进行访问了(相当于越界访问了)。野指针的成因有很多种,我们在这里举出最常见的原因。我们既然已经知道了野指针的成因,那么我们应该如何规避野指针呢?

  • 指针初始化:当我们知道指针的明确指向时,我们就要进行赋值初始化。当我们没有明确的指向时,我们可以赋予 NULLNULL是C语言中规定的一个标识符常量它的值是0(0也是个地址,这个地址我们是不能访问的,一旦访问就会报错)。当一个指针变量被赋值NULL时,它就有个新的名字——空指针。展示它的定义:
 # ifdef __cplusplus		//在C++中,NULL就是0
       # define NULL 0
 # else
       #define NULL ((void *)0)   //在C语言中,NULL是由0强制转换类型而来的。
 # endif
#define  _CRT_SECURE_NO_WARNINGS	1
#include<stdio.h>
int main()
{
	int a = 10;
	int* p1 = &a;
	int* p2 = NULL;		//p2就是个空指针
	return 0;
}

当我们用完指针后,不想再用时,也对其进行赋值NULL,防止成为野指针。我们来举个例子,以便我们理解NULL。野指针就相当于一条野狗,如果我们不进行管制的话,它就会有伤人的风险,当我们把它拴在一棵上,它救不会乱伤人了,而这棵树就相当于NULL。当一个指针被赋值NULL时,就表明它是个野指针(野狗),我们要绕道走,这就是NULL的作用,而且一但被NULL赋值的指针变量,我们是无法使用的,就像我们明明知道它是条野狗,非要去碰它,肯定会有危险。

  • 避免越界访问:我们使用指针时,要注意不要越界访问,这个没什么好的方法,要注意下就OK了。
  • 避免使用局部变量返回的地址,就像我们上面举例的代码(空间被释放)。

assert断言

是人都会犯错误,我们有时候不注意到野指针,就会报错。那么怎么避免呢?这个时候我们就可以用assert进行一个判断。assert的头文件为:<assert.h> assert的使用格式

//	assert(表达式)
//	表达式的结果为真(表达式成立),程序正常执行
//	表达式的结果为假(表达式不成立),程序就会立即中断,无法正常执行

进行代码展示:

1 #define  _CRT_SECURE_NO_WARNINGS	1
2 #include<stdio.h>
3 #include<assert.h>
4 int main()
5 {
6	int* p = NULL;
7	assert(p != NULL);		//条件不成立,程序无法执行
8	printf("hhh");
9	return 0;
10 }

结果运行图:在这里插入图片描述
当我们修改代码后:

#define  _CRT_SECURE_NO_WARNINGS	1
#include<stdio.h>
#include<assert.h>
int main()
{
	int a = 10;
	int* p = &a;
	assert(p != NULL);		//表达式为真,程序可以执行
	printf("hhh");
	return 0;
}

结果运行图:在这里插入图片描述
assert对程序员是很友好的,虽然程序走到这里要判断一下(会耽误点运行时间),但可以防止我们出错。一般我们可以在Debug中使用,在Release版本中选择禁用assert就行,在VS这样的集成开发环境中,在Release版本中,直接就是优化掉了。这样在Debug版本写有利于程序员排查问题,在Release版本不影响用户使用时程序的效率我们不想使用assert时,除了直接删除以外,我们还可以在# include <assert.h>前面加个# define NDEBUG就会禁止assert的使用。进行代码的展示:

#define  _CRT_SECURE_NO_WARNINGS	1
#include<stdio.h>
#define  NDEBUG		//禁止了assert的使用
#include<assert.h>
int main()
{
	int* p = NULL;
	assert(p != NULL);		//就是个摆设
	printf("hhh");
	return 0;
}

结果运行图:在这里插入图片描述
assert不只是用来判断是否为空指针(空指针只是举个例子),还可以判断别的东西。进行代码展示:

#define  _CRT_SECURE_NO_WARNINGS	1
#include<stdio.h>
#include<assert.h>
int main()
{
	int a = 10;
	assert(a != 20);
	printf("hhh");
	return 0;
}

结果运行图:在这里插入图片描述

指针的使用和传址调用

  • 传值调用:我们先写个程序来感受一下,然后在讲解。
//	我们写一个程序,当我们输入两个整数时,就会交换两个数据。
//	比如,输入 :a=100 , b=200 。
//		 得到 :	a=200 , b=100 。

进行代码展示:

#define  _CRT_SECURE_NO_WARNINGS	1
#include <stdio.h>
void swap(int x, int y)
{
	int c = 0;
	c = x;
	x = y;
	y = c;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d%d", &a, &b);
	printf("交换前:a=%d b=%d\n", a, b);
	swap(a, b);
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

结果运行图:在这里插入图片描述
我们发现,结果事与愿违。我们创建了swap函数,我们把a ,b的值传给了swap中的x , y了,也就是x就是a ,y就是b。我们进行交换,按理说应该就是交换的,但为什么不是这样呢?这就牵扯到形参与实参的知识了(前面咱们已经讲过了)。实参向形参传的是值,而且形参是单独创立的空间,形参是实参的一个copy版本,形参的改变不会影响到实参。所以,形参里面无论怎样变化,都不会影响到实参,这也就是为什么前后结果一致

  • 传址调用传址调用就是传递的是地址。上面我们进行传值是无法进行值的交换,那么我们就进行传址调用,是否能交换呢?进行代码展示:
#define  _CRT_SECURE_NO_WARNINGS	1
#include <stdio.h>
void swap(int* x, int* y)
{
	int c = *x;
	*x = *y;
	*y = c;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d%d", &a, &b);
	printf("交换前:a=%d b=%d\n", a, b);
	swap(&a, &b);
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

结果运行图:在这里插入图片描述
我们可以看出来,传址调用改变了值,因为我们直接调用地址,直接就可以找到对应的值,就可以进行更改(类似远程遥控一样)。

彩蛋时刻!!!

https://www.bilibili.com/video/BV15H4y1F7fc/?spm_id_from=333.337.search-card.all.click&vd_source=7d0d6d43e38f977d947fffdf92c1dfad
在这里插入图片描述
每章一句想,全是问题。做,才有答案。感谢你能看到这里,点赞+关注+收藏+转发是对我最大的鼓励,咱们下期见!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值