C++ ---------------- 成员函数指针揭秘
1 前言
2 语法
3 与其它语言机制的混合使用
3.1 继承
3.2 虚函数
3.2 多继承
4 实现
4.1 Microsoft的实现
4.1.1 内部表示
4.1.2 Vcall_addr实现
4.1.3 This指针调整
4.1.4 结论
4.2 GCC的实现
4.2.1 内部表示
4.2.2 实现机制
5 语言限制与陷阱
5.1 例子
5.2 static_cast干了些什么
5.3 默认的转换
5.4 教训
5.5 如何避开陷阱
1 前言
关键字:成员函数指针,继承,虚函数,this指针调整,static_cast
2 语法
C++成员函数指针(pointerto member function)的用法和C语言的函数指针有些相似.
class ClassName {public: int foo(int); } int (ClassName::*pmf)(int) = &ClassNmae::foo; ClassName c; //.*的用法,经由对象调用 (c.*pmf)(5); // A ClassName *pc = &c; //->*的用法,经由对象指针调用 (Pc->*pmf)(6); // B |
typedef int (ClassName::*PMF)(int); PMF pmf = &ClassName::foo; |
1)不能使用括号:例如&(ClassName::foo)不对。
2)必须有限定符:例如&foo不对。即使在类ClassName的作用域内也不行。
3)必须使用取地址符号:例如直接写ClassName::foo不行。(虽然普通函数指针可以这样)
C++里面引入了“引用”(reference)的概念,可是却不存在“成员函数的引用”,这也是一个特殊的地方。(当然,我们可以使用“成员函数指针”的引用,呵呵)
3 与其它语言机制的混合使用
C++是一种Multi-Paradigm的语言,各种语言机制混合使用也是平常的事。这里我们只提几种会影响到成员函数指针实现和运行的语言机制。
3.1 继承
根据C++语言规定,成员函数指针具有contravariance特性,就是说,基类的成员函数指针可以赋值给继承类的成员函数指针,C++语言提供了默认的转换方式,但是反过来不行。
3.2 虚函数
首先要说明,指向虚拟成员函数(virtual functionmember)的指针也能正确表现出虚拟函数的特性。举例说明如下:
class B { public:virtual intfoo(int) {/* B'simplementation */return 0; } };
class D : publicB { public: virtualint foo(int) { /* D's implementation */ return0; } };
(pb->*pmf)(0); //这里执行D::foo,多态
因为这些不同,所以成员函数指针碰上虚函数的时候,也需要作特殊的处理,才能正确表现出所期望的虚拟性质。
3.2 多继承
这里扯上多继承,是因为多继承的存在导致了成员函数指针的实现的复杂性。这是因为编译器有时候需要进行”this”指针调整。
class D: publicB1, public B2{}
假设上面三个对象都不涉及到虚函数,D在内存中的典型布局如下图所示(如果有虚函数则多一个vptr指针, 差别不大):
现在假设我们经由D对象调用B2的函数,
D d;
d.fun_of_b2();
这里传给fun_of_b2的this指针不能是&d, 而应该对&d加上一个偏移,得到D内含的B2子对象的首地址处。
成员函数指针的实现必须考虑这种情况。
多继承总是不那么受欢迎。不过即使是单继承,上面的情况也会出现。考虑下面的例子:
class B{}; //non-virtualclass
class D :public B{}; //virtual class
假设B是一个普通的类,没有虚拟成员函数。而D加上了虚拟成员函数。那么D的典型内存布局如下图所示:
因为D引入了vptr指针,而一般的实现都将vptr放在对象的开头,这就导致经由D对象访问B的成员函数的时候,仍然需要进行this指针的调整。
D d;
d.fun_of_b(); //this 指针也需要调整,否则fun_of_b的行为就会异常
4 实现
从上面一节我们可以看到,编译器要实现成员函数指针,有几个问题是绕不过去的:
1) 函数是不是虚拟函数,这个涉及到虚函数表(__vtbl)的访问。
2) 函数运行时,需不需要调整this指针,如何调整。这个涉及到C++对象的内存布局。
事实上,成员函数指针必须记住这两个信息。为什么要记住是否为虚函数就不用解释了。但是this指针调整为什么要记住呢?因为在.*和->*运算符求值时必须用到。 考虑上面那个多继承的例子:
int (D::*pmf)(int) = &B2::foo_of_b2; //A
4.1 Microsoft的实现
4.1.1 内部表示
Microsoft VC的实现采用的是Microsoft一贯使用的Thunk技术(不知道这个名字怎么来的,不过有趣的是把它反过来拼写就变成了大牛Knuth的名字,呵呵)。
对于Mircosoft来说,成员函数指针实际上分两种,一种需要调节this指针,一种不需要调节this指针。
先分清楚那些情况下成员函数指针需要调整this指针,那些情况下不需要。回忆上一节讨论的c++对象内存布局的说明,我们可以得出结论如下:
如果一个类对象obj含有一些子对象subobj,这些子对象的首地址&subobj和对象自己的首地址&obj不等的话,就有可能需要调整this指针。因为我们有可能把subobj的函数当成obj自己的函数来使用。
根据这个原则,可以知道下列情况不需要调整this指针:
1)继承树最顶层的类。
2)单继承,若所有类都不含有虚拟函数,那么该继承树上所有类都不需要调整this指针。
3)单继承,若最顶层的类含有虚函数,那么该继承树上所有类都不需要调整this指针。
下列情况可能进行this指针调整:
1)多继承
2)单继承,最顶的base class不含virtual function,但继承类含虚函数。那么这些继承类可能需要进行this指针调整。
Microsoft把这两种情况分得很清楚。所以成员函数的内部表示大致分下面两种:
struct pmf_type1{ void* vcall_addr; }; |
struct pmf_type2{ void* vcall_addr; int delta; //调整this指针用 }; |
这两种表示导致成员函数指针的大小可能不一样,pmf_type1大小为4,pmf_type2大小为8。有兴趣的话可以写一段代码测试一下。
4.1.2 Vcall_addr实现
上面两个结构中出现了vcall_addr, 它就是Microsoft 的Thunk技术核心所在。简单的说,vcall_addr是一个指针,这个指针隐藏了它所指的函数是虚拟函数还是普通函数的区别。事实上,若它所指的成员函数是一个普通成员函数,那么这个地址也就是这个成员函数的函数地址。若是虚拟成员函数,那么这个指针指向一小段代码,这段代码会根据this指针和虚函数索引值寻找出真正的函数地址,然后跳转(注意是跳转jmp,而不是函数调用call)到真实的函数地址处执行。
看一个例子。
//源代码 class C { public: int nv_fun1(int) {return 0;} virtual int v_fun(int) {return 0;} virtual int v_fun_2(int) {return 0;} }; void foo(C *c) { int (C::*pmf)(int); pmf = &C::nv_fun1; (c->*pmf)(0x12345678); pmf = &C::v_fun; (c->*pmf)(0x87654321); pmf = &C::v_fun_2; (c->*pmf)(0x87654321); } |
; foo的汇编代码,release版本,部分地方进行了优化 :00401000 56 push esi :00401001 8B742408 mov esi, dword ptr [esp+08] ; pmf = &C::nv_fun1; ; (c->*pmf)(0x12345678); :00401005 6878563412 push 12345678 :0040100A 8BCE mov ecx, esi ;this :0040100C E81F000000 call 00401030 ; pmf = &C::v_fun; ; (c->*pmf)(0x87654321); :00401011 6821436587 push 87654321 :00401016 8BCE mov ecx, esi ;this :00401018 E803070000 call 00401720 ; pmf = &C::v_fun_2; ; (c->*pmf)(0x87654321); :0040101D 6821436587 push 87654321 :00401022 8BCE mov ecx, esi ;this :00401024 E807070000 call 00401730 :00401029 5E pop esi :0040102A C3 ret |
:00401030 33C0 ;函数实现 xor eax, eax :00401032 C20400 ret 0004 |
:00401720 8B01 ; vcall mov eax, dword ptr [ecx] :00401722 FF20 jmp dword ptr [eax] |
:00401730 8B01 ; vcall mov eax, dword ptr [ecx] :00401732 FF6004 jmp [eax+04] |
从上面的汇编代码可以看出vcall_addr的用法。00401030, 00401720, 00401730都是vcall_addr的值,其实也就是pmf的值。在调用的地方,我们不能分别出是不是虚函数,所看到的都是一个函数地址。但是在vcall_addr被当成函数地址调用后,进入vcall_addr,就有区别了。00401720,00401730是两个虚函数的vcall,他们都是先根据this指针,计算出函数地址,然后jmp到真正的函数地址。00401030是C::nv_fun1的真实地址。
Microsoft的这种实现需要对一个类的每个用到了的虚函数,都分别产生这样的一段代码。这就像一个template函数:
template <intindex>
voidvcall(void* this)
{
jmp this->vptr[index];//pseudo asm code
}
每种不同的index都要产生一个实例。
Microsoft就是采用这样的方式实现了虚成员函数指针的调用。
4.1.3 This指针调整
不过还有一个this调整的问题,我们还没有解决。上面的例子为了简化,我们故意避开了this指针调整。不过有了上面的基础,我们再讨论this指针调整就容易了。
首先我们需要构造一个需要进行this指针调整的情况。回忆这节开头,我们讨论了哪些情况下需要进行this指针调整。我们用一个单继承的例子来进行说明。这次我们避开virtual/non-virtual function的问题暂不考虑。
class B { public: B():m_b(0x13572468){} int b_fun(int) { std::cout<<'B'<<std::endl; return 0; } private: int m_b; }; class D : public B { public: D():m_d(0x24681357){} virtual int foo(int) { std::cout<<'D'<<std::endl; return 0; } private: int m_d; }; |
// 注意这个例子中virtual的使用 |
void test_this_adjust(D *pd, int (D::*pmf)(int)) { (pd->*pmf)(0x12345678); } |
:00401000 mov eax, dword ptr [esp+04] ; this入参 :00401004 mov ecx, dword ptr [esp+0C] ; delta入参 :00401008 push 12345678 ;参数入栈 :0040100D add ecx, eax ; this = ecx= this+delta :0040100F call [esp+0C] ; vcall_addr入参 :00401013 ret |
void test_main(D *pd) { test_this_adjust(pd, &D::foo); test_this_adjust(pd, &B::b_fun); } |
; test_this_adjust(pd, &D::foo); :00401020 xor ecx, ecx :00401022 push esi :00401023 mov esi, dword ptr [esp+08] ; pd, this指针 :00401027 mov eax, 004016A0 ; D::foo vcall地址 :0040102C push ecx ; push delat = 0, ecx=0 :0040102D push eax ; push vcall_addr :0040102E push esi ; push this :0040102F call 00401000 ; call test_this_adjust ; test_this_adjust(pd, &B::b_fun); :00401034 mov ecx, 00000004;和上面的调用不同了 :00401039 mov eax, 00401050 ; B::b_fun地址 :0040103E push ecx ; push delta = 4, exc=4 :0040103F push eax ; push vcall_addr, B::b_fun地址 :00401040 push esi ; push this :00401041 call 00401000 ; call test_this_adjust :00401046 add esp, 00000018 :00401049 pop esi :0040104A ret |
注意这里和上面一个例子的区别:
在调用test_this_adjust(pd,&D::foo)的时候,实际上传入了3个参数,调用相当于
test_this_adjust(pd, vcall_address_of_foo, delta(=0));
调用test_this_adjust(pd,&B::b_fun)的时候,也是3个参数
test_this_adjust(pd, vcall_address_of_b_fun, delta(=4));
两个调用有个明显的不同,就是delta的值。这个delta,为我们后来调整this指针提供了帮助。
再看看test_this_adjust函数的汇编代码,和上一个例子的不同,也就是多了一句代码:
:0040100D add ecx, eax ; this= ecx= this+delta
这就是对this指针作必要的调整。
4.1.4 结论
Microsoft根据情况选用下面的结构表示成员函数指针,使用Thunk技术(vcall_addr)实现虚拟函数/非虚拟函数的自适应,在必要的时候进行this指针调整(使用delta)。
struct pmf_type1{ void* vcall_addr; }; |
struct pmf_type2{ void* vcall_addr; int delta; //调整this指针用 }; |
4.2 GCC的实现
GCC对于成员函数指针的实现和Microsoft的方式有很大的不同。
4.2.1 内部表示
GCC对于成员函数指针统一使用类似下面的结构进行表示:
void* __pfn; //函数地址,或者是虚拟函数的index |
4.2.2 实现机制
先来看看GCC是如何区分普通成员函数和虚拟成员函数的。
不管是普通成员函数,还是虚拟成员函数,信息都记录在__pfn里面。这里有个小小的技巧。我们知道一般来说因为对齐的关系,函数地址都至少是4字节对齐的。这就意味这一个函数的地址,最低位两个bit总是0。(就算没有这个对齐限制,编译器也可以这样实现。) GCC充分利用了这两个bit。如果是普通的函数,__pfn记录该函数的真实地址,最低位两个bit就是全0,如果是虚拟成员函数,最后两个bit不是0,剩下的30bit就是虚拟成员函数在函数表中的索引值。
使用的时候,GCC先取出最低位两个bit看看是不是0,若是0就拿这个地址直接进行函数调用。若不是0,就取出前面30位包含的虚拟函数索引,通过计算得到真正的函数地址,再进行函数调用。
GCC和Microsoft对这个问题最大的不同就是GCC总是动态计算出函数地址,而且每次调用都要判断是否为虚拟函数,开销自然要比Microsoft的实现要大一些。这也差不多可以算成一种时间换空间的做法。
在this指针调整方面,GCC和Mircrosoft的做法是一样的。不过GCC在任何情况下都会带上__delta这个变量,如果不需要调整,__delta=0。
这样GCC的实现比起Microsoft来说要稍简单一些。在所有场合其实现方式都是一样的。而且这样的实现也带来多一些灵活性。这一点下面“陷阱”一节再进行说明。
GCC在不同的平台其实现细节可能略有不同,我们来看一个基于Intel平台的典型实现:
//source code int test_fun(Base *pb, int (Base::*pmf)(int)) { return (pb->*pmf)(4); } //assembly 8048478: push %ebp 8048479: mov %esp,%ebp 804847b: sub $0x18,%esp 804847e: mov 0xc(%ebp),%eax ;__pfn,入参 8048481: mov 0x10(%ebp),%edx ;__delta,入参 8048484: mov %eax,0xfffffff8(%ebp) ; __pfn 8048487: mov %edx,0xfffffffc(%ebp) ; __delta 804848a: sub $0x8,%esp ; 804848d: mov 0xfffffff8(%ebp),%eax ; __pfn 8048490: and $0x1,%eax ; __test last 2 bits, 判断是否为虚拟函数 8048493: test %al,%al 8048495: je 80484b6 <_Z8test_funP4BaseMS_FiiE+0x3e> ;不是虚函数就跳到 non-virtual fun处 ; virtual fun,是虚拟函数,计算函数地址 8048497: mov 0xfffffffc(%ebp),%eax ;__delta 804849a: mov 0x8(%ebp),%ecx ;get pb,入参 804849d: add %eax,%ecx ;ecx = this=pb+__delta 804849f: mov 0xfffffff8(%ebp),%eax ;eax=__pfn 80484a2: shr $0x2,%eax ;eax=__pfn>>2 (fun index) 80484a5: lea 0x0(,%eax,4),%edx ;edx=eax * 4 80484ac: mov (%ecx),%eax ;eax=vtble 80484ae: mov (%eax,%edx,1),%edx ;edx为函数地址 80484b1: mov %edx,0xfffffff4(%ebp) ;存起来 80484b4: jmp 80484bc <_Z8test_funP4BaseMS_FiiE+0x44> ; non-virtual fun,不是虚拟函数,直接取出函数地址 80484b6: mov 0xfffffff8(%ebp),%eax ;__pfn, fun addr 80484b9: mov %eax,0xfffffff4(%ebp) ;__pfn, fun addr ; common invoking ; 0xfffffff4(%ebp) contains fun address 80484bc: push $0x4 ;push parameters 80484be: mov 0xfffffffc(%ebp),%eax ; delta 80484c1: add 0x8(%ebp),%eax ; this = pb+delta, this指针调整 80484c4: push %eax ; this 80484c5: call *0xfffffff4(%ebp) ;invoke 80484c8: add $0x10,%esp 80484cb: leave 80484cc: ret 80484cd: nop |
5 语言限制与陷阱
如果我们一定要反其道而行,则存在this指针调整的陷阱,需要注意。这一节我们通过两个例子,说明为什么这样操作是危险的。
5.1 例子
class B { public: B():m_b(0x13572468){} /* virtual */ int b_fun(int) { //A std::cout<<'B'<<std::endl; return 0; } private: int m_b; }; class D : public B { public: D():m_d(0x24681357){} virtual int foo(int) { // B std::cout<<'D'<<std::endl; return 0; } private: int m_d; }; void test_consistent(B* pb, int (B::*pmf)(int)) { (pb->*pmf)(0x12345678); } void test_main(D *pd) { typedef int (B::*B_PMF)(int); //test_consistent(pd, &D::foo); error! test_consistent(pd, static_cast<B_PMF>(&D::foo)); // crash in MSVC } int main() { D d; test_main(&d); return 0; } |
这句话在Microsoft Visual C++6.0下面一运行就crash。 表面上看我们传的指针是D的指针,函数也是D的函数。但实际上不是那么简单。函数调用的时候,pd赋值给pb,编译器会进行this指针调整,pb指向pd内部B的子对象。这样到了test_consistent函数内部的时候,就是用D::B对象调用D::foo函数,this指针不对,所以就crash了。 |
我们可以很简单的解决这个问题,在上面的代码中A处,把注释掉的virtual打开,也可以把B处的virtual注释掉,使得所有地方都无需进行this调整,问题也就不再出现了。
这个例子可能有些牵强,我们把上面的代码稍做修改,再举一个涉及到多继承的例子。
class B { public: B():m_b(0x13572468){} virtual int b_fun(int) { std::cout<<"B "<<std::hex<<m_b<<std::endl; return 0; } private: int m_b; }; class B2 { public: B2():m_b2(0x24681357){} int b2_fun(int) { std::cout<<"B2 "<<std::hex<<m_b2<<std::endl; return 0; } private: int m_b2; }; class D :public B , public B2 { public: D():m_d(0x24681357){} int foo(int) { std::cout<<"D "<<std::hex<<m_d<<std::endl; return 0; } private: int m_d; }; void test_consistent(B* pb, int (B::*pmf)(int)) { (pb->*pmf)(0x12345678); } void test_main(D *pd) { typedef int (B::*B_PMF)(int); //test_consistent(pd, &B2::b2_fun); //A //test_consistent(pd, static_cast<B_PMF>(&B2::b2_fun)); // B typedef int (D::*D_PMF)(int); // C D_PMF pmf = &B2::b2_fun; // D test_consistent(pd, static_cast<B_PMF>(pmf)); // E 结果错误! } int main() { D d; test_main(&d); return 0; } |
|
先用Microsoft Visual C++进行测试。这段代码执行结果是错误的。(没有crash,比crash更糟)。先看注释A处,语法错误,VC给出了正确的编译错误。
B处,进行static_cast, VC也能给出正确的编译错误,说int (B2::*)(int)类型不能转换成int (B::*)(int)类型。这也很好。
这个例子,GCC表现也相当好。这都归根于GCC采用一致的成员函数指针的表示和实现!
在Microsoft新发布的Visual C++2005中, 上面的问题仍然存在。(再重复一下, 这不怪Microsoft, C++标准本来就不允许这样用。)
5.2 static_cast干了些什么
GCC里面,不同类型的成员函数指针使用static_cast进行转换,就是计算出合适的__delta值。
5.3 默认的转换
C++规定编译器必须提供一个从基类成员函数指针到继承类成员函数指针的默认转换。这个转换,最关键的地方,其实也是this指针调整。
5.4 教训
2)一般情况下不要将继承类的成员函数指针赋值给基类成员函数指针。不同编译器可能有不同的表现。这可能导致潜在的可移植性问题。
5.5 如何避开陷阱
1)不要使用static_cast将继承类的成员函数指针赋值给基类成员函数指针,如果一定要使用,首先确定没有问题。(这条可能会限制代码的可扩展性。)
2)如果一定要使用static_cast, 注意不要使用多继承。
3)如果一定要使用多继承的话,不要把一个基类的成员函数指针赋值给另一个基类的函数指针。
4)单继承要么全部不使用虚函数,要么全部使用虚函数。不要使用非虚基类,却让子类包含虚函数。
最后,用Herb Sutter的话结个尾(如果我没记错的话):do what you know,and know what you do!
参考书目
1) Modern C++ design, Andrei Alexandrescu
2) Inside the C++ Object model, Stanley B. lippman
3) C++ Common Knowledge: Essential Intermediate Programming, StephenC.Dewhurst
4) The C++ Programming Language (special edition), Bjarne Stroustrup,
C++ ---------------- 成员函数指针揭秘相关推荐
- c++成员函数指针揭秘
原帖地址 http://blog.csdn.net/xlie/article/details/3031966#_Toc133650388 目 录 1 前言 2 语法 3 与其它语言机制的混合使用 3 ...
- C++成员变量指针和成员函数指针【The semantics of funcitons】
原文:https://blog.csdn.net/laojiu_/article/details/68946915 (原文有笔误) 1. #include <cstdio> #includ ...
- 类的成员函数指针和mem_fun适配器的用法
先来看一个最简单的函数: void foo(int a) {cout << a << endl; } 它的函数指针类型为 void (*)(int); 我们可以这样使用: vo ...
- 类成员函数指针的语法
/*类成员函数指针的语法*/ /*****************************类.h文件************************************/ #if !defined ...
- 成员函数指针与高性能的C++委托(三)
委托(delegate) 和成员函数指针不同,你不难发现委托的用处.最重要的,使用委托可以很容易地实现一个Subject/Observer设计模式的改进版[GoF, p. 293].Observer( ...
- C++ Primer 5th笔记(chap 19 特殊工具与技术)成员函数指针
1. 使用 classname::*的形式声明一个指向成员函数的指针. eg. / / pmf 是一个指针, 它可以指向 Screen 的某个常量成员函数 / / 前提是该函数不接受任何实参, 并且返 ...
- 成员函数指针与高性能的C++委托
成员函数指针与高性能的C++委托 http://www.cnblogs.com/jans2002/archive/2006/10/13/528160.html Member Function Poin ...
- C++学习之普通函数指针与成员函数指针
http://blog.csdn.net/lisonglisonglisong/article/details/38353863 函数指针(function pointer)是通过指向函数的指针间接调 ...
- C++成员函数指针的应用
转载:http://www.cppblog.com/colys/archive/2009/08/18/25785.html C++中,成员指针是最为复杂的语法结构.但在事件驱动和多线程应用中被广泛用于 ...
最新文章
- a href=# 与javascript:void(0)的区别
- vue计算多列和_解决vue 表格table列求和的问题
- ERROR (ClientException): Unexpected API Error
- html 像素跟百分比,html – 将百分比宽度与边距(以像素为单位)组合起来
- SpringBoot整合MyBatis并实现简单的查询功能
- hbuilder能用python_Hbuilder之开发Python
- activiti 作业执行器定时开始事件
- 不懂投资的基金会计不是好的产品经理
- python达内培训视频
- SoapUI接口测试-基本操作
- 虚拟机未安装音频设备_如何使用虚拟音频设备录制PC的音频
- 【初学数据结构系列】 顺序表的实现——通讯录
- 2017年电力职称计算机考试题,2017年职称计算机考试Excel试题(1)
- npm install报错ERR! code ETIMEDOUT的解决办法
- driver: linux下如何模拟按键输入和模拟鼠标
- html如何在搜索栏里加放大镜,css 如何在html页面上输出一个“放大镜”形状呢?...
- DELL新版BIOS重装系统win10
- 字节跳动 Go RPC 框架 KiteX 性能优化实践
- 微信公众号自动回复机器人
- HTML+CSS+JS实现轮播效果
热门文章
- jquery ajax和servlet,浅谈ajax在jquery中的请求和servlet中的响应
- java web 的map_javaweb开发过程中小工具系列之将Map转换成对象
- python输入10个数字排序案例_介绍十个Python小案例,新手入门就在这里
- 腾讯老鸟谈,软件测试的完整流程/过程
- 在线协作编辑OT算法简介
- 前端学习与“IT界大佬告诉你,程序员接私活的7大平台利器”
- 测试开发新手:从0到1开展性能测试必备的性能测试要点!
- python+selenium自动化测试-Windows环境搭建
- html中span标签w3c,HTML col 标签
- 矩阵特征值的一些特点