lucene可以存储对象吗_都知道C++语言有对象,可是你知道它在内存中是如何存储的吗?...

之前一节讨论了C++语言中类在内存中分布模型,提到了C++语言编译器会自动为每一个拥有虚函数的类创建虚表和虚表指针,其中虚表指针指向虚表,而虚表则用于存放虚函数指针,如下图所示:

81f94623169264e236ebc280aeef8830.png

C++虚函数的内存分布

基类的内存模型

那么,若是拥有虚函数的类 A 作为基类,派生出其他类,这些派生类如何处理类 A 的虚表呢?换句话说,C++语言中的派生类是如何继承基类的虚函数的呢?为了便于讨论,先将类 A 的C++语言代码补充完整,请看:

class A {public:    void foo1()  {        printf("A::prv_i1  addr: %p", &prv_i1);        printf("A::prv_i2  addr: %p", &prv_i2);    }    void foo2() {}    virtual void vfoo1() {}    virtual void vfoo2() {}    int pub_i1;private:    int prv_i1;    int prv_i2;};
1dc23c73bb80b14c9ac12b702eb1fc79.png

类 A 的C++语言代码

在上述C++语言代码中,类 A 拥有两个常规函数,两个 int 型变量,此外它还有两个虚函数 vfoo1() 和 vfoo2(),因此编译器处理类 A 时,会自动为它分配一个虚表,用于存放 vfoo1() 和 vfoo2() 两个函数的地址。我们先定义一个 A 对象,并将相关成员的地址打印出来,相关的C++语言代码如下,请看:

...A a;print_addr(A, a);...

因为稍后还要分析基类 A 的派生类成员地址,所以为了方便,将打印地址的功能定义为 print_addr() 宏了,它的C++语言代码如下,请看:

#define print_addr(class, obj)     printf(#obj" addr: %p", &obj);     printf("sizeof "#obj" : %d", sizeof(obj));    printf(#class"::pub_i1  addr: %p", &obj.pub_i1);     obj.foo1();     printf(#class"::foo1()  addr: %p", (void *)&class::foo1);     printf(#class"::foo2()  addr: %p", (void *)&class::foo2);     printf(#class"::vfoo1()  addr: %p", (void *)&class::vfoo1);     printf(#class"::vfoo2()  addr: %p", (void *)&class::vfoo2)
c51b3c8ee5585e046226d0f10e74fea9.png

print_addr()是一个宏

编译并执行,得到如下输出:

a addr: 0x7ffdcf3c8d50sizeof a : 24A::pub_i1        addr: 0x7ffdcf3c8d58A::prv_i1        addr: 0x7ffdcf3c8d5cA::prv_i2        addr: 0x7ffdcf3c8d60A::foo1()        addr: 0x400b62A::foo2()        addr: 0x400ba4A::vfoo1()       addr: 0x400baeA::vfoo2()       addr: 0x400bb8

在C++语言类的内存分布一节我们曾提到类的成员函数是独立存储的,只有成员变量和虚表指针(如果该类有虚函数的话)才会为类占据内存空间,因此对象 a 的 size 为 24,正是它的 3 个 int 型的成员变量与一个虚表指针占用的内存大小。

在我的机器上,int 型变量占用内存为 4 字节,指针占用内存大小为 8 字节。到这里可能有读者迷惑了,3个int型成员变量占用的内存为 12 字节,加上虚表指针的 8 字节,也才 20 字节,而 sizeof(a) 等于 24,多出的 4 字节从哪里来呢?请参考我之前关于内存对齐的文章。

内存对齐后,对象 a 的内存分布如下图所示:

e531b38a7f000b2a8d3293f2c91e39e8.png

对象 a 的内存分布

派生类的内存模型

类 A 作为基类,肯定可以被其他派生类继承的,下面是一段C++语言代码示例:

class B : public A {public:    void foo1() {        printf("B::prv_i1  addr: %p", &prv_i1);        printf("B::prv_i2  addr: %p", &prv_i2);    }    void foo2() {}    virtual void vfoo1() {}    //void vfoo2() {}private:    int prv_i1;    int prv_i2;public:    int pub_i1;};class C : public B {public:    void foo1()  {        printf("C::prv_i1  addr: %p", &prv_i1);        printf("C::prv_i2  addr: %p", &prv_i2);    }    void foo2() {}    //void vfoo1() {}    virtual void vfoo2() {}private:    int prv_i1;    int prv_i2;};
504ad9e9ca316a39d5908184748bceb6.png

基类A及其派生类

上述C++语言代码定义了继承基类 A 的派生类 B,接着又以类 B 为基类定义了派生类 C。其中类 B 重写了由基类 A 继承而来的虚函数 vfoo1(),类 C 重写了由基类 B 继承而来的虚函数 vfoo2()。为了弄清派生类的内存模型,我们使用print_addr()宏将类A、B、C相关的地址打印出来:

A a;B b;C c;cout << endl;print_addr(A, a);cout << endl;print_addr(B, b);cout << endl;print_addr(C, c);cout << endl;

编译并执行这段C++语言代码,得到的输出如下:

a addr: 0x7ffe4eeacd50sizeof a : 24A::pub_i1        addr: 0x7ffe4eeacd58A::prv_i1        addr: 0x7ffe4eeacd5cA::prv_i2        addr: 0x7ffe4eeacd60A::foo1()        addr: 0x400b9eA::foo2()        addr: 0x400be0A::vfoo1()       addr: 0x400beaA::vfoo2()       addr: 0x400bf4b addr: 0x7ffe4eeacd70sizeof b : 32B::pub_i1        addr: 0x7ffe4eeacd8cB::prv_i1        addr: 0x7ffe4eeacd84B::prv_i2        addr: 0x7ffe4eeacd88B::foo1()        addr: 0x400bfeB::foo2()        addr: 0x400c40B::vfoo1()       addr: 0x400c4aB::vfoo2()       addr: 0x400bf4c addr: 0x7ffe4eeacd90sizeof c : 40C::pub_i1        addr: 0x7ffe4eeacdacC::prv_i1        addr: 0x7ffe4eeacdb0C::prv_i2        addr: 0x7ffe4eeacdb4C::foo1()        addr: 0x400c54C::foo2()        addr: 0x400c96C::vfoo1()       addr: 0x400c4aC::vfoo2()       addr: 0x400ca0

先关注类 B 的内存分布。注意到对象 b 的 size 为 32,这是类 B 继承基类 A 的结果。而基类 A 的 size 等于 24,类 B 仅多出 8 字节,问题来了:且不谈类 B 拥有自己的虚函数,可能拥有虚表指针,仅仅 3 个 int 型变量就要占用 12 字节内存,8 字节怎么放得下呢?

仔细考虑下,这个问题并不难回答。在分析基类 A 的内存分布时,提到了类 A 占据的内存中其实是有 4 个字节被填充的,这 4 个字节内存当然不会永远被白白浪费——在不违背内存对齐规则下,编译器会在“恰当的”时机使用这 4 个字节。在本例中,“恰当的时机”就是类 B 中的成员变量 prv_i1恰好为 4 字节,既然基类 A 可以被填充无意义的数据,自然也可以填充“有用的数据(prv_i1)”。这就解释了派生类 B 多出了 3 个 int 型成员变量,占用的内存却只比基类 A 多出 8 字节的原因。

仔细观察类 B 对象 b 的输出,应该能够发现对象 b 的地址与它的第一个成员变量(prv_i1)的地址偏移了 0x14 也就是 20 字节,在上一节我们已经知道对象的前 8 字节用于存储了虚表指针,接下来的 12 字节恰好存储了由基类 A 继承而来的三个 int 型变量,因此此时对象 b 占用的的内存模型如下图所示:

45b61bf05487cf655a9b596774ceeb52.png

对象 b 的内存模型

对象 b 占用的内存空间为 32 字节一目了然了。再来分析对象 b 的成员函数,首先常规非虚函数就不必说了,它独立于对象 b 存储,不管类 B 实例化多少个对象,这些对象都共用同一地址处的成员函数,需要仔细考虑的是 b 的虚函数。

类 B 继承基类 A,并且重写了虚成员函数 vfoo1(),从对象 b 打印的地址可以看出,由基类 A 继承而来的 vfoo2() 函数地址与对象 a 中的 vfoo2() 函数地址是一样的,这说明对象 a 和 b 共用同一个虚函数 vfoo2(),同样的,对象 b 和对象 c 公用同一个虚函数 vfoo1()。

可以在 main() 函数中添加下面这段C++语言代码,分别将对象 a 和对象 b 的虚表中记录的函数指针打印出来:

...    void **p = (void **)&b;    void **vptr = (void **)*p;    printf("b vptr[0]: %p", vptr[0]);    printf("b vptr[1]: %p", vptr[1]);    p = (void **)&a;    vptr = (void **)*p;    printf("a vptr[0]: %p", vptr[0]);    printf("a vptr[1]: %p", vptr[1]);...

编译并执行修改后的C++语言代码,得到的输出如下,请看:

...A::vfoo1()       addr: 0x400c7cA::vfoo2()       addr: 0x400c86...B::vfoo1()       addr: 0x400cdcB::vfoo2()       addr: 0x400c86...b vptr[0]: 0x400cdcb vptr[1]: 0x400c86a vptr[0]: 0x400c7ca vptr[1]: 0x400c86

应注意 b vptr[1] 和 a vptr[1] 的值都是 0x400c86,该地址对应的函数为 vfoo2(),这进一步验证了前文的推测,事实上,派生类 B 及其基类 A 的实例化对象包括成员函数在内的内存模型如下图所示:

f669fc2387af48d2cadc3fff89e9d061.png

内存模型

该图清晰的描述了C++语言中的派生类是如何继承基类包括虚函数在内的成员的。弄懂了这张图,读者应该能够轻易的分析出派生类 C 的内存模型,这里就不再赘述了。

小结

本文主要在上一节的基础上,进一步分析了C++语言中带虚函数的基类与派生类的内存模型,值得注意的是C++语言中的对象和C语言的结构体有些相似,在追求效率时,也是会执行内存对齐操作的。另外,C++语言在处理虚函数的继承与派生时,的确有一些不同,例如自动分配虚表指针与虚表,共用同一个虚函数等,不过从本质上来看,虚函数又的确没有什么特殊的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值