0

0

C++如何使用指针访问联合体成员

P粉602998670

P粉602998670

发布时间:2025-09-19 11:21:07

|

729人浏览过

|

来源于php中文网

原创

使用指针访问联合体成员与结构体语法相同,但联合体内存共享,需注意未定义行为。1. 联合体所有成员共享同一内存地址,任一成员赋值会覆盖其他成员;2. 通过指针用->操作符访问成员,语法与结构体一致;3. 读取非最后写入的成员导致未定义行为;4. 可通过判别器字段或封装类提升类型安全;5. C++17推荐使用std::variant替代传统联合体以增强安全性;6. 联合体指针适用于内存受限、低层数据解析及C语言互操作等场景。

c++如何使用指针访问联合体成员

在C++中,使用指针访问联合体(Union)成员与访问结构体(Struct)成员的方式在语法上是相似的,核心在于获取联合体变量的地址,然后通过指针解引用操作符(

->
)来访问其成员。不过,理解联合体成员共享同一块内存的本质,对于正确且安全地使用指针至关重要,否则可能导致未定义行为。

解决方案

要使用指针访问联合体成员,首先你需要定义一个联合体类型,然后声明一个该联合体类型的变量。接着,你可以声明一个指向该联合体类型的指针,并将其指向联合体变量的地址。一旦指针被正确初始化,就可以通过箭头操作符

->
来访问联合体的各个成员。

这里有一个简单的例子来展示这个过程:

#include 
#include  // 包含string头文件以使用std::string

// 定义一个联合体
union Data {
    int i;
    float f;
    char c;
    // 注意:联合体不推荐包含非平凡(non-trivial)类型如std::string,
    // 因为它们有自己的构造函数、析构函数和赋值操作符,
    // 联合体无法妥善管理它们的生命周期。
    // 这里仅为演示目的,实际项目中应避免。
    // std::string s; 
};

int main() {
    Data myData;       // 声明一个联合体变量
    Data* dataPtr;     // 声明一个指向Data联合体的指针

    dataPtr = &myData; // 将指针指向联合体变量的地址

    // 通过指针访问并设置成员i
    dataPtr->i = 123;
    std::cout << "通过指针设置 i = " << dataPtr->i << std::endl;

    // 此时,内存被重新解释为float类型
    // 注意:读取非最后写入的成员是未定义行为(UB),尽管在许多系统上可能“正常”工作
    std::cout << "当 i 被设置后,f 的值 (UB): " << dataPtr->f << std::endl;
    std::cout << "当 i 被设置后,c 的值 (UB): " << dataPtr->c << std::endl;

    // 通过指针访问并设置成员f
    dataPtr->f = 45.67f;
    std::cout << "通过指针设置 f = " << dataPtr->f << std::endl;
    std::cout << "当 f 被设置后,i 的值 (UB): " << dataPtr->i << std::endl;

    // 通过指针访问并设置成员c
    dataPtr->c = 'Z';
    std::cout << "通过指针设置 c = " << dataPtr->c << std::endl;
    std::cout << "当 c 被设置后,f 的值 (UB): " << dataPtr->f << std::endl;

    return 0;
}

这段代码直观地展示了指针如何操作联合体成员。但需要强调的是,联合体的所有成员都起始于同一内存地址,并且共享同一块内存空间。这意味着当你给一个成员赋值时,实际上是覆盖了这块内存中的数据。当你试图通过指针访问另一个成员时,你是在以不同的数据类型来解释同一块内存,这在C++标准中通常被认为是未定义行为(Undefined Behavior, UB),除非你确切地知道哪个成员是当前活动的。

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

联合体指针与结构体指针有何异同?

从语法层面看,联合体指针和结构体指针在使用上几乎一致:它们都通过

.
->
操作符来访问成员,都存储了内存地址,并且都可以作为函数参数传递。然而,它们在底层内存管理和语义上的差异,使得它们在实际应用中有着截然不同的考量。

