C++ 中 struct 与 union 的区别:从内存共享到底层优化
2026/7/28 19:57:57 网站建设 项目流程

C++ 中 struct 与 union 的区别:从内存共享到底层优化


一、引言:看似相似的两种聚合类型


structunion是 C++ 中两种可以包含多个成员的复合类型。它们都能容纳不同类型的数据,但在内存使用方式上有着本质差异。struct是我们日常编程中最常用的数据聚合方式,而union则是一种特殊的内存复用机制。


理解union的工作原理和适用场景,对于底层系统编程、网络协议处理、嵌入式开发和内存优化都至关重要。C++17 引入的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: 成员共享存储

偏移 0

char 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 匿名 union


struct 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; // 浪费空间:只在 stateType==0 时使用 double doubleValue; // 浪费空间:只在 stateType==1 时使用 std::string stringValue; // 浪费空间:只在 stateType==2 时使用 }; // 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 中技术上属于未定义行为但几乎所有编译器都支持),但读取非最后写入的成员在不同标准版本中有不同规定。C++20 起这种用法更加明确。


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 有非平凡构造/析构 // C++11 起:编译器不会自动生成构造/析构函数 // 必须手动提供 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; // 技术上这是读取非活跃成员 // C++20 起对某些类型放宽了限制


在 C++ 中,读取 union 中非最后写入的成员,行为是未定义的(C 语言中是合法的类型双关,但 C++ 不同)。如果需要进行类型双关,C++20 起可以使用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::variant


std::variant(C++17)提供了类型安全的联合体替代:


#include <variant> #include <iostream> #include <string> using Variant = std::variant<int, double, std::string>; int main() { Variant v = 42; // 存储 int v = 3.14159; // 存储 double v = std::string("Hello"); // 存储 string // 安全的访问方式 if (std::holds_alternative<std::string>(v)) { std::cout << std::get<std::string>(v) << std::endl; } // 使用 std::visit 进行类型安全的访问 std::visit([](const auto& value) { using T = std::decay_t<decltype(value)>; if constexpr (std::is_same_v<T, int>) { std::cout << "int: " << value << std::endl; } else if constexpr (std::is_same_v<T, double>) { std::cout << "double: " << value << std::endl; } else if constexpr (std::is_same_v<T, 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 字节) |


八、总结


structunion代表了两种截然不同的内存组织方式:


  1. struct — 成员共存:所有成员同时存在,各自占有独立空间。适用于需要在对象中同时保持多个属性的场景,是面向对象编程和数据结构的基础。


  1. union — 成员互斥:所有成员共享同一块内存,同一时间只有一个成员有效。核心价值在于内存复用和底层数据表示操作。


  1. union 的优化场景
  • 节省内存:当多个数据字段不会同时使用(如状态机、变体类型)时,用 union 显著减少内存占用
  • 类型双关:在不改变二进制表示的前提下,以不同数据类型查看同一段内存(如浮点数与整数的位操作)
  • 协议解析:将网络数据包或文件格式的原始字节与结构化字段映射到同一内存区域
  • 底层硬件操作:在嵌入式系统中将硬件寄存器映射为不同位字段的组合


  1. 现代 C++ 建议union是底层工具,在使用非平凡类型(如std::string)时需要手动管理生命周期(placement new 和显式析构),极易出错。C++17 引入的std::variant提供了类型安全的联合体替代方案,在大多数应用场景中应优先使用。只有在纯粹的位操作、C 兼容性和极致性能要求的底层代码中,union仍然是不可替代的工具。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询