超详细的c进阶教程(二)!手撕c指针

作者的码云地址:https://gitee.com/dongtiao-xiewei
后续作者会更新力扣的每日一题系列,原代码会全部上传码云,推荐关注哦,笔芯~

还像更深入地了解c语言?快来订阅作者的c语言进阶专栏!作者承诺本系列不会TJ!预计更新:指针,字符串处理,内存管理,结构体,预处理等等

C指针我相信,对于许多正在新手村发育的程序员来说,都是一道过不去的坎(包括我新手期呜呜呜TAT)。为了解决广大新手玩家在指针关卡卡关的问题,特地推出此攻略~

废话不多说,让我们开始吧!
在这里插入图片描述

指针快速入门

指针的概念

到底什么是指针?以下是度娘给出的定义:

指针也就是内存地址,指针变量是用来存放内存地址的变量,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作。

指针在c语言如此流行,也是因为指针可以有效的实现例如树,链表等数据结构。

听了度娘这么说,感觉还是一头雾水啊,说了个寂寞啊。。

别急,接下来我用生活中的例子给大家引出指针这一概念吧

我们可以把计算机内存看做一条街道上的一排房子,就像这样,每个房子都有自己的编号,如下图

在这里插入图片描述
每个房子都有且仅有一个门牌号与它对应,每个房子都可以容纳一口人家

对应到计算机科学中就是:

计算机中创建的每个变量(人家)都会在内存中占据一定的位置,每个内存位置都由一个特定的地址(门牌号)唯一确定

指针它就是一种能存储地址的变量。我们可以通过一些操作,利用指针找到某一个变量,去间接的操作它。

对应到计算机中就是这样

在这里插入图片描述
为什么一个格子对应一个字节呢?

指针大小

我们知道,我们使用的操作系统有32位和64位之分(在此电脑上点击右键后再点击属性查看)

在这里插入图片描述
操作系统位数的意义是:电脑可以使用多少根地址线

我们拿32位操作系统来举例:

32的机器对应32根地址线,假设每根地址线可以产生一个电信号(正电/负电),对应二进制中用1/0表示

32位就可以产生如下的电信号
注:一根地址线对应一个比特位,每8个比特位对应1个字节

00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
....
11111111 11111111 11111111 11111111

我们简单的算一下:每根地址线可以产生2个电信号,32个地址就可以产生232个不同的电信号。

如果我们把每个字节表示成一个地址的话,可以表示232字节内存,按一个单位1024的比例扩大,算出可以操控4GB的空闲空间进行编址

问题来了,那为什么不拿其它单位来标识一个地址呢?

就拿1kb来举例,我们知道一个int变量大小仅有4b,如果我们拿1kb大小来标识一个地址,将会造成大量空间的浪费(本例浪费1020b左右)

按一个字节1b的大小来计算:

  • 32位操作系统下的指针变量大小为4b
  • 64位操作系统下的指针变量大小为8b
    可以简单写个程序来验证一下
int main()
{
	printf("%d\n", sizeof(int*));//int*的指针变量
	printf("%d\n", sizeof(char*));//可以用不同类型的指针变量来验证,是不是指针变量大小是固定的
	return 0;
}

VS2019设置系统的方法如下图

在这里插入图片描述
这里的x86代表我们使用的是32位平台

输出结果如下

在这里插入图片描述
64位平台上输出如下
在这里插入图片描述

指针操作符类型

可能又有好奇的小伙伴会问了,既然不同指针类型大小都一样,那为啥还要用这么多的指针类型啊?

哦对了,我们还是先介绍一下指针该怎么用吧~

指针的操作主要靠两个运算符:*—— 解引用操作符和 & ——取地址操作符

比如以下的代码

int a=10;
int* p=&a;
*p=20;
printf("%d\n",a);

注:定义指针变量int p这里的星号表明p是一个指针变量,而不是解引用的意思*

我们通过画图解释这两个操作符

在这里插入图片描述

所以最后的输出结果就应该是20

在这里插入图片描述
当然,这里我们把int类型的变量存放在了int指针中,那可不可以把int a放在char* 类型的指针变量中呢?

int main()
{
	int a = 10;
	char* p = &a;
	return 0;
}

程序确实能编过去,但是报了一个警告

在这里插入图片描述

虽然这个程序可以编过去,不过我们最好还是把指针存放在相同类型的变量中,不然就可能导致一些意外:

比如还是本节第一个程序,只不过我们换成char*类型的变量,并将a的值修改到足够大

int a=0x11223344;
char* p=&a;
*p=0x55667788;
printf("%x\n",a);

输出如下图

在这里插入图片描述

很显然,输出的值不是我们想要的值,为什么会造成这种错误呢

让我们调试一下,看看内存情况。

在这里插入图片描述

所以我们可以得出结论:

指针类型决定了向前或向后走一步或者操作的权限有多大

指针的运算

这一节全是概念性质的知识点,只需要记住就行了

指针±整数

