0

0

C++联合体内存共享与大小计算

P粉602998670

P粉602998670

发布时间:2025-09-18 11:31:01

|

1035人浏览过

|

来源于php中文网

原创

C++联合体是共享内存的特殊类,所有成员共用同一块内存空间,大小由最大成员决定并按最大对齐要求对齐。

c++联合体内存共享与大小计算

C++联合体,说白了,就是一种特殊的类,它让不同的数据成员共享同一块内存空间。这意味着,它的内存大小不是所有成员的总和,而是由它内部最大的那个成员所决定的,并且还会考虑内存对齐的要求。当你向联合体的一个成员写入数据时,实际上就覆盖了之前存储在那块内存中的其他成员的数据。

解决方案

理解C++联合体(

union
)的核心在于“内存共享”这个概念。它与我们更熟悉的结构体(
struct
)形成了鲜明对比。结构体中的每个成员都有自己独立的内存地址,它们在内存中是顺序排列的(可能中间有填充字节)。而联合体则不然,它的所有非静态数据成员都起始于同一个内存地址。这就好比一个房间,可以住张三,也可以住李四,但同一时间只能住一个人。张三住进去,李四就得搬出来。

这种设计哲学带来了两个直接的后果:

  1. 内存效率: 当你确信在任何给定时间点,你只需要存储多种类型中的一种数据时,联合体可以显著节省内存。比如,一个消息包可能包含文本、图片ID或文件路径,但绝不会同时包含所有这些。
  2. 类型双关(Type Punning): 联合体提供了一种方式,允许你通过一种类型来解释存储在同一内存位置的另一种类型的数据。但这需要非常小心,因为在大多数情况下,这种操作会导致未定义行为(Undefined Behavior),除非你通过
    char*
    unsigned char*
    来访问原始字节,或者在C++20及更高版本中使用
    std::bit_cast

关于大小计算,一个联合体的大小至少要能容纳其所有成员中最大的那个。但“至少”这个词很重要,因为内存对齐会介入。联合体的大小必须是其所有成员中最大对齐要求的倍数。例如,如果一个联合体包含

char
(1字节,对齐1)、
int
(4字节,对齐4)和
double
(8字节,对齐8),那么这个联合体的最小大小就是
sizeof(double)
,即8字节。同时,它的对齐要求是8字节。因此,
sizeof
运算符会返回8。如果最大的成员是5字节,对齐是4字节,那么联合体的大小就可能是8字节(为了满足4字节对齐,并容纳5字节数据)。

立即学习C++免费学习笔记(深入)”;

#include <iostream>
#include <string> // For std::string, though generally union with non-POD types is tricky

// 示例联合体
union Data {
    int i;
    float f;
    char c;
    double d; // 最大的成员
};

int main() {
    Data myData;

    std::cout << "Size of Data union: " << sizeof(myData) << " bytes" << std::endl;
    std::cout << "Alignment of Data union: " << alignof(myData) << " bytes" << std::endl;

    // 写入一个成员
    myData.i = 123;
    std::cout << "After writing myData.i = 123:" << std::endl;
    std::cout << "  myData.i: " << myData.i << std::endl;
    // 此时访问其他成员是未定义行为,但为了演示内存共享,我们还是看一眼
    // 注意:这里的输出结果是不可预测的,仅作演示
    // std::cout << "  myData.f (potentially garbage): " << myData.f << std::endl;
    // std::cout << "  myData.c (potentially garbage): " << myData.c << std::endl;

    // 写入另一个成员,会覆盖之前的数据
    myData.d = 3.14159;
    std::cout << "After writing myData.d = 3.14159:" << std::endl;
    std::cout << "  myData.d: " << myData.d << std::endl;
    // 此时myData.i的值已经被覆盖,再次访问是未定义行为
    // std::cout << "  myData.i (potentially garbage): " << myData.i << std::endl;

    return 0;
}

运行上述代码,你通常会看到

Size of Data union: 8 bytes
Alignment of Data union: 8 bytes
,因为
double
是其中最大的成员,且其对齐要求也是8字节。

