函数指针深入探索

函数指针再探

问题提出

  • 函数指针的本质是什么?
  • 函数指针的效率和直接调用一样吗? 为什么?
  • 函数指针可以跟模板结合吗?
  • 函数指针可以作用于静态函数、成员方法、静态方法、纯虚函数、lamda吗?不能的话有什么方法解决?
  • 除了函数指针外还有哪些代码可以实现类似功能?

探索

函数指针本质是什么?

引用维基百科上记录的对函数指针的定义,其上是这样说到的

A function pointer, also called a subroutine pointer or procedure pointer is a pointer that point to a function,

函数是执行特定功能的代码块,是一块可执行的区间,在汇编语言层面,调用函数一般是先将当前的某些信息比如程序计数器和其它信息保存到堆栈,然后再跳转到函数执行的入口地址,完成操作之后,从函数体返回,重新回到当前的主程序中来。

一句话说:

函数指针的本质就是指向一块可执行的代码块的首地址,而函数就是这个可执行代码块的首地址

调用函数直接调用即可,而函数指针调用函数时则需要对其进行解引用,但是两者实现的功能是一样的,只是一个是直接用,一个是间接用

在汇编层面比较两种函数调用的形式,比如有一个函数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百科上查找到对函数指针调用函数性能的描述为

Extensively using function pointers to call functions may produce a slow-down for the code on modern processors, because branch predictor may not be able to figure out where to branch to (it depends on the value of the function pointer at run time) although this effect can be overstated as it is often amply compensated for by significantly reduced non-indexed table lookups.

在现代处理器上,大量使用函数指针来调用函数可能会导致代码速度变慢,因为分支预测器可能无法确定分支到哪里(这取决于运行时函数指针的值),尽管这种效果可能被夸大了,因为它经常被大量减少的非索引表查找所弥补

来自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

从上面的代码中可以得到以下结论

  • 函数指针可以作用于函数、成员方法、静态成员方法、纯虚函数、lamda
  • 函数指针指向成员方法和纯虚函数时,其大小为普通函数指针的两倍,而且其定义方法有差异,大小为普通函数指针的两倍,这是因为这些方法的执行是要有特定的对象的,函数指针不仅仅要指向这个方法的地址,还要指向这个方法的所有者(对象)的地址。所以其大小要比普通的函数指针大两到三倍

除了函数指针外,还有什么可以实现类似的功能

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截取的原文

A typical use of a function object is in writing callback functions. A callback in procedural languages, such as C, may be performed by using function pointers.[2] However it can be difficult or awkward to pass a state into or out of the callback function. This restriction also inhibits more dynamic behavior of the function. A function object solves those problems since the function is really a façade for a full object, carrying its own state.

Many modern (and some older) languages, e.g. C++, Eiffel, Groovy, Lisp, Smalltalk, Perl, PHP, Python, Ruby, Scala, and many others, support first-class function objects and may even make significant use of them.[3] Functional programming languages additionally support closures, i.e. first-class functions that can ‘close over’ variables in their surrounding environment at creation time. During compilation, a transformation known as lambda lifting converts the closures into function objects.

Function object - Wikipedia

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值