我们通过打印数组的例子来说明这一概念

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(arr + i));
	}
	return 0;
}

这个程序可以打印出每一个元素

在这里插入图片描述

所以,指针加减整数,可以表示跳过的元素个数

指针-指针

int main()
{
	int arr[10] = { 0 };
	int* parr1 = (arr + 9);
	int* parr2 = &arr[0];
	printf("%d\n", parr2 - parr1);
	return 0;
}

输出结果
在这里插入图片描述
结论:指针减去指针的值的绝对值是两个指针中间的元素个数

指针的比较

我们知道,其实指针都是有一个十六进制的数字表示的,也算作一个值,所以理所当然的可以比较大小

int main()
{
	int arr[10] = { 0 };
	int* parr1 = (arr + 9);
	int* parr2 = &arr[0];
	if (parr1 > parr2)
		printf("haha\n");
	else
		printf("hehe\n");
	return 0;
}

在这里插入图片描述

结论:数组中的元素随着下标从小到大,其地址也由低到高

指针和数组的联系

我们平时在学习c语言的时候或者我们在看一些c语言教材的时候,我们发现作者通常会把指针和数组放在一起谈,这是为啥呢?指针和数组到底有什么关系呢?

用一个代码说明

int main()
{
	int arr[10] = { 0 };
	int* p1 = &arr[0];
	int* p2 = arr;
	printf("%p\n", p1);
	printf("%p\n", p2);
	return 0;
}

输出结果如下

在这里插入图片描述

这两个表示形式的地址是一模一样的

所以可以得出以下结论:数组名代表的是首元素地址

但是有以下两种特殊情况

  1. sizeof(arr)(其实arr未参加任何运算),这里的arr代表整个数组
  2. &arr,这里也代表的是取出整个arr数组的地址

用代码举例吧

int main()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);
	printf("%p\n", &arr + 1);//检测跳过了一整个元素还是一个元素
	printf("%u\n", sizeof(arr));//比较是否与首元素地址大小一样
	printf("%u\n", sizeof(&arr[0]));
	return 0;
}

输出结果如下
在这里插入图片描述

数组的传参

我们知道,数组元素名代表的是首元素地址,所以以下两种传参方式都是可行的。

void test(int* p)//1
void test(int p[])//2
int main()
{
	int arr[10];
	test(arr);
	return 0;
}

数组传参的时候会退化成首元素的地址传给函数体

所以我们可以使用一个指针接受arr

当然,利用第二种数组的方式接受也可以,第二种方式更容易使萌新理解,我们不用管方块里面该填什么,随便你怎么填,要么不填,要么填一个大于原数组的数字,都无所谓

但是,这样传参,我们在有些使用场景下可能会出现一些问题

比如,我们要利用函数打印一个数组

void print_arr(int* arr)
{
	int sz = sizeof(arr) / sizeof(arr[0]);//我们必须要知道一个数组有多少元素才能打印,这是通用的计算方法
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}

int main()
{
	int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
	print_arr(arr);
	return 0;
}

但是,程序的输出只打印了一个元素

在这里插入图片描述

其实是因为函数传进去的是一个指针,指针大小是固定的4或者8个字节,所以在函数体内计算数组大小永远是4/类型大小,结果是1,这个结果显然是错误的

所以,我们必须在函数外先算好数组元素个数,把个数参数传进去,才能达到我们想要的结果

void print_arr(int* arr,int sz)
{
	
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}

int main()
{
	int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	print_arr(arr,sz);
	return 0;
}

在这里插入图片描述
这个结果就是正确的

[]操作符与指针

以下两种程序的效果完全等价

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);//第一种
		printf("%d ", *(arr + i));//第二种
	}
}

第二种的解释:arr代表首元素的地址,首先进行加法运算跳过某一些元素,最后在对其解引用找到数组中某一个元素的数字

二级或更高级指针

二级指针可以存放比它低一级指针变量的地址

在这里插入图片描述

int main()
{
	int a = 10;
	int b = 20;
	int* pa = &a;
	int* ppa = &pa;
	*ppa = &b;
	return 0;
}

字符指针和字符串

我们知道,有一种指针类型叫char*类型,它可以存放字符类型的地址

int main()
{
	char ch = 'w';
	char* p = &ch;
	*p = 'q';
	return 0;
}

但是我们知道,c语言不像python,它是没有字符串类型的

但是,我们可以使用字符指针来处理字符串

在数组阶段,我们通常是使用数组来保存字符串的

char arr[]="hello world!";

我们同样可以使用字符指针来保存字符串

char* p="hello world!";

这种保存方式,是把整个字符串地址传进来了吗?

其实不是,我们简单用一个程序验证一下

int main()
{
	char* p = "hello world!\n";
	printf("%c\n", *p);
	return 0;
}

输出结果如下
在这里插入图片描述
结果是h,所以我们可以初步得出结论

字符指针在储存字符串时,本质还是把首元素地址储存起来,而存放空间又是连续的,所以可以通过这个指针找到整个字符串

