深入理解指针(二)

深入理解指针(二)

前言:

一、const修饰指针

1.const修饰变量

在C语言中,const修饰的变量是为常变量,常变量具有常属性,所以不能随意改变值。

c 复制代码
#include<stdio.h>
int main()
{
	const int a = 10;//这时候表示a的本质是不希望被改变的
	//这时候a被const修饰,a变成了常变量,但是本质还是变量,所以不能改变。
	a = 1;
	printf("%d\n", a);
	int*p = &a;
	*p = 20;
	printf("%d ", a);
	//通过解引用操作改变了赋值
	在C++中:const int n = 0;这里的n是常量,不是常变量。
	return 0;
}

所以我们可以用解引用操作去修改常变量的值

2.const修饰的指针变量

const可以放在*左边和右边,都是可以去限制p的变化

c 复制代码
//左边:
const int* p = &a;
int const* p = &a;
//这两个是一样
c 复制代码
//右边:
 int* const p = &a;
c 复制代码
#include<stdio.h>
int main()
{
	int a = 100;
	int* pa = &a;//pa是一个指针变量,里面存入的是地址。
	*pa = 0;//*pa是pa的指向对象(a)
	printf("%d\n", a);
	printf("%d\n", *pa);
	return 0;
}

下面是我对于它们的理解:

代码示例:
理解1:在左边时,const限制的是*p,即指向的内容无法改变,但是通过改变指针变量本身内容去进行改变

c 复制代码
#include<stdio.h>
int main()
{
	int a = 100;
	int b = 1000;
	const int* p = &a;
	//*p = 0;//err
	p = &b;//ok
	printf("%d",*p);
	return 0;
}	


理解2:在右边时,修饰的是指针变量本身p,所以指针变量本身不能被修改,指针指向的内容是可以根据指针来进行改变
注意:此时一定要对指针变量进行初始化

c 复制代码
#include<stdio.h>
int main()
{
	int a = 100;
	int b = 1000;
	int* const p = &a;
	*p = 0;//ok
	//p = &b;//err
	printf("%d\n", a);
	return 0;
}

二、野指针

野指针的概念:野指针就是指针所指向的位置是随机的(没有明确的限制)。

1.野指针的成因

(1).指针的未初始化
c 复制代码
#include<stdio.h>
int main()
{
	int* p;//局部变量不初始化的时候,里面存放的是随机值
	*p = 20;//这时候属于非法访问。
	return 0;
}
(2).指针的越界访问
c 复制代码
#include<stdio.h>
int main()
{
	int arr[10] = { 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++)
	{
		//因为这个循环本身能循环11次,当循环第11次的时候
		//p指向了数组最后一个元素后面的空间,这时候就存在非法访问的问题
		*p = i;//*p是指向的对象,即i;
		p++;//p本身在移动,这时候的p是指针变量的本身
		//*(p++) = i;也可以这么去写。
	}
	return 0;
}

便会出现如上的这种错误。

(3).指针的空间释放

在这同时p既得到了地址,又是野指针

c 复制代码
#include<stdio.h>
int* test()
{
	int n = 100;//n所占的是4个字节,但是因为它是局部变量,所以出了作用域会销毁。
	//所以也表明它的空间也会还给操作系统
	return &n;
}
int main()
{
	int* p = test();//但是它还给了操作系统的同时,指针变量的p也接收到了n的地址。
	printf("%d\n", *p);//这时候虽然会打印出结果,但是会形成了非法访问内存的结果。
	return 0;
}

2.如何规避野指针

(1).指针初始化

NULL为空指针

如果指针有明确的指向,那就直接赋给明确的地址。

c 复制代码
int a = 10;
int* p = &a;

如果指针变量,当前还不知道该指向哪里,这个时候应初始化为NULL;

当然这个时候也是无法进行使用的,比如通过解引用。

c 复制代码
#include<stdio.h>
int main()
{
	int* p = NULL;//这个时候就是NULL为空指针
	//*p = 200;//想要使用它,就必须绕过NULL,即当它不等于NULL时候可以去使用,否则会非法访问内存。
	if (p != NULL)
	{
		*p = 200;
	}
	return 0;
}
(2).小心访问越界

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

防止指针在使用的时候访问而超过原有值的边界。

(3)指针不再使用的时候,及时置NULL,指针使用之前检查有效性

如果NULL指针就可以不去访问,并且在用指针时还可以起到判断指针是否为NULL

c 复制代码
#include<stdio.h>
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的时候,判断p不为NULL的时候再使用
	//...
	p = &arr[0];//重新让p获得地址
	if (p != NULL) //判断
	{
		//...
	}
	return 0;
}

理解: 指针在用完这块区域的时候,后期我们不用这块区域(空间)的时候,我们可以把指针置为NULL。

在下次还要使用这个指针,再把地址赋值后,需要判断这个指针还是不是NULL类型即可。

(4)避免返回局部变量的地址(函数)(栈空间)
c 复制代码
#include<stdio.h>
int* test()
{
	int n = 100;
	return &n;//局部变量的地址,容易出现野指针
}
int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

三、assert断言

作用:判断指针是否为空

