指针(三)带你搞定闻之色变的指针——超详细版!(指针、函数、数组及其应用转移表)


1.字符指针变量

字符串是常量,不能够进行更改,如下图所示,调试异常·。

在这里插入图片描述
但是这个代码在运行的时候是不会报错,但是也没有打印任何元素。就像我们用%s的占位符打印本该用%c打印的字符’c’,也就是占位符格式与所打印的内容格式不一致,系统不会报错,但是其实已经出错了。那么为了避免此类“隐形”错误出现,特别是在做大项目的时候,运行结果不对都不知道错误从哪找,因此我们直接用const修饰,编译器直接报语法错误,如下图所示。
在这里插入图片描述
这里需要注明,p是字符指针,存的是字符串"abcdef"首字符的地址,因此解引用p打印出来的是’a’。
其次,打印字符串,只要提供首字符的地址即可,如下图所示。
在这里插入图片描述


2.数组指针变量

数组指针,类比来看,整型指针int存放整型变量的地址编号,指向整型;字符指针char存放字符变量的地址编号,指向字符;同理,数组指针是指针,指向数组。如下图,arr3中存放了数组arr, arr1, arr2共3个数组的地址,int**[3]中的int* [3]表明arr3指向的类型是整型数组,不过存放的是数组首元素的地址,类型是int*,第二个 *表明arr3是指针。

在这里插入图片描述
此处&数组名也可直接写成数组名,如下图所示。
在这里插入图片描述


3.二维数组传参的本质

当我们写一个Print函数来打印二维数组的内容,与一维数组传参类似,参数不仅要有数组,还要有行数和列数。

void Print(int(*arr)[5], int col, int row)//int(*arr)[5] 一维数组指针
//void Print(int arr[3][5], int col, int row) //int arr[3][5] 二维数组
arr[i]=&arr[i][0]
{
	for (int i = 0; i < col; i++)
	{
		for (int j = 0; j < row; j++)
			printf("%d ", arr[i][j]);
//arr[i]是第i行的数组名,数组名表示数组首元素的地址,因此arr[i]==arr[i][0]
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
	Print(arr, 3, 5);
	return 0;
}

数组传参,传的是数组首元素的地址,二维数组可以看做由多个一维数组组成,二维数组首元素是arr[0],如下图所示,

在这里插入图片描述
也就是一维数组,因此形参是一维数组指针也可,也可以是二维数组。其实形参形成二维数组便于理解,编译器会将其转写成一维数组指针进行识别。


4.函数指针变量

4.1 理解函数指针变量

Add(函数名)和&Add是不一样的,二者的值一致,是函数地址的值,但是如下图所示,二者类型不同,Add是函数,&Add是函数指针!

在这里插入图片描述
先说两点,
1.在辩认数组指针变量和函数指针变量的时候注意,当名称如pf遇到[3,5]或(int, int) 是会自动跟后面的内容结合的,比如下图中倒数第二行注释* pf(4,5),就是调用了pf指向的函数Add, * 与得到的结果9结合,系统报错。
2.确认变量类型:将初始化式子左侧变量名拿掉,剩下的就是变量类型。如下图所示,函数指针变量pf的类型与Add的类型一致,都是int( * )(int,int),* 一定要括起来!

在初始化函数指针变量的时候,&Add与Add的效果是一致的,因为上面我们提到过二者值一致,是Add的地址编号,指针的定义就是存储某个变量的地址编号。

在使用指针变量调用函数时,pf(4,5)与(*pf)(4,5)的效果是一致的,(*pf)(4,5)是对指针解引用,得到原函数进行调用。Add(4,5),Add的值是其地址编号,pf(4,5),pf也是Add的地址编号。
在这里插入图片描述

4.2 应用(识别两个代码)

在笔者看来,这两个代码犹如“犹抱琵琶半遮面”!

4.2.1 代码1

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

先看第一个,

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

找到各自对应的(),从0入手,0前面的(类型)是强制类型转换,()里面是函数指针类型,返回参数类型为void,(*)表明是指针,()是无参数,因此

( void (*)() )0 

表明将0强制转换为函数指针类型,*对其解引用得到函数,后面的括号表明该函数无参,对该函数进行调用,因此整个代码先是对0进行强制转换为函数指针类型,再解引用调用地址为0的函数,具体分析如下图所示。在这里插入图片描述

这个代码出自《C陷阱与缺陷》,如下图所示。

在这里插入图片描述

4.2.2 代码2

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

前面是void,函数返回参数类型,后面是(int),函数参数类型,即整个与函数有关,函数包括三种操作:定义、声明、调用,显然没有实参不是调用,没有{}不是定义,因此是函数声明,函数名包括“返回类型 函数名 函数参数”,signal是函数名,(int, void(*)(int))是参数类型,剩下的void( *)(int)是返回类型,具体分析如下图所示。

在这里插入图片描述
相信很多小伙伴跟我一样迷惑,为什么函数返回类型没有放在一起呢?我们可以想一下函数指针的初始化,比如int (*pf)(int, int) = &Add;,我们把pf也就是函数指针名换成更为复杂的内容,那么其形式与上面很相似,可以通过这个角度理解,当然二者不是一个内容。
这个代码出自《C陷阱与缺陷》,如下图所示。

在这里插入图片描述
如抽丝剥茧,遇到这种代码,莫急,先抓主要矛盾,外观像什么、能看懂什么,一步一步来!

4.2.3 typedef关键字

typedef用于给复杂的名称重命名,typedef 原名 简化名称
如下图,调试时监视窗口可以看到a的变量类型是unsigned int.
在这里插入图片描述

我们看到函数的返回类型是void( *)(int),好复杂呀,我们可以勇typedef重定义,很简单,把原来名字出现的地方换成简化的就好啦,注意是在主函数外部,如下图所示。
在这里插入图片描述


5.函数指针数组

函数指针数组,是数组,存放函数指针,也就是函数的地址。可以用一个数组调用不同的函数。

int (*arr[4])(int, int) = { Add, Sub, Mul, Div };

我们看一下函数指针数组的初始化,arr与紧随其后的[4]结合,构成数组,存放的元素是int(*)(int, int),是返回类型、参数类型为int的函数指针,下图中我们看到该数组的类型是
int(*)(int, int)[4]

在这里插入图片描述


6.函数指针应用——转移表

我们接下来看一个函数指针数组的具体应用——转移表!
当我们想实现一个简易的计算器,实现加减乘除等运算,我们写下的代码可能是这样的。
先抛砖引玉吧,

//首先实现加减乘除等函数
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:Add  2:Sub ****\n");
	printf("**** 3:Mul  4:Div ****\n");
	printf("**** 0:exit       ****\n");
	printf("**********************\n");
}
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	//do while循环结合变量input实现多次运算
	do {
		menu();
		printf("请输入:>");
		scanf("%d", &input);
		//switch case语句实现不同的选择 
		switch (input)
		{
		case 1:
			printf("请输入您要相加的两个值:>");
			scanf("%d %d", &x, &y);
			printf("%d\n", Add(x, y));
			//此处直接进行打印没有计算,可能会有风险,
			//也可引入局部变量ret存储计算值并进行打印,如下
			//ret = Add(x,y);
			//printf("%d\n", ret);
			break;
		case 2:
			printf("请输入您要相减的两个值:>");
			scanf("%d %d", &x, &y);
			printf("%d\n", Sub(x, y));
			break;
		case 3:
			printf("请输入您要相乘的两个值:>");
			scanf("%d %d", &x, &y);
			printf("%d\n", Mul(x, y));
			break;
		case 4:
			printf("请输入您要相除的两个值:>");
			scanf("%d %d", &x, &y);
			printf("%d\n", Div(x, y));
			break;
		case 0:
			printf("退出计算机\n");
			break;
		default:
			printf("输入错误,请重新输入\n");
			break;
		}
	} while (input);
	return 0;
}

