第3章 Data语意学
在C++中经常会遇到一个类的大小问题,关于一个类的大小一般受到三个方面的影响。
- 语言本身所造成的额外负担,如在虚拟继承中会遇到如派生类中会包含一个指针指向base class subobjec,这样会造成内存的额外负担。
- 编译器对于特殊情况所提供的的优化。如果编译器不提供优化,则会在一个空的基类中会形成至少一个char类型的内存空间,传统上这一个char类型的空间会被放在该类内存的固定尾部,但是有些编译器会提供优化的策略,在空类型中不会生成这一个char成员对象的空间。
- Alignment的限制。如果一个中含有虚函数,其中如果包含一个char类型的函数成员,则会发生内存空间对其的情况,从而出现sizeof class_name = 8的情况,相当于struct中的内存对齐。
3.1 Data Member的绑定
在类中如果有一个函数成员的名称和一个全局变量的名称相同,在类内的某些函数对其进行参阅(取用)操作,相信很多人会说该操作是对类内成员的操作,对于今天的编译器来说答案是正确,但是在很早的时候,这个操作是针对的global x object的操作。当然对于这种在当时的情况下出现了两种防御措施。
- 把所有的数据成员放在类的声明起头处,以确保正确的绑定:
class X{ float x, y, z; public: float get_X(){return x} }
2. 把所有的内联函数不管大小都放在类声明之外。
class X{ float x, y, z; public: float get_X(){} } inline float get_X(){ return x; }
上面的情况在C++2.0之后就已经消失了,但是关于成员函数的形参中关于类型的操作应该加以注意如:
typedef int length; class Point3d{ void numble(length val){_val = val}//此处的length为int length numble(){return _val} //此处的length为int private: typedef length float; length _val; };
在这种情况下以上两种防御型风格仍然需要。
3.2 Data Member的布局
非静态成员的顺序和声明的顺序一样(同一访问权限中(public, protected, private)较晚出现的成员在类对象的较高的地址处),静态成员不会放在一个对象的布局之中。
3.3 Data Member的存取
1. 静态成员
class_name::static_data_member;
class_obj.static_data_member;
2. 非静态成员
Point3d Point3d::translate(const Point3d &pt){ x += pt.x; y += pt.y; z += pt.z; } 将被转化为 Point3d Point3d::translate(const Point3d &pt){ this->x += pt.x; this->y += pt.y; this->z += pt.z; }
关于取地址操作
origin._y = 0;
&origin._y的操作为&origin + (&origin._y - 1)
在类内声明的变量只会按照声明顺序来进行安排其内存地址如:
#include<stdio.h> //#include<string.h> #include<string> #include<windows.h> class A; void p_printf(); void print(char s[], int *b){ char szbuffer[1024] = {}; wsprintf(szbuffer, s, b); printf(szbuffer); } class A{ void friend print(char, A *); void friend p_print(); public: int public_A; protected: int protected_A; private: int private_A; public: int public_B; protected: int protected_B; private: int private_B; }; void p_print(){ A a; printf("the a address is %#x\n\0", &a); print("the a.public_A address is %#X\n\0", &a.public_A); print("the a.protected_A address is %#X\n\0", &a.protected_A); print("the a.private_A address is %#X\n\0", &a.private_A); print("the a.public_B address is %#X\n\0", &a.public_B); print("the a.protected_B address is %#X\n\0", &a.protected_B); print("the a.private_B address is %#X\n\0", &a.private_B); } int main(void){ p_print(); } 输出为: the a address is 0x28fef8 the a.public_A address is 0X28FEF8 the a.protected_A address is 0X28FEFC the a.private_A address is 0X28FF00 the a.public_B address is 0X28FF04 the a.protected_B address is 0X28FF08 the a.private_B address is 0X28FF0C Process returned 0 (0x0) execution time : 0.021 s Press any key to continue.
3.4 继承与数据成员
1. 只继承无多态
这是因为在进行向上转换的时候采用memberwise拷贝,如果不各自对其,当由基类向派生类拷贝时容易出现
2. 加上多态
多态作为C++的类的三大特性(封装,继承,多态),在内存中的布局为继承体系中每个类内存对其后的按照继承顺序叠加在一起,最后是该类自身的数据成员,由于含有虚函数,所以会产生一个虚函数表,至于虚函数指针(一个指向虚函数表的指针)(C++标准对虚函数指针的存放位置并没有严格规定,但是编译器一般情况是放在一个类的头部或者是尾部)。
3. 多重继承
如上所说,按照继承顺序进行叠加数据成员,每个类的自身虚函数指针仍然在自己类的相应部分。
4. 虚拟继承
在类的继承体系中,有的时候会出现某几个父类自于继承自同一个base class,如下图所示,为避免空间的浪费,采用虚拟继承
使得继承模型变为
但是在这种模型设计过程中发现会出现随着继承类的增加会增加指向虚拟类的指针内存负担,从而出现将这些指针转移至虚函数指针在-1处添加共享类的偏移地址,0处代表type_info for class, 正值为该类的virtual func的函数地址,如: