一篇文章带你由浅入深去感受四种自定义类型的魅力——结构体类型,位段,枚举类型,联合体(共用体)类型

Posted 凛音Rinne

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一篇文章带你由浅入深去感受四种自定义类型的魅力——结构体类型,位段,枚举类型,联合体(共用体)类型相关的知识,希望对你有一定的参考价值。

一篇文章让你由浅入深去感受四种自定义类型的魅力——结构体类型,位段,枚举类型,联合体(共用体)类型

老规矩,笔记放在目录前自取
自定义类型笔记—————欢迎自取



一、结构体类型

1. 创建结构体变量

组合类型-创建自定义类型

typedef struct Stu
{
 int x;
}mine;

struct Book//内部类型定义
{
    char name[30];
    float price;
    char id[20];
    mine a;
}b1, b2;//这里可以直接写b1 = {"c语言",63.2f,"A1", {1}};
//注意分号不能漏掉
//此处b1,b2可以省略,b1,b2为全局变量

struct Book b3;//也是全局变量

int main()
{
    struct Book b4={"c语言",63.2f,"A1", {1}};//内部成员初始化
    printf("%d", b4.a.x);//打印1
    return 0}
//简单访问其中成员

1.1 创建隐藏结构体变量

将变量名舍去

如果用指针指向它会怎么样

struct    //直接省略变量名
{
    char name[30];
    float price;
    char id[20];
}b1, b2;

struct
{
    char name[30];
    float price;
    char id[20];
}*p;

int main()
{
    *p = &b1;//这样的写法是错误的
    //编译器会认为,上面两种是不同类型,虽然类型的内容相同
    
    return 0;
}

1.2 结构体变量的重命名

利用typedef

typedef struct Stu
{
 int x;
}mine;//重新命名

int main()
{
 struct Stu a;
 mine b;
 //以上两种写法均可

 return 0;
}

2. 结构体的自引用

先了解一下,数据结构里面的链表结构

链表中,元素是没有顺序的,但可以通过链表链接


假设:如果像函数中嵌套循环的方式,每个节点都可以传送到下一个节点,可否实现链表的引用

struct Node
{
 int ID;
 struct Node n;
}

但是,问题出现了,如果这样循环下去,什么时候停止,它不像普通的变量的大小可以通过条件语句停止

所以,这个假想不成立


那么,一个节点是肯定要有个东西,让我们可以访问到下一个节点,但不会死循环

这个东西,就是指针

struct Node
{
 int ID;
 struct Node* n;//指向下一个节点的地址就好了
}

上面就实现了,结构体的自引用


typedef 重命名结构体 自引用

typedef struct Node
{
int ID;
struct Node* n;
}Node;

3. 结构体类型对齐

结构体变量类型的大小

struct s
{
 int x;
 int y;
 char a;
};

int main()
{
 printf("%d", sizeof(struct s));

 return 0;
}

那么是否是 int(4) + int(4) + char(1) 一共九个字节的大小呢?

打印出来结果是12

显然,不是简单的组成结构体中各类型相加

那么,具体的规则到底是什么?


结构体变量对齐规则

  1. 第一个成员在与结构体变量偏移量为0的地址处

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处

    对齐数 = 编译器默认的一个对齐数 与 该成员大小中 较小值

    • VS 默认的值为8
    • Linux 没有默认值(成员自身大小就是对齐数)
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍

  4. 如果嵌套了结构体的情况,

    嵌套的结构体对齐到自己的最大对齐数的整数倍处

    结构体的整体大小(字节数)就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍


图解:


为什么要内存对齐?

  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
    • 有些硬件只能在某种规律下访问,所以数据最好在特定的位置上
  2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
    • 一次访问4个字节,如果不对齐,则需要访问两次(比如一个char和一个int类型连在一起)
    • 而对齐则只需要访问一次,且是完整的数据

结构体类型为了对齐,浪费那么多空间怎么办

  • 我们只能将小的变量放在一起
  • 这样就能尽可能节省空间

4. 改变结构体变量默认对齐数

