C++类的内存存储
1. 成员函数的内存存储
class ClassA
{
public:
void Test() {}
public:
int64_t a;
};
如上定义一个类,定义一个成员函数void Test()
和一个成员变量int64_t a
。那么sizeof(ClassA)会是多少呢?
实测结果是: 8
8也就是成员变量a占用内存的大小。那么成员函数void Test()
在哪存储呢?还有default的构造函数,析构函数,拷贝构造,赋值运算符重载都在哪存储呢?
事实上,C++编译器为了节省空间,不会为每个对象存储对应的成员函数,而是把成员函数存储在一段内存中(代码段),这些成员函数是属于这个类的,也即这个类的各个对象共有的。访问时将对象的this指针传参给成员函数来区分是哪个对象访问的。
2. 虚成员函数的内存存储
将上述ClassA稍作改动:
class ClassB
{
public:
virtual void Test() {}
virtual void Test1() {}
virtual void Test2() {}
public:
int64_t a;
};
测试结果是sizeof(ClassB)为16。使用工具调试可知多出的8字节是虚表指针的大小
因此每个对象所占用的存储空间只是该对象的数据部分(虚函数表指针vfptr和虚基类表vbptr指针也属于数据部分)所占用的存储空间,而不包括函数代码所占用的存储空间。
导出类修改的潜在风险
假如一个dll被某个应用程序依赖,由于需求变动,需要修改该dll的导出接口。我们期望的情况是修改dll,只用重新编译dll然后替换,而应用程序不再需要重新编译。这就需要保证dll的接口带动不影响应用程序的调用。
首先改dll接口定义一定是会导致应用程序链接出错的。那是否我们只要不改动原接口就能保证不出错呢?
以上面的ClassB为例:
- 添加新的虚函数或者删除不用的虚函数或者调整虚函数的位置
比如在虚函数Test和Test1之间添加一个虚函数Test3。执行会出错。因为虚函数指针是保存在虚表中,访问时通过虚表的地址偏移找到对应的函数指针。上述操作会导致寻址错误。但是可以在所有虚函数的最后添加虚函数定义,这样不会影响原有虚函数的地址偏移。 - 添加成员变量
在应用程序中直接访问了public成员变量是根据该变量的地址偏移,如果添加成员变量,则该变量在类中的地址偏移也会发生变化。导致寻址错误。如果是通过类似get/set接口访问,dll重新编译,总能找到正确的变量地址。但是添加成员变量首先会改变类的大小,而应用程序对此一无所知。虽然通过函数接口可以正确寻址,但是却存在访问越界的风险。有时之后的一段地址无人使用测试看似没问题,实则是潜藏着风险。所以要么接口全是静态成员函数;要么接口类中只有一个实现类的指针,所有的实现都在实现类中,这样接口类的size就是确定的,而不确定的实现类的size在dll中去new。 - 添加普通成员函数。
通过上面成员函数内存存储的分析,可知普通成员函数是对象共有,访问并不通过地址偏移寻址。所以普通成员函数的添加和顺序改变,对应用程序的使用无影响。
根据以上可以总结出导出类的设计注意点:
- 尽量不要导出虚函数,而且也无必要。如果是要应用程序继承,其实又增加了耦合。
- 不要导出public成员变量,不要添加成员变量。
当我总结了以上规律以后我才意识到,之前工作中多模块协作开发会有一个模块明明没有修改已有接口,别的依赖它的模块就会起不来的问题,很影响开发效率,大概就是没有遵守上面的设计规则。