【C语言】指针如何花样无限套娃?深入指针数组&数组指针&函数指针&函数指针数组!

一、字符指针

其实这只是一种基本的指针类型,但字符指针不仅仅用于指向寻常的单个字符变量地址,字符指针还可以做到下面的操作,指向一个字符串,进而操作字符串:

int main()
{
const char* pstr = "hello bit.";
printf("%s\n", pstr);
return 0;
}

但要注意的是:

  • 这里不是pstr指向整个字符串的地址,其实pstr指向的是该字符串的第一个字符的地址,那么如果想操作该字符串,通过指针的±运算操作即可。
  • 字符指针pstr指向的字符串为常量字符串,该字符串再内存中常量区存储,就算字符串长得一样,实际也只存在一份而不是多份,存在多份的也只能是指针变量。而不是像字符数组中存储的字符串那样,就算字符串长得一样,也依然会继续开辟栈区内存存储。

请看下面例子能更深刻地认识到区别:

#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char *str3 = "hello bit.";
const char *str4 = "hello bit.";
if(str1 ==str2)
	printf("str1 and str2 are same\n");
else
	printf("str1 and str2 are not same\n");
if(str3 ==str4)
	printf("str3 and str4 are same\n");
else
	printf("str3 and str4 are not same\n");
return 0;
}

结果:
在这里插入图片描述
原因和上述第二点一样:这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同。

二、指针数组

可以类比一下前面学的数组,如整型数组存放的是整型数据,那么不难推断出指针数组存放的必然是指针,也就是地址。

int main()
{
	int a = 1,
		b = 2,
		c = 3;
	int* arr1[10] = { &a, &b, &c }; // 每个元素都是int类型变量的地址
	// ... 以及char*、double*等其它指针类型的数组。
	return 0;
}

三、数组指针

数组指针是指针?还是数组?答案是:指针。

类比其它基础类型指针:

  • 整形指针: int * pint; 能够指向整形数据的指针。
  • 浮点型指针: double * pf; 能够指向浮点型数据的指针。

那数组指针应该是:能够指向数组的指针。

数组指针长这个样:

int main()
{
	int arr[10] = { 1, 2, 3};
	// 数组指针
	int (*parr)[10] = &arr;
	return 0;
}

我们使用数组指针该注意的是,既然数组指针是指向数组的指针,那么应该指向整个数组的地址而不是数组首元素的地址,关于数组地址,请看文章:数组名不是首元素地址的的2个例外

解读数组指针的语法:int (*parr)[10]

  1. 首先用大括号把*和parr结合,表示parr是一个指针;
  2. 这个指针指向一个大小为10的数组,数组元素类型为int。

千万不能写成int * parr[10] = &arr;这样就变成指针数组了。

数组指针也能指向二维数组,我们知道二维数组的首元素实际上是一个一维数组,那么其实直接把二维数组首元素地址传给数组指针接收即可。

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

扩展:

int (*parr[10])[5];

看到这段代码,请问大家现在是不是现在这个表情 🤯 ,别怕我来给大家解读。

  • 首先parr和[]结合,表名parr是一个数组,为啥呢?为啥不是先和*结合表示parr是一个指针?因为[]优先级要高很多,[]甚至是最高优先级的那一个档次。

  • 那么我们知道它是一个数组后,拆分得到剩下的:int (*)[5],这是啥?这不就是数组指针,实际上这就是数组的元素类型。

  • 合起来说就是:parr数组的大小为10,每个元素类型是一个数组指针,这个数组指针指向的数组大小为10,元素类型为int。

四、数组和指针的相互传参

很多时候,我们都会有这样的操作,就是将函数参数设计为指针,然后数组作为实参传入函数,那么如何设计函数参数呢?

指针接收一维数组传参

void test(int *arr)
{}
void test2(int **arr)
{}
int main()
{
	int arr[10] = {0};
	int *arr2[20] = {0};
	test(arr);
	test2(arr2);
}

对于那个一维指针数组传参的函数参数设计,或许有些人会感到疑惑,为何使用的是一个二级指针接收?其实道理很简单,二级指针就是用来接收一级指针的,且一维指针数组的每个元素都是一个指针,数组名代表首元素地址自然也是一个指针。

二维数组传参,指针接收

void test1(int *arr)
{}
void test2(int* arr[5])
{}
void test3(int (*arr)[5])
{}
void test4(int **arr)
{}
int main()
{
	int arr[3][5] = {0};
	test1(arr);
	test2(arr); 
	test3(arr);
	test4(arr);
}

先说结果,上述四个函数的参数设计,只有test3()是对的,其余全错,那么这是为啥呢?首先二维数组的首元素肯定是一个一维数组,这个数组有5个元素,那么二维数组的首元素地址就是整个一维数组地址。

  • 对于test1(),显然用一个int指针指向整个一维数组的地址是说不通的,int指针也只能指向一维数组的首元素地址。

  • 对于test2(),同样说不通,很显然实参是一个元素为int类型的一维数组,而不是指针数组。

  • 对于test4(),二级指针是用来指向一级指针的,那么也是无法指向一个整个一维数组的地址。

