• 从零开始探索C语言(十一)----共用体和位域


    1. 共用体

    共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。

    1.1 定义共用体

    为了定义共用体,必须使用 union 语句,方式与定义结构类似。

    union 语句定义了一个新的数据类型,带有多个成员。

    union 语句的格式如下:

    union [union tag]
    {
       member definition;
       member definition;
       ...
       member definition;
    } [one or more union variables];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    union tag 是可选的,每个 member definition 是标准的变量定义,比如 int i; 或者 float f; 或者其他有效的变量定义。

    在共用体定义的末尾,最后一个分号之前,可以指定一个或多个共用体变量,这是可选的。下面定义一个名为 Data 的共用体类型,有三个成员 i、f 和 str:

    union Data
    {
       int i;
       float f;
       char  str[20];
    } data;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    现在,Data 类型的变量可以存储一个整数、一个浮点数,或者一个字符串。

    这意味着一个变量(相同的内存位置)可以存储多个多种类型的数据,我们可以根据需要在一个共用体内使用任何内置的或者用户自定义的数据类型。

    共用体占用的内存应足够存储共用体中最大的成员。例如,在上面的实例中,Data 将占用 20 个字节的内存空间,因为在各个成员中,字符串所占用的空间是最大的。

    下面的实例将显示上面的共用体占用的总内存大小:

    #include 
    #include 
     
    union Data
    {
       int i;
       float f;
       char  str[20];
    };
     
    int main( )
    {
       union Data data;        
     
       printf( "Memory size occupied by data : %d\n", sizeof(data));
     
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    当上面的代码被编译和执行时,它会产生下列结果:

    Memory size occupied by data : 20
    
    • 1

    1.2 访问共用体成员

    为了访问共用体的成员,我们使用成员访问运算符(.),成员访问运算符是共用体变量名称和我们要访问的共用体成员之间的一个句号。

    我们可以使用 union 关键字来定义共用体类型的变量。

    下面的实例演示了共用体的用法:

    #include 
    #include 
     
    union Data
    {
       int i;
       float f;
       char  str[20];
    };
     
    int main( )
    {
       union Data data;        
     
       data.i = 10;
       data.f = 220.5;
       strcpy( data.str, "C Programming");
     
       printf( "data.i : %d\n", data.i);
       printf( "data.f : %f\n", data.f);
       printf( "data.str : %s\n", data.str);
     
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    当上面的代码被编译和执行时,它会产生下列结果:

    // linux 系统下
    data.i : 1917853763
    data.f : 4122360580327794860452759994368.000000
    data.str : C Programming
    
    // windows系统下
    data.i : 1917853763
    data.f : 4122360580327794900000000000000.000000
    data.str : C Programming
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里,我们可以看到共用体的 i 和 f 成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是 str 成员能够完好输出的原因。

    现在让我们再来看一个相同的实例,这次我们在同一时间只使用一个变量,这也演示了使用共用体的主要目的:

    #include 
    #include 
     
    union Data
    {
       int i;
       float f;
       char  str[20];
    };
     
    int main( )
    {
       union Data data;        
     
       data.i = 10;
       printf( "data.i : %d\n", data.i);
       
       data.f = 220.5;
       printf( "data.f : %f\n", data.f);
       
       strcpy( data.str, "C Programming");
       printf( "data.str : %s\n", data.str);
     
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    当上面的代码被编译和执行时,它会产生下列结果:

    data.i : 10
    data.f : 220.500000
    data.str : C Programming
    
    • 1
    • 2
    • 3

    在这里,所有的成员都能完好输出,因为同一时间只用到一个成员。

    2. 位域

    C 语言的位域(bit-field)是一种特殊的结构体成员,允许我们按位对成员进行定义,指定其占用的位数。

    如果程序的结构中包含多个开关的变量,即变量值为 TRUE/FALSE,如下:

    struct
    {
      unsigned int widthValidated;
      unsigned int heightValidated;
    } status;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这种结构需要 8 字节的内存空间,但在实际上,在每个变量中,我们只存储 0 或 1,在这种情况下,C 语言提供了一种更好的利用内存空间的方式。如果在结构内使用这样的变量,可以定义变量的宽度来告诉编译器将只使用这些字节。

    例如,上面的结构可以重写成:

    struct
    {
      unsigned int widthValidated : 1;
      unsigned int heightValidated : 1;
    } status;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    现在,上面的结构中,status 变量将占用 4 个字节的内存空间,但是只有 2 位被用来存储值。

    如果你用了 32 个变量,每一个变量宽度为 1 位,那么 status 结构将使用 4 个字节,但只要你再多用一个变量,如果使用了 33 个变量,那么它将分配内存的下一段来存储第 33 个变量,这个时候就开始使用 8 个字节。

    让我们看看下面的实例来理解这个概念:

    #include 
    #include 
     
    /* 定义简单的结构 */
    struct
    {
      unsigned int widthValidated;
      unsigned int heightValidated;
    } status1;
     
    /* 定义位域结构 */
    struct
    {
      unsigned int widthValidated : 1;
      unsigned int heightValidated : 1;
    } status2;
     
    int main( )
    {
       printf( "Memory size occupied by status1 : %d\n", sizeof(status1));
       printf( "Memory size occupied by status2 : %d\n", sizeof(status2));
     
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    当上面的代码被编译和执行时,它会产生下列结果:

    Memory size occupied by status1 : 8
    Memory size occupied by status2 : 4
    
    • 1
    • 2

    位域的特点和使用方法如下:

    1. 定义位域时,可以指定成员的位域宽度,即成员所占用的位数。
    2. 位域的宽度不能超过其数据类型的大小,因为位域必须适应所使用的整数类型。
    3. 位域的数据类型可以是 int、unsigned int、signed int 等整数类型,也可以是枚举类型。
    4. 位域可以单独使用,也可以与其他成员一起组成结构体。
    5. 位域的访问是通过点运算符(.)来实现的,与普通的结构体成员访问方式相同。

    2.1 位域声明

    有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有 0 和 1 两种状态,用 1 位二进位即可。为了节省存储空间,并使处理简便,C 语言又提供了一种数据结构,称为"位域"或"位段"。

    所谓"位域"是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

    典型的实例:

    用 1 位二进位存放一个开关量时,只有 0 和 1 两种状态。
    读取外部文件格式——可以读取非标准的文件格式。例如:9 位的整数。

    2.2 位域的定义和位域变量的说明

    位域定义与结构定义相仿,其形式为:

    struct 位域结构名 
    {
    
     位域列表
    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    其中位域列表的形式为:

    type [member_name] : width ;
    
    • 1

    下面是有关位域中变量元素的描述:

    type 只能为 int(整型),unsigned int(无符号整型),signed int(有符号整型) 三种类型,决定了如何解释位域的值。
    member_name 位域的名称。
    width 位域中位的数量。宽度必须小于或等于指定类型的位宽度。

    带有预定义宽度的变量被称为位域。位域可以存储多于 1 位的数,例如,需要一个变量来存储从 0 到 7 的值,您可以定义一个宽度为 3 位的位域,如下:

    struct
    {
      unsigned int age : 3;
    } Age;
    
    • 1
    • 2
    • 3
    • 4

    上面的结构定义指示 C 编译器,age 变量将只使用 3 位来存储这个值,如果您试图使用超过 3 位,则无法完成。

    struct bs{
        int a:8;
        int b:2;
        int c:6;
    }data;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    以上代码定义了一个名为 struct bs 的结构体,data 为 bs 的结构体变量,共占四个字节:

    对于位域来说,它们的宽度不能超过其数据类型的大小,在这种情况下,int 类型的大小通常是 4 个字节(32位)。

    相邻位域字段的类型相同,且其位宽之和小于类型的 sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止。

    让我们再来看一个实例:

    struct packed_struct {
      unsigned int f1:1;
      unsigned int f2:1;
      unsigned int f3:1;
      unsigned int f4:1;
      unsigned int type:4;
      unsigned int my_int:9;
    } pack;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    以上代码定义了一个名为 packed_struct 的结构体,其中包含了六个成员变量,pack 为 packed_struct 的结构体变量。

    在这里,packed_struct 包含了 6 个成员:四个 1 位的标识符 f1…f4、一个 4 位的 type 和一个 9 位的 my_int。

    让我们来看下面的实例,

    实例 1:

    #include 
    
    struct packed_struct {
       unsigned int f1 : 1;   // 1位的位域
       unsigned int f2 : 1;   // 1位的位域
       unsigned int f3 : 1;   // 1位的位域
       unsigned int f4 : 1;   // 1位的位域
       unsigned int type : 4; // 4位的位域
       unsigned int my_int : 9; // 9位的位域
    };
    
    int main() {
       struct packed_struct pack;
    
       pack.f1 = 1;
       pack.f2 = 0;
       pack.f3 = 1;
       pack.f4 = 0;
       pack.type = 7;
       pack.my_int = 255;
    
       printf("f1: %u\n", pack.f1);
       printf("f2: %u\n", pack.f2);
       printf("f3: %u\n", pack.f3);
       printf("f4: %u\n", pack.f4);
       printf("type: %u\n", pack.type);
       printf("my_int: %u\n", pack.my_int);
    
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    以上实例定义了一个名为 packed_struct 的结构体,其中包含了多个位域成员。

    在 main 函数中,创建了一个 packed_struct 类型的结构体变量 pack,并分别给每个位域成员赋值。

    然后使用 printf 语句打印出每个位域成员的值。

    输出结果为:

    f1: 1
    f2: 0
    f3: 1
    f4: 0
    type: 7
    my_int: 255 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    实例 2:

    #include 
    #include 
     
    struct
    {
      unsigned int age : 3;
    } Age;
     
    int main( )
    {
       Age.age = 4;
       printf( "Sizeof( Age ) : %d\n", sizeof(Age) );
       printf( "Age.age : %d\n", Age.age );
     
       Age.age = 7;
       printf( "Age.age : %d\n", Age.age );
     
       Age.age = 8; // 二进制表示为 1000 有四位,超出
       printf( "Age.age : %d\n", Age.age );
     
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    当上面的代码被编译时,它会带有警告,当上面的代码被执行时,它会产生下列结果:
    在这里插入图片描述

    Sizeof( Age ) : 4
    Age.age : 4
    Age.age : 7
    Age.age : 0
    
    • 1
    • 2
    • 3
    • 4

    计算字节数:

    实例

    #include 
    
    struct example1 {
       int a : 4;
       int b : 5;
       int c : 7;
    };
    
    int main() {
       struct example1 ex1;
    
       printf("Size of example1: %lu bytes\n", sizeof(ex1));
    
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    以上实例中,example1 结构体包含三个位域成员 a,b 和 c,它们分别占用 4 位、5 位和 7 位。

    通过 sizeof 运算符计算出 example1 结构体的字节数,并输出结果:

    Size of example1: 4 bytes
    
    • 1

    对于位域的定义尚有以下几点说明:

    一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域,也可以有意使某位域从下一单元开始。

    例如:

    struct bs{
        unsigned a:4;
        unsigned  :4;    /* 空域 */
        unsigned b:4;    /* 从下一单元开始存放 */
        unsigned c:4
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这个位域定义中,a 占第一字节的 4 位,后 4 位填 0 表示不使用,b 从第二字节开始,占用 4 位,c 占用 4 位。

    位域的宽度不能超过它所依附的数据类型的长度,成员变量都是有类型的,这个类型限制了成员变量的最大长度,: 后面的数字不能超过这个长度。

    位域可以是无名位域,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:

    struct k{
        int a:1;
        int  :2;    /* 该 2 位不能使用 */
        int b:3;
        int c:2;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    从以上分析可以看出,位域在本质上就是一种结构类型,不过其成员是按二进位分配的。

    2.3 位域的使用

    位域的使用和结构成员的使用相同,其一般形式为:

    位域变量名.位域名
    位域变量名->位域名
    
    • 1
    • 2

    位域允许用各种格式输出。

    实例:

    #include 
     
    int main(){
        struct bs{
            unsigned a:1;
            unsigned b:3;
            unsigned c:4;
        } bit,*pbit;
        bit.a=1;    /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
        bit.b=7;    /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
        bit.c=15;    /* 给位域赋值(应注意赋值不能超过该位域的允许范围) */
        printf("%d,%d,%d\n",bit.a,bit.b,bit.c);    /* 以整型量格式输出三个域的内容 */
        pbit=&bit;    /* 把位域变量 bit 的地址送给指针变量 pbit */
        pbit->a=0;    /* 用指针方式给位域 a 重新赋值,赋为 0 */
        pbit->b&=3;    /* 使用了复合的位运算符 "&=",相当于:pbit->b=pbit->b&3,位域 b 中原有值为 7,与 3 作按位与运算的结果为 3(111&011=011,十进制值为 3) */
        pbit->c|=1;    /* 使用了复合位运算符"|=",相当于:pbit->c=pbit->c|1,其结果为 15 */
        printf("%d,%d,%d\n",pbit->a,pbit->b,pbit->c);    /* 用指针方式输出了这三个域的值 */
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    编译执行结果:

    1,7,15
    0,3,15
    
    • 1
    • 2

    上例程序中定义了位域结构 bs,三个位域为 a、b、c,说明了 bs 类型的变量 bit 和指向 bs 类型的指针变量 pbit,这表示位域也是可以使用指针的。

    2.4 位域小结

    位域是C语言中的一个高级特性,它允许你在结构体中以位为单位来分配内存,以便有效地存储和操作位数据。位域常用于处理硬件寄存器、压缩数据、或者在嵌入式系统中节省内存。以下是位域的使用详解:

    1. 位域的定义:位域是通过在结构体或联合体中的成员后面加上冒号和位宽来定义的。例如:

      struct Flags {
          unsigned int flag1 : 1;  // 1位用于表示flag1
          unsigned int flag2 : 2;  // 2位用于表示flag2
          unsigned int flag3 : 3;  // 3位用于表示flag3
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
    2. 位宽:位宽表示了每个位域所占用的位数。它必须是一个非负整数。

    3. 位域的取值和赋值:你可以像操作普通变量一样来操作位域的值。例如:

      struct Flags myFlags;
      myFlags.flag1 = 1;  // 设置flag1为1
      myFlags.flag2 = 2;  // 设置flag2为2
      
      • 1
      • 2
      • 3
    4. 位域的范围:每个位域的位宽决定了它可以表示的数值范围。在上面的例子中,flag1可以表示0或1,flag2可以表示0、1、2或3,flag3可以表示0到7。

    5. 注意位域的存储顺序:位域的存储顺序在不同编译器下可能不同。通常,它们从结构体的低位向高位存储,但这并不是C语言标准要求的。如果你需要确保特定的位域顺序,可以使用#pragma pack等编译指令。

    6. 位域的注意事项

      • 不同编译器对位域的实现可能有所不同,特别是在不同硬件平台上。
      • 位域应谨慎使用,因为它们可能导致可移植性问题。
      • 位域不能取地址,也不能用于数组。
      • 不同位域不能共享存储位置,因此它们不能重叠。

    总的来说,位域是C语言中的一个强大工具,但在使用时需要注意编译器的实现细节和平台兼容性,以确保程序的可靠性和可移植性。

  • 相关阅读:
    146.LRU缓存
    【数据结构】归并排序
    基于java+SpringBoot+VUE+Mysql社区家庭医生服务系统
    Spring-AOP-加强
    51单片机STC89C52RC——5.1 LCD1602液晶显示屏
    FreeRTOS教程7 事件组
    软件定制vs现成,定制软件开发的优势
    TUI界面容器管理工具Oxker
    SPARK中的wholeStageCodegen全代码生成--以aggregate代码生成为例说起(1)
    【MySQL】索引 详解
  • 原文地址:https://blog.csdn.net/weixin_44327634/article/details/133531852