在这里插入图片描述

指针数组和数组指针

至于为什么把它们放在一起,是因为它们名字里面每个字都一样,但是意义却完全不一样~

前者是数组,后者是指针

指针数组

顾名思义,指针数组就是一个能存放地址的数组,表示方式如下

int* parr[10];

这个程序就代表,一个名为parr的数组,其中包含10个元素,每个元素都是int*指针类型

我们可以这样分析:

把名字去掉,剩下的是int*[10],就是它的类型了~

指针数组的初始化方式举例

int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	int d = 40;
	int* parr[4] = { &a,&b,&c,&d };
	for (int i = 0; i < 4; i++)
	{
		printf("%d ", *parr[i]);
	}
	return 0;
}

可以打印出我们想要的结果
在这里插入图片描述
内存图:
在这里插入图片描述

总结:初始化只需要传进地址就行了,使用的时候记得解引用操作

数组指针

结论:数组指针是一种指向数组的指针
表示方式:

int(*p2)[10];

注意:由于括号的优先级最高p2不会先与[]结合,而是先与*结合,说明其是一个指针变量,然后指向是一个含10个整型元素的数组

数组指针的应用:二维数组传参

我们知道,二维数组在内存的方式也是连续存放的,一行元素存完后,接着立刻存储下一行

int arr[3][3];

这个二维数组的内存情况如下

在这里插入图片描述

在这里插入图片描述
我们就可以把二维数组的每一行看做一个一维数组的元素

于是我们就可以这样理解二维数组:看做一个一维数组,每一行的元素都是另外一个一维数组

基于以上的推测,于是我们就可以使用数组指针来传参

void test(int(*p)[3])
{

}

int main()
{
	int arr[3][3] = { 0 };
	test(arr);
	return 0;
}

p就被看作一个指针,该指针指向一个有3个整型元素的数组

解引用方式:

*(*(p+1)+2)//等价于arr[1][2]

函数指针

其实,我们自己写的函数也是会使用特定的地址保存起来的

那函数的地址能不能保存呢?答案是当然可以,用函数指针就行了

函数指针包含以下几个成分:

  1. 指向的函数的返回值
  2. 指向的函数的参数类型

以下有两种可能会混淆的定义方式

void(*pfun1)()=test1;
void*pfun2()=test2;

第一种方式是正确的,因为变量必须要保存先与*结合,说明其是一个指针

使用方式

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int(*pfun)(int, int) = Add;
	//(返回类型)int(指针变量名称)(*pfun)(参数类型)(int, int) = (指向的函数)Add;
	int ret = (*pfun)(2, 5);
	//也可以进行简化
	int ret1 = pfun(2, 6);
	printf("%d,%d\n", ret, ret1);
	return 0;
}

函数指针数组

类比指针数组,就是把每个元素换成一个函数指针了

声明方式

void(*parr[10])(int,int);

函数指针应用(回调函数)

定义:就是通过函数指针调用的函数,这个函数的参数通常是一个函数指针

例子:计算器

函数定义部分:

float Add(float x, float y)
{
	return x + y;
}

float Sub(float x, float y)
{
	return x - y;
}

float Mul(float x, float y)
{
	return x * y;
}

float Div(float x, float y)
{
	if (y == 0)
	{
		printf("error\n");
		return 0.0
	}
	else
	{
		return x / y;
	}
	
}

不适用函数指针的话,代码将会变得非常冗余,(输入操作数的部分)

int main()
{
	int input = 0;
	float x = 0.0;
	float y = 0.0;
	do
	{
		printf("1. Add      2.Sub\t");
		printf("3. Mul      4.Div\n");
		printf("你要做什么?\n");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("请输入两个操作数\n");
			scanf("%f%f", &x, &y);
			Add(x, y);
			break;
		case 2:
			printf("请输入两个操作数\n");
			scanf("%f%f", &x, &y);
			Sub(x, y);
			break;
		case 3:
			printf("请输入两个操作数\n");
			scanf("%f%f", &x, &y);
			Mul(x, y);
			break;
		case 4:
			printf("请输入两个操作数\n");
			scanf("%f%f", &x, &y);
			Div(x, y);
			break;
		default:
			break;

		}

	} while (input);
	return 0;
}

我们可以使用回调函数简化代码,有效避免函数冗余

回调函数的声明

void calc(float(*pfun)(float, float))
{
	float x = 0.0;
	float y = 0.0;
	printf("请输入两个操作数:\n");
	scanf("%f%f", &x, &y);
	float ret = (*pfun)(x, y);
	printf("%.2f\n", ret);
}

主函数的简化

switch (input)
		{
		case 1:
			calc(Add);
			break;
		case 2:
			calc(Sub);
			break;
		case 3:
			calc(Mul);
			break;
		case 4:
			calc(Div);
			break;
		default:
			break;

		}

	} while (input);
  • 25
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

东条希尔薇

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

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

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

打赏作者

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

抵扣说明:

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

余额充值