C++导出类修改的潜在风险

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为例:

  1. 添加新的虚函数或者删除不用的虚函数或者调整虚函数的位置
    比如在虚函数Test和Test1之间添加一个虚函数Test3。执行会出错。因为虚函数指针是保存在虚表中,访问时通过虚表的地址偏移找到对应的函数指针。上述操作会导致寻址错误。但是可以在所有虚函数的最后添加虚函数定义,这样不会影响原有虚函数的地址偏移。
  2. 添加成员变量
    在应用程序中直接访问了public成员变量是根据该变量的地址偏移,如果添加成员变量,则该变量在类中的地址偏移也会发生变化。导致寻址错误。如果是通过类似get/set接口访问,dll重新编译,总能找到正确的变量地址。但是添加成员变量首先会改变类的大小,而应用程序对此一无所知。虽然通过函数接口可以正确寻址,但是却存在访问越界的风险。有时之后的一段地址无人使用测试看似没问题,实则是潜藏着风险。所以要么接口全是静态成员函数;要么接口类中只有一个实现类的指针,所有的实现都在实现类中,这样接口类的size就是确定的,而不确定的实现类的size在dll中去new。
  3. 添加普通成员函数。
    通过上面成员函数内存存储的分析,可知普通成员函数是对象共有,访问并不通过地址偏移寻址。所以普通成员函数的添加和顺序改变,对应用程序的使用无影响。

根据以上可以总结出导出类的设计注意点:

  1. 尽量不要导出虚函数,而且也无必要。如果是要应用程序继承,其实又增加了耦合。
  2. 不要导出public成员变量,不要添加成员变量。

当我总结了以上规律以后我才意识到,之前工作中多模块协作开发会有一个模块明明没有修改已有接口,别的依赖它的模块就会起不来的问题,很影响开发效率,大概就是没有遵守上面的设计规则。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值