函数指针 & 回调函数

✏️ 1、指针函数

指针函数: 本质是一个函数,不过它的返回值是一个指针。其声明的形式如下所示:

ret *func(args, ...);

指针函数和普通函数没有区别,但有一点需要注意的是: 避免出现返回局部变量指针的情况。看例子:

int* getSum(int n)
{
    if (n < 0)
        return NULL;
    int sum = 0;   // static int sum = 0;
    for (int i = 0; i < n; i++)
    {
        sum += i;
    }
    return &sum;
}

int main() {
    cout << *getSum(10) << endl;
    cout << "Wait for a while...\n" << endl;
    int* iptr = getSum(10);
    cout << *iptr << endl;
    return 0;
}

在GCC编译器下没有输出,也就是说程序运行到第14行就强行退出了,原因在于,一般的局部变量是存放于栈区的,当函数结束,栈区的变量就会释放掉,如果我们在函数内部定义一个变量,在使用一个指针去指向这个变量,当函数调用结束时,这个变量的空间就已经被释放,这时就算返回了该地址的指针,也不一定会得到正确的值。并且该地址可能会被后面的程序所使用,这时候得到的就不是我们想要的结果。甚至更严重的是,如果因此访问到了不可访问的内容,很有可能造成段错误等程序崩溃的情况。(需要说明的是该程序在VC++编译器下可以正常运行,这与编译器的优化有关,但不符合C语言的特性)

解决方法,将局部变量sum声明成static int, 用static修饰的变量是静态变量,而静态变量是存放在数据段的,它的生命周期存在于整个程序运行期间,只要程序没有结束,该变量就会一直存在,所以该指针就能一直访问到该变量。还有一种解决方案是使用全局变量,因为全局变量也是放在数据段的,但是并不推荐使用全局变量。

✏️ 2、函数指针

函数指针 的本质是一个指针,该指针的地址指向了一个函数。函数的定义是存在于代码段,因此,每个函数在代码段中有着自己的入口地址,函数指针就是指向代码段中函数入口地址的指针。其声明的形式如下所示:

ret (*p)(args, ...);

ret为返回值,*p作为一个整体,代表的是指向该函数的指针,args为形参列表。其中p被称为函数指针变量

  1. 函数指针的定义形式中的数据类型是指函数的返回值的类型

  2. 指向函数的指针变量不是固定指向哪一个函数的,而只是表示定义了一个这样类型的变量,它是专门用来存放函数的入口地址的;在程序中把哪一个函数的地址赋给它,它就指向哪一个函数。

  3. 在给函数指针变量赋值时,只需给出函数名,而不必给出参数。

  4. 在一个程序中,指针变量p可以先后指向不同的函数,但一个函数不能赋给一个不一致的函数指针(即不能让一个函数指针指向与其类型不一致的函数)。

  5. 定义了一个函数指针并让它指向了一个函数后,对函数的调用可以通过函数名调用,也可以通过函数指针调用(即用指向函数的指针变量调用)。

  6. 函数指针只能指向函数的入口处,而不可能指向函数中间的某一条指令。不能用*(p+1)来表示函数的下一条指令。

  7. 函数指针变量常用的用途之一是把指针作为参数传递到其他函数。、

🎤 2.1、三种声明方式

int func(int a, int b)
{
    cout << "func(int, int)" << endl;
    return 0;
}

int func(int a, int b, int c)
{
    cout << "func(int, int, int)" << endl;
    return 0;
}

//方式1 :声明一种函数类型
typedef int(MY_FUNC)(int, int);  // MY_FUNC是一个函数

//方式2 :声明一种函数类型的指针类型
typedef int(*MY_FUNC_P)(int, int);// MY_FUNC_P是一个函数指针

int ()
{
    //1.方式1举例
    MY_FUNC* fp = NULL;
    fp = func;
    fp(15, 20);

    //2.方式2举例
    MY_FUNC_P fp1 = NULL;
    fp1 = func;
    fp1(25, 30);

    //方式3 :直接通过指针类型创建,不需用typedef预定义。
    int(*fp3)(int, int) = NULL;    // pf3是指向某函数的指针
    fp3 = func; //fp3 ---> func(int,int)
    fp3(27, 89);

    //实际上在给函数指针赋值的时候,是会发生函数重载匹配的
    //在调用函数指针的时候,所调用的函数就已经固定了。
    int(*fp4)(int, int, int) = NULL;
    fp4 = func; //fp4 ---> func(int,int,int)
    fp4(10, 30, 30);
    
    // 获取函数指针的大小
    unsigned psize = sizeof ( int (*) (int, int));
    cout << psize << endl;  // 4

    return 0;
}

✏️ 3、回调函数

函数注册和回调:回调函数是对函数指针的应用,通过使用函数指针来调用一个函数,而函数注册就是把函数指针作为参数传递出去便于别的模块使用的过程。回调函数可以把调用者与被调用者分开,所以调用者不关心谁是被调用者以及被调用者如何实现。

在设计模式中注册回调的方式叫做回调模式。在SDK开发中,为增强开发者的SDK通用性,排序或者一些算法逻辑需要使用者进行编写。这时候就需要向SDK传递回调函数。 注册回调能使下层主动与上层通信。从而避免了上层不停询问下层的模式。SDK的接口会提供一个注册回调函数,来规范回调函数的格式,如返回值,参数等。使用者编写一个固定格式的回调函数,来调用SDK提供的注册回调函数。

在c中注册回调的实现方式是通过函数指针,在c++中可以通过function和bind实现。

// SDK
typedef void(*P)(string s);//使用函数指针通常先typedef
P p = nullptr;//sdk模块创建函数指针对象

void print(P p, string str)//回调函数
{
    p = prin;
    p(str);
}
//使用者
void my_print(string str)//回调函数
{
    printf(str.c_str());
}

int test1()
{
    print(my_print, "hello word");
    return 0;
}

许多编译器有几种调用规范。如在Visual C++中,可以在函数类型前加_cdecl_stdcall或者_pascal来表示其调用规范(默认为_cdecl)。C++ Builder支持_fastcall调用规范。调用规范影响编译器产生的给定函数名,参数传递的顺序(从右到左或从左到右),堆栈清理责任(调用者或者被调用者)以及参数传递机制(堆栈,CPU寄存器等)。

最后更新于