主要相同点:

  1. 语法一致性: 访问成员的语法
    ptr->member
    对于结构体和联合体都是相同的。
  2. 地址存储: 指针都存储了其所指向对象(无论是结构体还是联合体)在内存中的起始地址。
  3. 内存分配: 编译器会为结构体或联合体变量在上或堆上分配一块内存,指针则指向这块内存。

关键不同点:

Adobe Flex 简介 中文WORD版
Adobe Flex 简介 中文WORD版

Flex是一个基于组件的开发框架,可以生成一个由Flash Player运行的富互联网应用程序。Flex将基于标准的语言和各种可扩展用户界面及数据访问组件结合起来,使得开发人员能够构建具有丰富数据演示、强大客户端逻辑和集成多媒体的应用程序。 Flex是一个建立在Flash平台上的富客户端应用开发工具包,Flex 作为富 Internet 应用(RIA)时代的新技术代表,自从 2007 年 Adobe 公司将其开源以来,Flex 就以前所未有的速度在成长。感兴趣的朋友可以过来看看

下载
  1. 内存布局: 这是最根本的区别
    • 结构体 (Struct): 为每个成员分配独立的内存空间,成员之间通常是顺序排列的(可能存在填充字节以满足对齐要求)。因此,结构体的大小是所有成员大小之和(加上填充)。
    • 联合体 (Union): 所有成员共享同一块内存空间。联合体的大小等于其最大成员的大小。这意味着在任何给定时刻,联合体只能“有效”存储其中一个成员的值。
  2. 数据存储与访问:
    • 结构体: 每个成员都有自己的独立存储,你可以同时访问和修改所有成员,它们的值互不影响。
    • 联合体: 当你为一个成员赋值时,它会覆盖之前存储在同一内存位置上的任何其他成员的值。读取一个非当前活动的成员(即非最后写入的成员)会导致未定义行为。这使得联合体在类型安全方面比结构体更具挑战性。
  3. 类型双关 (Type Punning): 联合体是C++中实现类型双关的一种常见且相对安全的方式(相比于
    reinterpret_cast
    )。通过将不同类型的成员放在联合体中,你可以将同一块内存解释为不同的数据类型。结构体则不具备这种直接的类型双关能力。
  4. 生命周期管理: 对于包含非平凡(non-trivial)成员(如带有自定义构造函数、析构函数或赋值操作符的对象,如
    std::string
    )的联合体,其生命周期管理变得非常复杂且危险。因为联合体本身不会为这些成员调用构造函数或析构函数,这需要手动管理,而结构体则由编译器自动处理。

联合体指针访问成员时,如何避免潜在的类型安全问题?