C++联合体与结构体在内存管理上有何本质区别?

C++中的联合体(

union
)和结构体(
struct
)在内存管理上的差异,是理解它们各自用途的关键。简单来说,结构体是“并集”,联合体是“交集”。

结构体是把多个不同类型的数据项打包成一个单一的复合类型。它的内存布局是:每个成员都占据自己独立的内存空间,并且按照它们在结构体中声明的顺序依次排列。当然,为了满足内存对齐的要求,编译器可能会在成员之间插入一些填充字节(padding)。因此,一个结构体的大小通常是其所有成员大小之和,再加上可能存在的填充字节。访问结构体成员时,它们的数据是完全独立的,互不影响。这很符合我们日常对“一组相关数据”的认知,比如一个学生的信息(姓名、学号、年龄),这些信息是并存的。

而联合体则不同,它的所有非静态数据成员都共享同一块内存空间,起始地址也相同。这意味着在任何给定时间点,联合体中只有一个成员可以“活跃”并持有有效数据。当你给联合体的一个成员赋值时,这块共享的内存就被这个成员的数据占据了,之前存储的任何其他成员的数据都会被覆盖。因此,联合体的大小仅仅是其最大成员的大小,并向上对齐到其所有成员中最大的对齐要求。它的设计初衷就是为了在多种可能的数据类型中,只存储其中一种,从而节省内存。

举个例子:

struct S {
    char a;
    int b;
    double c;
};

union U {
    char a;
    int b;
    double c;
};

// 在64位系统上,通常:
// sizeof(S) 可能是 16 或 24 字节 (取决于对齐和填充)
//   a (1 byte) + padding (3 bytes) + b (4 bytes) + c (8 bytes) = 16 bytes
// sizeof(U) 肯定是 8 字节 (因为 double 是最大的,且对齐是8)

所以,如果你需要同时存储多个数据项,并且它们之间逻辑上是独立的,那就用结构体。如果你只需要在多个数据项中选择一个来存储,并且注重内存效率,那么联合体可能是个选择,但要清楚它带来的类型安全挑战。

如何准确计算C++联合体的实际内存占用

准确计算C++联合体的实际内存占用,不仅仅是找到最大成员的

sizeof
值那么简单,内存对齐(alignment)是另一个不可忽视的关键因素。

联合体的内存大小由以下两个主要因素决定:

  1. 最大成员的大小: 联合体必须足够大,以容纳其所有成员中最大的那个。这是显而易见的。
  2. 所有成员的最大对齐要求: 联合体作为一个整体,其自身的内存地址必须能够满足其所有成员中最大的那个对齐要求。这意味着,联合体的大小必须是这个最大对齐要求的倍数。

我们来一步步分析:

  • 确定每个成员的大小和对齐要求: 使用
    sizeof()
    运算符获取成员的大小,使用
    alignof()
    运算符(C++11及更高版本)获取成员的对齐要求。
  • 找出最大成员的大小
    max_member_size
  • 找出所有成员中最大的对齐要求
    max_alignment_requirement
  • 计算联合体的最终大小: 联合体的最终大小必须至少是
    max_member_size
    ,并且是
    max_alignment_requirement
    的倍数。如果
    max_member_size
    本身就是
    max_alignment_requirement
    的倍数,那么
    sizeof(union)
    就等于
    max_member_size
    。否则,它会被向上填充到
    max_alignment_requirement
    的下一个倍数。

举个例子:

#include <iostream>

struct ExampleUnion {
    char a;    // sizeof=1, alignof=1
    short b;   // sizeof=2, alignof=2
    int c;     // sizeof=4, alignof=4
    long long d; // sizeof=8, alignof=8 (通常)
};

union MyUnion {
    char a;
    short b;
    int c;
    long long d;
};

