文章目录
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;
}
如果后续加入其他运算,实现该运算并对循环条件和数组大小和内容进行更改即可。
这就是转移表的实现!
总结
好了,今天就到这里啦,这是第三篇文章,其实指针重要很大程度上在于应用很多,与各种内容相结合,理解起来不是很容易,但往前总比站在原地更接近目标!这一讲主要是函数和指针,也有字符指针数组等,头脑风暴一下,这篇文章学到了什么呀?
“愿你如山间清爽的风,如古城温暖的光”。
下期见啦~