平地起C楼—第五层《你好!指针理解—壹》

在这里插入图片描述

🚩🚩前言

在这里插入图片描述

欢迎来到C语言指针讲解部分,指针?它是C语言中一个很重要的内容,正确理解和使用指针,是很关键的。使用指针可以更好地利用内存资源,描述复杂的数据结构,灵活地处理字符串和数组,从而设计出简洁、高效的C程序。

🌈🌈指针与内存关系

🚀🚀内存

内存是计算机中用于存储数据的存储器,以一个字节作为存储单元,然而为了正确地访问内存单元,必须为每一个内存单元编号,这个编号就叫作该单元的地址。比如:一个旅店看作内存,那么旅店的房间就是内存单元,房间号码就是该单元的地址。

在C语言中,指针就是被用来表示内存单元的地址。若把这个地址用一个变量保存起来,则这个变量就被称为指针变量。当然,指针变量也分为不同的类型,用来保存不同类型变量的地址。严格地说,指针和指针变量是不同的,为了描述方便,就简说指针变量为指针了。

在这里插入图片描述

补充:
其实在计算机内部也是把整个内存划分为一个一个的内存单元的,每一个内存单元的大小是1个字节,即1Byte。那么在这里会用到一下计算机中常见的单位:

【bit 、Byte、KB、MB、GB、TB、PB等】

这些之间的进制为:
1Byte=8bit、1KB=1024Byte、1MB=1024KB、1GB=1024MB、1TB=1024GB等

图解指针:
可以把每一个单元划分为一个8人间宿舍,这整个宿舍相当于一个内存单元,即1Byte。里面的8个人相当于8bit,所以1Byte=8bit。然而,在这宿舍门口的门牌号就是宿舍的位置所在,用计算机话语描述就是这个宿舍的地址。因此,我们在计算机中把内存单元称为地址,而在C语言中,给地址取了一个名字——指针
在这里插入图片描述

总的说,可以把他们联系起来理解就是,内存单元的编号 = = 地址 = = 指针,三者等价的。

🌈🌈指针变量

🚀🚀地址

我们了解了地址的概念,以及指针的由来,接下来我们需要知道这指针可以用来干什么呢?👇👇👇👇

  • 在C语言中,我们创建变量的实质是像内存申请空间,必须有位置放这个变量才可以。我们可以从下面的创建变量的内存中看到:创建a变量以后,通过调试可以看到在内存当中存下了a的值,用16进制表示的:00 00 00 14,在这前面的就是a所在内存当中的地址编号。
#include<stdio.h>
int main()
{
	int a = 20;//创建变量
	return 0;
}

在这里插入图片描述

🚀🚀指针变量的定义

  • 定义形式:
[存储类型]  数据类型  *指针变量名[=初始值];//存储类型在开始接触的时候一般没怎么写。默认为auto

1、存储类型:register型、static型、extern型、auto型4种。
2、数据类型是该指针变量所指向的数据类型。
3、*表示为后面的变量是指针变量。
4、初始值通常初始为某个变量名的地址或者NULL。

我们看到了a变量的地址,那么它可以像变量一样,把他们的值打印出来呢?是可以的,需要用到下面的操作符。

🚀🚀指针变量的使用—‘&’

在定义指针变量后,必须将其与某个变量的地址关联起来在可以,关联的方式有2种:

1、赋值方式。将变量的地址赋值给指针变量

<指针变量名>=<普通变量名>;

//比如;
int i,*p;
p=&i;

2、定义时赋初始值。定义指针变量的时候直接指向变量的地址。

//比如:
int i,*p=&i;

无论采用上面哪一种方式,都是把指针 p 指向了变量 i 的地址。也可以将指针初始化为NULL,表示p不指向任何存储单元。int *p=NULL;

  • 例如下面的代码:
#include<stdio.h>
int main()
{
	int a = 20;
	int* ptr = &a;//把a的地址取出来放到指针变量ptr中去

	return 0;
}

🚀🚀如何理解指针变量类型

用上面的例子:
int a = 20;
int* ptr = &a;

对上面进行解释:
ptr 的左边是 int*这里的 * 说明 ptr 是指针变量,前面的 int 说明该指针变量指向的是整型类型的对象。
在这里插入图片描述

  • 上面说的是int类型的指针变量,那么我想存储其他类型变量的地址,该怎样写?

