问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

C/C++中的联合体union介绍

创作时间:
作者:
@小白创作中心

C/C++中的联合体union介绍

引用
CSDN
1.
https://m.blog.csdn.net/qq_35662333/article/details/139830451

联合体(union)是C/C++中一种特殊的数据结构,它允许在相同的内存位置存储不同的数据类型。与结构体(struct)不同,联合体中的成员是互斥的,即任何时候只有一个成员有效。这种特性使得联合体在处理内存共享、数据转换等场景时非常有用。本文将详细介绍联合体的定义、使用方法、内存分配特点以及实际应用案例。

union定义与简单使用

联合体(union),中文名“联合体、共用体”,在某种程度上是类似结构体struct的一种数据结构,联合体(union)和结构体(struct)同样可以包含很多种数据类型和变量。

两者区别如下:

  • 结构体(struct)中所有变量是“共存”的——优点是“有容乃大”,全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。

  • 联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”,即任何两个成员不会同时有效;但优点是内存使用更为精细灵活,也节省了内存空间。

当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union)。在C Programming Language 一书中对于联合体是这么描述的:

  1. 联合体是一个结构体;

  2. 它的所有成员相对于基地址的偏移量都为0;

  3. 此结构空间要大到足够容纳最"宽"的成员;

  4. 其内存对齐方式要适合其中所有的成员;

定义形式如下:

union 名称{
    public: //此行可不写,默认为public
        公有成员
    protected:
        保护型成员
    private:
        私有成员
};

无名联合体

union { 
    int i; 
    float f; 
};
i = 5; // 无名联合体可直接用变量名使用
f = 2.0; // 此时i赋值的内容已无效

使用举例:

union Mark{ //表示成绩的联合体,同一门课程只会存在一种成绩表示方法
    char level; // 表示等级制的成绩‘A’ ‘B’ 'C'等
    bool pass; // 只计是否通过课程的成绩 0 1
    int grade; // 表示分数制的成绩
};

使用联合体保存成绩信息,并且输出:(下面代码在vs2013、vs2017正常运行,vs2015上会报错,提示枚举类未定义,感觉是vs2015默认配置的原因)

#include<iostream>
#include<string>
using namespace std;
class ExamInfo {
private:
    string name; //课程名
    enum { LEVEL, PASS, GRADE } mode; //计分方式
    union {
        char level; // 表示等级制的成绩‘A’ ‘B’ 'C'等
        bool pass; // 只计是否通过课程的成绩 
        int grade; // 表示分数制的成绩
    };
public:
    ExamInfo() {};
    ExamInfo(string name, char level) : name(name), mode(ExamInfo::LEVEL), level(level) {};
    ExamInfo(string name, bool pass) : name(name), mode(ExamInfo::PASS), pass(pass) {};
    ExamInfo(string name, int grade) : name(name), mode(ExamInfo::GRADE), grade(grade) {};
    void show() {
        cout << name << ":";
        switch (mode) {
        case ExamInfo::LEVEL: cout << level << endl; break;
        case ExamInfo::PASS: cout << (pass ? "PASS" : "FAIL") << endl; break;
        case ExamInfo::GRADE: cout << grade << endl; break; 
        }
    }
};
int main() {
    ExamInfo course1("English", 'B');
    ExamInfo course2("Math", true);
    ExamInfo course3("C++ Programming", 89);
    course1.show();
    course2.show();
    course3.show();
    system("pause");
    return 0;
}

输入结果:

内存分配与所占空间

联合体所占的空间不仅取决于最宽成员,还跟所有成员有关系,即其大小必须满足两个条件:

1. 大小足够容纳最宽的成员

2. 大小能被其包含的所有基本数据类型的大小所整除。

测试样例如下:

#include<iostream>  
using namespace std;
int main() {
    union U1 {
        int n;
        char s[11];
        double d;
    };
    union U2 {
        int n;
        char s[5];
        double d;
    };
    U1 u1;
    U2 u2;
    cout << sizeof(u1) << '\t' << sizeof(u2) << endl;
    cout << "u1各数据地址:\n" << &u1 << '\t' << &u1.d << '\t' << &u1.s << '\t' << &u1.n << endl;
    cout << "u2各数据地址:\n" << &u2 << '\t' << &u2.d << '\t' << &u2.s << '\t' << &u2.n << endl;
    system("pause");
    return 0;
}