int main() {
    std::cout << "sizeof(char): " << sizeof(char) << ", alignof(char): " << alignof(char) << std::endl;
    std::cout << "sizeof(short): " << sizeof(short) << ", alignof(short): " << alignof(short) << std::endl;
    std::cout << "sizeof(int): " << sizeof(int) << ", alignof(int): " << alignof(int) << std::endl;
    std::cout << "sizeof(long long): " << sizeof(long long) << ", alignof(long long): " << alignof(long long) << std::endl;

    std::cout << "\nsizeof(MyUnion): " << sizeof(MyUnion) << std::endl;
    std::cout << "alignof(MyUnion): " << alignof(MyUnion) << std::endl;

    return 0;
}

在大多数64位系统上,

long long
的大小是8字节,对齐要求也是8字节。它是
MyUnion
中最大的成员,也是对齐要求最高的成员。因此:

  • max_member_size
    =
    sizeof(long long)
    = 8
  • max_alignment_requirement
    =
    alignof(long long)
    = 8
  • 由于 8 是 8 的倍数,所以
    sizeof(MyUnion)
    将是 8 字节。

如果我们的联合体是这样:

Chromox
Chromox

Chromox是一款领先的AI在线生成平台,专为喜欢AI生成技术的爱好者制作的多种图像、视频生成方式的内容型工具平台。

下载
union StrangeUnion {
    char a[5]; // sizeof=5, alignof=1
    int b;     // sizeof=4, alignof=4
};
  • max_member_size
    =
    sizeof(char[5])
    = 5
  • max_alignment_requirement
    =
    alignof(int)
    = 4 现在,
    max_member_size
    (5) 不是
    max_alignment_requirement
    (4) 的倍数。编译器会将其向上填充到4的下一个倍数,也就是8。所以
    sizeof(StrangeUnion)
    将会是8字节。

通过这种方式,我们能准确地预测联合体的内存占用,避免因对齐规则不熟悉而导致的误解。

C++联合体在实际编程中常见的应用场景与潜在风险有哪些?

联合体在C++中是一个相对低级且需要谨慎使用的特性,但它确实有一些特定的应用场景,同时也伴随着不小的潜在风险。

常见的应用场景:

  1. 内存优化(Memory Optimization): 这是联合体最直接的用途。当一个数据结构在不同时间点只需要存储多种类型中的一种时,使用联合体可以显著减少内存占用。例如,在一个图形渲染器中,一个“材质”对象可能包含纹理ID、颜色值或着色器参数,但同一时间只需要其中一种。

    enum MaterialType { TEXTURE, COLOR, SHADER_PARAM };
    
    struct Material {
        MaterialType type;
        union {
            int textureId;
            struct { float r, g, b, a; } color;
            void* shaderHandle;
        }; // 匿名联合体
    };

    这里通过一个

    type
    字段(通常称为“标签”或“判别器”)来指示联合体中当前哪个成员是活跃的。

  2. 实现变体类型(Variant Types): 在C++17引入

    std::variant
    之前,联合体是实现类似“可以存储多种类型之一”的变体类型的基础。例如,一些旧的C风格API(如COM的
    VARIANT
    )就是基于联合体构建的。

  3. 硬件寄存器映射(Hardware Register Mapping): 在嵌入式系统编程中,有时会用联合体来定义硬件寄存寄存器的位域,以便于通过不同的方式访问同一块内存区域。

    // 假设一个32位寄存器
    union StatusRegister {
        uint32_t raw; // 整个寄存器值
        struct {
            uint32_t errorFlag : 1; // 第0位是错误标志
            uint32_t readyFlag : 1; // 第1位是就绪标志
            uint32_t : 30;          // 剩余位填充
        } bits;
    };

    这样,既可以整体读写寄存器

    reg.raw
    ,也可以单独操作某个位
    reg.bits.errorFlag

  4. 类型双关(Type Punning)/原始字节访问: 虽然大部分类型双关是未定义行为,但通过联合体和

    char*
    unsigned char*
    访问对象的原始字节序列是合法的。这在需要序列化/反序列化数据、或者实现自定义内存分配器时可能会用到。