char ch = 'A';
char* ptr = &ch; //ptr 的类型就是看你存储那个变量的类型,前面是什么类型,后面就是什么类型。

我们拿到了变量的地址,后续该怎样使用呢?👇👇👇👇

🚀🚀指针变量的解引用— ‘*’

好比我们通过地址找到了所在哪个房间,但是怎样才能进去,就需要一把钥匙。在这也是一样的,既然取出了变量的地址,通过地址能找到那个变量,但是找到了不一定愿意给你看里面的东西。所以就需要用到专用的🔑—— ‘ * ’ 解引用操作符

#include<stdio.h>
int main()
{
	int a = 20;
	int* ptr = &a;
	*ptr = 10;//通过解引用,改变a里面原来的值
	printf("a=%d\n",a);
	printf("ptr=%d\n",*ptr);//通过解引用打印出来
	return 0;
}

在这里插入图片描述

我们上面代码就是通过解引用操作符来改变原来变量的值,有些看到这会想为什么呢?干嘛这样麻烦,不把一开始的变量初始化我们想要的值,何必通过指针变量再来改一次?
其实,既然有这个东西的存在,必然有用武之地。俗话说:“万物有,必有其义!”
为什么能改变原来变量的值,可以这样想,它既然把地址交给指针变量ptr,而它通过这个地址找到了a的位置,并通过解引用操作符(“钥匙”),发现里面的数据,那么a和ptr就是一家人了。而为何通过指针变量来改变原来的值,就好比《狂飙》里面,强哥不好出面解决的事情,就会找到老默帮忙解决,说:”老默我想吃鱼🐬🐬🐬了!“🧨🧨🧨🧨

🚀🚀指针变量的大小

相信各位对计算机都有所知道,安装操作系统的有32位和64位的,对于32位的机器相当于有32根地址线,组成地址总线。每一根地址线接收的电信号会转换为数字信号,即0或1。那么32根地址线产生的二进制序列当作一个地址,则一个地址就有32个bit位,然而内存单元是用字节来描述,1Byte(字节) = 8 bit(位),所以地址有32bit,就需要4 Byte(字节)才能存储这个地址。
  而指针变量是用来存储地址的,那么指针变量的大小就应该是4个字节的空间大小。

同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变量的⼤⼩就是8个字节。

#include<stdio.h>
//指针变量的大小是取决于地址大小的,不是有指针变量类型决定的。
//地址大小是有多少位操作系统决定的
//32位平台下:有32个bit位,即4个字节大小
//64位平台下:有64个bit位,即8个字节大小

//sizeof() 操作符是计算所占内存空间大小的
int main()
{
	printf("%zd\n",sizeof(int *));
	printf("%zd\n",sizeof(short *));
	printf("%zd\n",sizeof(char *));
	printf("%zd\n",sizeof(float *));
	printf("%zd\n",sizeof(double *));
	return 0;
}
  • 结果如下:
    在这里插入图片描述

在这里插入图片描述
【注意:指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。】

🌈🌈指针变量类型存在的意义

在这里有人会想,既然上面通过计算,每种类型的指针变量在同一个平台下都是一样的大小,那么指针变量类型存在的意义是什么呢?接下来就讨论一下指针变量类型存在的意义。✍✍

🚀🚀指针的解引用操作

  • 通过对比下面两个代码:

用整型指针变量存储的时候:

#include<stdio.h>
int main()
{
	int a = 0X11223344;
	int* ptr1 = &a;//整型指针变量
	*ptr1 = 0;
	return 0;
}

在这里插入图片描述

在这里插入图片描述

用字符型指针变量存储的时候:

#include<stdio.h>
int main()
{
	int a = 0X11223344;
	char* ptr1 = &a;//字符型指针变量
	*ptr1 = 0;
	return 0;
}

在这里插入图片描述

在这里插入图片描述

在这总结:通过上面的调试对比,不同的指针变量类型,在改变原来变量的值得时候,是有不同得变化的。在上面例子中,用整型指针变量存储整型变量地址的时候,把指针变量解引用修改为0后,原来整型变量里面的4个字节全部改为0了;但是,当用字符指针变量存储的时候,修改后 ,原来变量里面只有1个字节变为了0。
因此:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。

在这里插入图片描述

🚀🚀指针变量类型对指针运算的影响

我们通过代码来展示指针的运算【指针±整数】:

#include<stdio.h>

