Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2359150
  • 博文数量: 527
  • 博客积分: 10343
  • 博客等级: 上将
  • 技术积分: 5565
  • 用 户 组: 普通用户
  • 注册时间: 2005-07-26 23:05
文章分类

全部博文(527)

文章存档

2014年(4)

2012年(13)

2011年(19)

2010年(91)

2009年(136)

2008年(142)

2007年(80)

2006年(29)

2005年(13)

我的朋友

分类: LINUX

2007-03-02 15:29:00

C++中指向成员函数的"指针"


在{C++ Common knowledge}的Item 16的标题明确指出: 指向类的成员函数的指针并非指针, 作者的意思是说指向类的成员函数的指针并非象C语言中的函数指针一样本身就是一个地址. 该文对指向函数的指针给出了忠告:指向成员函数的指针必需存储一些信息, 以指明:

  • 它所指向的函数是虚拟的还是非虚拟的(并非必需, 在下面GCC的实例中并没有存储这个信息)
  • 到哪里去找适当的虚函数表指针(真正必需的)
  • 从函数的this指针加上或减去一个偏移量
  • 以及其它一些信息{打哈哈的态度, 牛人也有流俗的时候}
另外补充说: 指向成员函数的指针通常实现为一个小型的结构, 里面存放着这些信息.

这是这一条款中最详尽深入的说明了, 没有再进一步的了, 不够解渴, 具体的机制是什么, 典型的实现呢, 运行时的开销有多大? 下面以C/C++本身的代码来展示GCC中类的成员函数的典型实现

显然, 对于单个的类, 没有virtual函数的情形时是最简单的, 此时成员函数的指针完全可以实现为一个普通的C函数指针:

#include "stdio.h"     //HTML会吃掉<, 所以干脆用""
class A {
public:
void foo() { puts("I'm foo()") ; }
};
int main()
{
typedef void (*global_fp)(void *);
union {
void (A::*fp) ();
global_fp what;
} unknown = { &A::foo };
printf("sizeof(pointer to member function):%d\n", sizeof(unknown.fp) );
printf("function pointer: %#x\n", unknown.what);
A a;
(a.*unknown.fp)(); //最规矩的用法, 第一对括号是必要的, 否则fp会先与()结合
unknown.what( &a ); //通过一般的函数指针调用, 因为该成员函数并没有通过
//this指针访问任何其它成员, 所以unknown.what(0) 也可以
return 0;
}
当类之间有了继承关系, 基类的成员函数指针赋值给了派生类的函数指针时, 再加上虚函数, 实现没有那么简单了:
#include "stdio.h"

class A{ public:
virtual void bar() { puts("I'm bar()" ); } //故意让它是virtual的
virtual void foo() { puts("I'm foo()" ); }
};
class B{ public: virtual void b_foo(){} }; //多出一个virtual只是为了让C对象的布局中B这个子
//对象是第一个
class C: public B, public A { };
int main()
{
typedef void (*global_fp)(void *);
union {
void (C::*fp) ();
struct {
int fp;
int delta;
}mem_fp; //这个结构是从GCC的 -g -Wa,-ahls=test.s -fverbose-asm 输出中得到的
//此时的C::*fp是本身是一个结构了, 其中fp其实是该函数在虚函数表中以字节为
//单位的偏移量+1, 而delta则是指对象A在对象C中的偏移量, 如此才能得到对象A
//的vptr. 这个偏移量是根据下面赋值的右值&A::bar计算来的
global_fp what;
} unknown = { &A::bar }; //注意A类的成员函数指针赋值给了C类中的函数指针变量
C a;
// 模拟编译器获取基类对象的vptr的方法. &a取得C对象的真正的地址,
// 把它看成简单的整数, 加上A基类对象在它里面的偏移量, 得到的是
// 基类对象的地址, 只是碰巧, GCC的实现里基类A对象的vptr指针地址
// 就是对象本身的地址, 也就是说它是基类对象A里的第一个物件,
// 而且地址也码的整整齐齐
void * vptr = (void*) ( (int)&a + unknown.mem_fp.delta );
// 一次间接引用, 这才得到存放虚函数指针的表的首地址
void * vtbl = * (void **)vptr;
// 看到得到的值, 虚函数表对一个类只有一份, 由编译器在编译期放在数据区,
// 所以它的地址应该跟一个静态的数据对象比较接近, 验证一下:
static int g_i = 10; //赋值10可以避免它被放在BSS区
printf("vtbl = %p, data: %p, __fp = %d, delta=%d\n",
vtbl, &g_i, unknown.mem_fp.fp, unknown.mem_fp.delta);
// 得到指向虚成员函数的指针, 这个多出来的1就是上面提到的Item 16中
// 作者声称的为了让0来表示一个空的成员函数指针.
global_fp g_fp = (global_fp) * (void**) ( (int)vtbl + (unknown.mem_fp.fp - 1) ;
// 看看得到的普通函数指针
printf("g_fp = %#x\n", g_fp) ;
(a.*unknown.fp) () ; // 常规的调用
g_fp( 0 ); // 费半天劲才搞定的调用, 由此可以规规矩矩照语言规范
// 的套路写代码才是正道

// 验证一下对象C中两个基类对象的布局.
A * xa = &a;
B * xb = &a;
printf("a = %#x, b = %#x, c= %#x\n", xa, xb, &a );
return 0;
}

但 是, 要先祷告一番不要在任何实用的代码里面写这种依赖于具体实现的code, 这是GCC 3.2 20020903 的实现, 不一定是其它编译器的做法, 这么做只是让自己对整个的解析过程有一个了解, 可以认为, 通过成员函数指针的调用有一个小小的代价, 也可以相当然其它编译器不会偏离这个性能指标太远, 否则太丢人了.

上面的代码看起来是顺利, 其实那些巧妙的union构造是通过查看汇编输出加上一些猜测尝试出来的, 这里只是结果, 琐碎的过程就不足道了

阅读(1066) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~