潜在风险:

  1. 未定义行为(Undefined Behavior, UB): 这是使用联合体最主要的风险。标准规定,向联合体的一个成员写入数据后,除了通过

    char*
    unsigned char*
    访问原始字节,或者在某些特定情况下(如写入公共前缀成员),读取另一个成员会导致未定义行为。编译器可能做出任何事情,程序可能崩溃,也可能给出看似正确但实际上是错误的结果。

    union Value {
        int i;
        float f;
    };
    Value v;
    v.i = 10;
    // std::cout << v.f; // 潜在的UB!
  2. 类型安全缺失: 联合体本身不提供任何机制来追踪当前哪个成员是活跃的。你需要手动添加一个“标签”字段(如上面

    Material
    例子中的
    type
    )来管理状态,这增加了代码的复杂性和出错的可能性。如果标签和实际活跃成员不一致,就会出现逻辑错误。

  3. 非平凡类型成员(Non-trivial Members)的限制: C++标准对联合体成员的类型有一些限制。如果联合体包含具有非平凡构造函数、析构函数、拷贝构造函数、移动构造函数、拷贝赋值运算符或移动赋值运算符的成员,那么使用联合体变得非常复杂。你需要手动管理这些成员的生命周期(例如,使用placement new和显式析构函数),否则会导致资源泄漏或未定义行为。通常,联合体成员最好是“平凡的”(Plain Old Data, POD)类型,或者在C++11后是“标准布局”(Standard Layout)和“平凡可复制”(Trivially Copyable)的类型。

  4. 代码可读性和维护性下降: 联合体的语义不如结构体直观,需要额外的判别逻辑。这使得代码更难阅读、理解和维护,尤其是在大型项目中。

鉴于这些风险,在现代C++中,除非有非常明确的内存或硬件交互需求,并且能够严格控制其使用,否则通常更推荐使用

std::variant
(C++17)或
std::any
(C++17)来处理多类型存储的需求,它们提供了更好的类型安全和生命周期管理。如果追求极致的性能和内存控制,且对C++标准有深入理解,联合体依然是不可替代的工具

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
数据类型有哪几种
数据类型有哪几种

数据类型有整型、浮点型、字符型、字符串型、布尔型、数组、结构体和枚举等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

338

2023.10.31

php数据类型
php数据类型

本专题整合了php数据类型相关内容,阅读专题下面的文章了解更多详细内容。

225

2025.10.31

c语言 数据类型
c语言 数据类型

本专题整合了c语言数据类型相关内容,阅读专题下面的文章了解更多详细内容。

138

2026.02.12

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1567

2023.10.24

Go语言中的运算符有哪些
Go语言中的运算符有哪些

Go语言中的运算符有:1、加法运算符;2、减法运算符;3、乘法运算符;4、除法运算符;5、取余运算符;6、比较运算符;7、位运算符;8、按位与运算符;9、按位或运算符;10、按位异或运算符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

241

2024.02.23

php三元运算符用法
php三元运算符用法

本专题整合了php三元运算符相关教程,阅读专题下面的文章了解更多详细内容。

150

2025.10.17

golang结构体相关大全
golang结构体相关大全

本专题整合了golang结构体相关大全,想了解更多内容,请阅读专题下面的文章。

490

2025.06.09

golang结构体方法
golang结构体方法

本专题整合了golang结构体相关内容,请阅读专题下面的文章了解更多。

202

2025.07.04

Python异步编程与Asyncio高并发应用实践
Python异步编程与Asyncio高并发应用实践

本专题围绕 Python 异步编程模型展开,深入讲解 Asyncio 框架的核心原理与应用实践。内容包括事件循环机制、协程任务调度、异步 IO 处理以及并发任务管理策略。通过构建高并发网络请求与异步数据处理案例,帮助开发者掌握 Python 在高并发场景中的高效开发方法,并提升系统资源利用率与整体运行性能。

37

2026.03.12

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Java 教程
Java 教程

共578课时 | 81.2万人学习

国外Web开发全栈课程全集
国外Web开发全栈课程全集

共12课时 | 1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号