
本文讨论C++类中数据成员(static和non-static)的排列、静态函数、成员函数和虚函数在内存中的布局以及调用过程,认识指向成员的指针,以便加深对类对象布局的理解。
指向data member的指针 static data members 静态数据成员存放于类之外,被视为一个全局变量,但仅在class生命范围内可见。因此如果一个类Obj有一个静态类型的整型变量a,则
会获得如下内存地址:
类中的静态成员的名称一般被使用name-mangling编码为一个独一无二的名称,以便不同类中的相同名称的静态变量不会冲突。
nonstatic data members nonstatic data members在class object中的排列顺序和其被声明的顺序一样,任何介入的static data members都不会被放入到对象布局中。我们也可以对nonstatic 类型的data member取地址,如同static类型的变量一样,假设有一个Obj类,包含一个整型的数据成员b,则我们也可以进行如下操作:
不同于static类型的成员,上述操作不会得到b的内存地址,这是显而易见的,因为每个不同的对象都有一份b。其实它的类型为**int Obj::*,而不是**int *,含义是b成员在对象中的 偏移 (offset)。
如下测试程序,Foo类包含一个成员方法,用于打印其内部数据成员的对象内部偏移:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class Foo { public : int x; static int sx; private : int y; float z; static void *sp; public : double d; protected : char c; public : char a; public : void print () const { printf ("static data: %p %p\n" , &Foo::sx, &Foo::sp); printf ("%d %d %d %d %d %d\n" , &Foo::x, &Foo::y, &Foo::z, &Foo::d, &Foo::c, &Foo::a); cout << sizeof (*this ) << endl; } }; int Foo::sx = 19 ; void *Foo::sp = nullptr ; int global; int main () { Foo foo; foo.print (); cout << &global << endl; return 0 ; }
编译运行:
1 2 3 4 5 > g++ two.cc -g -o two && ./two static data: 0x563c95253010 0x563c95253158 0 4 8 16 24 25 32 0x563c95253160
第一行打印的是静态数据的地址,第二行是对象内部按照声明次序,打印其offset,可见成员的实际排列次序和声明次序完全吻合,与其修饰符(public、protected、private)以及类型无关,也不会将同种修饰符的成员专门安放在一起。对象还需要添加padding部分以满足对齐,因此大小为32,
反汇编查看.data段和.bss段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 > objdump -d -j .data two two: file format elf64-x86-64 Disassembly of section .data: 0000000000004000 <__data_start>: ... 0000000000004008 <__dso_handle>: 4008: 08 40 00 00 00 00 00 00 .@...... 0000000000004010 <_ZN3Foo2sxE>: # Foo::sx 4010: 13 00 00 00 .... > objdump -d -j .bss two two: file format elf64-x86-64 Disassembly of section .bss: 0000000000004040 <_ZSt4cout@GLIBCXX_3.4>: ... 0000000000004150 <completed.0>: ... 0000000000004158 <_ZN3Foo2spE>: # Foo::sp ... 0000000000004160 <global>: # global 4160: 00 00 00 00 .... 0000000000004164 <_ZStL8__ioinit>: 4164: 00 00 00 00
符合我们的预期。
多态——单继承 当类存在基类时,基类的数据会被作为类的一部分存在,我们也可以通过打印类成员指针的值,了解到类中数据成员是如何排列的。修改上述测试程序如下,首先我们考虑单继承:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Base { public : int ba; char bb; }; class Derived : public Base{ public : char da; int db; }; int main () { Derived derived; printf ("%d %d %d %d\n" , &Derived::ba, &Derived::bb, &Derived::da, &Derived::db); char * p1 = reinterpret_cast <char *>(&derived); char * p2 = reinterpret_cast <char *>(&derived.ba); char * p3 = reinterpret_cast <char *>(&derived.bb); char * p4 = reinterpret_cast <char *>(&derived.da); char * p5 = reinterpret_cast <char *>(&derived.db); printf ("%lld %lld %lld %lld\n" , p2 - p1, p3 - p1, p4 - p1, p5 - p1); cout << sizeof (Base) << endl; cout << sizeof (Derived) << endl; return 0 ; }
编译运行:
第一行打印成员指针获取的成员offset;
第二行打印通过指针算术实际求出的对象中成员的offset,可见和第一行确实一致;
三、四行打印基类和子类的大小。
可以发现,基类的数据被存放在对象的起始处,子类自定义的数据成员相继摆放在后面。值得注意的是,编译器并未把子类和基类的数据成员“混为一谈”,编译器并未把子类的第一个char类型的成员和基类最后一个char类型成员相邻放置以节省padding的部分。基类的部分是作为一个整体存在于子类之中。
多态——多继承 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 class Base { public : int b; }; class Base1 { public : int ba{}; char bb{}; }; class Base2 { public : int bc{}; char bd{}; }; class Base3 { public : int be; }; class Derived : public Base1, public Base2, public Base3{ public : char da{}; int db{}; }; int main () { Derived derived; printf ("%d %d\n" , &Derived::ba, &Derived::bb); printf ("%d %d %d\n" , &Derived::bc, &Derived::bd, &Derived::be); char * p1 = reinterpret_cast <char *>(&derived); char * p2 = reinterpret_cast <char *>(&derived.bc); char * p3 = &derived.bd; printf ("%lld %lld\n" , p2 - p1, p3 - p1); printf ("%lld %lld\n" , &Derived::da, &Derived::db); cout << sizeof (Base1) << endl; cout << sizeof (Base2) << endl; cout << sizeof (Derived) << endl; int Derived::* p = nullptr ; printf ("%d %p" , sizeof p, p); return 0 ; }
在多继承,尤其是多继
指向member function的指针 static function member function virtual function