int main()
{

	int n = 20;
	char* ptr1 = (char*) & n;//此处做强制类型转化
	int* ptr2 = &n;

	printf("%p\n\n",&n);

	printf("%p\n",ptr1);
	printf("%p\n\n",ptr1+1);

	printf("%p\n",ptr2);
	printf("%p\n",ptr2+1);
	return 0;
}

在这里插入图片描述

通过指针的运算,可以看出char* 的指针变量加1,是跳过1个字节,而char类型本身就是占1个字节;而int* 的指针变量加1,则是跳过4个字节,和它自身所占的字节一样,都是4个字节。因此,可以说是,指针+1,是跳过这个指针所指向的元素;同理,-1 也是如此。
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。

🌈🌈void* 指针

对于void* 指针,我们可以理解为无具体类型的指针,也可以叫做—— 泛型指针。这种类型的指针可以接受来自各种类型的变量的地址。但是,也有局限性,void* 指针不能直接进行指针的加减整数和解引用的运算

#include<stdio.h>

int main()
{
	int a = 10;
	int* ptr1 = &a;
	char* ptr2 = &a;
	return 0;

在这里插入图片描述

上面将int 类型变量的地址赋值给一个char* 类型的指针变量,会出现下面的警告,因为类型不兼容。但是,使用void* 类型的指针变量就不会出现这种警告。

#include<stdio.h>

int main()
{
	int a = 10;

	int* ptr1 = &a;
	void* ptr2 = &a;

	*ptr1 = 20;
	//*ptr2 = 30;	//会出现报错,不能进行运算	
	return 0;
}

在这里插入图片描述

上面的例子可以看出 void* 类型指针的局限性,但是它也有真正的用处,用于函数参数的部分,我会在后面更新回调函数来说 void* 类型的指针变量。

🌈🌈const 修饰指针

在这里插入图片描述

🚀🚀const 修饰变量

我们通过上面看到,一个变量的地址赋值给一个指针变量,那么我们可以通过指针变量来改变这个变量的值。但是,我们希望指针不可以改变变量原来本身的值,该怎么办呢?在这 const 就起到了限制的作用

#include<stdio.h>
int main()
{
	int number = 20;
	number = 30;//number的值是可以修改的

	const int n = 10;
	//n = 20;//n是不能修改的,被const修饰了

	return 0;
}

在这里插入图片描述

上述代码中,看到 number、n 都是变量,number 的值是可以修改的,而 n 被 const 修饰后就不可以修改了。既然,直接不能修改n的值,何不找一下后门,通过n的地址来修改 n 的原来的值。代码如下:👇👇👇👇

#include<stdio.h>
int main()
{
	int number = 20;
	number = 30;//number的值是可以修改的

	const n = 10;
	//n = 20;//n是不能修改的,被const修饰了
	//通过n的地址来修改
	int* ptr = &n;
	*ptr = 20;
	printf("n=%d\n",*ptr);
	return 0;
}

在这里插入图片描述

我们通过地址确实修改了n的值,那么我们想一下,为了不让变量不被修改,在变量名前面加上 cosnt 修饰,确实不能直接改,但是可以通过地址来修改,这样还不是打破了const的作用。所以,为了根治这修改的行为,必须让通过地址来修改变量的值的这种方法也得到控制,只有两种方法都不可以修改才达到效果,那么我们该如何来控制地址修改变量值的行为呢?请看下面:👇👇👇👇

🚀🚀const修饰指针变量

我们首先会想到指针变量创建的格式:int* ptr=NULL; 那么const是放在 * 号的左边还是右边呢?其实,左右都可以,但是到达的效果不一样。用代码来展示:
int const * ptr1=NULL;//放在* 号左边
int* const ptr2=NULL;//放在 * 号右边

#include<stdio.h>

//无const 修饰
void test()
{
	int number1 = 20;
	int number2 = 40;
	int* ptr1 = &number1;//无const修饰
	*ptr1 = 10;
	ptr1 = &number2;
	printf("number1=%d\n",*ptr1);

}

//const在*号右边
void test1()
{
	int number1 = 20;
	int number2 = 30;
	int* const ptr1 = &number1;
	*ptr1 = 50;
	//ptr1 = &number2;//出现报错是因为const 修饰的是指针变量本身,保证指针变量的内容不能修改,但是指针指向的内容是可以修改的。
	printf("number1=%d\n",*ptr1);
}

//const在*号左边
void test2()
{
	int number1 = 20;
	int number2 = 30;
	const int* ptr1 = &number1;
	//*ptr1 = 60;//修饰的是指针指向的内容不能修改,但是指针变量本身是可以变的。
	ptr1 = &number2;
}

void test3()
{
	int number1 = 20;
	int number2 = 30;
	int const* const ptr1 = &number1;
	//两边都是修饰的,都不能改。
	//*ptr = 50;
	//ptr1 = &number2;
}

int main()
{
	test();
	test1();
	test2();
	test3();
	return 0;
}

在这里插入图片描述

在这里插入图片描述

总结:const 如果放在 * 的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。
• const如果放在 * 的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
左定值,右定向

在这里插入图片描述

🌈🌈指针运算

指针的三种基本运算:
1、指针 + - 整数
//指针 + - 整数 <==> 日期 + - 天数
2、指针 - 指针
//为什么没有指针 + 指针呢?这个和日期一样的,用日期+日期是没有意义的,所以指针也是。日期 - 日期是中间有多少天,所以指针-指针就是中间间隔多少个元素。
3、指针的关系运算

🚀🚀指针 + - 整数

我们先来说说数组,数组它是在内存空间中连续存储的,我们只要知道数组的第一个元素的地址,就可以很好的找到后面的地址了,我们下面先用最常用的下标方式来打印数组:

#include<stdio.h>
int main()
{
	int array[] = { 1,2,3,4,5,6,7,8,9,10 };
	//计算数组大小
	int len = sizeof(array) / sizeof(array[0]);
	for (int i = 0; i < len; i++)
	{
		printf("%d ",array[i]);
	}
	printf("\n");
	return 0;
}
  • 输出结果:

在这里插入图片描述

通过下标来访问当然可以,但我们学了指针,就得把指针用起来,通过地址来访问数组的内容,下面代码展示:

#include<stdio.h>
int main()
{
	int array[] = { 1,2,3,4,5,6,7,8,9,10 };
	//计算数组大小
	int len = sizeof(array) / sizeof(array[0]);
	int* p = &array[0];
	for (int i = 0; i < len; i++)
	{
		printf("%d ",*p);
		p++;//用到指针运算p=p+1;跳过4个字节
	}
	return 0;
}

在这里插入图片描述

通过上面指针运算来打印数组,可以看到是可以的,但是上面的运算会改变指针变量的地址是发生改变的,而下面的写法可以防止指针变量地址的变化:
在这里插入图片描述

所以防止指针变量地址的变化,可以改写为下面形式:可以看到打印初和打印结束,p的地址没变,始终是数组首地址。
在这里插入图片描述

从上面可以得出:(指针1) + 1 == (指针2);表示指针跳过1个元素,但始终是指针,所以我们可以交换一下,(指针2) - (指针1) = = 1;表示指针之间的元素个数吧。所以通过下面来了解指针 - 指针吧。👇👇👇👇

🚀🚀指针 - 指针

前提是:必须只想同一块内存空间。

#include<stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	//数组大小
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("sz=%d\n",sz);

	int sz1 = &arr[9] - &arr[0];

	printf("sz1=%d\n", sz1);
	return 0;
}

