有希带你深入理解指针(1)

一、前言🥳

在这里插入图片描述

距离上一次更新已经好几天了,这里我向读者深表歉意😭,这一次的内容确实比较多,我也花了不少时间整理,但是我向你们保证绝对不会断更的😎,请大家继续支持😝。在前一篇文章中,我们学习了指针的部分内容,感兴趣的小伙伴可以移步上篇文章进行阅读和学习。现在我们接着学习指针的有关内容。

二、正文😁

1.const修饰指针😋

const这个关键字有两个作用:

  1. const修饰普通变量
  2. const修饰指针变量

1.1 const修饰普通变量

首先,我们对const修饰普通变量进行讲解。
对于一个普通变量,它的值是可以进行修改的,如图:
在这里插入图片描述

对于一个被const修饰的变量,它的值是不可以通过上图的方式进行修改😣。const是常属性的意思,被const修饰后,变量具有了常属性(不能被修改)。const可以放在int前,也可以放在int后,只是我们一般放在int前。
在这里插入图片描述
这里做一个小小的知识延申:🥺
在C语言中,图示中的a是常变量,它的本质还是变量,只是在const修饰的情况下,编译器在语法层面上不允许修改这个变量。
在C++语言中,图示的a是常量

在C语言中,C99之前,在一个数组的创建中,数组的大小,我们只能给一个常量值,结合我们上述知识,我们将a用const修饰之后,使a具有常属性,现在我们可以用a作为数组的大小吗?试试看!
在这里插入图片描述

很显然,这样是行不通的,因为在此时a的本质还是变量。
前文中,我们说到在C++语言中,经过const修饰,我们的a变为了常量,我们不妨试试在C++语言下的情况😤。
在这里插入图片描述
我们的编译通过了!所以在C++中,const修饰的变量会变为常量。🤫

但是呢,一开始我提到不可以用以上的方式进行修改,意味着通过其他方式可以进行修改,我们可以首先拿出a的地址交给p,通过p找到a进行修改,如图:
在这里插入图片描述
不过,我们使用const对变量进行修饰,就是为了是它具有常属性,不让别人对它进行改变,我们这样做有些刻意违反规则,我们不如对指针也进行const修饰。

1.2 const修饰指针变量

const修饰指针有3种情况:

  1. const放在*的左边
  2. const放在*的右边
  3. const放在*的两边

我们先对没有加const的进行理解,我在这里引入一个例子:

int num =100;
int* p =#

num在内存申请了一块空间存放值100,这块空间的地址我们假设为0x0012cc60,我们在上述中,用p来存放num的地址。这里我们对num的值的修改可以有两种方法👻:

  1. 对p里面存的地址进行修改,如:
int n =10;
p = &n;
  1. 利用地址找到num进行修改,如:*p =200
情况1:const放在*的左边

例:

int const *p
const int *p

此时const限制的是* p,表示指针指向的内容不能通过指针来改变了。
但是指针变量本身的值是可以进行修改的,如图:😲
在这里插入图片描述
在这里插入图片描述

情况2:const放在 * 的右边
int * const p

此时const限制的是p本身,表示指针变量p本身不可以修改了,但是指针指向的内容可以通过指针变量来修改。
在这里插入图片描述

在这里插入图片描述

情况3: const放在*的两边
const int* const p;

此时,指针变量p不能被修改,指针变量的内容也不能被修改。
在这里插入图片描述
在这里插入图片描述

2.指针的运算😃

指针的基本运算有三种:

  1. 指针 ± 整数
  2. 指针 - 指针
  3. 指针的关系运算

2.1 指针 ± 整数

对于一个数组,里面的元素在内存中的存放是连续的,我们只需知道第一个元素的地址,我们就能找到后续所有元素的地址。
在这里插入图片描述
在这里插入图片描述
这里我们通过指针,分别拿到了数组中各个元素的地址,我们再进行解引用,就可以拿到各个元素,如下图:
在这里插入图片描述
通过上面的例子,不难想到只要我们拿到某个内存单元的地址,我们就可以对它周边的内存单元进行访问,因为内存单元是连续的。🥰
在这里插入图片描述

2.2 指针 - 指针