联合体在提供内存效率和类型双关能力的同时,也带来了显著的类型安全风险,尤其是在通过指针访问成员时。C++标准明确指出,读取一个非最后写入的联合体成员会导致未定义行为。为了规避这些问题,同时又能利用联合体的优势,有一些实践和替代方案值得考虑:

  1. 使用“标签”或“判别器”字段 (Discriminator Field): 这是最经典的解决方案。在一个包含联合体的结构体中,添加一个额外的枚举或整型成员,作为联合体的“标签”或“判别器”,用来指示当前联合体中哪个成员是活动的。

    #include 
    
    enum DataType {
        INT_TYPE,
        FLOAT_TYPE,
        CHAR_TYPE
    };
    
    struct Value {
        DataType type; // 判别器,指示当前哪个成员是活动的
        union {
            int i;
            float f;
            char c;
        } data; // 匿名联合体
    };
    
    int main() {
        Value val;
        Value* valPtr = &val;
    
        valPtr->type = INT_TYPE;
        valPtr->data.i = 100;
        std::cout << "类型: INT, 值: " << valPtr->data.i << std::endl;
    
        valPtr->type = FLOAT_TYPE;
        valPtr->data.f = 3.14f;
        std::cout << "类型: FLOAT, 值: " << valPtr->data.f << std::endl;
    
        // 安全地访问:在访问前检查判别器
        if (valPtr->type == FLOAT_TYPE) {
            std::cout << "当前活动的是float: " << valPtr->data.f << std::endl;
        } else {
            std::cout << "当前活动的不是float。" << std::endl;
        }
    
        return 0;
    }

    通过这种方式,在访问

    valPtr->data
    中的任何成员之前,你总是可以先检查
    valPtr->type
    来确保你正在访问正确的、活动的成员。

  2. 封装在类中提供安全接口: 将联合体和判别器封装在一个类中,并通过公共方法提供类型安全的访问。这些方法可以在内部检查判别器,并在尝试访问不活动的成员时抛出异常或返回错误状态。

    #include 
    #include  // 用于std::runtime_error
    
    class SafeData {
    public:
        enum DataType {
            NONE_TYPE,
            INT_TYPE,
            FLOAT_TYPE,
            CHAR_TYPE
        };
    
        SafeData() : currentType(NONE_TYPE) {}
    
        void setInt(int val) {
            data.i = val;
            currentType = INT_TYPE;
        }
    
        int getInt() const {
            if (currentType != INT_TYPE) {
                throw std::runtime_error("Attempted to get int when current type is not int.");
            }
            return data.i;
        }
    
        void setFloat(float val) {
            data.f = val;
            currentType = FLOAT_TYPE;
        }
    
        float getFloat() const {
            if (currentType != FLOAT_TYPE) {
                throw std::runtime_error("Attempted to get float when current type is not float.");
            }
            return data.f;
        }
    
        DataType getType() const {
            return currentType;
        }
    
    private:
        DataType currentType;
        union {
            int i;
            float f;
            char c;
        } data;
    };
    
    int main() {
        SafeData sd;
        SafeData* sdPtr = &sd;
    
        sdPtr->setInt(42);
        std::cout << "Int value: " << sdPtr->getInt() << std::endl;
    
        sdPtr->setFloat(3.14159f);
        std::cout << "Float value: " << sdPtr->getFloat() << std::endl;
    
        try {
            // 尝试访问不活动的成员,会抛出异常
            std::cout << "Int value (error expected): " << sdPtr->getInt() << std::endl;
        } catch (const std::runtime_error& e) {
            std::cerr << "Error: " << e.what() << std::endl;
        }
    
        return 0;
    }

    这种方式虽然增加了代码量,但大大提升了类型安全性,是管理复杂联合体的推荐做法。

  3. C++17

    std::variant
    对于现代C++项目,
    std::variant
    是一个更优、更类型安全的替代方案,它在很大程度上取代了传统联合体的需求。
    std::variant
    可以在编译时确保你只访问当前活动的成员,并提供
    std::get
    std::holds_alternative
    std::visit
    工具来安全地操作其内容。它解决了联合体在类型安全、生命周期管理和复杂性上的所有痛点。

    #include 
    #include  // C++17
    
    int main() {
        std::variant myVariant;
        // std::variant* variantPtr = &myVariant; // 通常不直接用指针访问variant内部
    
        myVariant = 100; // 存储int
        std::cout << "Current value (int): " << std::get(myVariant) << std::endl;
    
        myVariant = 3.14f; // 存储float
        std::cout << "Current value (float): " << std::get(myVariant) << std::endl;
    
        if (std::holds_alternative(myVariant)) {
            std::cout << "Holds float: " << std::get(myVariant) << std::endl;
        }
    
        try {
            // 尝试获取非当前活动的类型,会抛出std::bad_variant_access异常
            std::cout << "Current value (int, error expected): " << std::get(myVariant) << std::endl;
        } catch (const std::bad_variant_access& e) {
            std::cerr << "Error: " << e.what() << std::endl;
        }
    
        return 0;
    }

    虽然

    std::variant
    不直接涉及“联合体指针”的概念,但它提供了相同的“存储多种类型之一”的功能,且具有更高的类型安全性,是处理这类问题的首选方案。

在什么场景下,使用指针访问联合体成员会特别有用?