输出结果:

对于U1联合体,n占4字节,d占8字节,s的基本类型是char,字节数为1,s最多的时候占用11字节,所以U1至少要11字节,但11字节对于8字节的double类型来说无法整除,所以扩充到16字节。这是因为存在字节对齐的问题,11既不能被4整除,也不能被8整除。因此补充字节到16,这样就符合所有成员的自身对齐了。

对于U2联合体,同理知道其大小为8。

从结果中还可以发现,联合体中的各数据的存储地址都是相同的。

多种访问内存途径共存

联合体中数据是共享相同的存储空间,各种变量名都可以同时使用,操作也是共同生效。具体访问方式如下:

#include<iostream>  
using namespace std;
int main() {
    union U {
        unsigned int n;
        unsigned char s[4];
    };
    U u;
    u.n = 0xf1f2f3f4;
    cout << "数值大小:" << hex << u.n << '\t' << "地址:" << &u.n << endl;
    cout << "数值大小:" << hex << (int)u.s[0] << '\t' << "地址:" << (void*)&u.s[0] << endl;
    cout << "数值大小:" << hex << (int)u.s[1] << '\t' << "地址:" << (void*)&u.s[1] << endl;
    cout << "数值大小:" << hex << (int)u.s[2] << '\t' << "地址:" << (void*)&u.s[2] << endl;
    cout << "数值大小:" << hex << (int)u.s[3] << '\t' << "地址:" << (void*)&u.s[3] << endl;
    system("pause");
    return 0;
}

输出结果:

char s[4]代表四个变量,但是可以用一个int来操作,直接int赋值,无论内存访问(指针大小的整数倍,访问才有效率),还是时间复杂度(一次和四次的区别,而且这四次有三次都是不整齐的地址),都会低一些

优点:多种访问内存的手段可以灵活读取任意部分数据,也可整体进行赋值。在某些寄存器或通道大小有限制的情况下,可以分多次搬运,具体例子见后续内容。

缺点:由于所有变量都能使用,容易使用错误的变量造成逻辑错误。

数据存储位置

接下来研究具体每种类型数值都存储在哪里。上一节的代码中:

int变量 n = 0xf1f2f3f4,在存储时低位f4放在低地址上,故存储顺序为:f4 f3 f2 f1。

char s[4]数组中下标低的,地址也低,按地址从低到高存储。故在输出时顺序为:f4 f3 f2 f1。

因此,当使用数组s读取数据,拼接起来转为Int时,需要反转才能得到正确的数字。

union应用之寄存器读取

设想用C语言实现这样一个功能。我需要用单片机读取一个监控温度的i2c slave的寄存器数据。这个寄存器是12位有效位寄存器。读出来之后我们要通过数据手册给定的公式计算成实际温度(设想这个公式为 temp = reg_val *10)。我们怎么实现呢?要知道,i2c的数据传输是按照byte传送的,也就是说,你只能用char类型结束数据,说白了,每个时序你只能接收8个bit的数据。所以12个bit需要读两次,用两个char类型变量或一个char类型数据接收。

具体步骤如下:

  1. 读出寄存器数据(这个不在这篇文章的讨论范围内)。

  2. 将读出的数据转换成可计算的数据类型(两个char类型转换成一个short或int或float类型)。

  3. 根据公式计算。

下面看一下不用union实现的函数

int fun( void ) {
    int tmp_value = 0; 
    char reg_val[2] = {0,0}; 
    .... 
    i2c.read(addr<<1, reg_val, 2); 
    tmp_value = (reg_val[1]<<8 | reg_val[0]); 
    return tmp_value*10; 
}

用union

union REG_VAL { 
    int value; 
    char buf[2]; 
}reg_val; 
 
int fun( void ) { 
    .... 
    i2c.read(addr<<1, reg_val.buf, 2);
    return reg_val.value*10; 
}

可以看到虽然在这里用union的代码比不用union的多了几行,但是i2c sensor如果多的话,那就会少很多,而且i2c sensor的寄存器有效位数不是一样的,这个用两个char类型就解决了,但是其他的可能需要用三个,所以用最上面定义的union变量可以很好的实现,不需要考虑各种转换问题。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号