而对于test3()的参数设计恰好匹配,*arr是一个指针,指向一个大小为5个元素的数组,元素类型为int。

逆向思维:当函数参数设计为一级指针、二级指针时,实参用相应级别的指针或者相同类型变量地址传入肯定是没问题的,但函数参数能接收哪些数组传入呢?

五、函数指针

函数也有地址?首先看一段代码。

#include <stdio.h>
void test()
{
	printf("hehe\n");
}
int main()
{
	printf("%p\n", test);
	printf("%p\n", &test);
	return 0;
}

其实结果相同。

那么知道了怎么获取函数的地址,函数指针如何表示呢?其实道理和数组指针类型,我们可以照猫画虎仿照一个。

void test(int a)
{
	...
}
int main()
{
	int arr[10];
	int (*parr)[10];
	// 对比上面的数组指针,写出函数指针
	void (*ptest)(int) = test; // &test也可以
	
	// 使用函数指针调用函数
	ptest(5);
	(*ptest)(5); // 两种都行,其实在这*号就是摆设,写多个*都行,类比函数指针接收函数地址的两种写法,这里写个*只是一种指针的标准用法,但一定要加括号提升优先级结合使用
	return 0;
}

void (*ptest)(int) *与ptest结合代表它是一个指针,()则表示ptest指向的是一个函数,有一个参数为int类型,返回值类型为void。

这个例子能更深刻认识到函数指针的作用:

