对于绝大部分人来说,关于C语言指针的学习其实是混乱的,理解了指针和常规变量的互通,又理解了指针和数组的互通,最后又在函数指针这里吃瘪,确实如果只是常规学习中过了一遍指针,或者为了计算机二级去理解了指针,知道了指针的常规用法,但是自身对于指针的使用其实不是很擅长,本篇文章也不是说要从多么多么难的角度去说明指针,毕竟工作中,程序员们会大量使用指针,但是他们绝对不会大量去嵌套指针的使用,有个一两层就够了,再多的话,程序的可读性不高的同时,还可能出现代码作者自己都理不清的情况。下面我们直接进入正题。
1.函数指针的定义
现在我写了两个基本函数,一个是带参函数一个是不带参函数如下所示:
void test12(int x)
{
printf("x=%d\n", x);
}
int test14()
{
printf("This is test function\n");
return 0;
}
这个时候我们定义两个函数指针去指向这两个函数的做法是(funp1->test12、funp2->test14)
void(*funp1)(int);//定义函数指针funp1
int(*funp2)();//定义函数指针funp2
funp1 = test12;
funp2 = &test14;
从上面代码段可以得出以下几点结论:
(1)函数指针的定义方式是:需要指向函数的类型+(*函数指针名)+(函数的参数类型)
需要注意的是,函数指针名和函数参数的括号是不能去掉的,同时函数参数类型可以是没有参数也可以是各种参数类型,总之指向函数有几种类型的参数,函数指针就照样写下来即可,假如我现在问一下,函数指针funp1和funp2是什么类型的变量?我们直接去掉变量名就好;即funp1的类型为:void (*)(int),funp2类型为:int(*)()。
(2)在指针变量指向函数的时候,有两种方式,第一种是直接=函数名,第二个是=函数名的地址;这个地方会引发很多学习者的疑问(包括我),为什么可以这样?如果这样的话,函数名到底属于什么类型?函数名和函数指针有什么区别?带着疑问,我们打开编译器,输入随便输入一个函数,然后我们对一个函数的函数名做一些探究:
printf("test12=%p\n", test12);
printf("&test12=%p\n", &test12);
printf("*test12=%p\n", *test12);
输出的结果为
可以看出到上面的输出结果都是一致的(但是一般编译器会提示最后一个printf语句的参数类型不正确),至少说到这里说明函数名本身不是一个指针变量,其他地方对函数名是否是一个指针有很多种说法,现在说一种我比较认同的:函数名其实是一个函数指针常量,函数名标识的是一个函数的接口地址,这个接口地址在定义函数的时候已经确定了,所以函数名不能像自己定义的函数指针变量那样进行赋值。
2.函数指针的使用
在定义了函数指针后,将函数指针指向对应的函数,就可以通过函数指针调用函数
funp1(6);
(*funp2)();
从上面可以看出,函数指针调用有两种方式, 但是推荐使用第二种,因为可以清楚的知道我现在使用了函数指针调用了函数。
到此很多人就出现疑问了,函数指针的调用好像和函数的调用没什么区别,甚至觉得函数指针调用的方式有点画蛇添足,但是接下来一个案例可能会告诉你为什么要使用函数指针;
上面这个例子是某芯片的SDK的一小段,可以看到这个结构体后面几个成员都是函数指针。这个例子在之后的文章中我会慢慢吃透,现在关于函数指针的作用举两个简单的例子。
(1)提供调用的灵活性。设计好了一个函数框架,但是设计初期并不知道自己的函数会被如何使用。比如C的”stdlib”中声明的qsort函数,用来对数值进行排序。显然,顺序还是降序,元素谁大谁小这些问题,库程序员在编写qsort的时候不可能决定。这些问题是要在用户调用这个函数的时候才能够决定。那边qsort如何保证通用性和灵活性呢?采用的办法是让函数的使用者来制定排序规则。于是调用者应该自己设计comparator函数,传给qsort函数。这就在程序设计初期保证了灵活性。尽管使用函数指针使得程序有些难懂,但是这样的牺牲还是值得的。
(2)提供封装性能。有点面向对象编程的特点。比如设计一个栈结构
typedef struct _c_stack{
int base_size;
int point;
int * base;
int size;
int (*pop)(struct _c_stack *);
int (*push)(int,struct _c_stack *);
int (*get_top)(struct _c_stack);
}c_stack;
在初始化完之后,用户调用这个结构体上的pop函数,只需要s.pop(&s)即可。即使这个时候,工程内部有另外一个函数名字也叫pop,他们之间是不会发生名字上的冲突的。
原因很简单,因为结构体中的函数指针指向的函数名字可能是
int ugly_stupid_no_one_will_use_this_name_pop(c_stack *)
只是stack的用户是不知道他在调用s.pop(&s),实际上起作用的是这样一个有着冗长名字的函数。
函数指针这种避免命名冲突上的额外好处对于一些库函数的编写者是很有意义的,因为库可能被很多的用户在许多不同的环境下使用,这样就能有效的避免冲突而保证库的可用性。