C++-----深度探索对象模型-第三章-Data语意学(二)

    xiaoxiao2022-06-25  214

    1、Nonstatic data member在类对象中的排列顺序和声明顺序一样,任何中间介入的static data member都不会被放进对象布局中。

    2、同一个access section(也就是private、public、protected区段中),member的排列只需要符合较晚出现的member在类对象中有更高的地址。(从低到高排列)各个member不一定连续排列。有可能会有东西介于被声明的member之间,member的边界调整可能就需要填补一些字节。

    3、当一个类有虚拟机制时,对象中会有vptr,不同的编译器可能会把vptr放在对象的最前面或者最后面,常见的是最前面。

    4、目前各家的编译器会把一个以上的access section(也就是一个类里有多个public、private、protected)连锁在一起,依照声明的顺序成一个连续的区块,access section的多少并不会带来额外的负担,和在一起声明是一样的。

    5、对于静态数据成员,由于其在程序中只有一份实例,并且和对象无关,使用对象或对象指针或者类来存取并没有太大的差异,如果取一个静态成员的地址,得到的是一个指向其数据类型的指针,而不是一个指向其类成员的指针,因为静态成员并不在一个类对象里。

    6、如果两个类有相同名称的静态成员,当其存于data segment(数据段)时,会出现命名冲突,编译器解决的方法是暗中对每一个static data member编码(name_mangling),这样就可以获得独一无二的程序识别代码。

    7、Nonstatic data member直接存在每一个class object之中,必须经由类对象来存取,事实上成员函数中的数据的直接存取都是经过隐式的类对象(this指针)完成的。

    8、想对一个非静态成员进行存取,编译器会把类对象的起始地址加上一定的data member 偏移位置。

    class A{ public: float x; }; A a; a.x=0.0; // &a.x=&a+(&A::x-1);

        注意这个-1操作,指向数据成员的指针,其偏移量总是被加上1,这样可以使编译系统区分出一个指向数据成员的指针,用以指出类的第一个成员和一个指向数据成员的指针,没有指出任何成员的两种情况。

    9、通过对象指针还是对象来存取数据成员之间的差异:

        当类是一个派生类时,其继承链中标有一个虚基类存在,并且存取的成员是一个从该虚基类中继承来的成员时,就会有重大差异,此时如果通过指针来存取的话,由于多态机制的存在,指针所绑定的对象类型要到执行期才能确定,所以存取的操作要到执行期,但是如果用对象来存取就不会有这样的问题,成员的偏移量在编译期就能确定。

    10、关于对象如何通过偏移量来访问成员,参考下面程序

    #include<vector> #include<iostream> using namespace std; class Base { public: Base() { }; public: int a; int b; }; typedef void(*Pfun)(); int main() { Base base_test; //获得成员对于类的偏移量 int Base::*ptr = &Base::a; int Base::*ptr2 = &Base::b; //获得该对象成员的地址 int *a = &(base_test.a); int *b = &(base_test.b); //输出地址 cout << a << endl; cout << b << endl; //输出偏移量 printf("%p\n", ptr); printf("%p\n", ptr2); /*应该是ostream对象没有重载类成员指针的参数,故不能直接输出类成员指针的类型,而我们知道指针类 **型与bool类型的转换属于标准转换的(常常用来测试指针合法性是否为空),而ostream对象可以输出 **bool类型,故编译器将成员指针类型转换成了bool类型,从而输出,既然这样为什么全是输出1呢?说明 **地址全是合法的,即偏移量全是大于0,不对呀,第一个类成员的偏移量不是0么,因为指向数据成员的指 **针偏移量总是会加上1*/ cout << ptr << endl; cout << ptr2 << endl; return 0; }

        所以在这里经常会遇到一些笔试题,比如下面这道题

    #include <stdio.h> class A { public: A() {m_a = 1; m_b = 2;} ~A() {} void fun() {printf("%d %d", m_a, m_b);} private: int m_a; int m_b; }; class B { public: B() {m_c = 3;} ~B() {} void fun() {printf("%d", m_c);} private: int m_c; }; void main() { A a; B *pb = (B*)(&a); pb->fun(); }

        最后输出的是什么。内存中实例化了一个A类对象,然后将该地址强制转换成一个B类地址,即将该对象的地址内容强制看成一个B类对象。pb为B类的指针,理所当然调用的是B类中的fun()函数(可以跟多态的情形相比较),当调用fun()函数时,调用对象与该函数进行绑定,即fun()函数中隐含的形参this指针初始化为调用对象(A类对象)的地址,假设为0xff80。然后fun()函数打印值m_c。这里要注意,对象在访问类成员时,编译器并没有存储该对象各个成员的实际地址,而是存储了其相对于当前对象首地址的偏移量,由于B类只有一个成员m_c,在编译阶段,编译器就记录了m_c对于B类对象的偏移量为0,故访问m_c时,便是访问当前对象地址this+偏移量0,注意,this在这里绑定的是A类对象的首地址,在A类中,偏移量为0的成员是m_a,故打印出m_a的值。


    最新回复(0)