在C++中,指针是一个核心的部分,常规的指针用法作者已经在另外一篇文章中有过详细介绍,这篇文章主要是针对函数指针进行讨论。
首先我们先来认识一下函数指针的概念,我们平时在写函数的时候,调用函数时总是以函数名+小括号的形式来进行调用,事实上,函数在内存中也有相应的存储位置,也就是函数也有地址,那么我们也可以通过地址来获取到函数,提到地址,我们自然而然的就会联想到指针,因为指针用来保存的就是地址,那么函数指针的用途也很清楚了,就是用来保存函数的地址。
首先我们来看一下如何声明一个函数指针:
void (*func_ptr)();
如上形式就声明了一个函数指针,那么这个指针指向怎么样的函数呢?事实上,这个指针指向的是一个返回值为void,并且参数列表为空的函数,那么,假设我现在我一个函数:
void func();
// 我可以用上面声明的指针指向这个函数
func_ptr = func;
这样就可以完成一个函数指针的赋值(Tips: 在C++中,函数名就是这个函数对应的地址,所以可以直接用指针=函数名这样的形式为指针赋值)。
和普通的指针一样,我们可以通过这个指针获取到指针所指向内容,如果我们想要通过这个指针调用这个函数,我们可以用如下操作:
func_ptr();
这样的形式就和我们调用普通的函数一样,是不是非常的简单。
接下来我们来进阶一下,上面的函数指针指向一个返回值为void,参数列表为空的函数,现在我们假设我们的函数指针需要指向一个返回值为一个整形的指针,参数列表有两个整形变量的函数,那么我们可以这样声明:
// 函数指针声明
int* (*func_ptr)(int , int);
// 现在有一个函数, 仅仅是声明,没有做出实现
int* func(int, int);
// 我们可以这样使用
func_ptr = func;
// 通过函数指针调用函数
func_ptr(1, 2);
这个例子也是比较简单,接下来我们来试下更难的,假设一个函数返回一个函数指针(是的,因为函数指针是一个指针,所以是可以作为参数返回的!)这个函数的参数只有一个,也是一个函数指针,接下来我们要来开始见证C++的魅力了。
我们先做出假设,这个函数返回的指针指向一个返回值为void,参数为空的函数,这个函数的参数的函数指针指向一个返回值为int,参数列表为int的一个函数
那么我们可以这样声明:
// 我们的目标
void(* (*func_ptr)( int(*)(int) ))();
ok,我相信大部分同学估计到了这里看不懂了, 其实可以不用懂这么复杂的声明,因为确实很复杂哈哈哈,不过我们在C++中可以使用typedef和using关键字定义别名,这样我们可以简化声明
首先是typedef:
// 先声明返回值的函数指针
typedef void(*ret)();
// 再声明参数需要的函数指针
typedef int(*arg)(int);
// 最后声明我们需要的函数指针
typedef ret (*aim) (arg);
// 这样做之后,aim就是一个具有此类型的函数指针,如果我们需要多个此类型的指针,我们可以多次声明
aim ptr1;
aim ptr2;
aim ptr3;
// ptr1, ptr2, ptr3 都和 void(* (*func_ptr)( int(*)(int) ))() 等价
接下来是使using关键字,值得一提的是,using关键字有这样的功能是在C++11之后,并且功能比typedef更加强大,完全可以代替typedef。
// 同理,先声明返回值的指针
using ret = void (*) ();
// 再声明参数需要的指针
using arg = int (*) (int);
// 最后声明我们的目标
using aim = ret (*) (arg);
以上就是函数指针相关的语法知识,接下来我们来讨论函数指针的应用。
如果有小伙伴接触过C++的GUI编程的话,比如Qt或者Opencv,都可以发现用到了大量的函数指针,这些函数指针的作用主要就是用于回调函数,比方说我现在有一个函数,参数是一个函数指针,这个时候我们可以再这个函数运行的过程中去通过函数指针去调用这个函数指针指向的函数,这个函数指针指向的函数我们就称作回调函数。
看下面这个例子:
#include
using namespace std;
// 先利用using重命名一个函数指针
using FUNC_PTR = void (*) ();
// 接下来声明两个函数
void show_age() {
cout << 18 << endl;
}
void show_name() {
cout << "djs" << endl;
}
// 最后一个函数,参数需要一个函数指针
void test(string msg, FUNC_PTR func_ptr) {
cout << msg << " ";
func_ptr(); // 回调
}
// 在主函数中嗲调用测试
int main() {
test("name:", show_name);
test("age:", show_age);
return 0;
}
以上程序的运行结果为:
我们分析以上程序,我们有两个函数,一个函数的功能是打印年龄,另外一个函数的功能是打印姓名,现在又如下需求,就是在打印的信息前分别加上对应的字段,比如姓名加上name,年龄加上age,如果我们没有使用test这个函数的话,我们只有两种做法,第一种就是分别去修改show_name和show_age中的代码,这样做确实可以,但是并不好,因为这里代码量很少,所以我们修改起来很简单,但是假设我们的项目大了,代码量一多,这件事就会变得困难,事实上我们编程需要遵循一个叫做开闭原则的东西,就是对于代码的修改关闭,对于代码的扩充开启,我们可以看到我并没有去修改show_name和show_age, 而是新加了一个test函数,这个函数的作用就是对show_name和show_age功能的扩充,这样做的话代码的可维护性是可以大大提高的。第二种做法就是我们在主函数里面添加输出语句,但是这样做的话代码又过于死板。从上述例子中,我们可以从中提取出一个设计模式-------装饰器模式.
装饰器模式
装饰器模式是23种设计模式中的一种,我们通过一个例子来讲解什么是装饰器模式,比如说我们又如下代码:
#include
using namespace std;
void test() {
for (int i = 0; i < 100000; i++) {
// do something...
}
}
int main() {
test();
return 0;
}
现在我们有一个函数test,然后我现在又如下需求,就是计算test函数运行的时间,下列是一般的做法:
#include
#include
using namespace std;
void test() {
for (int i = 0; i < 100000; i++) {
// do something...
}
}
int main() {
clock_t start = clock();
test();
clock_t end = clock();
cout << "time:" << end - start << "ms" << endl;
return 0;
}
我们这样做确实可以知道解决这个需求,但是万一我程序中很多的地方都需要计算时间呢,难道我每个的地方都加上这样的代码吗?很明显这样的代码过于冗余,这时候我们就可以使用我们的装饰器模式来解决这个需。
#include
#include
using namespace std;
void test() {
for (int i = 0; i < 100000; i++) {
// do something...
}
}
void warp_test(void (*test_ptr)()) {
clock_t start = clock();
test_ptr();
clock_t end = clock();
cout << "time:" << end - start << "ms" << enddl;
}
int main() {
//test();
wrap_test(test);
return 0;
}
我们可以看到我又增加了一个叫做wrap_test的函数,这个函数的参数就是一个函数指针,在函数内部,我们可以通过函数指针来调用外部的函数,并且多了计算时间的功能,并且我没有对test函数做出任何的修改,这样做既符合我们的开闭原则,而且代码的复用性也可以提高,比如说我们日后又有类似于test这样的函数(返回值为void,参数列表为空),那么我们仍然可以使用wrap_test这个函数去计算运行的时间,我们称wrap_test这样的函数为test的装饰器,这就是装饰器模式的核心内容。