C语言 --- 进阶指针(未完待续.....)

目录

一,初级指针回顾

二,进阶指针

1,字符指针

1.1,基本定义与用法

1.2,使用细节

2,指针数组

2.1,基本定义及其使用

3,数组指针

3.1,基本定义

3.2,&数组名与数组名

3.3,数组指针的使用

4,数组传参,指针传参

4.1,一维数组传参

 4.2,二维数组传参

 4.3,一级指针传参

4.4,二级指针传参 

5,函数指针 

5.1,函数的地址

 5.2,通过函数指针调用函数

6,函数指针数组 

6.1,基本定义

6.2,函数指针数组的使用


一,初级指针回顾

1,首先,我们得认识到,指针的本质还是一个变量,只不过这个变量存放的是地址。在我们的计算机中,内存会划分为很多小的存储单元,而每一个存储单元都会有自己独有的内存编号,这个内存编号就是这块内存单元的地址。所以,内存编号 = 指针 = 地址。

2,指针的大小是固定的,不会取决于它所指向的变量的类型,指针变量的大小只与操作系统的种类有关。在32位操作系统下,指针是4个字节的大小,在64位操作系统下,指针是8个字节的大小,只会有这两种情况。

3,指针是有类型区分的,例如 int *p,这里定义的就是一个整形指针变量。而指针的类型,也就决定了它在做加减整数移位操作时移动的步长,p + 1,这个时候加一,移动的就是4个字节,类型不同,移动的字节长度也就不同。同时在进行解引用时,他的访问权限也是不同,int型的指针也就是只能访问4个字节的空间。

4,指针的运算:指针 +- 整数, 指针 - 指针 ,指针的关系运算。

二,进阶指针

1,字符指针

1.1,基本定义与用法

定义:char* p,这里的p就是一个字符指针,指向的是一个字符。

一般使用:

#include<stdio.h>;
int main() {
	char ch = 'w';
	char* pstr = &ch;
	*pstr = 's';
	printf("%c\n", ch);
	return 0;

}

定义字符后,将字符的地址取出赋给pstr这个字符型指针,这个时候,这个字符型指针指向的就是ch这个字符变量的存储空间,所以后面*pstr取值进行修改是能够达到修改的目的的。

其他用法:

const char* p = "abcdef";
//*p = 'w';
printf("%c\n", *p);//输出a
printf("%s\n", p);//输出abcdef

1,对于上面的字符型指针,我们可能会疑惑为什么会用来接受一个字符串,但实际不是的,这个时候指针p实际指向的是这个字符串的首字母的地址,也就是a的地址,也就是说你通过指针p来访问的话,来取出p的值就只是a,相当于把a这个字符的地址给了指针。并且换一种理解方式来说,这个char型的指针也就只有四个字节(32位系统),不可能放的下七个字节的字符串。

2,另外,这个字符串也是不能够修改的,这是一个常量字符串,不可修改,并且我们还会再用const来修饰这个指针,也就是一个常量指针,指针指向的内容不能够被修改。(这里常量字符串本身就是不能修改的,要在前面加一个const是因为在新版本的编译器中是必须要加的,不然会报错。)

3,虽然指针指向的是字符串的首字母,但由于这个字符串在常量区是连续存储的,所以我们用%s 进行输出字符串,然后将指针p传过去,也就是首字符的地址,是能够进行字符串的输出的。

这里用字符指针指向字符串,和用int型的指针指向一个整形数组是一个道理,这个整形指针指向的也只是首元素的地址,也就相当于数组的地址。

1.2,使用细节

const char* p1 = "abcdef";
const char* p2 = "abcdef";

char arr1[7] = "abcdef";
char arr2[7] = "abcdef";

if (p1 == p2) {
	printf("p1 = p2\n");
}
else {
	printf("p1 != p2\n");
}
if (arr1 == arr2) {
	printf("arr1 = arr2\n");
}
else {
	printf("arr1 != arr2\n");
}

