C++ 中 struct 与 union 的区别:从内存共享到底层优化
C 中 struct 与 union 的区别从内存共享到底层优化一、引言看似相似的两种聚合类型struct和union是 C 中两种可以包含多个成员的复合类型。它们都能容纳不同类型的数据但在内存使用方式上有着本质差异。struct是我们日常编程中最常用的数据聚合方式而union则是一种特殊的内存复用机制。理解union的工作原理和适用场景对于底层系统编程、网络协议处理、嵌入式开发和内存优化都至关重要。C17 引入的std::variant更是为union提供了类型安全的现代替代方案。二、核心区别速览| 维度 | struct | union ||------|--------|-------|| 内存分配 | 所有成员各自拥有独立存储空间 | 所有成员共享同一块存储空间 || 大小 | 至少是所有成员大小之和(考虑对齐) | 等于最大成员的大小(考虑对齐) || 同时有效 | 所有成员同时有效| 同一时间只有一个成员有效|| 读写安全 | 读写任意成员皆安全 | 只能读写最后写入的那个成员 || 默认访问权限 | public | public || 构造/析构支持 | 完全支持 | 有限支持(有非平凡成员时需手动管理) || 典型应用 | 数据聚合、对象建模 | 类型双关、节省内存、状态机、协议解析 || C 现代替代 | — |std::variant(类型安全联合体) |三、内存布局的根本差异3.1 struct 的内存布局struct MyStruct { char a; // 1 字节 int b; // 4 字节 double c; // 8 字节 }; // sizeof(MyStruct) 可能是 16 或 24 (考虑内存对齐)总大小 16 字节所有成员同时存在struct 内存布局 (每个成员独立空间)a (1 字节)填充 3 字节b (4 字节)c (8 字节)关键点struct中所有成员同时存在各自占用独立的内存空间。你可以随时访问任意成员。3.2 union 的内存布局union MyUnion { char a; // 1 字节 int b; // 4 字节 double c; // 8 字节 }; // sizeof(MyUnion) 8 (等于最大成员 double 的大小)总大小 8 字节成员重叠在同一块内存上union 内存布局 (所有成员共享空间)最大成员 c 的空间: 8 字节a 占最低 1 字节b 占最低 4 字节c 占全部 8 字节同一时间只有一个有效关键点union中所有成员共享同一块内存大小等于最大成员的大小。同一时间只有一个成员有效。3.3 内存布局对比图union: 成员共享存储偏移 0char a / int b / double c全部从偏移 0 开始共享同一块内存总大小 max(各成员大小)struct: 各成员独立存储偏移 0: char a偏移 4: int b偏移 8: double c总大小 ≈ 成员大小之和(填充)四、union 的基本使用4.1 基本语法#include iostream union Data { int i; double d; char c; }; int main() { Data data; // 写入 int data.i 42; std::cout data.i std::endl; // 42 // 注意此时 data.d 和 data.c 的值是未定义的 // 写入 double (覆盖之前的 int) data.d 3.14159; std::cout data.d std::endl; // 3.14159 // 注意此时 data.i 的值是未定义的 // 写入 char data.c A; std::cout data.c std::endl; // A return 0; }4.2 匿名 unionstruct Packet { int type; // 匿名 union成员直接访问 union { struct { int x, y; } point; // type 1 struct { int width, height; } rect; // type 2 char text[16]; // type 3 }; // 无需通过 union 变量名直接访问内部成员 }; int main() { Packet p; p.type 1; p.point.x 10; p.point.y 20; // 直接访问没有额外的 union 成员名 p.type 3; strcpy(p.text, Hello); }五、union 进行内存优化的典型场景5.1 场景一节省内存的状态机#include iostream #include string // 不使用 union每个状态都占用空间 struct NaiveState { int stateType; // 0: int, 1: double, 2: string int intValue; // 浪费空间只在 stateType0 时使用 double doubleValue; // 浪费空间只在 stateType1 时使用 std::string stringValue; // 浪费空间只在 stateType2 时使用 }; // sizeof(NaiveState) 非常大包含全部三种数据 // 使用 union只占最大成员 状态标记 union StateData { int intValue; double doubleValue; std::string stringValue; StateData() : intValue(0) { } // 默认初始化为 int ~StateData() { } // 析构函数需手动管理 }; struct OptimizedState { int stateType; // 0: int, 1: double, 2: string StateData data; }; // sizeof(OptimizedState) 约等于 int sizeof(StateData) // StateData 大小 max(sizeof(int), sizeof(double), sizeof(string))5.2 场景二类型双关(Type Punning) — 底层位操作#include cstring #include iostream // 将浮点数按位解释为整数 union FloatInt { float f; uint32_t i; }; int main() { FloatInt converter; converter.f 3.14159f; // 查看浮点数的 IEEE 754 内部表示 std::cout Float: converter.f std::endl; std::cout Hex: 0x std::hex converter.i std::endl; // 修改符号位 converter.i ^ 0x80000000; std::cout Negated: converter.f std::endl; // -3.14159 return 0; }注意在 C 中通过union进行类型双关是合法的(与 C 不同C 中技术上属于未定义行为但几乎所有编译器都支持)但读取非最后写入的成员在不同标准版本中有不同规定。C20 起这种用法更加明确。5.3 场景三网络协议解析#include cstdint #include cstring #include iostream // 以太网帧头 union EthernetHeader { struct { uint8_t destMac[6]; uint8_t srcMac[6]; uint16_t etherType; } fields; uint8_t raw[14]; }; // IP 地址的多种表示 union IPAddress { uint32_t asInt; uint8_t asBytes[4]; struct { uint8_t b1, b2, b3, b4; } octets; }; int main() { // 从网络接收的原始字节 uint8_t rawPacket[] { 0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E, // 目标 MAC 0x00, 0x6F, 0x7E, 0x8D, 0x9C, 0xAB, // 源 MAC 0x08, 0x00 // 以太类型(IPv4) }; EthernetHeader header; std::memcpy(header.raw, rawPacket, 14); std::cout EtherType: 0x std::hex header.fields.etherType std::endl; IPAddress ip; ip.octets {192, 168, 1, 100}; std::cout IP as int: std::dec ip.asInt std::endl; // 注意字节序小端机器上输出可能与直觉不同 return 0; }5.4 场景四变体类型(Variant Type) — 动态类型#include iostream #include string #include stdexcept // 一个可以存储不同类型值的变体 enum class ValueType { INT, DOUBLE, STRING, NONE }; class Variant { ValueType type ValueType::NONE; union Storage { int intVal; double doubleVal; std::string stringVal; Storage() : intVal(0) { } ~Storage() { } } storage; public: Variant() default; ~Variant() { if (type ValueType::STRING) { storage.stringVal.~basic_string(); } } void setInt(int val) { if (type ValueType::STRING) { storage.stringVal.~basic_string(); } storage.intVal val; type ValueType::INT; } void setDouble(double val) { if (type ValueType::STRING) { storage.stringVal.~basic_string(); } storage.doubleVal val; type ValueType::DOUBLE; } void setString(const std::string val) { if (type ValueType::STRING) { storage.stringVal val; } else { new (storage.stringVal) std::string(val); // placement new type ValueType::STRING; } } ValueType getType() const { return type; } int getInt() const { if (type ! ValueType::INT) throw std::runtime_error(Not an int); return storage.intVal; } // ... getDouble, getString 类似 }; int main() { Variant v; v.setInt(42); std::cout v.getInt() std::endl; v.setString(Hello); // v 现在存储字符串int 数据被覆盖 v.setDouble(3.14); // v 现在存储 double字符串被正确析构 }重要这个手动管理的union变体存在大量陷阱。在现代 C 中应优先使用std::variant。六、union 的限制与陷阱6.1 非平凡类型的成员需要手动管理union Problematic { int x; std::string s; // std::string 有非平凡构造/析构 // C11 起编译器不会自动生成构造/析构函数 // 必须手动提供 Problematic() : x(0) { } ~Problematic() { } }; int main() { Problematic p; // 需要使用 placement new 来构造 string new (p.s) std::string(Hello); // 使用完毕后需要手动析构 p.s.~basic_string(); }6.2 读取非活跃成员union Data { int i; float f; }; Data d; d.i 42; // float x d.f; // 技术上这是读取非活跃成员 // C20 起对某些类型放宽了限制在 C 中读取 union 中非最后写入的成员行为是未定义的(C 语言中是合法的类型双关但 C 不同)。如果需要进行类型双关C20 起可以使用std::bit_cast。6.3 union 不能包含引用类型成员union BadUnion { int x; // int ref; // 错误union 不能包含引用类型成员 };6.4 union 不能作为基类或被继承// union Base { }; // 错误union 不能作为基类 // class Derived : Base { }; // 错误不能从 union 继承七、现代 C 的替代方案std::variantstd::variant(C17)提供了类型安全的联合体替代#include variant #include iostream #include string using Variant std::variantint, double, std::string; int main() { Variant v 42; // 存储 int v 3.14159; // 存储 double v std::string(Hello); // 存储 string // 安全的访问方式 if (std::holds_alternativestd::string(v)) { std::cout std::getstd::string(v) std::endl; } // 使用 std::visit 进行类型安全的访问 std::visit([](const auto value) { using T std::decay_tdecltype(value); if constexpr (std::is_same_vT, int) { std::cout int: value std::endl; } else if constexpr (std::is_same_vT, double) { std::cout double: value std::endl; } else if constexpr (std::is_same_vT, std::string) { std::cout string: value std::endl; } }, v); return 0; }std::variant 的优势| 特性 | union | std::variant ||------|-------|--------------|| 类型安全 | 无(手动跟踪类型) | 是(自动跟踪编译期检查) || 析构管理 | 手动(placement new/delete) | 自动(RAII) || 非平凡类型支持 | 需手动管理 | 自动处理 || 错误访问 | 未定义行为 | 抛出 std::bad_variant_access || 访问方式 | 直接读取(危险) | std::get / std::visit || 内存开销 | 等于最大成员 | 最大成员 类型索引(通常 4-8 字节) |八、总结struct和union代表了两种截然不同的内存组织方式struct — 成员共存所有成员同时存在各自占有独立空间。适用于需要在对象中同时保持多个属性的场景是面向对象编程和数据结构的基础。union — 成员互斥所有成员共享同一块内存同一时间只有一个成员有效。核心价值在于内存复用和底层数据表示操作。union 的优化场景节省内存当多个数据字段不会同时使用(如状态机、变体类型)时用 union 显著减少内存占用类型双关在不改变二进制表示的前提下以不同数据类型查看同一段内存(如浮点数与整数的位操作)协议解析将网络数据包或文件格式的原始字节与结构化字段映射到同一内存区域底层硬件操作在嵌入式系统中将硬件寄存器映射为不同位字段的组合现代 C 建议union是底层工具在使用非平凡类型(如std::string)时需要手动管理生命周期(placement new 和显式析构)极易出错。C17 引入的std::variant提供了类型安全的联合体替代方案在大多数应用场景中应优先使用。只有在纯粹的位操作、C 兼容性和极致性能要求的底层代码中union仍然是不可替代的工具。