要想使用assert必须要使用assert头文件

#defeine NDEBUG是可以在assert头文件前使用,可以起到关闭assert的作用。

当已经确认了程序已经没有了问题,便可以用这个宏定义

想要开启时,把这个宏定义注释掉即可

c 复制代码
//#define NDEBUG
#include<assert.h>
int main()
{
	int arr[5] = { 1,2,3,4,5 };
	//int* p = arr;
	int* p = NULL;
	assert(p != NULL);
	//验证指针变量是否为NULL,若不是,则为程序继续运行,
	//若是,则会终止运行,并且给出信息错误的提示
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}
c 复制代码
#include<stdio.h>
#include<assert.h>
int main()
{
	int arr[5] = { 1,2,3,4,5 };
	int* p = arr;
	//int* p = NULL;
	assert(p != NULL);
	//验证指针变量是否为NULL,若不是,则为程序继续运行,
	//若是,则会终止运行,并且给出信息错误的提示
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

assert()的使用是对于程序员是有很大的好处的

好处:能够精确快速发现错误,并找到问题所在位置。可以用于测试和调试,并且可以增强代码的维护性。

劣势:

在Debug版本中assert能帮程序员在代码运行时检查错误,发现代码里的问题,方便修改和优化。但这些功能会让程序运行速度变慢,占用更多资源。

Release版本是面向用户的最终版本,关闭了调试功能,把 assert 优化掉了。它更注重程序运行效率和性能,运行速度更快,占用资源更少。

四、指针的使用和传址调用

1.strlen的模拟实现

上一节我们已经写过了strlen函数的模拟实现,现在用这节知识优化一下

代码如下:

c 复制代码
#include<stdio.h>
#include<assert.h>
int my_strlen(const char* s)//const防止被修改
{
	int count = 0;
	assert(s != NULL);//防止被为空指针
	while (*s != '\0')
	{
		count++;
		s++;
	}
	return count;
}
int main()
{
	char arr[] = "abcdef";
	size_t c = my_strlen(arr);
	printf("%zd\n", c);
	return 0;
}
c 复制代码
#include<stdio.h>
#include<assert.h>
int my_strlen(const char* str)
{
	const char* start = str;
	assert(str != NULL);
	while(*str)
	{
		str++;
	}
	return str - start;
}
int main()
{
	char arr[] = "abcdef";
	int len = my_strlen(arr);
	printf("%d ", len);
	return 0;
}

2.指针的传址调用与传值调用

这应该是一份交换两个变量的值的函数:

c 复制代码
#include<stdio.h>
int Swap(int x, int y)
{
	int temp = 0;
	temp = x;
	x = y;
	y = temp;
}
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函数中x,y接收了a,b的值,两组的内存地址也分别不同,那么就也说明了两组是不同的且独立的空间,所以在Swap函数中交换x和y的值,再回到main函数中,那么不会对main函数的a和b的值会造成影响。
这种是把变量的本身传递给函数,叫做传值调用

原因:实参传递给形参时,形参是实参的一份临时拷贝,因为形参会再创造一个独立的空间,所以对于形参的修改不会影响到实参。

既然这么做不能交换,那么我们把a和b的地址传递给这个函数,去试一试。

c 复制代码
#include<stdio.h>
int Swap2(int* px, int* py)
{
	//*py等于b;*px等于a
	int temp = 0;
	temp = *px;
	*px = *py;
	*py = temp;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前:a=%d b=%d\n", a, b);
	Swap2(&a, &b);//传址调用,传进去的是地址
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

这时候我们再通过调试看一看:

这时候把地址传递给了函数,变量储存的是地址,我们可以去进行交换了。

这种把地址传递给函数,再去调用的方式叫做传址调用。

传址调用是把函数与主调函数构成真正的联系,在函数内部可以修改主调函数的变量。

一般来说只是去调用变量的值去进行计算的这种用传值调用;

需要改变主调函数中的变量等,这时候需要传址调用。

相关推荐
刘阿去44 分钟前
lua C语言api学习4 编写C模块
c语言·学习·lua
誓约酱1 小时前
linux 下消息队列
linux·运维·服务器·c语言·c++
张胤尘2 小时前
C/C++ | 每日一练 (6)
c语言·c++·面试
小呀小萝卜儿6 小时前
2025-03-14 学习记录--C/C++-DS-头结点和头指针的区别
c语言·学习
ChiaWei Lee10 小时前
【C语言】函数和数组实践与应用:开发简单的扫雷游戏
java·c语言·游戏
四念处茫茫11 小时前
【C语言系列】C语言内存函数
c语言·开发语言·算法·visual studio
小呀小萝卜儿12 小时前
2025-03-13 学习记录--C/C++-PTA 练习2-9 整数四则运算
c语言·学习
小呀小萝卜儿12 小时前
2025-03-15 学习记录--C/C++-PTA 练习3-4 统计字符
c语言·学习
水瓶丫头站住13 小时前
C++和C的区别
c语言·c++
光军oi14 小时前
不像人做的题————十四届蓝桥杯省赛真题解析(上)A,B,C,D题解析
c语言·蓝桥杯·深度优先