输出结果:p1 = p2,arr1 != arr2

分析:如草图所示,对于字符指针p1 ,p2 来说,他们指向的是相同的常量字符串的首字母,因为这个字符串不可变且唯一,所以他在内存中的地址是唯一的,所以p1与p2是相等的,指向同一个地址也即是字符串首字母a的地址。但是对于数组arr1 与 arr2 而言,他们是两个不同的数组,在定义的时候两个数组开辟的空间地址是不一样的,对于他们来说,只是凑巧里面保存的值是一样的,除此之外,二者没有任何的关联,是两个不同的东西。

2,指针数组

2.1,基本定义及其使用

定义:指针数组:用来存放指针的数组就叫做指针数组。

int a = 10;
int b = 20;
int c = 30;
int* arr[3] = { &a,&b,&c };
for (int i = 0;i < 3;i++) {
	printf("%d ", *(arr[i]));
}

在定义数组时,int* 表示的是数组的类型,也就是里面存放的元素的类型,指针类型。要拿到具体的值,同样是遍历,先拿到地址,然后再解引用就可。

    int arr1[3] = { 1,2,3 };
	int arr2[3] = { 2,3,4 };
	int arr3[3] = { 3,4,5 };
	int* parr[3] = { arr1,arr2,arr3 };
	for (int i = 0;i < 3; i++) {
		for (int j = 0;j < 3;j++) {
			printf("%d ",parr[i][j]);
			//printf(printf("%d ",*(parr[i] + j)))
		}
		printf("\n");
	}

对于上面的指针数组parr来说,其实和二维数组类似了,因为它里面的元素都是指向数组的指针,代表着三个不同的数组。arr1,arr2,arr

3三个数组的数组名作为指针数组的元素,他们分别代表着各自数组首元素的地址,所以只要先通过指针数组parr拿到各个数组的首元素地址,然后在此基础上加上整数,就可以遍历得到数组元素的具体值。

有一个点需要注意,就是在遍历数组的时候,如何将数组元素进行输出的问题:

parr[i][j] = *(parr[i] + j)

这里可能会有很多人不懂为什么这二者是等价的,其实对于数组来说,其 [ ] 的实际作用就是解引用,parr[i] 我们先是拿到了首元素的地址,然后向后遍历加 j ,就能得到各个元素的地址,再将其解引用就能得到具体的值。从根本上说,两种方法不管你怎么写,其实计算机它最终都会转化到*(parr[i] + j)这种输出的方法上进行计算输出。

3,数组指针

3.1,基本定义

定义:用来指向数组的指针,其本质是一个指针。

注意:此时数组指针指向的是整个数组,不能说是指向数组首元素的地址。

int* arr1[10]
int(*p)[10]

注意区分一下上面的两种定义,第一个是指针数组,第二个是数组指针

//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个 指针,指向一个数组,叫数组指针。 //这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合

3.2,&数组名与数组名

    int a = 10;
	int* p = &a;
	int arr[10] = { 0 };
	//数组名是首元素地址
	printf("%p\n", arr);
	printf("%p\n", &arr[0]);
	printf("%p\n", &arr);

p是指针类型,所以在输出的时候,%p就可以控制输出地址类型的数据

输出结果看出,上面的三种方式求地址好像没有什么区别,我们知道数组名就表示数组首元素地址,所以arr与&arr[0]这二者肯定是没有任何区别的。但是对于&arr来说,在这里你看结果是一样,但其实它所表示的意思是不相同的,它代表的是整个数组的地址,我们在后面加个一就能看出区别来。

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

	printf("%p\n", &arr[0]);
	printf("%p\n", &arr[0] + 1);

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