在这里插入图片描述

✨✨指针±整数和指针 - 指针的示例

求字符串长度的库函数strlen(),和自己实现该库函数my_strlen()。
看代码:👀👀
在这里插入图片描述

指针 + 整数运算
用自己的函数my_strlen()
看代码:
在这里插入图片描述

指针 - 指针运算
在这里插入图片描述

🚀🚀指针关系运算

用指针比较大小,来打印数组,下面代码表示,arr为数组首元素地址,加上整个数组的内存大小,得到数组末尾的地址也就是元素10后面的地址,用指针变量先存储数组首元素地址,然后和数组末尾的地址相比较,如果小于末尾的地址,则让指针变量自增,依次打印出数组元素。:
在这里插入图片描述

🌈🌈野指针

在这里插入图片描述

🚀🚀野指针概念

什么是野指针呢?野指针是说指针指向的位置是不明确的,没有明确限制或不正确的。就好比大街上流浪的野狗,没有主人或家。那么这野指针又是怎样形成的呢?

🚀🚀野指针的成因

①指针未初始化。

//1、指针未初始化造成野指针
#include<stdio.h>
int main()
{
	//整形指针变量
	int a = 10;
	int* ptr;//作为局部变量,不初始化是随机值
	*ptr = 20;
	printf("%d\n",*ptr);
	return 0;
}

