• 认真地记录技术中遇到的坑!
  • 能摸鱼真是太好啦!嘿嘿嘿!

浅谈数据成员指针、函数成员指针、虚函数实现

后台 Moxun 6年前 (2018-05-28) 3644次浏览 0个评论

本文转载自:http://www.cnblogs.com/malecrab/p/5572119.html

我要手动抄一遍,加深印象。

1.数据成员指针
对于普通指针,其值就是它所指向的地址,0表示空指针。
而对于数据成员指针(这个描述实际上并不合适,它指的应该是类或者结构的数据成员的地址),它的值是相对于对象起始地址的偏移量,-1表示空指针。如图:
浅谈数据成员指针、函数成员指针、虚函数实现

代码示例:

struct X {
    int a;
    int b;
};
#define VALUE_OF_PTR(p)     (*(long*)&p)
int main() {
    int X::*p = 0;  // VALUE_OF_PTR(p) == -1
    p = &X::a;      // VALUE_OF_PTR(p) == 0
    p = &X::b;      // VALUE_OF_PTR(p) == 4
    return 0;
}

2.成员函数指针

成员函数指针与普通函数指针相比,其size是普通函数指针的两倍,分为ptr和adj两部分。
(1)非虚函数成员指针:
ptr内容为函数指针(指向一个全局函数,该函数的第一个参数为this指针),adj部分始终为0。例:
浅谈数据成员指针、函数成员指针、虚函数实现

代码示例:

extern "C" int printf(const char*, ...);

struct B {
    void foo() {  printf("B::foo(): this = 0x%p\n", this); }
};
struct D : public B {
    void bar() { printf("D::bar(): this = 0x%p\n", this); }
};
void (B::*pbfoo)() = &B::foo; // ptr: points to _ZN1B3fooEv, adj: 0
void (D::*pdfoo)() = &D::foo; // ptr: points to _ZN1B3fooEv, adj: 0
void (D::*pdbar)() = &D::bar; // ptr: points to _ZN1D3barEv, adj: 0

extern "C" void _ZN1B3fooEv(B*);
extern "C" void _ZN1D3barEv(D*);
#define PART1_OF_PTR(p)     (((long*)&p)[0])
#define PART2_OF_PTR(p)     (((long*)&p)[1])

int main() {
    printf("&B::foo->ptr: 0x%lX\n", PART1_OF_PTR(pbfoo));
    printf("&B::foo->adj: 0x%lX\n", PART2_OF_PTR(pbfoo));    // 0
    printf("&D::foo->ptr: 0x%lX\n", PART1_OF_PTR(pdfoo));
    printf("&D::foo->adj: 0x%lX\n", PART2_OF_PTR(pdfoo));    // 0
    printf("&D::bar->ptr: 0x%lX\n", PART1_OF_PTR(pdbar));
    printf("&D::bar->adj: 0x%lX\n", PART2_OF_PTR(pdbar));    // 0

    D* d = new D();
    d->foo();
    _ZN1B3fooEv(d); // equal to d->foo()
    d->bar();
    _ZN1D3barEv(d); // equal to d->bar()
    return 0;
}

(2)虚函数指针
ptr部分内容为虚函数对应的函数指针在虚函数表中的偏移地址+1(之所以+1是因为0表示了空指针),而adj部分为调节this指针的偏移字节数。
浅谈数据成员指针、函数成员指针、虚函数实现

