一聚教程网:一个值得你收藏的教程网站

热门教程

C语言回调函数和this指针详细介绍

时间:2022-06-25 04:41:54 编辑:袖梨 来源:一聚教程网

在C里面,经常需要提供一个函数地址,注册到结构里,然后在程序执行到特定阶段时,回调该函数。创建线程,注册线程运行的主函数就是一个典型的例子。这里以简单的回调实例,说明C++中回调函数为成员函数时有关this指针的问题。由于C++对C的继承关系,C++没有自己的线程封装技术,一般而言我们创建线程时,还是用C的回调函数机制。类似的例子也挺多的。在Java等纯粹的面向对象语言,则不一样,不光有自己的独立的线程类型,对于回调,也是注册整个对象,而不是注册一个方法,如常用的观察者模式。这里,在网上查阅了大量关于this指针、类成员函数和静态成员函数的相关知识点,结合自己的理解作一些总结。

关于回调函数,类的成员函数作为回调函数,一般而言大家已经形成了编程范式,讨论一些生僻的用法,可能被认为是腐朽的,无价值的。这里只想客观分析一下技术点,思想可能在类似的场景中遇到也说不准。

通常我们理解的成员函数和this指针是:

《深入探索C++对象模型》中提到成员函数时,当成员函数不是静态的,虚函数,那么我们有以下结论:

(1)  &类名::函数名 获取的是成员函数的实际地址;

(2) 对于函数x来讲obj.x()编译器转化后表现为x(&obj),&obj作为this指针传入;

(3) 无法通过强制类型转换在类成员函数指针与其外形几乎一样的普通函数指针之间进行有效的转换。

通常我们理解的是类的普通成员函数有一个隐藏的参数,即第一个参数,其值是this。如果希望一个成员函数既能访问类的数据成员,又能作为回调函数,有如下几种方法:

1、静态成员函数作为回调函数

为了不失封装性,可以将需要作为回调的函数声明为静态的。静态的成员函数,可以直接在类的外部调用。我们知道静态成员函数是不能直接访问类的非静态数据和接口的。那么此时需要知道具体的对象地址或者引用才能访问具体的对象成员。又有两个方法能实现这个:

1)将对象的地址用全局变量记录,在静态成员函数中通过该全局变量访问数据成员和方法。来看具体的代码实例:

#include
#include

typedef void (*func)(void*);

class CallBack;
class CallBackTest;

CallBack* g_obj = NULL;
CallBackTest* g_test = NULL;

class CallBackTest
{
public:
    CallBackTest()
    {
        m_fptr = NULL;
        m_arg = NULL;
    }

    ~CallBackTest()
    {

    }

    void registerProc(func fptr, void* arg = NULL)
    {
        m_fptr = fptr;
        if (arg != NULL)
        {
            m_arg = arg;
        }
    }

    void doCallBack()
    {
        m_fptr(m_arg);
    }

private:
    func m_fptr;
    void* m_arg;

};

class CallBack
{
public:
    CallBack(CallBackTest* t) : a(2)
    {
        if (t)
        {
            t->registerProc((func)display);
        }
    }

    ~CallBack()
    {

    }

    static void display(void* p)
    {
        if (g_obj)
        {
            g_obj->a++;
            printf("a is: %d", g_obj->a);
        }
    }

private:
    int a;

};

int main(int argc, char** argv)
{
    g_test = new CallBackTest();
    g_obj = new CallBack(g_test);
    g_test->doCallBack();

    return 0;
}
如上代码,实现对CallBack成员函数的回调。在callback类的构造函数中注册静态的成员函数到callbacktest类中。如果对该代码稍加该井,可以将g_obj变量放在callback类里面,作为一个静态成员,这样就更好了。更优雅的,将g_obj作为display的参数传入,就更好了。于是有了我们通常的做法,将成员函数声明为静态的,带一个参数,是其所在的类的对象指针,这样我们可以在注册的时候将this指针传递给静态成员函数,使用起来就好像是静态的成员函数有了this指针一样。

#include
#include

typedef void (*func)(void*);

class CallBack;
class CallBackTest;

class CallBackTest
{
public:
    CallBackTest()
    {

    }

    ~CallBackTest()
    {

    }

    void registerProc(func fptr, void* arg = NULL)
    {
        m_fptr = fptr;
        if (arg != NULL)
        {
            m_arg = arg;
        }
    }

    void doCallBack()
    {
        m_fptr(m_arg);
    }

private:
    func m_fptr;
    void* m_arg;

};

class CallBack
{
public:
    CallBack(CallBackTest* t) : a(2)
    {
        if (t)
        {
            t->registerProc((func)display, this);
        }
    }

    ~CallBack()
    {

    }

    static void display(void* _this = NULL)
    {
        if (!_this)
        {
            return;
        }
        CallBack* pc = (CallBack*)_this;
        pc->a++;
        printf("a is: %d", pc->a);
    }

private:
    int a;

};

int main(int argc, char** argv)
{
    CallBackTest* cbt = new CallBackTest();
    CallBack* cb = new CallBack(cbt);
    cbt->doCallBack();

    return 0;
}
最常用和正统的解决方法,借助于static成员函数对类数据成员的可见性,可以很方便的利用:

pc->a++;
printf("a is: %d", pc->a);
这样的语句来操作类的成员函数和成员数据。但是仍然不能像普通成员函数那样利用隐藏的this指针就直接操作类的成员函数。肯定有很多“好事”的同学希望直接像普通的成员函数那样访问类的成员。接下来就探讨一下这个方法。

2、非静态成员函数作为回调函数

既然我们知道,非静态成员函数有一个隐藏的参数,那么能否注册的时候,多传入一个参数,然后隐藏的那个指向对象的参数默认就转为this指针的值了,相当于在调用时给this赋值。可以做一个尝试,代码如下:

#include
#include

typedef void (*func)(void*);

class CallBack;
class CallBackTest;

class CallBackTest
{
public:
    CallBackTest()
    {

    }

    ~CallBackTest()
    {

    }

    void registerProc(func fptr, void* arg = NULL)
    {
        m_fptr = fptr;
        if (arg != NULL)
        {
            m_arg = arg;
        }
    }

    void doCallBack()
    {
        m_fptr(m_arg);
    }

private:
    func m_fptr;
    void* m_arg;

};

class CallBack
{
public:
    CallBack(CallBackTest* t) : a(2)
    {
        if (t)
        {
            t->registerProc((func)display, this);
        }
    }

    ~CallBack()
    {

    }

    void display()
    {
        a++;
        printf("a is: %d", a);
    }

private:
    int a;

};

int main(int argc, char** argv)
{
    CallBackTest* cbt = new CallBackTest();
    CallBack* cb = new CallBack(cbt);
    cbt->doCallBack();

    return 0;
}
尝试失败了,提示编译错误。在附录的引用[1]文中,作者采用了更直接的给指针变量赋值的方式,避开了编译错误的问题,但调用时仍然会报错。因此this指针并不是简单的在函数调用时以第一个参数的方式传递进去的,在理解成员函数访问数据的过程可以这样去理解,但是实际上的运行过程并不是这样的。在引文1、2中给出了一些可行的办法,进一步找了一下,这个也就是thunk技术,由于与平台和编译器的行为强相关。大体思路是,首先将this指针填写到指定的寄存器或者指定的地方,当调用成员函数名时,会自动根据寄存器的地址值加上偏移量实现跳转。这里不详细介绍了,有兴趣的同学可以参考链接。

使用静态成员函数加上参数传入this指针的方式应该说是目前比较完善的解决办法。不失封装性,又不失易用性。

热门栏目