指针的深入理解(二)

本文详细探讨了指针的野指针概念、如何规避野指针、使用assert断言确保程序正确性,以及指针的传值调用与传址调用的区别,以及指针与数组名的特殊性质。
摘要由CSDN通过智能技术生成

指针的深入理解(二)

前言

哈喽,各位小伙伴!今天给大家带来指针深入理解的第二期。通过上期内容,大家对指针的基本概念有了认识,今天小编带大家继续深入学习指针内容。话不多说,咱们向着大厂前进!

一.野指针

1.1野指针概念

什么是野指针野指针就是指向位置不可知的(随机的,不正确的,没有明确限制的)。
野指针三大成因:

  • 指针未初始化

    以这个代码为例,代码运行时编译器报错,说局部变量p没有初始化。p是个指针,但是里面我们没有存有地址,也就是没有对初始化,局部变量未初始化时,它里面的值是随机值(0xcccccc,涉及函数栈帧的创建和销毁),这时对p进行解引用操作,由于p里面存的地址是随机值,我们解引用访问是不知到他访问的空间的,这时候的p指针就像一条不知去向的野狗,是非常危险的,也就是野指针。

  • 指针越界访问

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
	int a[10] = { 0 };
	int* p = a;
	for (int i = 0; i < 10; i++)
	{
		*(p++) = i;//循环10次,p由首元素跳到10个整型,第十次循环后指向第十个元素后的空间
	}
	return 0;
}

在这里插入图片描述
我们创建一个十个元素的数组,把数组首元素(数组加粗样式名)地址赋给p指针,第一次循环先使用p,后p+1跳过一个整型指向第二个元素,以此类推,第十次循环完后p会指向第11个元素,但数组是十个元素,这时p越界,指向不属于他的空间,此时p为野指针

  • 指针指向的空间释放
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int* test()//开辟空间,进入test函数
{
	int n = 100;
	return &n;//返回n地址,test函数空间销毁,n也销毁。空间还给操作系统。
}
int main()
{
	int* p = test();
	printf("%d", *p);//野指针
	return 0;
}


为了创建test函数,内存开辟空间。之后空间内创建变量n,将n地址返回给指针p,但注意,返回后,函数空间也销毁了n也随之销毁,空间还给操作系统。此时在通过解引用访问n时,指针指向的不一定是n,指向哪里我们也不得而知,此时p就是野指针

  • 结论一:指针指向不输入他的空间时,这时的指针就是野指针。

1.2如何规避野指针

  • 指针初始化。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
	int* p = NULL;//空指针,地址为0,但不可访问。
	printf("%d", *p);
	return 0;
}

我们知道未初始化是导致野指针的一个原因。所以我们创建指针时想要指针指向哪里就直接赋值,如果不知道指向哪里就赋值为空指针。也就是NULL。空指针就相当于把野狗(野指针)用绳子拴住,不给他乱跑。其实空指针也并非是空的,它里面存的地址为0,但是不可访问。如果访问就会产生访问权限冲突。这就相当于野狗被拴住,你就站在旁边挑逗他,这也是很危险的。
在这里插入图片描述

  • 检查指针有效性
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
	int a = 20;
	int* p = NULL;//空指针,地址为0,但不可访问。
	printf("%d", *p);
	if (p != NULL)
	{
		*p = 100;
	}
	return 0;
}

我们在使用指针前可以检查指针的有效性判断是否为空指针。这样就可以避免野指针。但是不代表这样我们就可以不初始化了。因为前面说过指针不初始化是是随机值,随机值不一定是0,也就是不一定为空指针。但是你仍然使用这个指针,这时也是野指针。所以好习惯要相辅相成

  • 小心数组越界
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
	int a[10] = {0};
	int* p = a;
	for (int i = 0; i < 10; i++)
	{
		*(p++)=i;//循环后数组越界
	}
    p = NULL;//置空
	p = a//置空后在使用
	if (p != NULL)//判断后再使用
	{
		*p = 100;
	}
	return 0;
}

这个代码前面讲过了,循环完后指针已经越界。但是我们前面讲过两个避免野指针的方法,我们在使用指针后置空,再去使用,每次使用之前都进行判断是否为空指针即可。

  • 避免返回局部变量的地址
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int* test()//开辟空间,进入test函数
{
	int n = 100;
	return &n;//返回n地址,test函数空间销毁,n也销毁。空间还给操作系统。
}
int main()
{
	int* p = test();
	printf("%d", *p);//野指针
	return 0;
}

这也是导致野指针的原因返回局部变量的地址后使用指针访问空间。但空间在地址返回后已经销毁,被操作系统回收了,此时p就是野指针,我们以后要对这种特殊情况多加小心,养成置空和判断的好习惯。

二.assert断言

在这里插入图片描述
assert.h头文件中定义了宏assert(),用于确保程序在运行时符合指定条件,如果不符合就直接报错终止运行,这个宏通常被称为“断言”

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
int main()
{
	int a = 10;
	int* p = NULL;//置空
	assert( p != NULL);//断言如果表达式为真这不影响程序,如果为终止程序。
	*p = 100;
	printf("%d", *p);
	return 0;
}



可以发现assert可以接收一个表达式作为参数,如果表达式为,则不影响程序运行结果,如果为假则终止程序,并报出错误文件位置。这对我们程序员是非常友好的,它就相当于无需更改代码就能确认程序是否正确的开关。那这个开关如何关闭呢?这是我们就可以在assert头文件前定义一个宏NDEBUG即可。