我们上下对比输出可以看出,对于第一二种方法来说,加1就是后移访问数组的下一个元素,所以移动的是一个int的字节。但是第三种我们加1后它移动的是40个字节,也就是一整个数组的大小,所以对于&arr来说,它代表的是一整个数组的地址,在后面加上整数就是以整个数组的大小为单位移动的,而不是首元素。

那么既然&arr指向的是整个数组,就可以解释定义的来源了。int(*p)[10] = &arr,&arr的类型就是数组指针类型,即int(*)[10],其中10是不能省略的,它明确的指出了我们这个指针指向的数组的大小。同时,&arr[0],arr的类型就是int*。

小练习:请问p的类型是什么?

char* arr[5]
p = &arr //完整正确的定义为 char* (*p)[5] = &arr

首先arr是一个指针数组,其中每个元素的类型都是char*,所以&arr将整个数组的地址赋给p,p的类型就是一个数组指针,具体为 char* (*)[5],前面的char*表示的是数组的类型。

总结,数组名的理解情况:

除了上述两种情况之外,所有的数组名表示的都是首元素的地址。

3.3,数组指针的使用

1,用于一维数组(不推荐,小题大做,绕弯)

    int arr[3] = { 1,2,3 };
	int(*p)[3] = &arr;
	for (int i = 0;i < 3;i++) {
		printf("%d\n", *(*p + i));
	}

分析:这里主要需要理解的就是*(*p + i),p是一个数组指针,它所指向的是整个数组的地址,所以我们对其解引用*p,就相当于拿到了整个数组,拿到整个数组,就相当于拿到了数组名,因为数组名代表这个数组,所以有了数组名就知道了首元素地址,后面的工作也就是顺理成章的事。

2,用于二维数组(重要)

void printf1(int (*p)[5], int c, int r) {
	for (int i = 0;i < c;i++) {
		for (int j = 0;j < r;j++) {
			//printf("%d ",*( * (p + i) + j ));
			printf("%d ", p[i][j]);
		}
		printf("\n");
	}
}
int main() {
	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
	//写一个函数输出这个二维数组
	int sz1 = sizeof(arr) / sizeof(arr[0]);
	int sz2 = sizeof(arr[0]) / sizeof(arr[0][0]);
	
	printf1(arr,sz1,sz2);
	return 0;
}

首先,计算数组长度的方法:整个数组的大小除以单个元素的大小,就能够得到数组的长度。注意,数组的大小和长度是不一样的,不能把两者混为一谈。

首先看上面的代码,可能会有疑惑,数组指针接受的不应该是整个数组的地址吗,为什么这里传入的是arr,也就是这个二维数组的首元素地址呢?其实,本质是一样的,因为在二维数组里面,每一个元素都是一个一维数组,所以这里的首元素地址,也就是一整个一维数组的地址,所以数组指针p接受的也是整个数组的地址(降维了)。当我们拿到二维数组第一个元素的地址后,这个时候指针p加上整数,就是直接跨过的一整个一维数组元素。*( * (p + i) + j ),指针p代表整个一维数组的地址,对其解引用就得到了一维数组的首元素地址,在后面加上j循环遍历,就能够输出二维数组的每一个一维数组的具体元素值。

4,数组传参,指针传参

4.1,一维数组传参

 对于一维数组传参而言,在形参接收的时候,接收的是数组名,也就是首元素的地址,所以在本质上我们应该是用指针来接收。这里用数组来接收只是为了初期方便我们的理解,而且有一点很重要就是我们数组做形参接收的话,实际上是不会真正创建一个数组的,所以这个形参数组的大小你可以不写甚至随便写。其实在底层上,编译器最终还是会把它转成一个指针来计算。

 4.2,二维数组传参

 对于二维数组传参而言,其实和一维数组差不多,也有两种写法,这里有几个要注意的点,首先形参的二维数组的行可以不写,但是列必须要写出,因为二维数组的每行其实是连续存储的,你不写出列数,就相当于你不知道它每一行的结束位置。然后就是传首元素地址要用数组指针来接收。

 

 

 4.3,一级指针传参