尽管存在类型安全挑战,但在某些特定场景下,联合体及其指针访问方式仍然非常有用,甚至不可替代。这些场景通常涉及对内存的极致控制、低级别数据解释或与C语言接口的兼容性。

  1. 内存优化与嵌入式系统: 在内存资源极其有限的环境(如嵌入式系统、微控制器编程)中,联合体可以帮助你最大限度地减少内存占用。当你知道某个变量在不同时间只会持有不同类型数据中的一种时,使用联合体可以避免为每种类型都分配独立内存,从而节省宝贵的RAM。通过指针访问,可以更灵活地在不同函数或模块间传递这个共享内存区域。

  2. 低级别数据解析与协议处理 (Type Punning): 当你需要将一块原始字节数据(例如从网络接收的数据包、文件读取的二进制数据)解释为不同的结构或数据类型时,联合体提供了一种有效的方式来实现“类型双关”。你可以定义一个联合体,其中包含不同布局的结构体或基本数据类型,然后将指针指向这块原始数据,通过联合体成员访问来“查看”数据的不同解释。 例如,解析一个包含不同消息类型的数据帧:

    // 假设这是从网络接收的原始字节
    unsigned char raw_buffer[16] = { /* ... 填充数据 ... */ };
    
    // 定义联合体来解释数据
    union Message {
        struct Header {
            unsigned short id;
            unsigned short length;
        } header;
        struct PayloadA {
            int value;
            char status;
        } payloadA;
        struct PayloadB {
            float temperature;
        } payloadB;
    };
    
    Message* msgPtr = reinterpret_cast(raw_buffer); // 将字节缓冲区解释为Message
    // 此时可以通过 msgPtr->header.id, msgPtr->payloadA.value 等来访问
    // 但必须确保当前内存中的数据确实符合你正在访问的成员类型。

    这种用法非常强大,但也最容易引入未定义行为,需要极其小心地管理当前数据的实际类型。

  3. 与C语言API或遗留代码的互操作性: C语言广泛使用联合体来实现多态性或变体类型,尤其是在系统级编程中。当C++代码需要与这些C语言库或遗留系统交互时,使用联合体及其指针是保持兼容性和直接访问这些数据结构的必要方式。C语言的联合体语义在读取非活动成员时通常是定义明确的(它只是将内存解释为另一种类型),这与C++的严格规则有所不同,因此在C++中使用时仍需谨慎。

  4. 实现自定义的“变体”类型(在C++17之前): 在C++17引入

    std::variant
    之前,联合体是实现自定义变体类型(即一个对象可以持有多种类型中的一种)的主要手段。通常,这会结合一个枚举标签字段来指示当前联合体中存储的是哪种类型,并通过指针来操作这些自定义变体对象。这本质上就是前面“使用判别器”模式的应用,通过指针来传递和操作这种复合类型。

总结来说,使用指针访问联合体成员主要服务于那些需要精细内存控制和低级别数据解释的场景。在现代C++中,对于更高级别的、类型安全的需求,

std::variant
通常是更优的选择。但对于与硬件直接交互、解析二进制协议或与C语言接口集成等特定任务,联合体及其指针仍然是不可或缺的工具,但必须以极高的警惕性和严谨性来使用。

相关专题

更多
C语言变量命名
C语言变量命名

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

397

2023.06.20

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

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

618

2023.07.25

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

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

354

2023.08.02

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

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

258

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,随机排序。

600

2023.09.05

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

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

525

2023.09.20

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

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

640

2023.09.20

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

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

601

2023.09.22

excel表格操作技巧大全 表格制作excel教程
excel表格操作技巧大全 表格制作excel教程

Excel表格操作的核心技巧在于 熟练使用快捷键、数据处理函数及视图工具,如Ctrl+C/V(复制粘贴)、Alt+=(自动求和)、条件格式、数据验证及数据透视表。掌握这些可大幅提升数据分析与办公效率,实现快速录入、查找、筛选和汇总。

0

2026.01.21

热门下载

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

精品课程

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

共94课时 | 7.1万人学习

C 教程
C 教程

共75课时 | 4.1万人学习

C++教程
C++教程

共115课时 | 12.9万人学习

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

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