#include <stdio.h>
int add(int a, int b)
{
	return a + b;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int div(int a, int b)
{
	return a / b;
}
int main()
{
	int x = 0, y = 0;
	int input = 1;
	int ret = 0;
	do
	{
		printf("*************************\n");
		printf(" 1:add 2:sub \n");
		printf(" 3:mul 4:div \n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		printf("输入操作数:");
		scanf("%d %d", &x, &y);
		switch (input)
		{
		case 1:
			ret = add(x, y);
			printf("ret = %d\n", ret);
			break;
		case 2:
			ret = sub(x, y);
			printf("ret = %d\n", ret);
			break;
		case 3:
			ret = mul(x, y);
			printf("ret = %d\n", ret);
			break;
		case 4:
			ret = div(x, y);
			printf("ret = %d\n", ret);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}

配合使用函数指针修改后:

void cal(int (*pcal)(int, int))
{
	int x = 0, y = 0;
	printf("输入操作数:");
	scanf("%d %d", &x, &y);
	printf("%d\n", pcal(x, y));
}
int main()
{
	int input = 0;
	do
	{
		printf("*************************\n");
		printf(" 1:add 2:sub \n");
		printf(" 3:mul 4:div \n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			cal(Add);
			break;
		case 2:
			cal(Sub);
			break;
		case 3:
			cal(Mul);
			break;
		case 4:
			cal(Div);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}

大家可以分析一下下面两段代码,扩展一下思维:

(*(void (*)())0)(); 

void (*signal(int , void(*)(int)))(int); 

分析:

  • (*(void (*)())0)(); 是一个函数调用。看懂这段代码,0是突破口。void (*)()是函数指针,把0强转成这个类型,然后*解引用()调用。等于是把0强转成函数了,再对0的地址处解引用,()调用。

  • void (*signal(int , void(*)(int)))(int); 是一个函数声明。这段确实不好理解,容易理解错误。signal不是一个函数指针变量,因为如果是的话,那么左边一定有一个返回值类型,但是这里没有。正确的理解是:signal是函数名,有两个参数,一个是int类型,一个是函数指针类型(这个函数指针指向的函数,返回值类型是void,参数是一个int类型参数),然后最后这里一定要拆开看,把中间的signal(int, void(*)(int))拿走,剩下的void(*)(int)其实就是signal函数的返回值类型,是函数指针类型,返回的函数指针返回值是void类型,参数有一个int类型。

我只能说,最后一个看我的描述非常绕,但确实就是这样 🤯。

六、函数指针数组

函数指针是指针,那么函数指针数组实际上也是指针数组,只是结合起来看起来更复杂了。

int Add(int x, int y) 
{
	return x + y;
}
int main()
{
	// 指针数组
	int* arr[10];
	// 函数指针
	int (*padd)(int, int) = &Add;
	// 函数指针数组
	int (*padd[5])(int, int);
	rerurn 0;
}

那么我们要怎么理解函数指针数组的语法表示呢?首先padd先和[]结合代表它是一个数组,剩下的int(*)(int, int)就是数组的元素类型,等于说padd数组的每个元素都是一个函数指针。

那么函数指针数组有啥用呢?答案是用作“转移表”,如计算器的实现,请看例子。

#include <stdio.h>
int add(int a, int b)	
{
	return a + b;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a*b;
}
int div(int a, int b)
{
	return a / b;
}
int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
	do
	{
		printf( "*************************\n" );
		printf( " 1:add 2:sub \n" );
		printf( " 3:mul 4:div \n" );
		printf( "*************************\n" );
		printf( "请选择:" );
		scanf( "%d", &input);
		printf( "输入操作数:" );
		scanf( "%d %d", &x, &y);
		switch (input)
		{
		case 1:
			ret = add(x, y);
			printf( "ret = %d\n", ret);
			break;
		case 2:
			ret = sub(x, y);
			printf( "ret = %d\n", ret);
			break;
		case 3:
			ret = mul(x, y);
			printf( "ret = %d\n", ret);
			break;
		case 4:
			ret = div(x, y);
			printf( "ret = %d\n", ret);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf( "选择错误\n" );
			break;
		}
	} while (input);
	return 0;
}

其实这个代码还是有点冗余的,只是方法名不同,其它的完全长得一个样。这时我们可以使用函数指针数组,减少代码书写,如case语句等,并且更能直观感受到函数指针数组的一个用途,在以后或许能参考使用到。

int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
	int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
	while (input)
	{
		printf( "*************************\n" );
		printf( " 1:add 2:sub \n" );
		printf( " 3:mul 4:div \n" );
		printf( "*************************\n" );
		printf( "请选择:" );
		scanf( "%d", &input);
		if ((input <= 4 && input >= 1))
		{
			printf( "输入操作数:" );
			scanf( "%d %d", &x, &y);
			ret = (*p[input])(x, y);
		}
		else
			printf( "输入有误\n" );
		printf( "ret = %d\n", ret);
	}
	return 0;
}

每当增加一个函数,只需要往转移表中添加函数名,再修改下if的判断范围就行了。

七、回调函数

回调函数可以看成是另外一种函数调用的方法、机制。

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

如qsort库函数中就是用到了回调函数:

#include <stdio.h>
#include <stdlib.h>
int int_cmp(const void * p1, const void * p2)
{
	return (*( int *)p1 - *(int *) p2);
}
int main()
{
	int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
	int i = 0;
	// 将函数地址传入qsort函数
	qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
	for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf( "%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

qsort()函数的参数设计是这样的(只是表达的意思与源码一直,但源码不长这样):

void qsort(void* src, 
			size_t size, 
			size_t eleSize, 
			int (*cmp) (const void* e1, const void* e2));

qsort函数为啥这么设计函数参数呢?为啥使用回调函数机制?主要还是为了函数的通用性,它既然是快排算法,是一种排序算法,那么就应该可以对任何类型数据进行排序。通过我们程序员自己实现的compare()函数,我们自己指定排序什么类型数据,传入qsort使用,当然能这么实现得益于void*参数类型的功劳,它能接收任何类型指针,然后强转成任何你想要的指针类型。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Stu {
	char name[20];
	int age;
	char gender;
};
int compare_stu_by_name(const void* ele1, const void* ele2) {
	return strcmp(((struct Stu*)ele1)->name, ((struct Stu*)ele2)->name);
}
int main() {
	// qsort对struct类型数据排序(根据字符串字符大小)
	struct Stu s[] = { {"zhangsan", 20, 'm'}, {"lili", 19, 'f'}, {"wangwu", 18, 'm'} };
	int s_size = sizeof s / sizeof s[0];
	qsort(s, s_size, sizeof s[0], compare_stu_by_name);
	for (int i = 0; i < s_size; i++) {
		printf("%s, %d, %c\n", s[i].name, s[i].age, s[i].gender);
		//printf("%s, %d, %c\n", (struct Stu*)s->name, (struct Stu*)s->age, (struct Stu*)s->gender);
	}
	printf("\n");
	return 0;
}

八、指向函数指针数组的指针

首先说明,基本没啥用。首先很明显它是一个指针,指向一个数组,数组的每一个元素都是函数指针,理解起来其实就这么简单,真正难的只是代码的表示形式,或许能唬住许多人。虽然说这样套娃感觉很复杂,而且既然是指针,那么理论上来说可以一直套娃下去,指针数组、然后又来指向指针数组的指针…

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	// 函数指针
	int (*padd)(int, int) = &Add;
	// 函数指针数组
	int (*paddArr[10])(int, int) = { &test };
	// 指向函数指针数组的指针
	int (*(*ppaddArr)[10])(int, int) = &paddArr;
	return 0;
}

这个层次的指针,拆开来看也不算复杂。要写出这个指针,按函数指针数组基础上来改就行。paddArr[10]是个数组,给它变成数组指针,写成(*paddArr)[10]不就变成了指向数组的指针,剩下的代表数组元素类型。

  • 11
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

念来过倒字名qwq

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

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

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

打赏作者

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

抵扣说明:

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

余额充值