文章目录
- 字符指针变量
- 数组指针变量
- 数组指针变量是什么?
- 数组指针变量怎么初始化
- 二维数组传参的本质
- 函数指针变量
- 函数指针变量的创建
- 函数指针变量的使用
- 两段关于函数的有趣代码
- typedef关键字
- 函数指针数组
- 转移表
- 第一种写法:
- 第二种写法(函数指针数组):
- 第三种写法:
字符指针变量
我们知道有一种指针类型为字符指针:char*
我们原来对字符指针的用法:存放一个字符的地址
现在我们再学习一种,字符指针可以指向一个常量字符串
我们写段代码:
int main()
{//char ch = 'w';//char* pc = &ch;此时pc就是一个指针变量,可以存放一个字符的地址char arr[10] = "abcdef";char* p = arr;return 0;
}
我们将这段代码的图画出来:
我们再写一种代码:
int main()
{//char ch = 'w';//char* pc = &ch;此时pc就是一个指针变量,可以存放一个字符的地址//char arr[10] = "abcdef";//char* p = arr;char* p = "abcdef";return 0;
}
写出这段代码我们肯定会疑问:
这句代码是把这个字符串完整的赋值给p
吗?
我们对这段代码进行编译,发现编译器并没有发出警告:
说明这种写法是正确的。
那么这种写法的意思是什么呢?
注意:这个字符串是常量字符串,上面那个是字符数组。
我们敲出来的这个常量字符串在内存中也有属于它自己的空间。
char* p = "abcdef";
这条代码是将它首字符的地址放进指针变量p
里面去:
因为常量字符串在内存中也是连续存放的,所以找到首字符的地址,也就可以找到其他字符了。
我们可以看到,这两种形式非常的像,那么字符数组与常量字符串的区别是什么呢?
区别就在于:
第一个p
指向的是数组,数组的内容是可以被改变的。
但是第二个p
指向的是常量字符串,常量字符串的内容是不可以被改变的。
我们不妨试一下:
int main()
{char arr[10] = "abcdef";char* p1 = arr;*p1 = 'w';//试着将a改为wchar* p2 = "abcdef";*p2 = 'w';//也试着将a改为wreturn 0;
}
写这样一段代码进行调试:
首先查看arr
数组里的内容:
确实放着10个元素,接着往下:
当我们执行完*p1 = 'w'
后第一个元素确实被改变为w
了:
接着往下调试:
接着往下调试:
执行到*p2 = 'w'
时却报出警告:
这是因为abcdef
是个常量字符串,常量字符串是不能被修改的,一旦修改它,就会报错。
所以:数组的内容是可以被修改的,而常量字符串是不能被修改的。
此时这段代码应该这样写:
在*
的左边加上const
,这样就限制了p2
指向的内容,一旦内容出现修改,编译器就会报错,可能出现的错误就直接被扼杀在编译期间了。
正确写法:
int main()
{//char arr[10] = "abcdef";//char* p1 = arr;//*p1 = 'w';//试着将a改为wconst char* p2 = "abcdef";//也试着将a改为wreturn 0;
}
所以const char* p2 = "abcdef"
这种写法并不是将整个字符串都放进指针变量里,而是将首字符的地址放进指针变量里。
接下来我们将它们打印出来:
这里为什么写一个p1
,p2
就能打印出来呢?
因为我们打印字符串的时候只提供一个起始地址就可以了,就像之前一直写的printf("%s\n",arr)
当我们用
%s
打印字符串的时候,只需要提供起始地址就行了。
不需要解引用,用了解引用是错的。
因为*p1
是a
,一个字符不能用%s
打印。
我们接下来看《剑指offer》中收录的一道和字符串相关的笔试题:
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
不相等呢?
在代码中,先后创建了两个数组str1
与str2
,然后再用"hello bit."
初始化了这两个数组:
因为两个数组是在不同的内存空间里,所以str1
所对应的h
的地址与str2
对应的h
的地址不同,并不是同一个地址,所以str1
不等于str2
,打印str1 and str2 are not same
那为什么str3
与 str4
相等呢?
因为hello bit.
在这里是个常量字符串:
常量字符串是不能被修改的,既然不能被修改,所以在内存空间里只有一个字符串。
因为这里str3
和str4
指向的是一个同一个常量字符串:"hello bit."
。
既然是同一个常量字符串,那么str3
和str4
中的地址都是同一块空间,所以str3==str4
。
那为什么指向的是同一个常量字符串呢?
因为常量字符串是不能被改变的,相同的常量字符串没必要保存两份(只有能被改变的才有必要存原本的字符串进行备份),所以在内存中只有一份这样的常量字符串,即str3
和str4
指向的都是同一个常量字符串,所以str3
和str4
里面存的地址相同。
我们可以进行调试看看str3
和str4
存的地址是否像我们说的相同:
数组指针变量
数组指针变量是什么?
之前我们学习了指针数组,指针数组是一种数组,数组中存放的是地址(指针)。
那么数组指针变量是指针变量、还是数组呢?
答案是:指针变量。
我们已经熟悉:
- 整型指针变量:
int* pint;
存放的是整型变量的地址,能够指向整型数据的指针。 - 浮点型指针变量:
float* pf;
存放的是浮点型变量的地址,能够指向浮点型数据的指针
所以,数组指针变量就应该是:存放的是数组的地址,能够指向数组的指针变量。
例如:
我们取出整个数组的地址,将数组的地址放进指针变量p
中去,这个p
就是数组指针变量
注意:我们要将数组指针与指针数组区分开
指针数组:是数组,是存放指针的数组
数组指针:指向数组的指针(变量)
那么这个p
的类型是什么呢?
我们首先来分析一下这段代码,看p1
是指针还是p2
是指针呢?
-
对
p1
的分析:
我们可以看到,p1
左边有个*
,右边放着[10]
,它首先是与方块结合的。
既然是与方块结合的,那么p1
就是一个数组名,方块里表示的就是这个数组里有几个元素。
最后前面的int*
表示p1
数组里面的10个元素都是int*
类型的
结论:p1
是个指针数组——存放指针的数组 -
对
p2
的分析:
我们可以看到p2
与前面的*
被括号括起来了,所以它没办法与后面的方块结合了。
我们在深入理解指针(1)中学习过,变量左边放个*
就表示这个变量是指针变量:
所以p2
前面放个*
就表示了这个p2
是个指针变量。
既然我们说p2
是指针变量,那么它指向什么呢?
我们看到括号的后面有个[10]
,就说明这个指针变量指向的应该是个数组,这个数组有10个元素,又因为最前面有个int
,所以数组的每个元素都应该是int
类型的。
我们之前学过数组是有类型的,数组名去掉就是这个数组的类型:
所以p2
是个指针变量,指向数组类型为int [10]
的数组
结论:p2
是个指针变量,指向的是数组
既然p2
是个指针变量,指向的是数组,所以p2
就应该是个数组指针变量。
而p
也是数组指针变量,所以p
类型的写法就与p2
类型的写法相似:
p2
指向10个元素都为int
的数组,写成int (*p2)[10]
p
也指向10个元素都为int
的arr
数组,所以写成:int (*p)[10]=&arr;
拆解:
首先p
得是个指针,所以要在p
前面加个*
说明:
*p=&arr;
下一步加括号来保证它俩先结合:
(*p)=&arr;
强调:括号一定不要掉!掉了会与后面的方块结合,此时p
就会变为数组名,成为指针数组,而不是指针变量
int* p[10]=&arr;
//err
最后:让它指向10个元素都为int
的数组:
int (*p)[10]=&arr;
解释::p
先和*
结合,说明p是一个指针变量,然后指针指向的是一个大小为10个整型的数组。
最后形态:
即p
是⼀个指针,指向⼀个数组,叫做数组指针。
数组指针变量怎么初始化
数组指针变量是用来存放数组地址的,那怎么获得数组的地址呢?
这就是我们之前学习的:&数组名
int arr[10]={0};
&arr;//得到数组的地址
如果要存放数组的地址,就得存在数组指针变量中,数组指针的初始化:
int (*p)[10]=&arr;
这里我们再梳理一些知识:
由这段代码:
int main()
{int arr[10]={0};int* p1=arr;int(*p2)[10] = &arr;return 0;
}
我们可知p1
的类型为int*
那么数组指针变量p2
的类型是什么呢?
去掉变量名就是该数组指针变量的类型了,p2
的类型为:int(*)[10]
我们让这两个指针分别+1,再将地址打印出来观察:
可以观察到p1
+1跳过了4个字节,p2
+1跳过了40个字节
我们之前学过:指针类型决定了指针进行+1或-1操作的时候,一次跳过多少个字节。
例如:
字符指针+1跳过1个字节;整型指针+1跳过4个字节…这里的数组指针+1就跳过一整个数组的字节大小。
int arr[10]
这个数组共有10个元素,每个元素4个字节,所以共跳过了40个字节。
这与深入理解指针(2)(数组与指针)中的这段笔记相似:
当p2
跳过一整个数组时指向哪里呢?
我们画个图:
当p2
跳过一个数组的大小时,我们并不知道这个指针最后指向哪里了(此时这个指针指向的位置是不可知的)这样会不会造成野指针呢?
答案是不会
总结:p2 + 1
本身不是野指针,因为它指向的是一个确定的内存地址(arr数组后的那个地址)。然而,对 p2 + 1
进行解引用操作是危险的,可能会引发程序异常,因为它超出了数组 arr 的有效范围。
那么数组指针到底有什么用呢?
它非常可能应用错场景。
例如,我们写段代码用指针遍历打印出数组内容:
那我们今天学过数组指针变量后,那我们能不能这样写呢?
我们将整个数组的地址取出来放进数组指针变量p
中:
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int(*p)[10] = &arr;return 0;
}
用数组指针变量打印出数组所有的元素:
对(*p)[i]
该如何理解呢?
我们在深入理解指针(1)中学习过,&
与*
可以相互抵消:
*&arr=arr
这条语句就是取出arr
的地址,再对这个地址进行解引用,其实最后找到的就是arr
例如:
所以在这段代码中,p
就是&arr
。再在p
前面加上*
后得到的就是个数组名
(*p)==(*&arr)==arr
此时要打印出所有元素就得使用下标打印
arr[i]
虽然这种写法最终也可以打印出正确结果,但是这是个不好的示范,不推荐这样使用数组指针。
数组指针可以使用在二维数组的传参上。
二维数组传参的本质
在过去,我们有一个二维数组需要传参给一个函数时,是这样写的:
我们创建个二维数组,再写个函数将这个数组打印出来
学一维数组时我们说:数组名是数组首元素的地址,那么对于二维数组来说是否也一样呢?
答案:对于二维数组来说,数组名也是数组首元素的地址。
那么对于这个二维数组来说,到底谁是首元素呢?真的是数字1吗?
我们之前学习二维数组的时候,说过:把一维数组做为元素,这个数组就是二维数组
所以,二维数组的每个元素是一个一维数组,二维数组的首元素就应该是第一行,首元素的地址就是第一行的地址。
而第一行的地址就是一个一维数组的地址,所以形参接收时的指针类型应该是数组指针类型
虽然二维数组在我们眼里是有行有列的,但是在内存中是连续存放的。
所以:arr
是指向第一行,让它+1就会跳过整个一维数组来到第二行…
完整代码:
void print(int(*arr)[5], int r, int c)
{int i = 0;for (i = 0; i < r; i++){int j = 0;for (j = 0; j < c; j++){printf("%d ", *(*(arr + 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;
}
打印成功:
关键点分析,指针操作:
arr+i
:表示指向第i
行的指针(类型为int(*)[5]
)*(arr+i)
:解引用得到第i
行数组的首地址(类型为int*
)*(arr+i)+j
:指向第i
行第j
列元素的指针*(*(arr+i)+j)
:获取第i
行第j
列的元素值
所以,二维数组传参的本质其实是传一个地址,并且是一个一维数组的地址。但是形参的部分既可以写成指针的形式,也可以写成数组的形式。
函数指针变量
函数指针变量的创建
什么是函数指针变量呢?
根据前面学习整型指针,数组指针的时候,我们类比关系,不难得出结论:
函数指针变量应该就是用来存放函数的地址的,通过这个地址能够调用这个函数。
首先,函数是否有地址呢?
我们写个函数,打印它的地址,看能不能打印出来:
发现确实打印出来一个数值。
我们之前学过:
&数组名
是整个数组的地址- 数组名是数组首元素的地址
两个地址的值是一样的!
那么对于函数来说呢?
我们再将函数名打印出来对比一下:
确实与&函数名
一样。
那么我们能不能说&函数名
是整个函数的地址,函数名是函数首元素的地址呢?
答案:不能。在函数里,&函数名
与函数名都是函数的地址,它们是没有区别的(在数组里面,数组名与&数组名
是有区别的)
所以,函数名就是函数的地址,当然也可以通过
&函数名
的方式来获得函数的地址。
如果我们要将函数的地址存放起来,就得创建函数指针变量,函数指针变量的写法其实与数组指针变量的写法非常相似。如下:
但是pf
的类型该怎么写呢?
首先得给pf
加个*
表面它是个指针变量:
(*pf)=&Add;
又因为pf
指向一个函数,函数后面是个()
,所以也要在后面加上()
(*pf)()=&Add;
函数参数类型是int,int
,所以括号里面写int,int
;函数返回类型也是int
,所以返回类型也是int
(返回类型写在最前面)
int (*pf)(int,int)=&Add;
这就是pf
的完整写法:
pf
:函数指针变量名(int,int)
:pf
指向函数的参数类型和个数交代int
:pf
指向函数的返回类型
所以pf
的类型为:
int (*)(int,int)
//pf函数指针变量的类型
我们再将数组指针写出来类比一下:
注意,括号绝对不能掉,掉了之后会变成什么样呢?
因为括号的优先级比*
高,掉了之后pf
会与括号相结合。
此时pf
就变成函数名了,函数的参数是int,int
,返回类型是int *
。
这是有问题的,所以pf
外面的括号绝对不能掉!
检验一下学习成果:
写一个函数test
,将这个函数的地址取出来放进函数指针变量p
中,这个函数指针变量p
该怎么写呢?
char* test(char c, int n)
{//...
}int main()
{p= &test;return 0;
}
答案:
char* (*p)(char, int) = &test;
还要说明一点:
函数指针也可以把形参名字c,n
写出来:
char* (*p)(char c, int n) = &test;
类型为
char* (*)(char c, int n)
但其实这里的c,n
加不加都,只需要将指向函数的 参数类型,返回类型 交代清楚就行了。(指向某个函数,参数的名字叫什么不重要,因为这里不会用c,n
)
函数指针变量的使用
原本我们是这样使用函数的:
接下里试试使用函数指针:
所以,把一个函数的地址存在函数指针变量里,未来就能通过这个函数指针变量调用这个函数。
注意:如果函数没有参数的话,我们解引用后面也就不需要传参了;如果函数返回值是
void
,我们也就不需要写变量来接收返回值…根据不同的情况而变化
在这里,我们通过对pf
这个函数指针变量里的地址解引用找到了该函数,然后再进行了函数的调用。
我们又从上面的内容知道:函数名就是该函数的地址,
所以函数名调用就是通过函数的地址直接调用。
那当我们将这个地址放进pf
中时,也通过函数的地址直接调用,不解引用呢?
写段代码试试:
所以,C语言规定,函数指针的解引用操作(*
)是可选的。既可以省略不写,也可以写多个:
两段关于函数的有趣代码
代码1:
(*(void (*)())0)();
我们首先得认出:
void (*)()
这是个函数指针类型。
那么括号里放着类型是干什么的呢?
答案:强制类型转换。
( void (*)() )0
所以,这一步就是将整型0强制类型转换成函数指针类型:void (*)()
。
void (*)()
:表示一个指向无参数、无返回值函数的指针- 转换后,0的类型变为
void (*)()
(函数指针类型),但它的值仍然是0(即内存地址0)
此时的0被看作为一个无参数、无返回值函数的地址。
对这个0(地址)解引用,找到这个函数。
*( void (*)() )0;
调用这个函数:
(*( void (*)() )0) ();
总结:这段代码的作用是将地址为0处的内存当作一个无参数、无返回值的函数来调用。
代码2:
void (*signal(int , void(*)(int)))(int);
我们看到signal
后面有括号,通常会猜测signal
会不会是个函数名呢?
再往后看,我们可以清楚的看到:
参数1可以传个整型值,参数2可以传个函数的地址。
函数名、参数都有了,对于函数来说来缺少什么呢?
可以看到,还缺少返回类型。
我们再将已经分析过的内容省略掉:
剩下的void (*)(int)
就是返回类型
那么这整段代码在表达什么呢?
注意,这种写法是错误的,这样写只是方便表达意思:
void(*)(int) signal(int, void(*)(int))
*
必须在函数名的旁边,所以写成:
void (*signal(int , void(*)(int)))(int);
这也是一种函数声明:
但是这种写法不容易难理解,所以我们可以对它进行简化。
怎么简化呢?
这里就要学习一个关键字typedef
。
typedef关键字
typedef
是用来类型重命名的,可以将复杂的类型简单化。
比如,当觉得unsigned int
写起来不方便,如果能写成u_int
就方便多了,那么我们可以使用:
typedef unsigned int u_int;
//将unsigned int 重命名为uint
由此,下面两种写法,a1
、a2
都为无符号整型:
int main()
{unsigned int a1;u_int a2;return 0;
}
如果是指针类型,能否重命名呢?
其实也是可以的,比如,将int*
重命名为ptr_t
:
typedef int* ptr_t;
类型重命名对于数组指针和函数指针稍微有点区别:
例如我们有数组指针类型int(*)[5]
,需重命名为parr_t
,
不能像上面那样写:
typedef int(*)[5] parr_t;
需要这样写:
typedef int(*parr_t)[5];
//新的类型名必须在*的右边
//这里的parr_t就是int(*)[5]
这里的parr_t
就是int(*)[5]
,已经将int(*)[5]
这个数组指针类型重命名了。
int main()
{int(*pa1)[5];//这里的pa1是个数组指针变量,指向数组类型为int [5]的数组parr_t pa2;//这里的pa2也是个数组指针变量,指向数组类型为int [5]的数组return 0;
}
这两种写法表达的意思也是一模一样的。
函数指针类型的重命名也是:
比如,将void(*)(int)
类型重命名为pf_t
:
不能这样写:
typedef void(*)(int) pf_t;
要写成
typedef void(*pf_t)(int);
//新的类型名必须在*的右边
此时pf_t
就是void(*)(int)
(这种函数指针类型)。
学了简化之后,这段代码就可以简化了:
void (*signal(int , void(*)(int)))(int);
这个返回类型不就是函数指针类型吗?
我们将这个函数类型void(*)(int)
重命名为pf_t
:
typedef void(*pf_t)(int);
//pf_t就是void(*)(int)
简化之后这段代码为:
pf_t signal(int,pf_t)
所以部分1与部分2的作用是一样的:
函数指针数组
我们前面已经学了指针数组:
int* arr[10];
//指针数组
//数组的每个元素是int*(整型指针)
//可以存放5个整型的地址
我们知道,函数指针是函数的地址;指针数组可以存放多个地址。
如果要把多个函数的地址存到一个数组里面去,这个数组就是函数指针数组了。
所以,函数指针数组就是一个数组,数组里面的元素都是函数指针(函数的地址)。
那么我们该怎么定义函数指针数组呢?
我们首先写出四个函数:
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;
}int main()
{int (*pf1)(int, int) = &Add;//pf1是函数指针变量return 0;
}
这4个函数的参数与返回类型一模一样,所以这4个函数地址的类型一模一样,都是这样一种类型:int (*)(int,int)
(函数指针类型)
既然这4个函数地址的类型都是一样的,若想把这4个地址存在pfarr
这个数组里面去,该怎么写呢?
首先将这4个地址写进数组里面(函数名就是函数地址,所以直接将4个函数名写进数组就行了)
pfarr={Add,Sub,Mul,Div};
那么这个函数指针数组的类型该怎么写呢?
在函数指针变量的基础上去改造:
int main()
{int (*pf1)(int, int) = &Add;//pf1是函数指针变量pfarr={Add,Sub,Mul,Div};return 0;
}
使pfarr
根据上面的函数指针变量的写法先变为:
int main()
{int (*pf1)(int, int) = &Add;//pf1是函数指针变量int (*pfarr)(int, int) ={Add,Sub,Mul,Div};return 0;
}
再在pfarr
后面加上数组的方括号和元素个数:[4]
int main()
{int (*pf1)(int, int) = &Add;//pf1是函数指针变量int (*pfarr[4])(int, int) ={Add,Sub,Mul,Div};return 0;
}
我们对比一下:
我们将数组名与数组大小去掉之后就是数组的元素类型了:
int (*pfarr[4])(int, int) = { Add,Sub,Mul,Div };
int (*)(int, int) = { Add,Sub,Mul,Div };
//数组名、数组大小去掉
//此时就是这个数组的元素类型了
//为函数指针类型:int (*)(int, int)
此时这个pfarr
是函数指针数组(它是个数组,里面存放的都是函数指针)
在函数指针的基础上,只要在后面加上
[常量]
,就变成数组了
int (*pf1)(int, int) = &Add;//pf1是函数指针变量
int (*pfarr[4])(int, int) = { Add,Sub,Mul,Div };
//pfarr是函数指针数组
注意,在已经初始化的情况下,方块里的4是可以省略的(数组方块里的大小是否可以省略取决于是否初始化了)
int (*pfarr[])(int, int) = { Add,Sub,Mul,Div };
//后面花括号已经初始化了,所以4可以省略
函数指针数组里存放函数地址,得保证它们的类型都是同一种函数指针类型(参数、返回类型都是一样的)
那么我们该怎么用函数指针数组呢?
我们写段代码,让这段代码依次完成8与4的“加法、减法、乘法、除法”:
因为pfarr
是个数组,所以可以使用下标访问到具体的元素:
函数名就是函数的地址,拿到地址后就可以直接调用()
:传参8,4
再用一个变量r
接收函数返回的值,将这个值打印出来:
转移表
函数指针数组的用途:转移表
举例:计算器的一般实现,完成整数的加、减、乘、除。
还可以选择:
1——加法、2——减法、3——乘法、4——除法、0——退出。
第一种写法:
-
先打印出一个简易的菜单
-
再将要用的“加法、减法、乘法、除法”的函数写出来
-
函数写完后,根据菜单,提示要“选择哪种运算”
-
接下来就要将输入的值放进一个变量里了,所以写
scanf
函数,创建变量input
-
接下来就是对
input
值的判断了
如果选的是0,就退出计算器
若选的是除“0、1、2、3、4”之外的值,就提示选择错误
-
我们在
while
表达式里写input
。因为当input
为0
的时候,直接停止了循环,也就对应了上面的“退出计算器”。当input
不为0的时候,就继续循环,选择进行加法、减法…
-
如果选的是1,就得提醒输入两个操作数,并创建两个变量存放输入的数
然后就得调用Add
函数了,将x,y
两个操作数传过去
算出的结果也要用变量接收,所以再创建一个变量z
接收结果,再将结果z
打印出来
-
同理,如果选2的话也得输入两个操作数…区别就是每次调用的函数不同,选择2实现的是减法
-
3、4同理
完整代码:
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 z = 0;do{menu();printf("请选择:");scanf("%d",&input);switch (input){case 1:printf("请输入两个操作数:");scanf("%d %d",&x,&y);z=Add(x, y);printf("%d\n",z);break;case 2:printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = Sub(x, y);printf("%d\n", z);break;case 3:printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = Mul(x, y);printf("%d\n", z);break;case 4:printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = Div(x, y);printf("%d\n", z);break;case 0:printf("退出计算器\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}
我们运行代码:
- 首先出现让我们选择哪种运算
- 假设我们选择6
再选择1
输入2、3
- 如果我们想要算乘法,选择3
输入3、3
- 如果不想算了,就选择0,退出计算器
这就是我们写的简易计算器。
这段代码只是实现了两个整数的加法、减法、乘法、除法的运算,那万一我们还要算两个数的右移(>>)、左移(<<)、按位与(&)、按位或(|)…
这样的话switch
的case
就会无限多,所以这种写法不行。
第二种写法(函数指针数组):
我们将这段代码简化:
-
我们发现这些函数的参数、返回类型都一模一样
因为它们的参数、返回类型都一模一样,所以我们可以将这些函数的地址都存到函数指针数组pfArr
里
-
将这个函数指针数组
pfArr
补充完整
先写成函数指针:
因为这个函数指针数组里的元素(函数)都是int (*)(int,int)
类型的,所以写成int (*pfArr)(int,int)
再加方块[]
,写上数组大小(只要初始化了数组,大小也可以省略不写):
-
注意,当这个数组元素仅仅是
{Add,Sub,Mul,Div}
时与菜单上对应的数字不匹配。
Add
下标是0,而菜单里Add
是1;
Sub
下标是1,而菜单里Sub
是2…
所以,我们给数组多加一个0,这样下标与菜单选项就对应了 -
那么这个函数指针数组有什么用呢?
所以这样写:
在调用函数实现计算之前先提示输入两个操作数,所以将这两部分移上来:
将函数的参数写上:
计算完的结果再赋值给变量z
,并把z
打印出来
这就是我们修改后的代码:
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 z = 0;//函数指针数组int (*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div };// 0 1 2 3 4do{menu();printf("请选择:");scanf("%d",&input);//3printf("请输入两个操作数:");scanf("%d %d", &x, &y);z=pfArr[input](x,y);printf("%d\n", z);} while (input);return 0;
}
但是这段代码还是有问题的,当我们输入1时,程序可以正常运行
但当我们输入菜单以外的数字7时,还会让我们继续输入两个操作数
所以我们还得给这段代码加一些判断
代码修改后再运行程序
输入1就可以实现加法
不想计算了,输入0就可以退出计算器
这就是简化后的完整代码:
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 z = 0;//函数指针数组——转移表int (*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div };// 0 1 2 3 4do{menu();printf("请选择:");scanf("%d",&input);//3if (input >= 1 && input <= 4){printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = pfArr[input](x, y);printf("%d\n", z);}else if (input == 0){printf("退出计算器\n");}else{printf("输入错误,重新输入\n");}} while (input);return 0;
}
并且,如果以后还想添加其他的运算,例如:按位与、按位或…只需要添加(相关的)函数,再在菜单里添加选项,在函数指针数组中添加(函数)元素即可。
这种写法就很好的利用了函数指针数组,我们有时候也把这个数组叫做转移表:
因为可以根据数组下标不断的跳转到相应的函数
函数指针数组——转移表
第三种写法:
如果不用转移表的方式,那么又该怎么简化这段代码呢?
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 z = 0;do{menu();printf("请选择:");scanf("%d",&input);switch (input){case 1:printf("请输入两个操作数:");scanf("%d %d",&x,&y);z=Add(x, y);printf("%d\n",z);break;case 2:printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = Sub(x, y);printf("%d\n", z);break;case 3:printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = Mul(x, y);printf("%d\n", z);break;case 4:printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = Div(x, y);printf("%d\n", z);break;case 0:printf("退出计算器\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}
我们观察这段代码,会发现这些部分非常冗余
这些部分除了调用的函数不同,其余一模一样。
那么我们试着将这些重复的部分封装成一个函数
cal()
{printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = Add(x, y);printf("%d\n", z);
}
但是这样以来就把函数写死了,只能完成加法的运算。
可我们得让这个函数能完成不同的运算。
这部分中就调用的函数是不同的,所以我们就把调用函数的部分抽出来。并且,函数的调用只需要知道函数的地址就可以了。
所以就有了一个想法:
把函数的地址以参数的形式传给calc
函数,再让函数的地址去调用对应的函数。这样就可以实现完成不同的运算,冗余的代码也只用写一次。
我们现在将calc
函数写出来:
- 传的的是函数的地址,所以写一个函数指针变量来接收这个函数的地址
再将冗余的代码加上:
接下来要修改冗余的代码,如果这个地方继续写Add
就会将代码写死
我们将Add
改为函数指针变量pf
。这样以来,实参传的是哪个函数的地址,就调用哪个函数
因为x,y,z
的创建原本是在主函数中,所以在calc
函数中会报警告
现在我们将x,y,z
的创建移到calc
函数中就好了
- 若想选1想实现加法运算,就将
Add
函数名(地址)传过去
- 同理,想实现减法运算就把
Sub
函数的地址传过去…
完整的代码为
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");}void calc(int (*pf)(int, int))
{int x = 0;int y = 0;int z = 0;printf("请输入两个操作数:");scanf("%d %d", &x, &y);z = pf(x, y);printf("%d\n", z);
}int main()
{int input = 0;do{menu();printf("请选择:");scanf("%d", &input);switch (input){case 1:calc(Add);break;case 2:calc(Sub);break;case 3:calc(Mul);break;case 4:calc(Div);break;case 0:printf("退出计算器\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}
这段代码中,如果给calc
函数传的是Add
函数的地址,那么pf
就指向Add
函数,实现加法运算;如果给calc
函数传的是Sub
函数的地址,那么pf
就指向Sub
函数,实现减法运算…
运行代码:
- 选1实现加法运算,输入1、2
结果正确,为3 - 再输入7
提示选择错误 - 选择0
此时就退出了计算器
此时就利用了calc
函数,既避免了冗余的代码,又可以进行加法、减法、乘法…的运算
我们再将整体的逻辑理一下:
-
在这段代码中,先选择进行哪种运算,若选了1,就进入
case1
中,执行calc(Sub)
,进入calc
函数中
-
在
calc
函数中依次往下执行语句。创建完变量后输入两个操作数,然后调用Add
函数,实现加法运算
-
调用完
Add
函数后结果返回到z
中,再将z
打印出来
-
在这一过程中
main
函数可以被称为主调函数
这个主调函数没有直接去调用Add Sub Mul...
等函数
而是将地址传给calc
函数,再在calc
函数内部通过指针的方式去调用Add Sub Mul...
等函数 -
Add/Sub/Mul...
被调用的函数就被称为回调函数
该怎么理解这里的回调函数呢
答:不会直接调用Add/Sub/Mul...
这些函数,而是把它们的信息传递给一个函数(calc
函数),然后在这个函数(calc
函数)内部,通过函数指针来调用它们。
所以,回调函数就是一个通过函数指针调用的函数。
函数指针的用法也在这里体现了:
主调函数将函数地址传给calc
函数,calc
函数用函数指针变量接收,后面再用函数指针变量调用指向的函数。
这里主调函数与calc
函数之间就是通过函数指针来沟通的,用函数指针实现了回调函数的机制。
回调函数在深入理解指针(4)中详细讲解。