#include <stdio.h>
void print(int* p, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d\n", *(p + i));
	}
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9 };
	int* p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	//一级指针p,传给函数
	print(p, sz);
	return 0;
}

一级指针传传参,比较简单,可以用来接收一个变量的地址,也可以直接接收别的一级指针变量,也可以接收一维数组首元素的地址。一级指针之间互指,其实就是将其中一个指针里面存放的地址给了另外的一级指针,两个指针指向的都是同一个地址。

4.4,二级指针传参 

void test(char **p)
{
 
}
int main()
{
 char c = 'b';
 char*pc = &c;
 char**ppc = &pc;
 char* arr[10];
 test(&pc);
 test(ppc);
 test(arr);//Ok?  是可以的
 return 0;
}

二级指针传参,可以用来接收一级指针的地址,也可以二级指针之间互指,还可以用来接收指针数组首元素的地址(解释一下,指针数组也就是以指针作为数组元素的数组,比如上面的arr,他的每一个元素都是一个char*的类型,那么你传首元素地址就相当于是传char*的地址,也即是一级指针的地址,所以可以用二级指针来接收)

 

5,函数指针 

定义:函数指针,顾名思义,就是指向函数的指针。

5.1,函数的地址

 

 在这里要注意了,对于函数的地址而言,上面的两种写法是一个意思,没有任何区别,都表示函数的地址。

 5.2,通过函数指针调用函数

int test(int x,int y) {
	return x + y;
}
int main() {
	int a = 1;
	int b = 1;
	int (*p)(int, int) = test;
	//int ret = (*p)(a, b);
	int ret = p(a, b);
	printf("%d\n", ret);
	return 0;
}

一般而言,我们对这个函数指针解引用就可以拿到这个函数,然后传参就可以调用了。但是我们发现既然可以直接把函数名赋给函数指针,那么他们的作用其实就是差不多的,所以编译器简略语法后,加不加那个解引用的* 都是一样的。

6,函数指针数组 

6.1,基本定义

定义:用来存放函数指针的数组就是函数指针数组

作用:当作一个跳板,可以由数组元素访问到函数,在某些情况下大大简化了函数调用的过程。(转移表)

6.2,函数指针数组的使用

简易计算器的实现:

#include<stdio.h>
int ADD(int x, int y) {
	return x + y;
}

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

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

int Div(int x, int y) {
	return x / y;
}
void menu() {
	printf("==================\n");
	printf("====1,加法操作====\n");
	printf("====2,减法操作====\n");
	printf("====3,乘法操作====\n");
	printf("====4,除法操作====\n");
	printf("====0,退出操作====\n");
	printf("==================\n");
}
int main() {
	int (*p[5])(int, int) = { 0,ADD,Sub,Mul,Div };
	int sz = sizeof(p) / sizeof(p[0]);
	int ret = 0;
	do {
		int a = 0;
		int b = 0;
		menu();
		printf("请输入您想要进行的计算操作>>>");
		scanf("%d", &ret);
		if (ret == 0) {
			printf("退出成功!");
			break;
		}
		else if (ret > 0 && ret < sz) {
			printf("请输入两个操作数>>>");
			scanf("%d %d", &a, &b);
			printf("计算结果为 %d\n", p[ret](a,b));
		}
		else {
			printf("输入错误!");
			break;
		}
	} while (ret);

	return 0;
}

运用函数指针数组之后你会发现主函数部分大大精简了,并且添加功能只需要定义好函数,然后把它的地址放到函数指针数组里面就完事了,相对于switch实现,函数指针数组的效率明显更高。

 

好了,今天的补充就这么多了,感谢大家的观看,后面还有一点会继续补充,希望大家能继续关注,谢谢!有错误也希望大家帮忙指出。 

评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

努力学习.java

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

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

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

打赏作者

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

抵扣说明:

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

余额充值