0

0

C++结构体与联合体在内存中的区别

P粉602998670

P粉602998670

发布时间:2025-09-21 14:51:01

|

413人浏览过

|

来源于php中文网

原创

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

c++结构体与联合体在内存中的区别

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++结构体内存对齐与填充(Padding)是如何影响其大小的?

在我看来,内存对齐和填充是C++结构体设计中一个非常微妙但又至关重要的细节,它直接影响着程序的性能和内存占用。CPU在访问内存时,通常会以字长(word size,比如4字节或8字节)为单位进行读取,如果数据没有对齐到这些字的边界上,CPU可能需要进行多次内存访问才能读取一个变量,这无疑会降低效率。

内存对齐 (Memory Alignment): 内存对齐是指变量存储的起始地址必须是其类型大小(或某个特定值)的整数倍。例如,一个

int
类型(通常4字节)的变量,其地址通常需要是4的倍数。一个
double
类型(通常8字节)的变量,其地址通常需要是8的倍数。这种要求是为了优化CPU访问数据的速度。

填充 (Padding): 为了满足内存对齐的要求,编译器会在结构体成员之间或者在结构体末尾插入一些无用的字节,这些字节就是填充字节。这些填充字节不存储任何有意义的数据,它们只是为了确保后续成员或整个结构体实例能够正确对齐。

考虑这样一个结构体:

struct Example {
    char a;   // 1 byte
    int b;    // 4 bytes
    char c;   // 1 byte
};

假设系统默认对齐是4字节。

  1. char a
    占用1字节。
  2. 接下来是
    int b
    。为了让
    b
    对齐到4字节的边界,编译器会在
    a
    之后插入3个填充字节。
  3. int b
    占用4字节。
  4. 接下来是
    char c
    占用1字节。
  5. 结构体总大小。为了让整个
    Example
    结构体在数组中也能正确对齐(例如,如果
    Example
    数组的起始地址是4的倍数,那么每个
    Example
    实例的起始地址也应该是4的倍数),编译器可能会在
    c
    之后再插入3个填充字节,使得整个结构体的大小是4的倍数。

所以,

Example
的实际大小可能是
1 (a) + 3 (padding) + 4 (b) + 1 (c) + 3 (padding) = 12
字节,而不是简单的
1+4+1 = 6
字节。

影响:

杰易OA办公自动化系统6.0
杰易OA办公自动化系统6.0

基于Intranet/Internet 的Web下的办公自动化系统,采用了当今最先进的PHP技术,是综合大量用户的需求,经过充分的用户论证的基础上开发出来的,独特的即时信息、短信、电子邮件系统、完善的工作流、数据库安全备份等功能使得信息在企业内部传递效率极大提高,信息传递过程中耗费降到最低。办公人员得以从繁杂的日常办公事务处理中解放出来,参与更多的富于思考性和创造性的工作。系统力求突出体系结构简明

下载
  • 内存浪费: 填充字节会增加结构体的实际大小,导致内存使用效率降低。在处理大量结构体实例时,这可能成为一个问题。
  • 性能提升: 虽然有内存浪费,但通过对齐,CPU能够更高效地读取和写入数据,从而提升程序的整体性能。这是一种典型的空间换时间策略。
  • 跨平台兼容性: 不同的编译器和CPU架构可能有不同的默认对齐规则,这可能导致同一结构体在不同平台上的大小不同。在进行网络传输或文件存储时,这需要特别注意,可能需要使用
    __attribute__((packed))
    (GCC/Clang) 或
    #pragma pack(1)
    (MSVC) 来强制禁用填充,但这样做可能会牺牲性能。

联合体在C++中除了节省内存,还有哪些实际应用场景?