我们思考一个问题,如果我们要实现加减乘除、取模、取余等运算,就要实现很多函数,我们的switch case语句会很长很长,代码的可读性降低;其次,每一个case语句调用函数其实大体内容是一样的,代码重复,这个时候我们可以用函数指针数组进行优化。

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:Add  2:Sub ****\n");
	printf("**** 3:Mul  4:Div ****\n");
	printf("**** 0:exit       ****\n");
	printf("**********************\n");
}
int main()
{
	int i = 0;
	int x = 0;
	int y = 0;
	int (*arr[5])(int, int) = { 0,Add, Sub, Mul, Div };
	//数组开头存放0元素,使下标和输入的序号相同,不用考虑下标转换
	do {
		menu();
		printf("请输入:>");
		scanf("%d", &i);
		//if判断语句代替switch case语句,代码简洁明了
		if (i >= 1 && i <= 4)
		{
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			printf("%d\n", arr[i](x, y));//使用数组调用函数
		}	
		else if (i < 0 || i>4)
			printf("输入错误,请重新输入\n");
		else
			printf("退出计算机\n");
	} while (i);
	return 0;
}

如果后续加入其他运算,实现该运算并对循环条件和数组大小和内容进行更改即可。
这就是转移表的实现!

总结

好了,今天就到这里啦,这是第三篇文章,其实指针重要很大程度上在于应用很多,与各种内容相结合,理解起来不是很容易,但往前总比站在原地更接近目标!这一讲主要是函数和指针,也有字符指针数组等,头脑风暴一下,这篇文章学到了什么呀?
“愿你如山间清爽的风,如古城温暖的光”。

下期见啦~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值