对上图的说明:
(1)A和B都没有基类,但是都有虚函数,因此各有虚函数指针(假设为vptr)。
(2)C同时继承了A和B,因此会继承两个虚函数指针,但是为了节省空间,C会和主基类A共用一个虚函数指针,即上图的vptr1,继承自B的虚函数指针假设为vptr2。
(3)C没有重写继承自A和B的虚函数,因此在C的虚函数表中存在A::foo和B::bar函数指针(如果C中重写了foo(),则C的虚函数表中A::foo会被替换为C::foo)。
(4)C中有两个虚函数指针vptr1和vptr2,相当于有两张虚函数表。
(5)A::foo(C::foo)、B::Bar(C::bar)都在虚函数表中偏移地址为0的位置,因此ptr为1(0+1=1)(这个地方注意一下,C有两张虚函数表)。而C::quz在偏移为8的位置,因此ptr为9(8+1=9)(这里需要注意的是,成员函数指针的大小是普通函数指针大小的两倍,即为8)。
(6)当我们使用pc调用C::bar()时,如:“(pc->pcbar)()”,实际上调用的是B::bar()(即_ZN1B3barEv(pc)),pc需要被转换为B类型指针,因此需要对this指针进行调节(调节至pb指向的地址),因此adj为8(adj为何为8呢,参见上图,C有两张虚函数表,vptr1和vptr2,而pb指向的是vptr2,vptr1的大小是8,所以在进行this调节时,要给adj为8才能偏移到pb指针所指的位置)。
代码示例:

extern "C" int printf(const char*, ...);

struct A {
    virtual void foo() { printf("A::foo(): this = 0x%p\n", this); }
};
struct B {
    virtual void bar() { printf("B::bar(): this = 0x%p\n", this); }
};
struct C : public A, public B {
    virtual void quz() { printf("C::quz(): this = 0x%p\n", this); }
};

void (A::*pafoo)() = &A::foo;   // ptr: 1, adj: 0
void (B::*pbbar)() = &B::bar;   // ptr: 1, adj: 0
void (C::*pcfoo)() = &C::foo;   // ptr: 1, adj: 0
void (C::*pcquz)() = &C::quz;   // ptr: 9, adj: 0
void (C::*pcbar)() = &C::bar;   // ptr: 1, adj: 8

#define PART1_OF_PTR(p)     (((long*)&p)[0])
#define PART2_OF_PTR(p)     (((long*)&p)[1])
int main() {
    printf("&A::foo->ptr: 0x%lX, ", PART1_OF_PTR(pafoo));   // 1
    printf("&A::foo->adj: 0x%lX\n", PART2_OF_PTR(pafoo));   // 0
    printf("&B::bar->ptr: 0x%lX, ", PART1_OF_PTR(pbbar));   // 1
    printf("&B::bar->adj: 0x%lX\n", PART2_OF_PTR(pbbar));   // 0
    printf("&C::foo->ptr: 0x%lX, ", PART1_OF_PTR(pcfoo));   // 1
    printf("&C::foo->adj: 0x%lX\n", PART2_OF_PTR(pcfoo));   // 0
    printf("&C::quz->ptr: 0x%lX, ", PART1_OF_PTR(pcquz));   // 9
    printf("&C::quz->adj: 0x%lX\n", PART2_OF_PTR(pcquz));   // 0
    printf("&C::bar->ptr: 0x%lX, ", PART1_OF_PTR(pcbar));   // 1
    printf("&C::bar->adj: 0x%lX\n", PART2_OF_PTR(pcbar));   // 8
    return 0;
}

有了上述解释,我们就可以知道为什么回调函数不能是普通成员函数了,普通成员函数有一个隐式的this指针,做了成员函数的第一个参数,而回调函数的参数没有办法添加这个this指针。

我把上一篇没写的虚函数的底层实现挪到这里来说,更加具有连贯性,博客同样出自本文引用博客作者的作品,只能说,膜拜大神。

虚函数实现的基本原理:(这其实也是继承体系消耗性能的一个原因)

1.简单的说,每一个含有虚函数(无论是其本身的还是继承而来的)类,都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针。例如:
浅谈数据成员指针、函数成员指针、虚函数实现
其中:

(1)B的虚函数表中存放着B::foo和B::bar两个函数指针。
(2)D的虚函数表中存放的既有继承自B的虚函数B::foo,又有重写(override)了基类虚函数B::bar的D::bar,还有新增的虚函数D::quz。
提示:为了描述方便,本文在探讨对象内存布局时,将忽略内存对齐对布局的影响。

注:内存布局:见下一篇博文《浅谈C++内存布局》

2.虚函数表的构造过程:
从编译器的角度来说,B的虚函数表很好构造,D的虚函数表构造过程相对复杂。下面给出了构造D的虚函数表的一种方式(仅供参考):
浅谈数据成员指针、函数成员指针、虚函数实现
构造D的虚函数表的过程是由编译器自主完成的,也就是说,这个替换过程发生在编译期间。

