• 从C++取地址操作看对象内存布局


    从C++取地址操作看对象内存布局

    对于一个C++对象,取地址存入一个指针,不同类型的指针拿到的值是一样的吗?

    答案是不一定!

    我们直接考察带虚函数的单继承和多继承两种场景。

    测试样例

    示例代码如下:

    #include <stdio.h>
    #include <stdint.h>
    
    class A {
    public:
        virtual void funA() {}
        int64_t a;
    };
    class B {
    public:
        virtual void funB() {}
        int64_t b;
    };
    class C : public A {
    public:
        virtual void funcC() {}
        int64_t c;
    };
    class D : public A, public B {
    public:
        virtual void funD() {}
        int64_t d;
    };
    
    int main() {
        {
            C c;
            void *p = &c;
            A *a = &c;
            printf("%p %p %p %p %p\n", p, a, &(c.a), &c, &(c.c));
        }
        {
            D d;
            void *p = &d;
            A *a = &d;
            B *b = &d;
            printf("%p %p %p %p %p %p %p\n", p, a, &(d.a), b, &(d.b), &d, &(d.d));
        }
    }
    

    本机运行结果如下:

    0x7fffe3d44ec0 0x7fffe3d44ec0 0x7fffe3d44ec8 0x7fffe3d44ec0 0x7fffe3d44ed0
    0x7fffe3d44ec0 0x7fffe3d44ec0 0x7fffe3d44ec8 0x7fffe3d44ed0 0x7fffe3d44ed8 0x7fffe3d44ec0 0x7fffe3d44ee0
    

    可以看到:

    • 对于单继承,直接取地址、用父类型指针存地址,结果都是一样的;
    • 对于多继承,直接取地址、用第一个父类型指针存地址,结果一样,用第二个父类型存地址结果不一样;

    我们结合 gcc 和 clang 查看对象内存布局,进一步分析。

    内存布局分析

    使用 gcc -std=c++17 -fdump-class-hierarchy test-class-layout.cpp 命令,文件输出如下:

    Vtable for A
    A::_ZTV1A: 3 entries
    0     (int (*)(...))0
    8     (int (*)(...))0
    16    (int (*)(...))A::funA
    
    Class A
       size=16 align=8
       base size=16 base align=8
    A (0x0x7f982b13e000) 0
        vptr=((& A::_ZTV1A) + 16)
    
    Vtable for B
    B::_ZTV1B: 3 entries
    0     (int (*)(...))0
    8     (int (*)(...))0
    16    (int (*)(...))B::funB
    
    Class B
       size=16 align=8
       base size=16 base align=8
    B (0x0x7f982b13e0c0) 0
        vptr=((& B::_ZTV1B) + 16)
    
    Vtable for C
    C::_ZTV1C: 4 entries
    0     (int (*)(...))0
    8     (int (*)(...))0
    16    (int (*)(...))A::funA
    24    (int (*)(...))C::funcC
    
    Class C
       size=24 align=8
       base size=24 base align=8
    C (0x0x7f982af7d1a0) 0
        vptr=((& C::_ZTV1C) + 16)
      A (0x0x7f982b13e180) 0
          primary-for C (0x0x7f982af7d1a0)
    
    Vtable for D
    D::_ZTV1D: 7 entries
    0     (int (*)(...))0
    8     (int (*)(...))0
    16    (int (*)(...))A::funA
    24    (int (*)(...))D::funD
    32    (int (*)(...))-16
    40    (int (*)(...))0
    48    (int (*)(...))B::funB
    
    Class D
       size=40 align=8
       base size=40 base align=8
    D (0x0x7f982af8e930) 0
        vptr=((& D::_ZTV1D) + 16)
      A (0x0x7f982b13e240) 0
          primary-for D (0x0x7f982af8e930)
      B (0x0x7f982b13e2a0) 16
          vptr=((& D::_ZTV1D) + 48)
    

    使用 clang -Xclang -fdump-record-layouts test-class-layout.cpp 命令,终端输出如下:

    *** Dumping AST Record Layout
             0 | class A
             0 |   (A vtable pointer)
             8 |   int64_t a
               | [sizeof=16, dsize=16, align=8,
               |  nvsize=16, nvalign=8]
    
    *** Dumping AST Record Layout
             0 | class C
             0 |   class A (primary base)
             0 |     (A vtable pointer)
             8 |     int64_t a
            16 |   int64_t c
               | [sizeof=24, dsize=24, align=8,
               |  nvsize=24, nvalign=8]
    
    *** Dumping AST Record Layout
             0 | class B
             0 |   (B vtable pointer)
             8 |   int64_t b
               | [sizeof=16, dsize=16, align=8,
               |  nvsize=16, nvalign=8]
    
    *** Dumping AST Record Layout
             0 | class D
             0 |   class A (primary base)
             0 |     (A vtable pointer)
             8 |     int64_t a
            16 |   class B (base)
            16 |     (B vtable pointer)
            24 |     int64_t b
            32 |   int64_t d
               | [sizeof=40, dsize=40, align=8,
               |  nvsize=40, nvalign=8]
    

    根据输出,可以得出类型D的内存布局情况:

    +--------------+    <- ptrA, ptrD
    |  vtable-A-D  |
    +--------------+
    |  members-A   |
    +--------------+    <- ptrB
    |  vtable-B    |
    +--------------+
    |  members-B   |
    +--------------+
    |  members-D   +
    +--------------+
    

    观察可以发现:

    • 父类虚函数表和成员变量排放在子类成员变量之前;
    • 从每个父类继承来的虚表和成员变量按照声明顺序依次排列,每个父类的虚表和成员变量紧密排列;
    • 成员变量按照声明顺序依次排列;
    • 子类型的虚函数追加在第一个父类的虚函数表尾部;
    • 子类型地址存入不同父类指针时,指向各自类型对应的虚函数表位置;
    • 直接获取子类型地址时,指向对象头部,也就是第一个虚函数表位置。

    结论

    可以看到,当使用父类指针操作子类对象时,指针指向父类虚函数表坐在偏移位置,此时内存分布和直接操作一个父类对象是一致的。
    这种设计,尽可能保证了多态之下成员变量操作的效率。

    扩展

    会有什么问题吗?
    有的!

    考虑菱形继承,此时祖先类型的成员变量和虚函数会在子类型中重复出现!
    这同样是出于上面所说的操作效率考虑,保证使用父类指针操作时,无论使用哪一个父类都能高效操作祖先类型的成员,因而必须在两个父类各自保存一份祖先类型的信息。

    存在解决办法吗?存在。
    如果是内存空间非常有限的情况,可以考虑使用虚继承,砍掉重复的成员;代价是操作效率会降低。
    对比一下多继承和虚继承,前者时间高效空间低效,后者空间高效时间低效。

    不过一般而言,空间都没有那么紧张,所以虚继承很少使用。事实上,就连多继承都是不提倡的,一般只使用单继承……

  • 相关阅读:
    问答
    正在设计taijilang的解析器,真可谓尸横遍地
    因为这些理由而坚持用grunt?其实它们都不成立。
    开始设计taijijs
    从grunt转到gulp
    google 索引
    :: operator
    用coffeescript写构造函数
    jade与angular.js
    angular.js 资料收集
  • 原文地址:https://www.cnblogs.com/zhcpku/p/16362695.html
Copyright © 2020-2023  润新知