初识C语言指针(2)

目录

1. 野指针的概念

2. 野指针的产生原因

2.1 指针变量未初始化

2.2 指针越界访问

2.3  指针指向的空间释放

3. 如何规避野指针 

3.1 指针初始化

 3.2 ⼩⼼指针越界

3.3 指针变量不再使⽤时,及时置NULL

3.4 避免返回局部变量的地址 

4. assert 断⾔

5. 传值调⽤和传址调⽤

5.1 传值调用

5.2 传址调用


1. 野指针的概念

在C语言中野指针是指那些指向的内存位置是不确定的、随机的、或者已经被释放的指针。这些指针可能指向了任意的内存地址,因此使用它们来访问或修改数据是极其危险的。

2. 野指针的产生原因

2.1 指针变量未初始化

任何指针变量在刚被创建时不进行初始化,其值是随机的,如果在使用前未对指针进行初始化,那么它可能会指向一个未知的内存地址。

2.2 指针越界访问

当我们使用指针访问数组中的元素时,超出数组范围,指针指向的就是一个未知的地址,此时的指针就是野指针。

2.3  指针指向的空间释放

根据下图:我们调用test函数,返回n的地址,但是函数调用结束后,函数内的n变量的内存空间被释放,所以此时主函数中p接收到的地址是一个未知的地址,即p此时就是一个野指针。

3. 如何规避野指针 

我们已经知道野指针的危害和野指针是如何形成的,那么该如何规避野指针的产生呢?

3.1 指针初始化

当我们创建指针变量的时候,如果明确该指针指向的哪里就进行赋值,如果不知道指针应该指向哪⾥,可以给指针赋值NULL(空指针)在C++中Null表示0的意思,在C语言中Null会将0强转成((void *)0)。

例如:

 3.2 ⼩⼼指针越界

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。

例如在数组创建的空间范围内访问数组元素:

int main()
{
	int arr[10] = { 0 };
	int* p = &arr[0];
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i<sz ; i++)
	{
		*(p++) = i;
	}
	return 0;
}

3.3 指针变量不再使⽤时,及时置NULL

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的 时候,我们可以把该指针置为NULL。当重新使用指针时,可以判断一下是否是空指针

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p++) = i;
	}
	//此时p已经越界了,可以把p置为NULL
	p = NULL;
	//下次使⽤的时候,判断p不为NULL的时候再使⽤

	p = &arr[0];//重新让p获得地址
	if (p != NULL) //判断
	{
		printf("重新使用指针");
	}
	else 
	{
		printf("空指针不能使用");
	}
	return 0;
}

3.4 避免返回局部变量的地址 

如之前调用test函数的例子,不要返回局部变量的地址,当函数调用结束后,局部变量的内存空间被释放,此时原局部变量的地址就会变成一个未知的地址,该地址指向的内容是未知的。

总结:野指针是编程中一个需要特别注意的问题。我们要养成良好的编程习惯,有效地减少野指针的产生,提高程序的稳定性和安全性。

4. assert 断⾔

assert 断⾔可以帮助我们检查程序运行中的一些错误,assert 断⾔的基本格式:assert (表达式),如果表达式为真,那么就不会发生报错,如果表达式为假,程序运行时就会报错停止运行。在使用assert 断⾔前,需要包含一个头文件:#include<assert.h>

例如我们要判断一个指针是否为空指针:

#define  _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include<assert.h>

int main()
{
	int* pa = NULL;
	assert(pa != NULL);
	return 0;
}

运行结果:

 圈起来的报错信息的意思是:assert断言失败,错误位置在xx路径下的xx文件中的第x行。

当我们排查完错误后,不需要进行断言时,在#include<assert.h>上面加上#define NDEBUG即可 :

这样assert断言就不会起作用。 

5. 传值调⽤和传址调⽤

之前在讲解函数的时候就提到过传值调用传址调用,现在具体的讲一讲这2者的区别。

5.1 传值调用

传值调用顾名思义传的是数值,假设我们要写一个函数用来交换2个变量的值,传值调用是否能够完成呢?

我们发现交换前和交换后的数据,并没有发生改变这是为什么呢? 

通过调试我们发现,在main函数内部,创建了a和b,a的地址是0x010ffbec,b的地址是0x010ffbe0,在调⽤change函数时,将a和b传递给了change函数,在change函数内部创建了形参x和y接收a和b的值,但是x的地址是0x010ffb08,y的地址是0x010ffb0c,x和y确实接收到了a和b的值,不过x的地址和a的地址不 ⼀样,y的地址和b的地址不⼀样,所以说x和y拥有自己独⽴的空间,那么在change函数内部交换x和y的值, 就不会影响a和b,当change函数调⽤结束后回到main函数,a和b的没法交换。所以交换前与交换后的数据是没有变化的。

传值调用的结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。

既然传值调用不能完成2个变量的值的交换,那我们来看看传址调用。

5.2 传址调用

传址调用传的是地址

我们发现通过传址调用可以实现2个变量的值的交换,这又是怎么回事呢?

通过调试,我们可以发现在调用change函数的时候,将a的地址传给了x,将b的地址传给了y。x和y通过地址找到了a的内存空间和b的内存空间,所以此时的 *x其实就是a,*y其实就是b,所以我们是通过指针(地址)间接访问a和b,对它们的值进行交换,这就是传址调用。

以上就是本章的全部内容,希望大家看完后能够有所收获,谢谢大家!!!

  • 19
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值