Data语意学
一 成员变量的绑定
如果在类内部和外部出现了同名成员变量,而类的内部在成员变量声明前有内联成员函数的话,会有以下误解:
extern float x;
class A{
public:
int func(){ return X; }
private:
int x;
};
这里的x不会出现绑定错误的原因,因为C++内部将内联函数放到整个类的后面进行处理,所以当内联函数被处理的时候,类内部的成员变量已经覆盖了外部extern进来的x。
但是如果是typedef的话,还是会出现问题:
typedef float length;
class A{
public:
length func(){ return X; }
private:
typedef int length;
int x;
};
这里,函数的返回值是float,而不是我们想要的int,所以在类内部进行typedef的话,最好是放到类的最前面。
二 成员变量的内部布局
同一个访问部分(也就是public,protected,private区段),成员的排列按照声明顺序在类的内部由低地址到高地址排列(不是写在一起的同一个访问部分也是按照声明顺序排列)。不同访问部分的成员变量顺序没有准确定义。
三 成员变量的存取
1 静态成员
类内静态变量只有一份,放在程序数据段,存取类内静态变量通过类名而不是对象名。
对类内静态成员取地址,得到的是指向其数据类型的指针,而不是指向类成员的指针。
2 非静态成员
非静态成员在每一个类对象中,通过显式/隐式类对象进行存取。并且在类对象中对成员变量的存取是通过隐式的this指针完成的(每一个成员函数都包含一个隐藏的this指针)。
类对象中每一个非静态成员变量的偏移位置在编译期就可以被知道,即使是继承而来的成员变量也是,所以在类内部进行数据存取和存取C结构体内部数据是一样的。但是虚基类的成员存取操作必须延迟到执行期。
四 “继承”和数据成员
1 只有继承没有多态(虚函数)
基类的子对象(子类中基类的数据成员部分)内存上处于派生类数据成员之前,内存的排列和非派生(将基类的数据移入派生类,取消继承)是一致的,唯一不同的是对齐的差别。
class T{ int a; char b; char c; };
这个类会产生对齐操作,sizeof(T)
返回值是8。(补2个char)
class A{ int a; };
class B : public A{ char b; };
class C : public B{ char c; };
在继承中也会产生内存对齐,但是内存对齐将会发生多次,在A,B,C类中都会发生,sizeof(A)
为4,sizeof(B)
为8,sizeof(C)
为12。
2 有多态
有多态情况比较复杂,C++标准没有规定虚函数表的指针必须放在类的头部还是尾部,所以不同编译器有不同的实现。但是毫无疑问的是,在多态的情况下,由于虚函数表指针的引入,指针的偏移将会需要特殊的计算,对于数据成员的存取将会造成额外的负担。
虚表指针放入类头部的话,很明显所有的数据成员都需要进行额外的偏移。虚表指针放入尾部的话,基类的虚表指针会导致派生类数据的偏移,仍然会产生数据的额外偏移。
多态的额外负担如下:
- 类多出一个虚函数表,用以存放虚函数地址,再加上一两个slots(支持RTTI)。
- 类中多出一个虚表指针,提供执行器链接,使每一个对象找到相应的虚表。
- 加强ctor的功能,使其可以为虚表指针设定初值。指向类对应的虚表。在派生类和基类的ctor中,虚表指针的值可能都是要重新赋值的。
- 加强dtor的功能,使其可以正确删除虚表指针。
3 多重继承
多重继承下,第一个继承的类的子对象(派生类中基类的数据部分)在类的最前方(这里以虚表指针在类尾部为例),指针无需额外偏移,但是第二个继承的类,将需要额外偏移sizeof(A)
的大小:
class A{
int a;
char aa;
virtual int funcA(){ return a; }
};
class B{
int b;
char bb;
virtual int funcB(){ return b; }
};
class C{
int c;
char cc;
int funcA(){ return c; };
int funcB(){ return cc; };
};
具体的内部实现是看编译器的,这只是理论上的一种排列方法。
4 虚拟继承
虚拟继承要解决两个问题:
- 对象腰围其虚基类产生一个额外的指针,但是我们希望类对象的大小是固定的,不希望类对象会因为虚基类的增多而变化大小。
- 虚继承串链的增长会导致间接存取层次的增加,我们不希望这种情况发生。
解决1的方法可以是产生虚基类表格,类对象只维护虚基类表格的指针;解决2的方法是将虚基类的偏移整合到虚函数表中去,正常调用虚函数采用正向偏移虚函数表指针的方式,而寻找虚基类则采用负偏移虚函数表指针的方式。
五 对象成员的效率
数据的存取效率不会因为继承而降低(优化后(-O)),但是会因为虚拟继承而降低,并且会因为虚拟继承层数的增加而降低的更明显。
六 指向成员数据的指针
对一个类的非静态成员取地址,得到的是这个成员在类中的偏移,但是通过对象取地址会得到成员的真正地址,对一个类的静态成员取地址,得到的也是真正的地址,因为静态成员不存储在类中,存储在成组的数据段中。
例子如下:
#include <iostream>
#include <cstdio>
using namespace std;
class A
{
public:
static int as;
int a;
char aa;
float aaa;
void func(){ cout << "A" << endl; }
};
int A::as = 1;
int main()
{
A a;
printf("%p\n", &A::a); //输出结果:00000000(十六进制)
printf("%p\n", &A::as); //输出结果:00C01024(十六进制)
printf("%p\n", &a.a); //输出结果:0030FB88(十六进制)
return 0;
}
很明显第一个输出了偏移,后两个输出了地址。