(指针 - 指针)相当于(地址 - 地址),得到的是指针之间的元素个数(会有情况带负号)。
在这里插入图片描述
但是呢,我们来看下一组例子:
在这里插入图片描述
此处我们可以看出,这里是有大小地址之分的,大地址减小地址是正数,小地址减大地址是负数。

注意:这种运算的前提是,两个指针指向同一块内存空间。
在这里插入图片描述
这段代码的错误有两点:😫

  1. arr1和arr2的两块空间在内存中是否连续
  2. 计算arr1和arr2之间的元素个数时,是按int还是char类型计算

讲了这么多,大家可能感到有些无聊。现在我举个例子来介绍它的作用🤩。

我们此时要计算一个字符串的长度。
在这里插入图片描述
很自然的我们就写出了这段代码,strlen统计的是字符串中 \0 之前的字符个数,我们可以利用指针的知识写一个我们自己的strlen函数来完成这个功能🥰。

思路:

  1. 数组名是数组首元素的地址,arr == &arr[0]
  2. 我们需要一个字符指针来接收数组首元素的地址
  3. 我们需要一个计数器在没有指向 \0 来进行不断加1来计算数组的元素个数

代码实现:

#include<stdio.h>
size_t My_strlen(char* p)
{
	size_t count = 0;
	while (*p != '\0')
	{
		count++;
		p++;
	}
	return count;
}

int main()
{
	char arr[] = "abcdef";
	size_t sz = My_strlen(arr);
	printf("%zd\n", sz);
	return 0;
}

用指针减指针也行,只用起始地址和\0的地址相减就行。

代码实现:

#include<stdio.h>
size_t My_strlen(char* p)
{
	char* start = p;
	while (*p != '\0')
	{
		p++;
	}
	return p - start;
}

int main()
{
	char arr[] = "abcdef";
	size_t sz = My_strlen(arr);
	printf("%zd\n", sz);
	return 0;
}

2.3 指针的关系运算

其实就是两个指针比较大小。🤓
此时我们利用指针的关系运算来打印数组内容。

思路:
我们用int * p来存放首元素的地址,在没有到达最后一个数之前不断移动,结合数组的知识,数组随着下标的增长,地址是由低到高变化的。
在这里插入图片描述
或者反过来也行。

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

在这里插入图片描述

3.野指针🤔

野指针指向的位置是不可知的,指向的空间是不属于我自己的。

3.1野指针的成因

  1. 指针未初始化
  2. 指针越界访问
  3. 指针指向的空间释放
3.1.1 指针未初始化

在这里插入图片描述
这里的p我们没有初始化,里面存放的地址是随机的,则 * p就是非法访问,p就是野指针。

3.1.2 指针越界访问

在这里插入图片描述
这里arr只能存放10个元素,但是循环体要循环12次,指针出现了越界访问,p会变为野指针。

3.1.3 指针指向的空间释放
#include<stdio.h>
int* test()
{
	int n = 100;
	return &n;
}

int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

我们来理解这段代码,首先调用了test这个函数,用int * p来接收返回值。在这个test函数中,创建了变量n,并存放了值100,并返回n的地址。最后打印p解引用以后的内容。

问题分析:😆
局部变量n在进入test这个函数时创建,出函数时被销毁。出函数时,创建的空间不再属于n,还给了操作系统。此时我们把n的地址返回给了p,p得到这个地址时,p就成为了野指针。

3.2 如何避免野指针

  1. 指针初始化
  2. 小心指针越界
  3. 指针变量不再使用时,及时置为NULL,指针在使用之前检查有效性
  4. 避免返回局部变量的地址
3.2.1 指针初始化

如果知道指针指向哪里,就直接赋值地址。如果不知道指针指向哪里,可以赋值NULL(空指针)。

int main()
{
	int a = 4;
	int* p1 = &a;//直接赋值
	int* p2 = NULL;
	return 0;
}

对于NULL的理解:👻
NULL是C语言中定义的一个标识符常量,值是0,同时0也是地址,该地址是无法使用的,读写该地址会报错。

在这里插入图片描述
当然我们可以统合一下其他内容进行理解:
在这里插入图片描述

3.2.2 小心指针越界

这个我们在前面已经讲过了,这里我们不再重复了😘。

