【C语言】带你手把手拿捏指针(3)(含转移表)
文章目录
- 一、字符指针变量
- 二、数组指针变量
- 1.数组指针变量是什么
- 2.数组指针变量的初始化
- 三、二维数组传参的本质
- 四、函数指针变量
- 1. 函数指针变量的创建
- 2.函数指针的使用
- 3.案例解析:
- 五、typedof关键字
- 六、函数指针数组和转移表
- 1.函数指针数组
- 2.转移表
一、字符指针变量
在指针的类型中我们知道有⼀种指针类型为字符指针 char* ,⼀般使⽤的方式如下:
这里我们将字符变量a的地址交给指针变量p,然后进行使用,但还有一种方式如下:
#include <stdio.h>
int main()
{const char* pstr = "hello bit.";//这⾥是把⼀个字符串放到pstr指针变量⾥了吗?printf("%s\n", pstr);return 0;
}
代码 const char* pstr = “hello bit.”,特别容易让同学以为是把字符串 hello bit. 放到字符指针 pstr ⾥了,但是本质是把字符串 hello bit. ⾸字符的地址放到了pstr中,如图:
上⾯代码的意思是把⼀个常量字符串的⾸字符 h 的地址存放到指针变量 pstr 中。《剑指offer》中收录了⼀道和字符串相关的笔试题,我们⼀起来学习⼀下:
#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");elseprintf("str1 and str2 are not same\n");if(str3 ==str4)printf("str3 and str4 are same\n");elseprintf("str3 and str4 are not same\n");return 0;
}
请问最后代码会输出什么信息呢?我们来看看运行结果,看看与你的想法是否一致:
为什么呢?我们可以思考一下,如果是两个变量,那么它们会开辟两个空间存放相应的数据,可能数据不同,所以本质上它们并不相同,而一个常量字符串
本身就是常量,不会被修改,所以不会像变量一样开辟两个空间存放,而是会将一个常量字符串存在同一个空间,所以str1和str2不同,str3和str4相同
二、数组指针变量
1.数组指针变量是什么
之前我们学习了指针数组,指针数组是⼀种数组,数组中存放的是地址(指针)
数组指针变量是指针变量?还是数组?答案是:指针变量
我们已经熟悉:
- 整形指针变量: int * pt; 存放的是整形变量的地址,能够指向整形数据的指针。
- 浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。
那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量
我们来看下面两个,哪个是数组指针变量:
1.int *p1[10];
2.int (*p2)[10];
思考一下p1、p2分别是什么?
我们先来看看第一个,这里的 * 号会和前面的int结合,为什么呢?因为[]操作符的优先级是比 * 号高的,也就导致了p1和[10]结合, * 和int结合, 变成了一个类型为int*,数组名为p1,元素个数为10的指针数组,如图:
所以这里p1并不是一个数组指针,而是一个指针数组
而第二段代码中,我们将p2和 * 用小括号()括起来了,也就改变了优先级,p先和 * 结合,说明p是⼀个指针变量,然后指针指向的是⼀个大小为10个整型的数组。所以p是⼀个指针,指向⼀个数组,叫数组指针
这⾥要注意:[]的优先级要⾼于 * 号的,所以必须加上()来保证p先和*结合
2.数组指针变量的初始化
数组指针变量是⽤来存放数组地址的,那怎么获得数组的地址呢?就是我们之前学习的 &数组名,如下:
int arr[10] = {0};
int (*p)[10] = &arr;//得到的就是数组的地址
数组指针类型解析:
int (*p) [10] = &arr;| | || | || | p指向数组的元素个数| p是数组指针变量名p指向的数组的元素类型
我们之前也做过比较,我们发现数组名,也就是首元素地址和数组地址看起来是一样的,如下:
它们三个的关系是什么呢?
我们知道这里数组名arr就是首元素地址,也就是&arr[0],但是这里的&arr是取出的数组的地址,它和另外两个的区别就是,前两个是元素的地址,±整数是跳过相应的元素个数,而&arr±整数时,则是跳过相应的数组个数
为了加深理解,也同时为我们的二维数组传参本质讲解做铺垫,这里我们举一个例子:
如何使用数组指针访问一维数组?我们来看一个代码,看看它是否正确,如下:
int main()
{int arr[5] = { 1,2,3,4,5 };int(*p)[5] = &arr;int i = 0;for (i = 0; i < 5; i++){printf("%d ", *(p + i));}return 0;
}
这个代码正确吗?根据我们前面学的知识,很容易判断出它是错误的,那么为什么呢?就是因为这里p实际上是&arr,也就是整个数组的地址,对它解引用应该是拿到整个数组,如下代码:
可以看到,我们对p解引用后拿到的是整个数组,那么p与这个一维数组的联系到底是什么呢?如下:
p = &arr
*p = *&arr
*p=arr
通过上文比较我们可以看到, * p本质上就是数组的数组名,那么我们就把 * p当作数组名来使用,如下:
int main()
{int arr[5] = { 1,2,3,4,5 };int(*p)[5] = &arr;for (int i = 0; i < 5; i++){printf("%d ", (*p)[i]);}return 0;
}
我们来看看运行结果:
可以看到确实实现了用数组指针访问一维数组,但是总感觉怪怪的,感觉就是为了完成任务硬拼起来的,那数组指针到底会在什么场景出现呢?我们就在讲解二维数组传参的本质时介绍
三、二维数组传参的本质
有了数组指针的理解,我们就能够讲⼀下⼆维数组传参的本质了
过去我们有⼀个⼆维数组的需要传参给⼀个函数的时候,我们是这样写的:
#include <stdio.h>
void print(int a[3][5], int r, int c)
{int i = 0;int j = 0;
for(i=0; i<r; i++){for(j=0; j<c; j++){printf("%d ", a[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}};print(arr, 3, 5);return 0;
}
这⾥实参是⼆维数组,形参也写成⼆维数组的形式,那还有什么其他的写法吗?
⾸先我们再次理解⼀下⼆维数组,⼆维数组其实可以看做是每个元素是⼀维数组的数组,我们在传参时会传这个二维数组的数组名,我们也都知道一个数组的数组名是首元素的地址,那么二维数组的首元素地址是什么呢?
由于二维数组可以看做是一维数组的数组,那么第一行就是它的第一个元素,第二行就是它的第二个元素,依此类推,所以⼆维数组的首元素就是第⼀⾏,是个⼀维数组,如下图:
所以,根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。根据上⾯的例⼦,第一行的⼀维数组的类型就是 int [5] ,所以第一行的地址的类型就是数组指针类型 int(*)[5]
那就意味着⼆维数组传参本质上也是传递了地址,传递的是二维数组第一行这个⼀维数组的地址,是个数组,那么形参也是可以写成数组指针形式的,如下:
void print(int (*p)[5}, int x, int y)
经过上一节的分析,我们也就知道了p其实就是二维数组第一行的地址,也就是二维数组的首元素的地址,也就相当于二维数组的数组名arr,那么我们此时就可以使用p访问二维数组,如下代码:
void print(int(*p)[5], int r, int c)
{int i = 0;int j = 0;for (i = 0; i < r; i++){for (j = 0; j < c; 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};print(arr, 3, 5);return 0;
}
运行结果如图:
可以看到确实使用这种方式可以完美访问二维数组了,那么还有其它方法吗?我们这里再讲一个更加深入,更加贴近指针用法的方法
首先我们来再深入一点了解二维数组,我们说二维数组的每一行都是一个一维数组,那么这个一维数组有数组名吗?当然有,如下图:
然后我们继续深入理解,p是二维数组中第一个一维数组的地址,那么p+1就应该是第二个一维数组的地址,因为p是数组指针,±整数可以跳过对应的数组个数,p+2就是第三个一维数组的地址,如图:
那么* (p+0),* (p+1),* (p+2),之前通过学习对一维数组的地址进行解引用就会得到该一维数组的数组名,也就是首元素的地址,如下例:
int arr[3][5] = { 1,2,3,4,5 ,2,3,4,5,6 ,3,4,5,6,7};
p+0 = &arr[0];
p+1 = &arr[1];
p+3 = &arr[2];
那么:
*(p+0) = *&arr[0]=arr[0];
*(p+1) = *&arr[1]=arr[1];
*(p+3) = *&arr[2]=arr[2];
所以我们可以总结为下图:
现在有了每个一维数组的数组名,也就是首元素地址,我们再对首元素地址进行±就可以得到其它元素的地址,然后解引用就可以得到对应的元素,如下图:
于是最终我们可以利用指针数组写出如下代码:
void print(int(*p)[5], int r, int c)
{int i = 0;int j = 0;for (i = 0; i < r; i++){for (j = 0; j < c; 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};print(arr, 3, 5);return 0;
}
运行结果如下:
总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式,它的本质就是使用数组指针,来访问二维数组的元素
四、函数指针变量
1. 函数指针变量的创建
什么是函数指针变量呢?
根据前⾯学习整型指针,数组指针的时候,我们的类⽐关系,我们不难得出结论:
函数指针变量应该是⽤来存放函数地址的,未来通过地址能够调⽤函数的。
那么函数是否有地址呢?我们做个测试:
#include <stdio.h>
void test()
{printf("hehe\n");
}
int main()
{printf("test: %p\n", test);printf("&test: %p\n", &test);return 0;
}
我们来看看运行结果:
确实打印出来了地址,所以函数是有地址的,函数名就是函数的地址,当然也可以通过 &函数名 的方式获得函数的地址
如果我们要将函数的地址存放起来,就得创建函数指针变量,函数指针变量的写法其实和数组指针非常类似
它也需要把变量名和*用()括在一起,最前面写上函数的返回类型,不同的是,数组后面跟的是元素的个数,而函数指针变量后面是一对小括号,里面写上函数的参数,参数的类型必须写出来,但是参数名可以省略,现在举两个例子演示:
举例1:函数无参数时:
void test()
{printf("hehe\n");
}
int main()
{void (*P)() = test;//或写成&test;return 0;
}
举例2:函数有参数时:
int Add(int x, int y)
{return x + y;
}int main()
{int (*p)(int x, int y) = Add;
//或写成:int (*p)(int, int) = Add;return 0;
}
函数指针类型解析:
int (*p) (int x, int y)| | ------------ | | || | p指向函数的参数类型和个数的交代| 函数指针变量名p指向函数的返回类型
int (*) (int x, int y) //p函数指针变量的类型
2.函数指针的使用
可以通过函数指针调用指针指向的函数,由于函数名就是函数的地址,所以我们在使用时,可以直接用函数指针变量名替换函数名,如下例:
int Add(int x, int y)
{return x + y;
}int main()
{int (*p)(int x, int y) = Add;printf("%d\n", p(2, 3));return 0;
}
输出结果:
或者我们也可以对p进行解引用,也可以拿到函数的地址,如下:
printf("%d\n", (*p)(2, 3));
输出结果:
3.案例解析:
我们来看两段代码,猜测它们的含义:
- 代码1
(*(void (*)())0)();
这段代码中比较特殊的就是数字0,随后我们来看它的左边是什么:
(void (*)())
很明显在括号里面的是一种函数指针类型,它的返回类型是void,参数为空,没有变量名,说明它只是一种函数指针类型,那么把它放在0的前面有什么用呢?我们举一个简单的例子:
(int)3.14
在一个数据前加上一个括号,里面写上类型,很明显就是我们的强制类型转换,这里是把浮点型3.14强制转换为整型,上述例子也是如此,将0强制转换为了一个函数指针类型,现在相当于0是一个指针
然后前面的*对它进行解引用,相当于就是这个函数本身,这个函数就是前面一堆:
(*(void (*)())0)
最后的一个小阔号就是函数本身的括号,如函数Add(int x,int y),函数名Add后面的括号,只是这个函数没有参数,而Add函数有参数
- 代码2
void (*signal(int , void(*)(int)))(int);
这个代码更加不可思议,我们首先可以从signal下手,它看起来很像一个函数声明,它的参数分别是int类型,以及一个返回类型为void,参数为int类型的函数指针类型,如下:
signal(int , void(*)(int))
那么剩下的那些是什么呢?既然我们猜测它是一个函数,那么现在函数名有了,函数参数也有了,是不是还差一个返回类型,我们把中间这一段去掉,如下:
void (*)(int);
很明显它变成了一个函数指针类型,小伙伴们肯定也可以猜到,这就是刚刚那个函数的返回类型,可是为什么这么奇怪,要把除了返回类型的东西塞进返回类型,不像传统这样写:
void (*)(int) signal(int , void(*)(int))
这样似乎更好理解,但是这是C中未定义的,还是要使用上面的写法,否则会出错
最后总结一下:这段代码是函数signal的声明,它的一个参数是int,一个参数是一种函数指针类型,返回类型也是一个函数指针类型
五、typedof关键字
typedef 是⽤来类型重命名的,可以将复杂的类型,简单化
比如,你觉得 unsigned int 写起来不⽅便,如果能写成 uint 就⽅便多了,那么我们可以使用:
typedef unsigned int uint;
//将unsigned int 重命名为uint
如果是指针类型,能否重命名呢?其实也是可以的,比如,将 int* 重命名为 ptr_t ,这样写:
typedef int* ptr_t
但是对于数组指针和函数指针稍微有点区别,比如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写:
typedef int(*parr_t)[5];
//新的类型名必须在*的右边
函数指针类型的重命名也是⼀样的,比如,将 void(*)(int) 类型重命名为 pf_t ,就可以这样写:
typedef void (*pf_t)(int)
//新的类型名必须在*的右边
那么要简化上面的第四点中的代码2,可以这样写:
typedef void(*pf_t)(int);
pf_t signal(int, pfun_t);
六、函数指针数组和转移表
1.函数指针数组
数组是⼀个存放相同类型数据的存储空间,我们已经学习了指针数组,比如:
int * arr[10];
//数组的每个元素是int*
那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?我们现在写出多种可能,来猜一下是哪一个:
1.int (*parr1[3])();2.int *parr2[3]();3.int (*)() parr3[3];
是哪一种呢?一般来说跟函数指针有关的时候,我们一般会把一些声明写进函数类型中,所以在上面的三个例子中,正确的写法是1
parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?是 int (*)() 类型的函数指针
那么函数指针数组有什么作用呢?就要涉及到下一个内容:转移表
2.转移表
函数指针数组的⽤途:转移表
举例:计算器的实现,要求可以根据菜单使用加减乘除
我们最初写过一个最简单的加法函数,如下:
int add(int x, int y)
{return x + y;
}
那么另外的函数也只需要一个一个实现,如下:
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;
}
到现在这一步还算简单,随后我们开始写主函数中的内容,我们可以设计一个菜单menu,用来打印选择输入0关闭计算器,输入1,2,3,4就对应加减乘除,具体的实现可以使用Switch语句,如下:
menu函数:
void menu()
{printf("*************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf(" 0:exit \n");printf("*************************\n\n");printf("请选择:");
}
主函数:
int main()
{int x, y;int input = 0;int ret = 0;do{menu();scanf("%d", &input);switch (input){case 0:printf("已退出计算器!");break;case 1:printf("输⼊操作数:");scanf("%d %d", &x, &y);ret = add(x, y);printf("ret = %d\n", ret);break;case 2:printf("输⼊操作数:");scanf("%d %d", &x, &y);ret = sub(x, y);printf("ret = %d\n", ret);break;case 3:printf("输⼊操作数:");scanf("%d %d", &x, &y);ret = mul(x, y);printf("ret = %d\n", ret);break;case 4:printf("输⼊操作数:");scanf("%d %d", &x, &y);ret = div(x, y);printf("ret = %d\n", ret);break;}} while (input);return 0;
}
这样我们的计算器就完成了,但是我们发现了严重的问题,这样写代码虽然简单,但是实在有点笨,有很多内容我们都没有必要重复,那么我们该怎么避免呢?这时我们发现这些函数的参数和返回类型都是一致的,我们就可以尝试使用函数指针数组:
我们现在创建一个函数指针数组,如下:
int (*pf[5])(int , int);
接下来我们对它们进行初始化,分别将每个函数的地址放进去,由于函数名就是函数地址,我们只需要填上函数名:
int (*pf[5])(int , int)={ add,sub,mul,div };
但是这时候我们就会发现一个小问题,add在数组中的下标是0,但是实际上在Switch语句中,输入1应该才是add,这时候有个小技巧就是,在最前面加上一个元素0,就可以让它们到对应的位置上,如:
int (*pf[5])(int , int)={ 0,add,sub,mul,div };
随后我们就可以利用这个数组方便的访问函数,pf就是数组名,input就是要访问的对应的下标,所以pf[input]相当于拿到对应函数的地址,也就是函数名,随后将其正常使用即可,如下:
pf[input](x,y);
知道了这个,我们代码就好写了,可以简化许多内容,所以完整的计算器代码如下:
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;
}void menu()
{printf("*************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf(" 0:exit \n");printf("*************************\n\n");printf("请选择:");
}int main()
{int x, y;int input = 0;int ret = 0;int (*pf[5]) (int, int) = { 0,add,sub,mul,div };do{menu();scanf("%d", &input);if (input>=1 && input<=4){printf("请输入两个整型数据:");scanf("%d %d", &x, &y);ret = *pf[input](x, y);printf("结果为:%d\n\n", ret);}else if(input==0){printf("已退出计算器\n"); \break;}else{printf("选择错误,请重新输入!\n\n");}} while (input);return 0;
}
那这个跟转移表有什么关系呢?其实它所指的就是运用函数指针数组以数组方式去调用里面的函数,从而在某些情况下替代冗长的switch函数,所以简单的说函数指针数组就叫转移表