定义宏后,程序运行起来并没有终止,说明此时assert()被关闭了。但是注意assert()只能在Degug版本中使用,在Release版本(用户使用版本)中assert是被禁用的,直接被优化掉了。因为assert发挥作用时,也引入额外的检查增加运行时间。所以这样可以方便程序员检查程序的同时,不影响用户使用体验

三.指针的使用和传址调用

1.1strlen的模拟实现(优化版)

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#define NDEBUG
#include<assert.h>
size_t my_strlen(const char* p)//统计次数,无需修改字符串,const修饰防止更改字符串
{//字符串长度不可能为负数,所以返回类型改为size_t(unsigned int也可),防止返回负数。
	int count = 0;
	assert(p != NULL);//断言,防止使用空指针
	while(*p!='\0')
	{
		count++;//统计次数
		p++;//指针移动
	}
	return count;//返回结果
}
int main()
{
	char a[] = "abcdef";
	int n = my_strlen(a);
	printf("%d", n);
	return 0;
}


我们用指针遍历字符数组,每次循环统计次数,遇到**\0结束,返回长度即可。我们这里对其进行优化。使用指针前,用assert断言判断是否为空指针**,再用const修饰形参,防止更改字符串。字符长度不为负数,返回类型改为size_t,防止返回负数。我们再来看看strlen库函数的介绍。()
在这里插入图片描述
可以发现strlen库函数形参就是和我们优化的版本一样的。

1.2传值调用和传址调用

我们学到这里,大家想想指针到底有什么用?如果没用的话,指针存在是不是多余了?接下来小编通过一道题目带大家体会一下指针的作用。

题目:写一个函数,实现两个整形变量交换。

相信有部分小伙伴觉得这有啥难。直接就写出来典型的反面教材代码(哈哈!)

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int swap(int x, int y)
{
	int z = 0;
	x = y;
	y = z;
}
int main()
{
	int a = 10;
	int b = 20;
	printf("a=%d\nb=%d\n", a,b);//交换前
	swap(a,b);
	printf("a=%d\nb=%d", a, b);//交换后
	return 0;
}


发现结果是错误的,这是为什么呢?这就涉及一个很重要的知识点了,大家可以当成结论来记。

  1. 结论二:调用函数时,形参是实参的一份临时拷贝,形参有自己独立的空间,修改形参,不会影响实参。

这里我们在swap函数中的x和y有自己独立的空间,对x和y交换是不会交换a和b的。这就是传值调用。那怎样才能交换呢?我们之前学习的解引用也可以修改变量的值,但是指针需要地址,那我们就给他地址。这就是传址调用

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int swap(int* x, int* y)
{
	int z = *x;
	*x = *y;
	*y = z;
}
int main()
{
	int a = 10;
	int b = 20;
	printf("a=%d\nb=%d\n", a,b);//交换前
	swap(&a,&b);
	printf("a=%d\nb=%d", a, b);//交换后
	return 0;
}


大家可以发现我们使用传址调用成功实现交换。这里大家可以发现指针也是有自己的作用的。指针能多给我们提供解决问题的方法,具体问题选择具体方法。

四.指针与数组

1.1数组名的理解

经过前面的学习我们知道数组名就是数组首元素的地址。但是,有两个特殊情况

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
	int a[10] = { 0 };
	printf("%d", sizeof(a));//表示整个数组,计算的是整个数组的长度,单位是字节。
	return 0;
}

按照之前的理解的话,应该打印int的4个字节大小
在这里插入图片描述
但是结果是40,为什么是40呢?40=4*1040=int类型大小*有多少个int。所以sizeof(a)这里的数组名表示的是整个数组。这是一种特殊情况

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


对比三种加一情况,发现a+1和&a[0]+1均是跳过4个字节****内存加4。因为他们都表示数组首元素加一跳过一个整型。而**&a加一加了40,说明这里的a表示整个数组**,加一跳过一个数组这是另一种情况。

结论三:数组名表示数组首元素地址,但sizeof(数组名)和&数组名表示整个数组的地址。

1.2指针访问数组

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void printf(int arr[],int b1)//int arr[]本质就是指针,所以也可以是int* p,只是为了方便理解写成数组的形式
{
	int* p = arr;
	//int b = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < b1; i++)
	{
		printf("%d ", *(p+i));//刚开始指针指向首元素,加上下标就是跳过多少个整型后的位置
	}
}
int main()
{
	int arr[] = { 1,3254,4,67,576,34,6,98 };
	int b = sizeof(arr) / sizeof(arr[0]);//统计数组长度
	printf(arr,b);//数组名是数组首元素的地址,所以传过去本质是地址,不是数组
	return 0;
}

这里我们用指针打印数组。我们观察一下指针打印和数组打印就能推出一些结论。

其实数组的本质就是指针,即使写成数组,编译器也会转化为指针的形式。所以其实i[p]的形式和上面四种形式也是等价的。因为**[]其实是个操作符**,左右两个数是它的两个操作数,他们的位置不影响结果

后言

今天带大家学习了指针的更深层次的内容,相信也对大家对指针的理解有所提升。今天就分享到这,感谢各位小伙伴的耐心阅读,咱们下期!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大白的编程日记.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值