关于回调函数和this指针探讨

新浪微博 QQ空间

在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 <stdio.h>
#include <stdlib.h>

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 <stdio.h>
#include <stdlib.h>

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 <stdio.h>
#include <stdlib.h>

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指针地址加上偏移量实现跳转。这里不详细介绍了,有兴趣的同学可以参考链接。

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

参考: [1] 深入探讨this指针:从汇编的角度考虑; [2] 深入探讨this指针

新浪微博 QQ空间

| 1 分2 分3 分4 分5 分 (4.89- 19票) Loading ... Loading ... | 这篇文章归档在:C/C++, 语言基础 | 标签: , , . | 永久链接:链接 | 评论(5) |

5 条评论

  1. 匿名
    评论于 六月 6, 2016 at 20:53:33 CST | 评论链接

    传说中的写垃圾代码的程序员?

  2. 匿名
    评论于 十一月 26, 2015 at 18:32:27 CST | 评论链接

    这个文章的观点太旧了吧。静态函数的方法太丑了。
    C++11后请参见std::function

    • 童燕群
      评论于 十一月 27, 2015 at 11:35:57 CST | 评论链接

      刚刚搜索了一下,有了lambda表达式和function变量,的确彻底解决了各种回调的问题。连C++这样的底层语言也在不断进化出这样的高级特性。

  3. 评论于 八月 28, 2014 at 18:34:29 CST | 评论链接

    我觉得”this指针作为第一个参数隐式传递”只是一个用于帮助理解的理论说法。就像我们说函数的参数是依次入栈传递。而其真实的实现,则是完全依赖编译器的。在64位平台上由于寄存器增加了,普通函数参数都可以通过寄存器传递。更何况this指针?不管是通过何种代码来hack,始终都是基于当前选用的编译器的具体实现,哪怕是类对象的内存布局。

    • 童燕群
      评论于 八月 28, 2014 at 18:43:09 CST | 评论链接

      嗯,同意,重要的是了解思想。

评论

邮箱地址不会被泄露, 标记为 * 的项目必填。

8 - 2 = *



You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <img alt="" src="" class=""> <pre class=""> <q cite=""> <s> <strike> <strong>

返回顶部