联合体,这种看似有些“古老”的C风格特性,在现代C++中虽然有了更安全的替代品(比如

std::variant
),但它在某些特定场景下依然有着不可替代的价值。除了最直观的节省内存,我发现它还有以下几个引人深思的应用:

  1. 实现变体类型 (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
    提供了更安全、更现代的解决方案。

  2. 类型双关 (Type Punning) 或位模式解释: 这是联合体一个比较“高级”且需要谨慎使用的功能。它允许你将同一块内存区域解释为不同的数据类型。最常见的例子是检查系统的字节序(Endianness),或者将浮点数的位模式作为整数来操作(例如,实现一些底层的浮点数操作算法)。

    #include 
    #include  // 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
    提供了更安全、标准化的方式来执行这种位模式转换。但在一些底层代码、嵌入式系统或与C语言接口时,联合体仍可能被用于此目的。

  3. 与硬件寄存器交互: 在嵌入式系统编程中,经常需要直接操作硬件寄存器。这些寄存器可能由多个字段组成,而这些字段又共享同一个物理地址。联合体可以非常自然地模拟这种内存布局,允许你通过不同的成员名来访问同一个寄存器中的不同位域或不同解释。

    // 假设一个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(0xDEADBEEF); // 假设寄存器地址
    reg->fields.enable = 1; // 设置使能位
    reg->fields.mode = 2;   // 设置工作模式
    uint32_t currentRawValue = reg->raw; // 读取整个寄存器的原始值

    这种方式使得对硬件寄存器的操作更加直观和类型安全(在一定程度上)。

总的来说,虽然联合体带来了类型不安全的风险,但其独特的内存共享机制在需要极度内存优化、底层位操作或与特定硬件/C接口时,仍然是值得考虑的工具

C++11后,如何更安全、现代地处理联合体带来的类型安全问题?

联合体固有的类型不安全问题,即你必须自己跟踪哪个成员是当前活跃的,否则就会有未定义行为的风险,这在现代C++中确实是一个痛点。好在,随着C++标准的演进,我们有了更优雅、更安全的替代方案来处理这类“可能是A也可能是B”的数据结构。在我看来,这极大地提升了代码的健壮性和可读性。

  1. 传统方式:标签联合 (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
    ,或任何带有自定义构造函数、析构函数、赋值运算符的类),你需要手动管理它们的生命周期(调用构造函数和析构函数),这极其容易出错,导致内存泄漏或未定义行为。

  2. C++17及以后:

    std::variant
    这是现代C++处理变体类型最推荐的方式。
    std::variant
    是一个类型安全的联合体,它解决了传统联合体的所有痛点,尤其是非平凡类型成员的生命周期管理。

    #include 
    #include 
    #include 
    
    // 定义一个可以存储 int, float 或 std::string 的变体
    using MyVariant = std::variant;
    
    int main() {
        MyVariant v; // 默认构造为第一个类型 (int)
    
        v = 10; // 存储一个 int
        std::cout << "Current value (int): " << std::get(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(v) << std::endl;
    
        v = "Hello, Variant!"; // 存储一个 std::string
        std::cout << "Current value (string): " << std::get(v) << std::endl;
    
        // 尝试访问非当前活跃类型会抛出 std::bad_variant_access 异常
        try {
            std::get(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;
            if constexpr (std::is_same_v) {
                std::cout << "Visited as int: " << arg << std::endl;
            } else if constexpr (std::is_same_v) {
                std::cout << "Visited as float: " << arg << std::endl;
            } else if constexpr (std::is_same_v) {
                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
      的性能可以媲美手写的标签联合。
  3. C++17及以后:

    std::any
    如果你的需求是存储任意类型的值,并且编译时无法预知所有可能的类型,那么
    std::any
    可能是一个选择。它比
    std::variant
    更灵活,但代价是运行时开销更大(通常涉及动态内存分配和类型擦除)。

    #include 
    #include 
    #include 
    
    int main() {
        std::any a;
        a = 10; // 存储一个 int
        std::cout << std::any_cast(a) << std::endl;
    
        a = std::string("Hello, any!"); // 存储一个 string
        std::cout << std::any_cast(a) << std::endl;
    
        // 尝试访问错误类型也会抛出异常
        try {
            std::any_cast(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语言变量命名
C语言变量命名

c语言变量名规则是:1、变量名以英文字母开头;2、变量名中的字母是区分大小写的;3、变量名不能是关键字;4、变量名中不能包含空格、标点符号和类型说明符。php中文网还提供c语言变量的相关下载、相关课程等内容,供大家免费下载使用。

377

2023.06.20

c语言入门自学零基础
c语言入门自学零基础

C语言是当代人学习及生活中的必备基础知识,应用十分广泛,本专题为大家c语言入门自学零基础的相关文章,以及相关课程,感兴趣的朋友千万不要错过了。

603

2023.07.25

c语言运算符的优先级顺序
c语言运算符的优先级顺序

c语言运算符的优先级顺序是括号运算符 > 一元运算符 > 算术运算符 > 移位运算符 > 关系运算符 > 位运算符 > 逻辑运算符 > 赋值运算符 > 逗号运算符。本专题为大家提供c语言运算符相关的各种文章、以及下载和课程。

348

2023.08.02

c语言数据结构
c语言数据结构

数据结构是指将数据按照一定的方式组织和存储的方法。它是计算机科学中的重要概念,用来描述和解决实际问题中的数据组织和处理问题。数据结构可以分为线性结构和非线性结构。线性结构包括数组、链表、堆栈和队列等,而非线性结构包括树和图等。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

255

2023.08.09

c语言random函数用法
c语言random函数用法

c语言random函数用法:1、random.random,随机生成(0,1)之间的浮点数;2、random.randint,随机生成在范围之内的整数,两个参数分别表示上限和下限;3、random.randrange,在指定范围内,按指定基数递增的集合中获得一个随机数;4、random.choice,从序列中随机抽选一个数;5、random.shuffle,随机排序。

577

2023.09.05

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

515

2023.09.20

c语言get函数的用法
c语言get函数的用法

get函数是一个用于从输入流中获取字符的函数。可以从键盘、文件或其他输入设备中读取字符,并将其存储在指定的变量中。本文介绍了get函数的用法以及一些相关的注意事项。希望这篇文章能够帮助你更好地理解和使用get函数 。

627

2023.09.20

c数组初始化的方法
c数组初始化的方法

c语言数组初始化的方法有直接赋值法、不完全初始化法、省略数组长度法和二维数组初始化法。详细介绍:1、直接赋值法,这种方法可以直接将数组的值进行初始化;2、不完全初始化法,。这种方法可以在一定程度上节省内存空间;3、省略数组长度法,这种方法可以让编译器自动计算数组的长度;4、二维数组初始化法等等。

595

2023.09.22

苹果官网入口直接访问
苹果官网入口直接访问

苹果官网直接访问入口是https://www.apple.com/cn/,该页面具备0.8秒首屏渲染、HTTP/3与Brotli加速、WebP+AVIF双格式图片、免登录浏览全参数等特性。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

6

2025.12.24

热门下载

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

精品课程

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

共28课时 | 3.8万人学习

Kotlin 教程
Kotlin 教程

共23课时 | 2万人学习

Go 教程
Go 教程

共32课时 | 2.9万人学习

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

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