利用 #pragma pack进行修改和恢复

#pragma pack(1) //相当于没有对齐,空间小,但效率可能没那么高了
struct stu
{
 int x;
 int y;
 char a;
};

#pragma pack() //用完结构体后,恢复默认对齐数

int main()
{
 struct stu a;
 printf("%d", sizeof(a));//打印9
}

5. 结构体类型每个变量的偏移量

利用 offsetof 这个函数

offsetof

头文件:<stddef.h>

size_t offsetof( structName, memberName );

参数:结构体类型名,结构体成员名

返回值:返回指定成员起始位置的字节偏移量


#include <stdio.h>
#include <stddef.h>
struct s
{
 int x;
 int y;
 char a;
};

int main()
{
	printf("%d", offsetof(struct s, x));//0
	printf("%d", offsetof(struct s, y));//4
    printf("%d", offsetof(struct s, a));//8
    return 0;
}

6. 结构体传参

引用符号 ->

访问符号 .

struct Book
{
 char name[30];
 float price;
 char id[20];
};

void Print(struct Book* b)
{
 printf("书名:%s\\n",(*b).name);
 //结构体指针 ->  成员名/x->name与上面等价
 printf("价格:%f\\n",(*b).price);
 printf("书名:%s\\n",(*b).id);
}
int main()
{
 struct Book b1 = { "c语言",63.2f,"A1" };//内部成员初始化 
 Print(&b1);
}

为了节省函数创建临时变量的空间大小,创建一个相同大小的结构体接受值也是可以的,但为了节省空间,我们将指针传进去,更好的节省空间大小,我们操作通过指针去操作

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降


二、位段

位段是什么:

位段和结构体类似

但又有区别:

  • 位段的成员必须是int、unsigned int 或signed int 和 char(char属于整型家族的)

  • 位段的成员名后边有一个冒号和一个数字, _ 不是必要的


位段可以节省空间

位段的位 —— 二进制位(1bit)

struct s
{
 int _a:2;//_a分配2个bit位
 int _b:4;//_b分配4个bit位
 int _c:10;//_c分配10个bit位

};

int main()
{
 printf("%d", sizeof(s));//16个bit位--4个字节
}


但事实上,有时不是正好是整字节,位段其实还是会浪费一点空间

所以位段到底是怎么分配内存空间的?


1. 位段的内存空间的分配

位段空间的需要,是按照char(1个字节)或者 int(4个字节)形式去开辟的

位段涉及很多不确定因素,位段是不跨平台的,不同编译器编译出来的结果可能不同,注重可移植的程序应该避免使用位段

但c语言标准里面没有说明,开辟的字节中,从低位到高位存储,还是从高位到低位存储

我们首先假设一个方向,每个字节从高位向地位存储

通过一个例子来看:

struct stu
{
    char a:6;//分配6bit
    char b:5;//分配5bit
    char c:4;//分配4bit
    char d:2;//分配2bit
};

int main()
{
    struct stu s = { 0 };//初始化结构体
    
    s.a = 10;
    s.b = 15;
    s.c = 20;
    s.d = 3;
    
    return 0;
}

分析:

a : 因为只分配了6个bit位,所以10的二进制存储时发生了截断,只存储了001010这几个数

其他变量分析同a,详细见图

最后我们运行一下,看是否如同我们假设一样

调试起来,等变量赋值之后,查看内存,和我们分析的一样,是0a0f34

但再次强调,每个编译器不同,不同的编译器可能会得出不同的结果,此结果只支持VS2019


2. 位段跨平台问题

  • 创建int位段时候,有无符号是未知
  • 位段中最大数目不确定(32位机器最大32,写成40,会出问题)
  • 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
  • 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,未知

在网络传送信息包装的时候,用位段节省空间,可以使得效率变高


三、枚举

描述生活中可以列举的东西

1. 枚举类型的定义

枚举类型的取值只能是正数,枚举类型的大小也是4个字节

//定义一个性别枚举常量
enum sex
{
    //默认从0开始,向下递增,但也可以字节定义数字
    male,
    female,
    secret
};