在这里插入图片描述

②指针越界访问

//指针越界访问造成野指针
#include<stdio.h>
int main()
{
	int arr[5] = { 0 };
	int* ptr = &arr[0];//把数组首元素地址赋给指针变量ptr
	for (int i = 0; i <= 6; i++)
	{
		*(ptr++) = i;//此处当指针指向的范围超出数组大小的时候,ptr就是野指针。
	}
	return 0;
}

在这里插入图片描述

③指针指向的空间被释放

//指针指向的空间被释放也会造成野指针
#include<stdio.h>
int* test()
{
	int number = 100;//局部变量,在调用过后会立马销毁
	return &number;
}
int main()
{
	int* ptr = test();
	printf("ptr=%d\n",*ptr);
	return 0;
}
  • 会出现警告:

在这里插入图片描述

就上面3种情况,出现野指针后,对于计算机内存来说是很危险的,会造成内存泄漏,就像没有被拴着的野狗会很危险的。所以我们在写代码的时候如何避免野指针的出现呢?

🌈🌈对野指针说拜拜

我们对照着问题找措施:

①指针变量创建的时候记得初始化。当我们创建了指针变量后不知道指向哪里,我们可以赋给它NULL。NULL是C语言中定义的一个标识符常量,值为0。

②小心指针越界访问,向内存申请多大空间,就只能访问多大的空间。

③指针变量不再使用的时候,记得及时置为NULL,指针变量使用之前检查指针的有效性。

④避免返回局部变量的地址。

🌈🌈assert断言

assert.h 头文件定义了 assert() 宏,它是用在检验程序是否符合指定条件的,如果不符合就会报错,终止运行。这个宏就称之为断言

🌈🌈传值调用和传址调用

我们学习了指针,目的是使用指针,哪什么时候,必须用指针呢?

✨✨写一个函数,实现两个整数的交换

//编写一个函数,实现两个整数的交换。
#include<stdio.h>
void swap1(int x,int y)
{
	int temp = x;
	x = y;
	y = temp;
						
}
int main()
{
	int number1 = 0, number2 = 0;
	printf("请输入两个整数:\n");
	scanf_s("%d%d",&number1,&number2);
	printf("\n交换前:\n");
	printf("number1=%d,number2=%d\n", number1, number2);
	swap1(number1,number2);
	printf("\n交换后:\n");
	printf("number1=%d,number2=%d\n", number1, number2);
	return 0;
}

我们看结果会发现,耶???怎么没得行呢?哪出问题了。别慌,我们来调试一哈。
在这里插入图片描述

从下面可以看到,number1和number2在输入两个值以后就被初始化为2和5了,并且一开始内存分配了两个地址给这两个数,接下来调试交换函数:
在这里插入图片描述
调试函数以后,初始化为0,并且分配了内存空间,但是和原来的两个变量的内存空间不一样,可以看出创建了新的内存空间来存储这两个整数了,接下来我们继续:

在这里插入图片描述

下面可以看出x和y确实拿到这两个整数,但是x和y的地址与原来的number1和number2的地址不一样,说明这两个形参是独立的两个内存空间,和原来的互不影响,所以不可以交换。

在这里插入图片描述

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

  • 既然上面的方法行不通,就得支招了,我们在交换的时候就应该用main函数里面变量自身的地址,这样才可以实现交换,所以我们就得把两个变量的地址传过去,既然传的是地址,那么函数形参就必须用指针来接收,接下来看代码:
//编写一个函数,实现两个整数的交换。
#include<stdio.h>
//void swap1(int x,int y)
//{
//	int temp = x;
//	x = y;
//	y = temp;
//						
//}

void swap2(int* p1, int* p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}

int main()
{
	int number1 = 0, number2 = 0;
	printf("请输入两个整数:\n");
	scanf_s("%d%d",&number1,&number2);
	printf("交换前:number1=%d,number2=%d\n\n",number1, number2);

	//swap1(number1,number2);//传值交换是没有用的
	swap2(&number1, &number2);//传地址就可以

	printf("交换后:number1=%d,number2=%d\n", number1, number2);
	return 0;
}

在这里插入图片描述

用swap2()函数就是传地址调用的,最终实现了交换,这就叫做传址调用。
传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;
所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改主调函数中的变量的值,就需要传址调⽤。

在这里插入图片描述

指针基础部分就描述完了,谢谢🤞🌹

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值