C++中继承空类且含Union的派生类为何内存占用更大?
先看你给出的代码和输出:empty_class的大小是1(C标准要求每个对象必须有唯一地址,所以空类会被分配1字节的占位符),small的大小是12(union的大小由最大成员int data[3]决定,12字节),但big的大小却变成了16,比small多了4字节。这个差异的核心原因和空基类优化(EBO)的限制以及C的对象地址规则有关。
问题根源:同类型子对象的地址唯一性要求
C++标准有一个关键规则:两个独立的、同类型的非位域对象,不能拥有相同的内存地址。我们来拆解big类的布局:
big继承自empty_class,所以它包含一个基类子对象(类型为empty_class)。big的union成员里又包含一个empty_class a的实例,这是另一个独立的empty_class对象。
如果编译器尝试对big应用空基类优化(让基类子对象和union成员共享内存地址),就会导致两个同类型的独立对象(基类子对象和a)地址相同,直接违反了C++标准。因此编译器不得不放弃EBO,给基类子对象分配独立的空间。
再加上内存对齐的要求:int的对齐边界是4字节,union的大小是12字节(已经是4的倍数)。基类子对象需要至少1字节的空间,但为了满足对齐要求,编译器会在基类子对象和union之间填充3字节,最终总大小就是1+3+12=16字节,这就是你看到的结果。
而small结构体没有继承关系,它的union里的empty_class a只是union的一个成员,不需要和其他独立的empty_class对象共享地址,所以union的大小直接由最大成员int data[3]决定,总大小就是12字节。
如何避免额外内存开销
针对这个问题,有两种可行的解决方法,取决于你使用的C++版本:
方法1:使用不同类型的空类替代(兼容C++11及以上)
既然问题出在同类型子对象的地址冲突,我们可以给union里的空类创建一个类型别名(派生自原空类),让它和基类子对象的类型不同。这样编译器就可以安全地应用空基类优化,因为不同类型的子对象允许共享地址。
修改后的代码示例:
#include <iostream> class empty_class { }; // 创建一个和empty_class不同的类型,功能完全一致 struct empty_alias : empty_class {}; struct big : public empty_class { union { int data[3]; empty_alias a; // 使用别名类型替代原empty_class }; }; struct small { union { int data[3]; empty_class a; }; }; int main() { std::cout << sizeof(empty_class) << std::endl; // 输出1 std::cout << sizeof(big) << std::endl; // 输出12 std::cout << sizeof(small) << std::endl; // 输出12 }
方法2:使用C++20的[[no_unique_address]]属性
如果你使用C++20或更高版本,可以直接在基类声明上添加[[no_unique_address]]属性。这个属性告诉编译器:该基类子对象不需要强制拥有唯一的内存地址,只要不违反其他核心规则即可。这样编译器就能重新应用空基类优化,消除额外的内存开销。
修改后的代码示例:
#include <iostream> class empty_class { }; // 给基类添加[[no_unique_address]]属性 struct big : [[no_unique_address]] public empty_class { union { int data[3]; empty_class a; }; }; struct small { union { int data[3]; empty_class a; }; }; int main() { std::cout << sizeof(empty_class) << std::endl; // 输出1 std::cout << sizeof(big) << std::endl; // 输出12 std::cout << sizeof(small) << std::endl; // 输出12 }
内容的提问来源于stack exchange,提问作者Daniel H

