结构体成员独立存储,联合体成员共享内存;结构体总大小受内存对齐和填充影响,可能大于成员之和;联合体可用于实现变体类型、类型双关和硬件寄存器操作;现代C++推荐使用std::variant替代联合体以提升类型安全。

C++中的结构体(struct)和联合体(union)在内存布局上有着根本性的区别:结构体的成员各自占据独立的内存空间,彼此互不影响,而联合体的所有成员则共享同一块内存区域,这意味着在任何时刻,联合体只能“容纳”其中一个成员的值。
理解C++中结构体与联合体在内存中的区别,核心在于它们如何分配和管理内部成员的存储空间。
结构体 (struct) 的内存布局: 当定义一个结构体时,它的每个成员都会被分配独立的内存地址。这些成员通常会按照它们在结构体中声明的顺序依次存储,但为了满足内存对齐(alignment)的要求,编译器可能会在成员之间插入一些填充字节(padding)。这意味着,一个结构体的总大小通常是其所有成员大小之和,再加上可能存在的填充字节。所有成员都可以同时被访问,互不干扰。例如:
struct MyStruct {
int a; // 占用4字节 (假设int是4字节)
char b; // 占用1字节
double c; // 占用8字节
};
// MyStruct 的总大小可能不是 4+1+8=13 字节,
// 而是为了对齐,可能会是 24 字节(例如,8字节对齐)。在这个例子中,
a
b
c
MyStruct.a
MyStruct.c
联合体 (union) 的内存布局: 与结构体不同,联合体的所有成员都从相同的内存地址开始,它们共享同一块内存区域。联合体的大小由其最大成员的大小决定(同样会考虑内存对齐)。这意味着在任何给定时间,联合体只能存储其众多成员中的一个值。当你给联合体的一个成员赋值时,它会覆盖之前存储在该内存区域的任何其他成员的值。
union MyUnion {
int i; // 占用4字节
float f; // 占用4字节
char c[8]; // 占用8字节
};
// MyUnion 的总大小将是其最大成员 `char c[8]` 的大小,即 8 字节。在这里,
MyUnion.i
MyUnion.f
MyUnion.c
MyUnion.i
MyUnion.f
MyUnion.i
MyUnion.i
MyUnion.f
int
立即学习“C++免费学习笔记(深入)”;
简而言之,结构体是“和”(AND)的关系,所有成员都存在;联合体是“或”(OR)的关系,同一时间只有一个成员有效。
在我看来,内存对齐和填充是C++结构体设计中一个非常微妙但又至关重要的细节,它直接影响着程序的性能和内存占用。CPU在访问内存时,通常会以字长(word size,比如4字节或8字节)为单位进行读取,如果数据没有对齐到这些字的边界上,CPU可能需要进行多次内存访问才能读取一个变量,这无疑会降低效率。
内存对齐 (Memory Alignment): 内存对齐是指变量存储的起始地址必须是其类型大小(或某个特定值)的整数倍。例如,一个
int
double
填充 (Padding): 为了满足内存对齐的要求,编译器会在结构体成员之间或者在结构体末尾插入一些无用的字节,这些字节就是填充字节。这些填充字节不存储任何有意义的数据,它们只是为了确保后续成员或整个结构体实例能够正确对齐。
考虑这样一个结构体:
struct Example {
char a; // 1 byte
int b; // 4 bytes
char c; // 1 byte
};假设系统默认对齐是4字节。
char a
int b
b
a
int b
char c
Example
Example
Example
c
所以,
Example
1 (a) + 3 (padding) + 4 (b) + 1 (c) + 3 (padding) = 12
1+4+1 = 6
影响:
__attribute__((packed))
#pragma pack(1)
联合体,这种看似有些“古老”的C风格特性,在现代C++中虽然有了更安全的替代品(比如
std::variant
实现变体类型 (Variant Types) 或标签联合 (Tagged Unions): 这是联合体最经典的应用之一。当一个数据结构可能存储多种类型中的一种,但你又不想为每种可能性都分配独立内存时,联合体就派上用场了。为了解决联合体固有的类型不安全问题(不知道当前哪个成员是有效的),通常会结合一个枚举(或整数)标签来指示当前联合体中存储的是哪种类型的数据。
enum ValueType { INT, FLOAT, STRING };
struct VariantValue {
ValueType type;
union {
int iVal;
float fVal;
char sVal[32]; // 假设字符串最大31字符
} data;
};
// 使用示例
VariantValue v;
v.type = INT;
v.data.iVal = 123;
// 切换到另一个类型
v.type = FLOAT;
v.data.fVal = 3.14f;这种模式在解析配置文件、实现解释器中的通用值类型等场景中很常见。当然,C++17的
std::variant
类型双关 (Type Punning) 或位模式解释: 这是联合体一个比较“高级”且需要谨慎使用的功能。它允许你将同一块内存区域解释为不同的数据类型。最常见的例子是检查系统的字节序(Endianness),或者将浮点数的位模式作为整数来操作(例如,实现一些底层的浮点数操作算法)。
#include <iostream>
#include <cstdint> // For uint32_t
// 检查系统字节序
union EndianCheck {
uint32_t value;
char bytes[4];
};
// 浮点数位模式操作
union FloatIntConverter {
float f;
uint32_t i;
};
int main() {
// 检查字节序
EndianCheck ec;
ec.value = 0x01020304; // 假设一个32位整数
if (ec.bytes[0] == 0x04) {
std::cout << "Little-endian system" << std::endl;
} else {
std::cout << "Big-endian system" << std::endl;
}
// 浮点数位模式
FloatIntConverter fic;
fic.f = 3.14f;
std::cout << "Float value: " << fic.f << std::endl;
std::cout << "As integer bit pattern: 0x" << std::hex << fic.i << std::endl;
return 0;
}需要强调的是,C++标准对类型双关有严格的规定(严格别名规则),直接通过联合体的非活跃成员访问数据可能导致未定义行为(Undefined Behavior, UB)。C++20引入的
std::bit_cast
与硬件寄存器交互: 在嵌入式系统编程中,经常需要直接操作硬件寄存器。这些寄存器可能由多个字段组成,而这些字段又共享同一个物理地址。联合体可以非常自然地模拟这种内存布局,允许你通过不同的成员名来访问同一个寄存器中的不同位域或不同解释。
// 假设一个32位控制寄存器
union ControlRegister {
uint32_t raw; // 原始的32位值
struct {
uint32_t enable : 1; // 第0位:使能
uint32_t mode : 2; // 第1-2位:工作模式
uint32_t reserved : 29; // 剩余位保留
} fields;
};
// 使用示例
volatile ControlRegister* reg = reinterpret_cast<volatile ControlRegister*>(0xDEADBEEF); // 假设寄存器地址
reg->fields.enable = 1; // 设置使能位
reg->fields.mode = 2; // 设置工作模式
uint32_t currentRawValue = reg->raw; // 读取整个寄存器的原始值这种方式使得对硬件寄存器的操作更加直观和类型安全(在一定程度上)。
总的来说,虽然联合体带来了类型不安全的风险,但其独特的内存共享机制在需要极度内存优化、底层位操作或与特定硬件/C接口时,仍然是值得考虑的工具。
联合体固有的类型不安全问题,即你必须自己跟踪哪个成员是当前活跃的,否则就会有未定义行为的风险,这在现代C++中确实是一个痛点。好在,随着C++标准的演进,我们有了更优雅、更安全的替代方案来处理这类“可能是A也可能是B”的数据结构。在我看来,这极大地提升了代码的健壮性和可读性。
传统方式:标签联合 (Tagged Union) 的封装 在C++11之前,我们通常会手动封装联合体,添加一个枚举或整型成员作为“标签”来指示当前活跃的类型。这本质上就是我们上面提到的
VariantValue
enum class MyType { Int, Float, String };
struct SafeUnion {
MyType type;
union {
int i;
float f;
std::string s; // 注意:联合体不能直接包含非平凡类型(如std::string),
// 需要手动管理其生命周期,这里仅为示意。
} data;
// 构造函数、析构函数和赋值运算符需要手动管理data.s的生命周期
// 这非常复杂且容易出错
};这种方式的缺点在于,如果联合体成员是非平凡类型(如
std::string
std::vector
C++17及以后:std::variant
std::variant
#include <variant>
#include <string>
#include <iostream>
// 定义一个可以存储 int, float 或 std::string 的变体
using MyVariant = std::variant<int, float, std::string>;
int main() {
MyVariant v; // 默认构造为第一个类型 (int)
v = 10; // 存储一个 int
std::cout << "Current value (int): " << std::get<int>(v) << std::endl;
// 或者使用索引访问,但不如类型安全
std::cout << "Current value (index 0): " << std::get<0>(v) << std::endl;
v = 3.14f; // 存储一个 float,旧的 int 值被销毁
std::cout << "Current value (float): " << std::get<float>(v) << std::endl;
v = "Hello, Variant!"; // 存储一个 std::string
std::cout << "Current value (string): " << std::get<std::string>(v) << std::endl;
// 尝试访问非当前活跃类型会抛出 std::bad_variant_access 异常
try {
std::get<int>(v);
} catch (const std::bad_variant_access& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
// 可以使用 std::visit 访问变体中的值,更灵活
std::visit([](auto&& arg){
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "Visited as int: " << arg << std::endl;
} else if constexpr (std::is_same_v<T, float>) {
std::cout << "Visited as float: " << arg << std::endl;
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "Visited as string: " << arg << std::endl;
}
}, v);
std::cout << "Currently holds index: " << v.index() << std::endl; // 0 for int, 1 for float, 2 for string
return 0;
}std::variant
std::variant
std::visit
if-else if
std::variant
C++17及以后:std::any
std::any
std::variant
#include <any>
#include <string>
#include <iostream>
int main() {
std::any a;
a = 10; // 存储一个 int
std::cout << std::any_cast<int>(a) << std::endl;
a = std::string("Hello, any!"); // 存储一个 string
std::cout << std::any_cast<std::string>(a) << std::endl;
// 尝试访问错误类型也会抛出异常
try {
std::any_cast<float>(a);
} catch (const std::bad_any_cast& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}std::any
std::variant
总结来说,在现代C++中,除非是极其底层的、对内存布局有严格要求且性能敏感的场景(例如与硬件直接交互),我个人更倾向于使用
std::variant
以上就是C++结构体与联合体在内存中的区别的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号