函数指针再探
问题提出
- 函数指针的本质是什么?
- 函数指针的效率和直接调用一样吗? 为什么?
- 函数指针可以跟模板结合吗?
- 函数指针可以作用于静态函数、成员方法、静态方法、纯虚函数、lamda吗?不能的话有什么方法解决?
- 除了函数指针外还有哪些代码可以实现类似功能?
探索
函数指针本质是什么?
在汇编层面比较两种函数调用的形式,比如有一个函数add,通过前面提到的两种方式调用add这个函数,反汇编并查看其底层调用形式,代码如下
int add(int a, int b){
return a + b;
}
typedef decltype(add) *FP;
int main(){
// 直接调用函数
int c1 = add(1,2);
// 通过函数指针调用函数
FP p = add;
int c2= p(1,2);
return 0;
}
反汇编汇编代码部分如下,删除其细枝末节,只看两种函数调用的部分,反汇编代码如下
.....
# add函数的首地址
0000000140001530 <_Z3addii>:
140001530: 55 push %rbp
140001531: 48 89 e5 mov %rsp,%rbp
140001534: 89 4d 10 mov %ecx,0x10(%rbp)
140001537: 89 55 18 mov %edx,0x18(%rbp)
14000153a: 8b 55 10 mov 0x10(%rbp),%edx
14000153d: 8b 45 18 mov 0x18(%rbp),%eax
140001540: 01 d0 add %edx,%eax
140001542: 5d pop %rbp
140001543: c3 retq
# main函数的首地址
0000000140001544 <main>:
......
140001551: ba 02 00 00 00 mov $0x2,%edx
140001556: b9 01 00 00 00 mov $0x1,%ecx
14000155b: e8 d0 ff ff ff callq 140001530 <_Z3addii>
140001560: 89 45 fc mov %eax,-0x4(%rbp)
140001563: 48 8d 05 c6 ff ff ff lea -0x3a(%rip),%rax # 140001530 <_Z3addii>
14000156a: 48 89 45 f0 mov %rax,-0x10(%rbp)
14000156e: 48 8b 45 f0 mov -0x10(%rbp),%rax
140001572: ba 02 00 00 00 mov $0x2,%edx
140001577: b9 01 00 00 00 mov $0x1,%ecx
14000157c: ff d0 callq *%rax
.....
从中分析,看到两个汇编代码,函数调用使用的汇编代码为
callq 140001530
而函数指针调用函数的汇编代码为
lea -0x3a(%rip), %rax ;将add函数的首地址放入rax寄存器
callq *%rax ;间接调用add,add的地址存放在rax里面
从中可以看出,函数指针调用只是将函数的首地址放入到一个寄存器中,然后间接的跳转到这个位置
函数指针的效率和函数调用的效率相比?
从Wiki百科上查找到对函数指针调用函数性能的描述为
在现代处理器上,大量使用函数指针来调用函数可能会导致代码速度变慢,因为分支预测器可能无法确定分支到哪里(这取决于运行时函数指针的值),尽管这种效果可能被夸大了,因为它经常被大量减少的非索引表查找所弥补
来自Function pointer - Wikipedia
意思可能是说,两者的性能差别并不是很大
函数可以跟模板结合吗?
可以跟模板结合
#include<stdio.h>
template<typename type>
type add(type type1, type type2){ return type1 + type2;}
void func1(double (*p1)(double, double), double a, double b){
printf("The double value %f + %f = %f\n", a, b, p1(a,b));
}
void func2(int (*p1)(int, int), int a, int b){
printf("The integer value %d + %d = %d\n", a, b, p1(a,b));
}
int main(int argc, char *argv[]){
func1(add<double>, 1.0, 2.0);
func2(add<int>, 1, 2);
return 0;
}
D:\ClionProject\C++Learn\cmake-build-debug\C__Learn.exe
The double value 1.000000 + 2.000000 = 3.000000
The integer value 1 + 2 = 3
函数指针可以指向哪些定义的函数模板
函数指针可以作用在静态函数、成员方法、静态方法、纯虚函数、lamda上面吗,如果不能用什么替代?
作用在静态函数
#include<stdio.h>
// 静态函数定义
static int add_func(int a, int b){
return a + b;
}
int main(int argc, char *argv[]){
typedef int (*calcu_ptr)(int, int); // 定义函数指针类型
printf("The func pointer's size is %d\n", int(sizeof(calcu_ptr)));
calcu_ptr c = &add_func; // 函数指针指向add_func
printf("The result of the %d + %d = %d", 1, 2, c(1,2));
return 0;
}
D:\ClionProject\C++Learn\cmake-build-debug\C__Learn.exe
The func pointer's size is 8
The result of the 1 + 2 = 3
作用在成员方法
#include<stdio.h>
class ALU{
// 完成一些基本的运算操作
public:
int alu_Add(int a, int b){
return a + b;
}
};
int main(int argc, char *argv[]){
typedef int (ALU::*calc_ptr)(int, int); // 定义指向成员方法的指针
calc_ptr c = &ALU::alu_Add; // 指向类的普通成员方法
printf("The member func pointer's size is %d\n", int(sizeof (calc_ptr)));
ALU *a = new ALU();
printf("The result of %d + %d = %d\n", 1, 2,(a->*c)(1,2)); // 普通成员方法的使用
return 0;
}
D:\ClionProject\C++Learn\cmake-build-debug\C__Learn.exe
The member func pointer's size is 16
The result of 1 + 2 = 3
指向成员静态方法
#include<stdio.h>
class ALU{
// 完成一些基本的运算操作
public:
static int alu_Add(int a, int b){
return a + b;
}
};
int main(int argc, char *argv[]){
typedef int (*calc_ptr)(int, int); // 定义函数指针
calc_ptr c = &ALU::alu_Add; // 指向静态成员方法
printf("The static member func pointer's size is %d\n", int(sizeof (calc_ptr)));
printf("The result of %d + %d = %d\n", 1, 2,c(1,2)); // 使用静态成员方法
return 0;
}
D:\ClionProject\C++Learn\cmake-build-debug\C__Learn.exe
The static member func pointer's size is 8
The result of 1 + 2 = 3
指向纯虚函数
#include<stdio.h>
class Operation{
public:
virtual int Calcu(int a, int b) = 0;
};
class Add:public Operation{
public:
int Calcu(int a, int b) override{ return a + b; }
};
class Sub:public Operation{
public:
int Calcu(int a, int b) override{ return a - b; }
};
int main(int argc, char *argv[]){
typedef int (Operation::*Calcu)(int, int);
Calcu c = &Operation::Calcu;
printf("The virtual member func's size is %d\n", int(sizeof(Calcu)));
Add a;
Sub s;
printf("%d + %d = %d\n", 1, 2, (a.*c)(1,2));
printf("%d - %d = %d\n", 2, 1, (s.*c)(2,1));
return 0;
}
D:\ClionProject\C++Learn\cmake-build-debug\C__Learn.exe
The virtual member func's size is 16
1 + 2 = 3
2 - 1 = 1
函数指针指向lamda
#include<stdio.h>
typedef int (*Calcu)(int, int);
int main(){
Calcu c = [](int a, int b){return a + b;};
printf("The func pointer size is %d\n", int(sizeof (Calcu)));
printf("%d + %d = %d\n", 1, 2, c(1,2));
return 0;
}
D:\ClionProject\C++Learn\cmake-build-debug\C__Learn.exe
The func pointer size is 8
1 + 2 = 3
除了函数指针外,还有什么可以实现类似的功能
functor,仿函数,函数说白了就是一个语句块,他能实现一些功能,并且能返回值,这就是一个函数,但是在函数调用的过程中,通过函数执行的功能只能通过传递参数和调用某些全局变量的形式进行处理,关键在于它不具有状态,而次,函数对象就不会这样,它的定义是,函数对象它不仅能实现函数的功能,还能带有自己的状态,如果你的一段代码能实现这样的一个功能,那么他就是一个函数对象,而在C++中,有一种实现函数对象的方式就是重载()运算符
比如说一个自动可乐售货机,它里面只有10罐可乐,用户通过buyCola
这个函数来获取可乐,当10罐买完后,就没有可乐了,所以需要给出售罄的提示,这样一个场景,仅仅靠函数是无法实现的,但是通过**函数对象(或者说仿函数functor)**是可以实现的,因为后者可以记录这个状态,请看代码
#include<stdio.h>
class BuyCola{
public:
BuyCola(int cola_count):cola_count(cola_count){}
// 重载运算符
void operator()(){
if(cola_count > 0){
printf("You will buy a cola\n");
cola_count--;
}else{
printf("The cola is sold out!\n");
}
}
private:
// 可乐数量
int cola_count;
};
int main(){
BuyCola vending_maching(10);
for (int i = 0; i < 13; ++i){
vending_maching();
}
return 0;
}
D:\ClionProject\C++Learn\cmake-build-debug\C__Learn.exe
You will buy a cola
You will buy a cola
You will buy a cola
You will buy a cola
You will buy a cola
You will buy a cola
You will buy a cola
You will buy a cola
You will buy a cola
You will buy a cola
The cola is sold out!
The cola is sold out!
The cola is sold out!
对于具体的仿函数的介绍可以参考Wiki或者是其它的资料,以上仅是个人理解,如有错误,欢迎指出!
下面是Wiki截取的原文
Function object - Wikipedia