3.2.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设置为NULL
	p = NULL;
	//下次使用之前进行判断,如果p不是NULL时,再使用
	//........
	p = &arr[0];//p重获地址

	if (p != NULL)//判断
	{
		//......
	}
	return 0;
}
3.2.4 避免返回局部变量的地址

这个我们在前面也举了例子,这里不再一一赘述😤。
在这里插入图片描述

4.assert断言😉

assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被定义为“断言”。

assert(表达式);

如果这个表达式为真,则什么都不发生,如果为假,则报错。
在这里插入图片描述
在这里插入图片描述

assert在报错的时候会显示文件名和行号,便于检查。

所以assert()的使用对程序员是非常友好的:
它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制。
如果已经确认程序没有问题,不需要再做断言,就在#include<assert.h>语句的前面,定义一个宏NDEBUG。

#define NDEBUG
#include<assert.h>
int main()
{
	int a = 10;
	int* p = NULL;
	assert(p != NULL);
	return 0;
}

assert()的缺点是:引入了额外的检查,增加了程序的运行时间。

注意:
assert我们一般在Debug中使用,在Release版本中选择禁用assert。
assert不是只能断言指针,我们这里只是以指针为例,我们想断言什么都行。
在这里插入图片描述

5.指针的使用和传值调用😗

指针的使用我们其实在前文中有展现,就是我们自己的计算字符串的长度的函数My_strlen,学到这里我们可以对刚才写的代码进行优化,添入assert()。

#include<assert.h>
#include<stdio.h>
size_t My_strlen(char* p)
{
	size_t count = 0;
	assert(p != NULL);
	while (*p != '\0')
	{
		count++;
		p++;
	}
	return count;
}

int main()
{
	char arr[] = "abcdef";
	size_t sz = My_strlen(arr);
	printf("%zd\n", sz);
	return 0;
}

我们创建My_strlen是求字符串长度的,我们不希望p指向的字符串被修改!我们现在接着优化🫡。

#include<assert.h>
#include<stdio.h>
size_t My_strlen(const char* p)
{
	size_t count = 0;
	assert(p != NULL);
	while (*p != '\0')
	{
		count++;
		p++;
	}
	return count;
}

int main()
{
	char arr[] = "abcdef";
	size_t sz = My_strlen(arr);
	printf("%zd\n", sz);
	return 0;
}

其实到这里代码已经没有问题了,我们进行简化就行😋。

#include<assert.h>
#include<stdio.h>
size_t My_strlen(const char* p)
{
	size_t count = 0;
	assert(p != NULL);
	while (*p )
	{
		count++;
		p++;
	}
	return count;
}

int main()
{
	char arr[] = "abcdef";
	size_t sz = My_strlen(arr);
	printf("%zd\n", sz);
	return 0;
}

5.2 传值调用和传址调用😶‍🌫️

这是函数调用的两种方式。
我们现在通过例子来理解:要求写一个函数交换两个整型变量的值。
刚开始时我们的代码可能写成这样:

#include<stdio.h>
void Swap(int x, int y)
{
	int tmp = 0;
	tmp = x;
	x = y;
	y = tmp;
}

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;
}

但是呢,我们会发现运行后结果并不是我们想要的🫠。
在这里插入图片描述
我们进行调试观察:
在这里插入图片描述
显然a和x、b和y的地址并不相同,当实参的值传递给形参的时候,形参是有独立的空间的,对形参的修改不会影响实参。此时我们使用的方法就是传值调用
那有没有什么方法使我们改变的是a、b变量本身呢?🤨
我们可以通过指针远程遥控我们的a和b实现交换。

#include<stdio.h>
void Swap(int* pa, int* pb)
{
	int tmp = 0;
	tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

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;
}

在这里插入图片描述
此时我们传递的是a和b的地址,并借助tmp完成了交换,这个就是传址调用

总结:🤓
未来函数中,如果只需要的是主调函数中的变量值来实现计算,就采用传值调用。如果函数内部要修改主调函数中变量的值,就需要传址调用。

此时我们的指针内容还没有完😆,下期blog我会接着讨论有关指针的内容!如果大家感兴趣,请一键三连。如果有问题请各位大佬在评论区斧正,十分感谢🥰!我一定会继续努力的!
在这里插入图片描述

  • 50
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 38
    评论
评论 38
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值