3.虚函数的调用过程
以下面的程序为例:
浅谈数据成员指针、函数成员指针、虚函数实现

编译器只知道pb是B*类型的指针,并不知道它指向的具体对象类型 :pb可能指向的是B的对象,也可能指向的是D的对象。

但对于“pb->bar()”,编译时能够确定的是:此处operator->的另一个参数是B::bar(因为pb是B*类型的,编译器认为bar是B::bar),而B::bar和D::bar在各自虚函数表中的偏移位置是相等的。

无论pb指向哪种类型的对象,只要能够确定被调函数在虚函数中的偏移值,待运行时,能够确定具体类型,并能找到相应vptr了,就能找出真正应该调用的函数。

在上文中讲过,虚函数指针中的ptr部分是虚函数表中的偏移值加1。
B::bar是一个虚函数指针, 它的ptr部分内容为9,它在B的虚函数表中的偏移值为8(8+1=9)。

当程序执行到“pb->bar()”时,已经能够判断pb指向的具体类型了:

如果pb指向B的对象,可以获取到B对象的vptr,加上偏移值8((char)vptr + 8),可以找到B::bar。
如果pb指向D的对象,可以获取到D对象的vptr,加上偏移值8((char
)vptr + 8) ,可以找到D::bar。
如果pb指向其它类型对象…同理…

4.多重继承
当一个类继承多个类,且多个基类都有虚函数时,子类对象中将包含多个虚函数表的指针(即多个vptr)。
示例:
浅谈数据成员指针、函数成员指针、虚函数实现
其中:D自身的虚函数与B基类共用了同一个虚函数表,因此也称B为D的主基类(primary base class)。

虚函数替换过程与前面描述类似,只是多了一个虚函数表,多了一次拷贝和替换的过程。
虚函数的调用过程,与前面描述基本类似,区别在于基类指针指向的位置可能不是派生类对象的起始位置,以如下面的程序为例:
浅谈数据成员指针、函数成员指针、虚函数实现

下面的内容,同样是同一位作者的研究结果:
博文原址为:http://www.cnblogs.com/malecrab/p/5573368.html#3736391

1.虚表与虚函数表
上文中引入虚函数表的概念,但实际上虚函数表并不独立存在,它只是虚表(virtual table)的一部分,例:
浅谈数据成员指针、函数成员指针、虚函数实现

从图中可已看出,虚表除了包含虚函数指针,还包含其它一些信息(如:RTTI(运行时类型识别)信息、偏移值等)。

顺便介绍一下gcc的-fdump-class-hierarchy选项,它可以用于输出C++程序的虚表结构(在当前目录下生成一个.class文件),例:
浅谈数据成员指针、函数成员指针、虚函数实现

2.虚表结构
一个虚表包含以下几个部分:
浅谈数据成员指针、函数成员指针、虚函数实现
其中:

橙色线框中的内容仅限于虚拟继承的情形(若无虚拟继承,则无此内容),虚拟继承的讨论已超过了本文的范围,暂且忽略。
“offset to top”是指到对象起始地址的偏移值,只有多重继承的情形才有可能不为0,单继承或无继承的情形都为0。
“RTTI information”是一个对象指针,它用于唯一地标识该类型。
“virtual function pointers”也就是我们之前理解的虚函数表,其中存放着虚函数指针列表。

前一节的示例是单继承的示例,下面列出了一个多继承的示例:
浅谈数据成员指针、函数成员指针、虚函数实现
从中可以看到:D的虚表中包含两个虚表结构,第一个也称之为“主虚表”(primary virtual table),另一个虚表又称之为“次虚表”(secondary virtual table)。

简单地概括一下:一个含有虚函数(无论是其本身的,还是继承而来的)的类,可以有一个主虚表和多个次虚表,主虚表和次虚表构成一个虚表组(virtual table group)。

原文作者说他的博客内容,参看了:Itanium C++ ABI

关于虚函数的实现,另外一篇博文地址:https://blog.csdn.net/zhanghow/article/details/52970975


喜欢 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址