数据成员的绑定
在早期的编译器上,,如果一个类中的成员有一个和全局对象同名的数据成员,那么当在成员函数中访问其数据成员时,就有可能被绑定成了全局对象,而非数据成员。在现有的编译器中对成员函数本身的分析直到整个类的声明都出现了才开始,因此最后的数据成员的绑定会达到预期结果。也就是不会出现上述情况。
然而,对于成员函数的参数列表却并不是这样的。如下所示:
typedef int Length;
class Bar{
public:
void setVal(Length val) { _val = val; }
private:
typedef float Length;
Length _val;
};
很明显我们期望的是参数列表中的Length为float,可事实上他却绑定的是全局的Length,也就是int。
数据成员的布局
(1)非静态数据成员在类对象中的排列顺序和其被声明的顺序一样。
(2)静态数据成员存放在程序的数据段,和类对象无关。
(3)由编译器安插的数据成员(如虚表指针),可以被安排在任意位置,一般是放置在最开始的位置,也有安插在末尾位置的。
数据成员的存取
(1)静态数据成员的存取和类对象无关,所有通过类对象的存取过程都会被编译器转化为对静态数据成员的直接存取。当多个类声明了同名的静态数据成员,为了防止名称冲突,编译器的解决方法是暗中对每一个静态数据成员编码,以获得一个独一无二的程序识别代码。
(2)对非静态数据成员的存取,编译器需要把类对象的起始地址加上数据成员的偏移量。每一个非静态数据成员的偏移量在编译期间即可获知。如果通过指针来存取一个成员,并且该成员是一个从虚基类继承而来的成员时,就会有重大差异。因为指针的指向在编译期无法确定,因此其存取操作只能延迟到执行期,经由一个额外的间接导引才能解决。
“继承”与数据成员
(1)没有多态的继承
一个派生类的大小是其自身的成员加上其基类成员的总和。但由于对齐的原因,各成员之间可能会填充一些无用的字节。如:
class A{
public:
char c;
int x; //偏移量为4,1-3字节为填充字节
}; //类大小为8字节
class B : public A{
public:
/*类A的成分
char c;
int x; //偏移量4
*/
int y; //偏移量8
}; //类大小为12字节(4字节对齐),如果是8字节对齐,大小就是16字节
(2)带多态的继承
在每一个类对象中导入一个vptr,提供执行期的链接,使每一个对象能够找到相应的虚表。这就需要编译器在每一个构造函数中插入对vptr初始化的代码,以及在析构函数中能够删除该指定。
那么vptr会被放置在什么位置呢?放在类的尾端,可以保留基类的对象布局因而允许在C程序代码中也能使用。
放在类的前端,在执行期可以直接通过对象的首地址获取vptr,从而能够访问虚函数表,而不需要计算offset。
(3)多重继承
对一个多重派生对象,将其地址指定给“最左端(也就是第一个)基类的指针”,情况将和单一继承时相同,因为二者都指向相同的起始地址,需付出的成本只有地址的指定操作而已。至于第二个或后继的基类的地址指定操作,则需要将地址修改过:加上或减去介于中间的基类子对象大小。
(4)虚拟继承
类如果内含一个或多个虚基类子对象,将被分割为两部分:一个不变局部和一个共享局部。不变局部中的数据,不管后继如何衍化,总是拥有固定的offset(从对象的开头算起),所以这一部分数据可以直接存取,至于共享局部,所表现的就是虚基类子对象。这一部分的数据,其位置会因为每次的派生操作而有所变化,所以它们只能被间接存取。