int main()
{
    enum sex a = male;
    //错误操作
    //enum sex a = 4;//整型和枚举类型不匹配
    //male = 4; //枚举常量是常量不能赋值,但可以在enum sex中赋值
    
    printf("%d\\n", male);//printf打印出0
    printf("%d\\n", female);//printf打印出1
    printf("%d\\n", secret);//printf打印出2    
   
    return 0}

2. 枚举的优点

枚举和define相似,但效果截然不同,枚举的优点有以下几点

  • 增加代码的可读性和可维护性,比如某些情境下选择选项的数字,换成字符,更容易理解
  • 和#define定义的标识符比较枚举有类型检查,更加严谨
  • 防止了命名污染(封装),相比较define全局变量要好
  • 便于调试,调试器在检验枚举变量时,可以显示符号值
  • 使用方便,一次可以定义多个常量,而#define宏一次只能定义一个

举个栗子吧

只是个大概的模型,并不是完整代码,简单感受一下

enum menu
{
    START,
    SAVE,
    EXIT    
};

void print_menu()
{
    printf("1.开始游戏 2.保存游戏 0.退出游戏");
}

int main()
{
    int input = 0;
    scanf("%d", &input);
    
    //switch(input)
    //{
        //case 1 : printf("开始游戏");break;
        //case 2 : printf("保存游戏");break;
        //case 0 : printf("退出游戏");break;
    //}
    //当我们选择0,1,2的时候,还需要看一下每个选项对应的数字
    switch(input)
    {
        case START: printf("开始游戏");break;
        case SAVE: printf("保存游戏");break;
        case EXIT: printf("退出游戏");break;
            
    }
    //但此时我们就不需要了,直接输入我们想要选择的选项就行
    
}

四、联合体(共用体)

1. 联合体变量的定义

先用一串代码感受一下,为什么它叫共用体

union U
{
    char a;
    int b;
};

int main()
{
    union U u = { 0 };//初始化
    
    printf("%p", &u);
    printf("%p", &(u.a));
    printf("%p", &(u.b));
    //让我们看看联合体类型变量内存的分配是如何的?
    return 0;
}

结果是

居然是相同的地址!到底是怎么实现的?

a和b共同利用这块空间,所以联合体变量大小至少是成员中最大成员的大小


2. 联合体变量的优点

使用联合体巧妙使用联合体计算计算机大小端存储

我们先用之前的知识实现一下

int main()
{
    int a = 1;
    char* p = (char*)a;
    
    if(*p == 1)
    {
        printf("小端\\n");
    }
    else
    {
        printf("大端\\n");
    }
    
    return 0;
}

我们还可以使用结构体来实现此功能

这样我们就可以不用指针了

union U
{
    char a;
    int b;
};

int main()
{
    union U u = { 0 };
    u.b = 1;
    if(u.b == 1)
    {
        printf("小端");
    }
    else
    {
        printf("大端");
    }
}

3. 联合体大小的计算

前面说,联合体类型大小至少是成员中最大成员的大小

但还有一个要求,就是当联合体成员中最大成员大小不是对齐数的整数倍时,

对齐到对齐数的整数倍


举个栗子

union U
{
    char a[3];
    int b;
};
union UU
{
    char a[13];
    int b;
};

int main()
{
    printf("%d\\n", sizeof(union U));//4
    printf("%d\\n", sizeof(union UU));//16

    return 0;
}

我们来分析:

关于union U

关于union UU


以上是关于一篇文章带你由浅入深去感受四种自定义类型的魅力——结构体类型,位段,枚举类型,联合体(共用体)类型的主要内容,如果未能解决你的问题,请参考以下文章

由浅入深,带你探索C语言指针的魅力

由浅入深,带你探索C语言指针的魅力

一篇文章带你领略Android混淆的魅力

一起来感受下eventfd的魅力(一eventfd使用介绍)

手写自定义springboot-starter,感受框架的魅力和原理

老Python带你从浅入深探究Tuple