关于
我的项目
相关阅读
热度排行
- [转] 宫崎骏用动漫教给我们的人生哲理,每一句都能说到心里! - (日期:[八月 24, 2013] 点击:[53,045])
- Google 网页爬虫报告无法连接站点解决办法 - (日期:[七月 20, 2014] 点击:[38,621])
- 架设Tiny Tiny RSS(TTRSS)阅读器,找回Google Reader! - (日期:[九月 27, 2013] 点击:[27,754])
- SkyDrive、DropBox和Google Drive三大公有云存储服务对比 - (日期:[六月 25, 2013] 点击:[25,547])
- 升级到至强E5440后,与i5 CPU笔记本性能对比 - (日期:[二月 18, 2014] 点击:[23,660])
- 公钥私钥加密解密数字证书数字签名详解 - (日期:[四月 19, 2014] 点击:[22,953])
- 本站建站技术合集 - (日期:[九月 20, 2013] 点击:[22,464])
- 使用OpenerDNS解决无法访问Google的问题 - (日期:[七月 5, 2014] 点击:[21,760])
- WordPress博客添加“返回顶部”按钮 - (日期:[七月 14, 2013] 点击:[21,181])
- Linux文件系统基础之inode和dentry - (日期:[三月 13, 2015] 点击:[20,158])
- 云存储中的HTTP鉴权算法分析 - (日期:[二月 7, 2014] 点击:[18,633])
- 存储基础知识之——磁盘阵列原理及操作实战 - (日期:[二月 9, 2014] 点击:[17,470])
- 精选37条强大的常用linux shell命令组合 - (日期:[九月 4, 2013] 点击:[17,424])
- DNS原理、架构和配置详解 - (日期:[九月 6, 2013] 点击:[16,789])
- Netty和Jetty的Java NIO 网络框架模型分析 - (日期:[七月 13, 2013] 点击:[16,326])
- CoreOS 初识之安装 - (日期:[十一月 16, 2014] 点击:[16,157])
- Windows与Linux文件系统互访的几种方法 - (日期:[八月 21, 2014] 点击:[15,716])
- Dijkstra算法求解最短路径分析 - (日期:[七月 12, 2014] 点击:[14,920])
- NAS解决方案实现多媒体文件共享播放 - (日期:[十二月 21, 2014] 点击:[13,879])
- 简介 - (日期:[九月 1, 2012] 点击:[13,736])
- 如何编程实现 2 + 2 = 5? - (日期:[六月 2, 2014] 点击:[13,265])
- 搭建了一个iNews程序 - (日期:[十月 15, 2013] 点击:[13,233])
- 2014年9月曝出的Bash ShellShock漏洞简析 - (日期:[九月 26, 2014] 点击:[13,132])
- 彻底解决WordPress博客垃圾评论的问题 - (日期:[八月 5, 2013] 点击:[13,076])
- 如何使用1M的内存排序100万个8位数 - (日期:[三月 27, 2014] 点击:[12,549])
- 全部日志列表 - (日期:[十一月 11, 2012] 点击:[12,300])
- 关于回调函数和this指针探讨 - (日期:[八月 24, 2014] 点击:[12,200])
- 给定一个long型常量,其值为x,给定long型变量a,要求a & x 的取值集合 - (日期:[九月 8, 2012] 点击:[11,695])
- WordPress建站必备实用插件 - (日期:[八月 7, 2014] 点击:[11,355])
- Amazon 云计算业务全面介绍 - (日期:[三月 9, 2014] 点击:[11,258])
分类目录
文章归档
- 2024年四月 (1)
- 2024年二月 (1)
- 2023年九月 (1)
- 2023年一月 (1)
- 2022年十月 (1)
- 2022年八月 (2)
- 2022年四月 (1)
- 2022年三月 (1)
- 2021年十二月 (2)
- 2021年十月 (2)
- 2021年九月 (1)
- 2021年八月 (1)
- 2021年五月 (1)
- 2021年三月 (2)
- 2021年一月 (2)
- 2020年十二月 (5)
- 2020年十一月 (2)
- 2020年十月 (2)
- 2020年九月 (1)
- 2020年八月 (5)
- 2020年七月 (2)
- 2019年九月 (1)
- 2018年八月 (1)
- 2018年七月 (1)
- 2018年六月 (1)
- 2018年五月 (1)
- 2018年三月 (1)
- 2018年二月 (1)
- 2018年一月 (2)
- 2017年十二月 (3)
- 2017年十月 (4)
- 2017年九月 (1)
- 2017年七月 (1)
- 2017年六月 (1)
- 2016年十二月 (1)
- 2016年十月 (1)
- 2016年九月 (1)
- 2016年七月 (2)
- 2016年六月 (1)
- 2016年二月 (3)
- 2015年十二月 (3)
- 2015年十一月 (2)
- 2015年十月 (1)
- 2015年八月 (2)
- 2015年七月 (4)
- 2015年六月 (1)
- 2015年三月 (2)
- 2015年二月 (1)
- 2015年一月 (4)
- 2014年十二月 (2)
- 2014年十一月 (2)
- 2014年十月 (5)
- 2014年九月 (8)
- 2014年八月 (11)
- 2014年七月 (17)
- 2014年六月 (7)
- 2014年五月 (15)
- 2014年四月 (16)
- 2014年三月 (14)
- 2014年二月 (5)
- 2013年十二月 (5)
- 2013年十一月 (3)
- 2013年十月 (13)
- 2013年九月 (13)
- 2013年八月 (13)
- 2013年七月 (9)
- 2013年六月 (8)
- 2013年五月 (1)
- 2013年三月 (3)
- 2013年一月 (1)
- 2012年十一月 (1)
- 2012年九月 (12)
- 2012年八月 (3)
- 2011年二月 (1)
- 2009年三月 (1)
- 2009年二月 (1)
- 2008年十一月 (1)
- 2008年六月 (1)
- 2008年四月 (1)
- 2008年三月 (1)
关于回调函数和this指针探讨
在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指针
5 条评论
传说中的写垃圾代码的程序员?
这个文章的观点太旧了吧。静态函数的方法太丑了。
C++11后请参见std::function
刚刚搜索了一下,有了lambda表达式和function变量,的确彻底解决了各种回调的问题。连C++这样的底层语言也在不断进化出这样的高级特性。
我觉得”this指针作为第一个参数隐式传递”只是一个用于帮助理解的理论说法。就像我们说函数的参数是依次入栈传递。而其真实的实现,则是完全依赖编译器的。在64位平台上由于寄存器增加了,普通函数参数都可以通过寄存器传递。更何况this指针?不管是通过何种代码来hack,始终都是基于当前选用的编译器的具体实现,哪怕是类对象的内存布局。
嗯,同意,重要的是了解思想。