• 【C语言三种自定义类型】


    前言

    大家好,我是熊猫,今天我们来认识一下C语言中的自定义数据类型,
    C语言中的char,short,int,long,float,double这些类型我们大家肯定已经非常熟悉了,
    这些都属于C语言自身所带的类型,但是在我们的日常生活中只具有单一属性的事物少之又少,
    更多的是同时具有各种各样的不同属性,
    比如作为在校大学生的我:“姓名”,“年龄”,“班级”,“学号”,“身高”等等等,
    再比如一本书:“书名”,“作者”,“编号”,“价格”等等等
    这些都不是用一个简单的数据类型就可以表示的,那么,我们就需要用到这些自定义类型
    来定义我们需要的数据类型。下面就让我们从结构体开始认识吧!


    一、结构体(struct)

    结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

    (一)结构体的声明

    1.结构的声明

    struct tag{
    member—list;
    };
    member—list是成员列表

    例如描述一本书籍:

    struct book{
    char title[20];//书名
    char writer[20];//作者
    float price;//价格
    };
    

    这里的struct是结构体关键字,book是结构体标签,struct book才是一个完整的结构体名。
    在创建结构体变量是必须写完全。
    创建结构体有两种方法:
    一种是直接在声明结构时直接创建,这种创建出来的是全局变量,
    另一种是通过结构体类型创建。

    例如:

    struct book{
    char title[20];//书名
    char writer[20];//作者
    float price;//价格
    }b1,b2;
    
    struct book b3,b4;
    

    2.特殊的声明(不完全声明)

    我们在声明一个结构体时,可以不给它“起名字”,这个称为不完全声明

    例如:

    struct {
    char ch;
    char str[20];
    int num;
    }d1,d2;//只能在声明结构体的同时创建变量
    //struct d3,d4; 这种是错误的,因为我们只有结构体关键字,而并不知道这个结构体的名字
    
    struct {
    char ch;
    char str[20];
    int num;
    }d3,d4;
    //这里我们需要注意:这里的d3和d4是相同的,
    //但是d3和d1、d2是不同的,虽然它们的类型看起来“完全一样”
    //但是编译器还是会判定为不同的类型
    

    3.结构体的自引用

    在结构中包含一个为结构体本身的类型的变量

    例如:

    struct str{
    int data;
    struct str* ps;
    };
    //在这里我们可以设置一个指向自身类型的指针,这属于数据结构中的链表的用法.
    //struct str{
    //int data;
    //struct str s;
    //};
    //这里的用法是不行的,这里s变量里还有一个结构体变量s,s变量里面还有一个s,属于无限套娃,无法计算内存大小.
    

    4.结构体的初始化与赋值

    看代码:

    //结构体的初始化
    struct str{
    char ch;
    int data;
    }s1={'a',10};  //  	1
    struct str s2={'b',20};	   //  2
    struct str s3={.data=30,.ch='c'};	//  3
    
    //结构体的赋值
    struct str s4;
    scanf("%c %d",&s4.ch,&s4.data);
    //如果是结构体指针,那就有两种赋值方式
    struct *p=s4;
    scanf("%c %d",&(*p).ch,&(*p).data);//  *p等同于s4
    scanf("%c %d",&p->ch,&p->data);//->为箭头运算符,可以令结构体指针直接指向结构体成员
    

    5.结构体内存对齐

    结构体的内存对齐是结构体的一个很重要的知识,这个与结构体在内存中的存储方式有关

    下面我们先来计算一下下面这两个结构体的大小:

    struct str1
    {
    char ch;
    short sh;
    int num;
    };
    
    struct str2
    {
    char ch;
    int num;
    short sh;
    };
    

    这里如果我们不知道结构体的内存对齐规则,那么肯定有很多朋友会认为这两个结构体的大小都是7,
    sizeof(str1)=1+2+4=7,
    sizeof(str2)=1+4+2=7;
    那么既然我们专门讲了这个例子的话那就说明这是错误的结果了,
    那么到底是第一个错了还是第二个错了还是两个大小都不对呢,

    下面看实际运行结果:
    在这里插入图片描述

    这里为什么会出现这样的结果呢?
    这里我们先来了解一下结构体内存对齐的规则。

    内存对齐规则:
    1. 第一个成员在与结构体变量偏移量为0的地址处。
      2.从第二个成员开始,偏移量必须是 对齐数(默认对齐数与它自身大小中的较小者) 的整数倍。
      3.结构体总大小为最大对齐数的整数倍。
      4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
      体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

    下面我们通过画图进行详细了解:
    在这里插入图片描述

    我们也可以通过offsetof()函数来得到哥哥成员的偏移量来进行验证:

    代码如下:

    #include
    #include
    
    struct str1
    {
    	char ch;
    	short sh;
    	int num;
    };
    
    struct str2
    {
    	char ch;
    	int num;
    	short sh;
    };
    
    int main()
    {
    	printf("offsetof(struct str1, ch) = \t%d\n", offsetof(struct str1, ch));
    	printf("offsetof(struct str1, sh) = \t%d\n", offsetof(struct str1, sh));
    	printf("offsetof(struct str1, num) = \t%d\n", offsetof(struct str1, num));
    	printf("offsetof(struct str2, ch) = \t%d\n", offsetof(struct str2, ch));
    	printf("offsetof(struct str2, num) = \t%d\n", offsetof(struct str2, num));
    	printf("offsetof(struct str2, sh) = \t%d\n", offsetof(struct str2, sh));
    	return 0;
    }
    

    在这里插入图片描述

    为什么存在内存对齐?
    大部分的参考资料都是如是说的:
    1.平台原因(移植原因):
    不是所有的硬件平台都能访问任意地址上的任意数据的;
    某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
    2. 性能原因:
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
    原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
    总体来说:
    结构体的内存对齐是拿空间来换取时间的做法。
    我们上面的两个例子中成员是完全相同的,但是一个大小为8,一个却为12;
    那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
    让占用空间小的成员尽量集中在一起。


    6.修改默认对齐数

    修改对齐数需要用到预处理指令#pragma

    代码实现:

    #include
    
    #pragma pack(1)//设置默认对齐数为1
    struct str1
    {
    	char ch;
    	short sh;
    	int num;
    };
    
    struct str2
    {
    	char ch;
    	int num;
    	short sh;
    };
    int main()
    {
    
    printf("%d\n", sizeof(struct str2));
    return 0;
    }
    

    在这里插入图片描述在这里插入图片描述

    还原默认对齐数:

    #include
    
    #pragma pack(1)//设置默认对齐数为1
    #pragma pack()//还原默认对齐数
    struct str1
    {
    	char ch;
    	short sh;
    	int num;
    };
    
    struct str2
    {
    	char ch;
    	int num;
    	short sh;
    };
    int main()
    {
    
    printf("%d\n", sizeof(struct str2));
    return 0;
    }
    

    在这里插入图片描述


    7.结构体传参

    我们在进行函数传参时既可以进行传值传参也可以进行传址传参
    结构体也同样可以使用以上两种方法

    struct S {
     int data[1000];
     int num;
    };
    struct S s = {{1,2,3,4}, 1000};
    //传值传参
    void print1(struct S s) {
     printf("%d\n", s.num);
    }
    //传址传参
    void print2(struct S* ps) {
     printf("%d\n", ps->num);
    }
    int main()
    {
     print1(s);  //传结构体
     print2(&s); //传地址
     return 0; }
    '
    运行

    如上面这种情况,
    结构体非常大,如果我们进行传址传参的话形参是实参的一份临时拷贝,
    编译器就会在内存中开辟一块和实参一样大的区域存放形参,这样做会浪费很大的空间,
    而使用传址传参就只是传出一个指针,而一个指针大小无非是4/8个字节,
    因此,我们在进行结构体传参时更建议使用传址传参。

    (二)位段

    结构体讲完就得讲讲结构体实现 位段 的能力。
    我想,大多数同学都没有听说过位段这个概念吧,所以接下来我们就不卖关子,
    直接通过下面的实例来了解它。

    1.位段的声明

    位段的声明和结构体是类似的,有两个不同:
    1.位段的成员必须是 int、unsigned int 、signed int 或 char 的整形家族。
    2.位段的成员名后边有一个冒号和一个数字。

    代码实例:

    #include
    struct str
    {
    int a:4;
    int b:10;
    int c:20;
    int d:8;
    };
    
    int main()
    {
    printf("%d\n",sizeof(struct str));
    return 0;
    }
    
    

    运行结果:
    在这里插入图片描述
    在这里插入图片描述

    2.位段的使用

    这里关于位段的知识不进行过多赘述,我们知道有这个知识点就好,
    当然他也有自己的使用场景:计算机网络里对数据的分段传输时需要加上描述信息,这时就可以使用位段,
    可以对空间进行合理地使用。

    在这里插入图片描述


    二、枚举(enum)

    枚举顾名思义就是–一一列举,把可能的情况全部都列举出来
    一周有七天,可以一一列举,
    一天有二十四个小时,可以一一列举,
    英文字母有二十六个,也可以一一列举。

    1.枚举类型的定义

    enum Day//星期
    {
    	 MON,
    	 TUES,
    	 WED,
    	 THUR,
    	 FRI,
    	 SAT,
    	 SUN
    };
    

    这里枚举类型默认从0开始,既:
    MON == 0 , TUES == 1 , WED == 2 ……
    在初始化时可以更改他们的值,eg:
    MON = 3,
    那么TUES就会变为4,往后依次增大1


    2.枚举的优点

    为什么使用枚举?
    我们可以使用 #define 定义常量,为什么非要使用枚举?
    枚举的优点:
    1.增加代码的可读性和可维护性
    2.和#define定义的标识符比较枚举有类型检查,更加严谨。
    3.防止了命名污染(封装)
    4.便于调试
    5.使用方便,一次可以定义多个常量


    3.枚举的使用

    enum Day
    {
     MON,
     TUES,
     WED
     THUR,
     FRI,
     SAT,
     SUN
    };
    
    int main()
    {
    	 enum Day d;
    	 scanf("%d",&d);
    	 switch(d)
    	 {
    	 case MON:
    		 printf("星期一\n");
    	 	break;
    	 case TUES:
    	 	 printf("星期二\n");
    	 	break;
    	 case WED:
    	 	printf("星期三\n");
    	 	break;
    	 case THUR:
    	 	printf("星期四\n");
    	 	break;
    	 case FRI:
    	 	printf("星期五\n");
    	 	break;
    	 case SAT:
    	 	printf("星期六\n");
    	 	break;
    	 case SUN:
    	 	printf("星期日\n");
    	 	break;
    	 	}
    return 0;
    }
    

    三、联合(union)

    1.联合类型的定义

    联合体我们也顾名思义一下那就是–站在一起,共同使用。
    联合体也是一个特殊的自定义类型,可以包含不同的成员,而这些成员共同使用同一块内存空间。(所以也叫公用体)

    union un
    {
    	int num;
    	float fa;
    	char str[10];
    };
    

    2.联合的特点

    联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联
    合至少得有能力保存最大的那个成员)。


    3.联合的使用

    经典例题:判断该计算机是大端存储还是小端存储

    int main()
    {
    	int a=0x1;
    	//p存放的是变量a的首地址(也就是低地址),
    	//因为小端存储时低位放在低地址处,所以当*p为1是则为小端存储,*p为0则是大端存储
    	char*p=(char*)&a;
    	printf("%d\n",*p);
    	return 0;
    }
    

    上面我们是使用了强制类型转换的方法取得了a的地址,但是根据今天我们讲的共用体,我们就可以设计一种更巧妙的方法进行判断

    如下:

    union un
    {
    	int a;
    	char ch;
    };
    
    int main()
    {
    	union un d = { 0 };
    	scanf("%d", &d.a);
    	printf("%d\n", d.ch);
    	return 0;
    }
    '
    运行

    4.联合大小的计算

    联合体也有对齐数,
    联合的大小至少是最大成员的大小。
    当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

    union un1
    {
    char ch1;
    char ch2;
    int data;
    };
    
    union un2
    {
    char ch1;
    char str[10];
    int data;
    };
    
    int main()
    {
    	printf("%zu\n",sizeof(union un1));
    	printf("%zu\n",sizeof(union un2));
    	return 0;
    }
    

    运行结果:
    在这里插入图片描述

    我是在VS下测试的,VS的默认对齐数为8

    下面看图解:
    在这里插入图片描述


    总结

    以上就是关于结构体、枚举、以及联合的知识,这里我再写几点熊猫自己的总结:

    1. 结构体和联合体都需要内存对齐,设计时尽量将小变量放在一起,内存对齐有时会造成内存的浪费,但是却可以提高成员访问速度,
      也就是我们常说的用内存换时间。
    2. 结构体位段的存在就是为了节省空间,所以位段不需要内存对齐,使用位段时要注意成员后面的 “:” 以及分配的比特位。
    3. 枚举类型各个成员之间是通过 “,” 连接的,也就是说枚举类型实际上只有一个变量,因此:sizeof(enum day)== 4。
    4. 在定义自定义类型时要注意大括号后面的 “;” ,这是一条语句结束的标志,如果有的编译器没有自动给出我们也不能忘记。

    那么今天的内容就写到这里,感谢大家的支持,欢迎大家来评论区一起探讨,大家的鼓励是在这里插入图片描述继续更新的巨大动力。

  • 相关阅读:
    网课没有摄像头,手机如何变成电脑摄像头?
    【Java】类和对象知识
    U_BOOT_DRIVER简析
    u-boot对设备树的支持__传递dtb给内核
    适用于音视频的弱网测试整理
    CentOS 7.3 Linux系统安装过程介绍
    Kubernetes CI/CD 实战:5分钟部署你的第一个应用
    Java(常用类01)
    第2章_瑞萨MCU零基础入门系列教程之面向过程与面向对象
    C++程序员入门需要怎么学?(InsCode AI 创作助手)
  • 原文地址:https://blog.csdn.net/m0_66363962/article/details/126922187