自定义类型:结构体(内存对齐),枚举,联合

Posted 捕获一只小肚皮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义类型:结构体(内存对齐),枚举,联合相关的知识,希望对你有一定的参考价值。

自定义类型:结构体(内存对齐),枚举,联合

前言:

在c语言中,类型有两种:

  • 内置类型(比如char, short, int, long等)
  • 自定义类型(结构体, 枚举, 联合体)

区别: 内置类型是c语言自带的,我们不能进行修改. 自定义类型是我们自己根据实际需要进行自定义

内置类型,博主在此不再赘述,本章的重点是 自定义类型


1.结构体

1.1结构体类型的声明

1.1.1结构的基础知识

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

举个例子,比如:

  • 就是一个结构,而人具有 手,脚,五官,毛发,内脏,这些散的称为成员变量

  • 学生就是一个结构,学生具有 学号,性别,姓名,身高,年龄, 后面的东西就是 成员变量


1.1.2结构的声明语法

struct tag
{	
    
    member-list;    
    
}variable-list;

tag: 结构名,(自己取)

member-list: 成员变量

variable-list: 结构体变量名,(这里一般不在这里声明)

例如 描述一个学生.

struct Stu
{
 char name[20];//名字
 int age;//年龄
 char sex[5];//性别
 char id[20];//学号
}//分号不能丢

1.1.3特殊的声明(匿名结构体)

在声明结构的时候,可以不完全的声明(在上面我们说variable-list一般不在后面声明,但是这种特殊声明必须声明变量)

例如:

//匿名结构体x
struct
{
 int a;
 char b;
 float c; 
}x;

//匿名结构体数组a,和匿名结构体指针p
struct
{
 int a;
 char b;
 float c; 
}a[20], *p;

上面的两个结构在声明的时候省略掉了结构体标签(tag)。

对于这种类型的结构体,只能使用一次(指的是声明变量,不是x,a只能用一次),在下面的结构体的变量的定义初始化会讲到

问题: 匿名结构体中上式子这样写会怎样? p = &x;

会报警告,因为该结构是匿名的,就相当于不知道类型,而实际情况也是 编译器会把p&x看做两个完全不一样的东西


1.2结构的自引用

顾名思义: 就是类似函数递归,结构体自引用就是在自己内部创建同类型成员变量

比如下面这样创建:

struct Node
{
 int data;
 struct Node pp;
};

大家仔细想想,这样创建真的可以行得通吗?

答案是否定的,最好的打脸问句--------sizeof(struct Node)的大小是多少???是不是发现根本计算不出来?

那么,正确的在自己内部创建自己类型的成员变量怎么弄呢?大家先想想前面用过的知识再往下面看哦

答案是 指针.我们可以利用指针进行创建自己类型的成员变量.为什么呢??大家以后会学习数据结构的链表,而链表就是利用结构体实现的

,而链表中每一个小块(结点),都被前后一条链给连接.比如有下面的这样一个学生结构体:

struct student
{
    char name[20]; //姓名
 	int age;//年龄
    char number[11]; //电话
 	struct Node *next; //注意,这里是指针哦~
};

然后我用图来绘了一下 abcde5个学生

在这里插入图片描述

会发现,abcde五个学生都是一样的信息,且都是结构体类型,但是想要讲这五个人的信息一一连接,只能用结构体指针,

学生a有自己的 姓名,年龄,电话,存储好后,a里面还有个指针指向下一个学生b

学生b有自己的 姓名,年龄,电话,存储好后,a里面还有个指针指向下一个学生c

学生c有自己的…

结论: 结构体的自引用需要用结构体指针


1.2.1 typedef控制结构体命名

typedef可以为类型改名

比如:

typedef int Element 那么声明整型变量时候可以有这种写法 Element b = 10;

typedef float good 那么声明整型变量时候可以有这种写法 good c = 10.123;

在上面介绍 结构体自引用时候,我们说到要用结构体指针进行,没错.但是我们可以为了方便,改改类型名,比如:

typedef struct node
{
 	int data;
 	NODE* next; 
}NODE;

这样修改后,我们就不需要声明变量时候写的很多(比如:struct node * next 而是NODE* next)

那如果匿名结构体这样行吗?

typedef struct
{
 	int data;
 	Node* next; 
}Node;

答案: NO!, 为何?因为匿名结构体他的类型名是没有的,且匿名结构体只可以声明一次,那么修改为 Node后,在里面的Node*到底谁先执行?

就好比是 先有鸡还是先有蛋 所以匿名结构体不可以这样写


1.3结构体变量的定义和初始化

有了结构体类型,那如何定义变量? 其实很简单,有两种方法.

1.3.1结构体变量的定义

方法一: 在创建结构体时候就声明变量

struct Point
{
 	int x;
 	int y; 
}p1; //声明类型的同时定义变量p1   此时的p1是全局变量

方法二: 先创建好结构体,然后哪里需要就在哪里声明结构体

struct Point
{
 	int x;
 	int y; 
};

int main()
{
    struct Point p1; //在这里需要创建,就在这里声明   此时的p1是局部变量
    return 0; 
}

1.3.2结构体变量的初始化

初始化:定义变量的同时赋初值。不然等声明好了变量后,在给值会报错.

struct Student 
{
 char name[15];
 int age;      
} xiaoming = {"小明",15};     //全局变量初始化
    
    
int main()
{
    struct Student xiaohua = {"小花",18};   //局部变量初始化
    return 0;
}

错误初始化示例:

struct Student 
{
 char name[15];
 int age;      
};

    
int main()
{
    struct Student xiaohua;
    int a = 0;
    xiaohua = {"小花",18};
    return 0;
}

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


1.4结构体内存对齐

我们已经掌握了结构体的基本使用了。

现在我们深入讨论一个问题:计算结构体的大小。

这也是一个特别热门的考点: 结构体内存对齐


1.4.1结构体内存对齐规则

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

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

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

    VS中默认的值为8

  3. 结构体总大小为该结构体最大对齐数(每个成员变量都有一个对齐数)的整数倍

    最大对齐数 = 结构体中所有成员中的最大对齐数

  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

说了这么文绉绉和枯燥的文字,到底什么意思呢??我们一条一条语句的解释


第一条: 偏移量

我们知道,在计算机中内存被划分为一个个字节作为基本单元空间.而一个变量的存储地址是不确定的.比如有变量a,b,那么他们的地址可能会像下面这样:
在这里插入图片描述

他们的地址是不确定.而结构体变量同样,因此我为了后面方便叙说第一条什么意思,便假设某结构体变量在某个位置

现在有下面这样一个结构体变量:

struct S1
{
 char c1;
 int i;
 char c2;
} p1;

然后我们用图解释偏移量

在这里插入图片描述


第二条:对齐数

对齐数,即一个数字,按照对齐数规则 对齐数 = 编译器默认的一个对齐数 与 该 成员大小较小值

还是按照刚才那个结构体例子,且我使用的编译器是vusial studio,即默认对齐数是8:

struct S1
{
  char c1;     //大小为1个字节
  int i;       //大小为4个字节
  char c2;     //大小为1个字节
} p1;

c1的大小是 1,小于8,所以c1对齐数 是 1,但是其偏移量为0.

i 的大小是 4,小于8,所以i对齐数 是 4.其偏移量必须是4的整数倍

c2的大小是 1,小于8,所以c2对齐数是 1.其偏移量必须是1的整数倍

第二条中的对齐规则图例

在这里插入图片描述


第三条:结构体总大小

在这里插入图片描述

1.4.2结构体对齐题型训练:

第一题

struct S1
{
     char c1;
     char c2;
     int i;
}p1;

解析图:

在这里插入图片描述

第二题

struct S2
{
     double d;
     char c;
     int i;
}p1;

解析图:
在这里插入图片描述

1.4.3第四条:嵌套结构体对齐规则

如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

比如有两个结构体:

struct S1
{
 double d;
 char c;
 int i;
};

struct S4
{
 char c1;
 struct S1 s;
 double d;
};

结构体中 嵌套的结构体s,按照上述叙述,s的对齐数就是8. 且s的大小前面我们已经知道了,是16字节.因此我们有下面的图解

图解:

在这里插入图片描述


1.4.4为什么会有结构体对齐?

  1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能

在某些地址处取某些特定类型的数据,否则抛出硬件异常。


比如一些单片机,他们访问地址遵循着一定的规则,不像我们的C语言编译器,可以任意访问内存.如果结构体不内存对齐,就会发生硬件异常

  1. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的

内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问


什么意思呢?不急,我们先弄清楚一个读取概念.这里以32位机器为例:

  • 32位机器 ,即代表着有32根地址线
  • 32位机器 ,即代表着有32根数据线
  • 32位机器 ,即代表着有一次性读取或传输32位空间大小的能力,即一次性可以传输 4个字节
  • 而我们的CPU寄存器大小刚刚也是32位,即4个字节

所以,我们的电脑在读取数据时,也是常常一次性读取4个字节

现在我们有以下一个结构体:

struct S
{
    char c;
    int i;
};

在这里插入图片描述

我们会发现:如果未对齐,当第一次,一次性读取4个字节后,只能读取c,想要读取i,就必须返回进行第二次读取,需要访问2次
在这里插入图片描述

我们会发现: 如果对齐,在第一次访问4个字节后,想要在读取i的内容,只需要再往后移动就行. 只需要访问内存一次

这就是结构体对齐的意义:

结构体的内存对齐是拿空间来换取时间的做法。


1.4.5 怎么设计结构体?

让占用空间小的成员尽量集中在一起。

//例如:
struct S1
{
 char c1;
 int i;
 char c2;
};

struct S2
{
 char c1;
 char c2;
 int i;
};

S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。


1.4.6 修改编译器默认对齐数

不同的编译器默认设置的对齐数不一样,甚至有的编译器根本不会设置默认对齐数,比如linux下的gcc,在gcc编译器下是默认成员变量自己大小为对齐数. VS编译器的默认大小是8.

那么我们会想,能不能修改,默认对齐数呢?答案是肯定的,可以修改.使用下面的语句

#pragma pack(num)
structure
#pragma pack()

#pragma pack(num) num参数就是代表着需要修改为的默认对齐数

structure 这个位置写结构体的相关内容

#pragma pack() 与第一条语句搭配使用,代表结束

示例:

#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
 char c1;
 int i;
 char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
 char c1;
 int i;
 char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
    //输出的结果是什么?
    printf("%d\\n", sizeof(struct S1));
    printf("%d\\n", sizeof(struct S2));
    return 0; }

答案是什么呢???我就留给大家了.


1.5结构体传参

假设有下面这样一个结构体:

struct S { int data[1000]; int num;};

然后我们需要设计一个初始化函数 Init(),用来初始化结构体S创建的对象.怎么设计了呢? 下面给出了两种方法

方法一:

void Init(struct S struc_name){    struc_name.data = {0};    struc_name.num = 100;}

方法二:

void Init(struct S* struc_name){    struc_name->data = {0};    struc_name->num = 100;}

大家觉得哪种设计会更合理??? 答案显而易见 ---------- 第二种

为什么这样说呢? 我们在前面的函数章节讲过,

  • 第一种方法是什么传参?----------------------> 传值
  • 第二种方法是什么传参?----------------------> 传址

区别: 传值调用时,形参只是实参的一份临时拷贝

​ 传址调用时,形参可以控制改变实参

优势对比: 传值调用时,如果实参的空间很大时,那么函数接收时候,就会接收这么大一个东西

​ 传址调用时,无论实参的空间多大,都只会接收四个字节.

看下面的一段代码:

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; 
}

上面的 print1print2函数哪个好些?

答案是:首选print2函数。 原因:

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

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

结论:结构体传参的时候,要传结构体的地址。

1.6结构体实现位段(位段的填充&可移植性)

讲解完毕结构体后怎么能够少得了位段呢?

什么是位段?

位,就是比特位.具体请往后看.

位段的声明和结构是类似的,有两个不同:

1.位段的成员必须是 int、unsigned intsigned int等整型

2.位段的成员名后边有一个冒号和一个数字。数字就代表是几个比特位.

比如:

struct A 
{
     int _a:2;
     int _b:5;
     int _c:10;
     int _d:30;
};

那么,sizeof(struct A)的大小是多少呢? 答案:8字节.

因为成员后面的数字代表位,所以 2+5+10+30 = 47位, 是不是接近8字节(64位)??

但是明明是47位大小,为何却占用了8字节呢?原来位段也有自己开辟空间的规则.(每次开辟都是按照需要每次4个字节4个字节的开辟或者一个字节一个字节的开辟)

,比如上面的开辟,我画图解释:

在这里插入图片描述

发现最终就是利用了8个字节.

再次测试一个题:

struct S {     char a:3;     char b:4;     char c:5;     char d:4;};

sizeof(struct S) 大小是?

在这里插入图片描述

发现最终开辟了3个字节,所以最终就是3字节大小.

位段与大小端存储,整型截断的综合运用题:

struct S
{
    char a : 3;
    char b : 4;
    char c : 5;
    char d : 4;
};

struct S s = { 0 };

int main()
{
    s.a = 10;
    s.b = 12;
    s.c = 19;
    s.d = 4;
    printf("%d\\n", s.a);
    printf("%d\\n", s.b);
    printf("%d\\n", s.c);
    printf("%d\\n", s.d);
    return 0;
}

问题一: 此时打印a,b,c,d的值分别是多少???

问题二:此时s的内存中的存储情况是??请用16进制表示出该3字节中的存储内容

希望大家一定要思考后再看下面的答案哦~~


答案:

10,12,19,4是正数, 二进制表示分别是 1010(10) 1100(12) 10011(19) 100(4)

但是a只有3个比特位,所以整型10会发生截断,最后只有末尾3位存进去了,也就是010放进a了

​ b有4个比特位,所以整型12末尾4个数字刚刚存进去,也就是1100放进b了

​ c有 5个比特位,所以整型3完全够存进去,也就是10011放进c了

​ d有4个比特位,所以整型4完全足够放进去,也就是100放进d了

所以此时abc在内存中的情况如图:

在这里插入图片描述

可以清晰看到,a,b,c,d的位范围,而printf要求是%d格式输出,即有符号形式

  • a有3位,最高位是0,所以当成正数输出,10的十进制是2,所以输出2.
  • b有4位,最高位是1,所以当成负数输出,1100负数补码还原回十进制是**-4**,所以输出-4
  • c有5位,最高位是1,所以当成负数输出,10011负数补码还原回十进制是**-13**,所以输出-13
  • d有4位,最高位是0,所以当成正数输出,0100的十进制是4,所以输出4

而根据上面三个字节的内容,又知每4位二进制对应一位16进制,所以下面开始转换

0110 0010 0001 0011 0000 0100 对应下面16进制

​ 62 13 04

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

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

是不是非常正确??,值分别为2 -4 -13 4 内存中的内容就是62 13 04

位段的跨平台问题

  1. int 位段被当成有符号数还是无符号数是不确定的。

  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机

器会出问题。 -------最大位的数目是指位段成员冒号后的最大位数

  1. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

  2. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是

舍弃剩余的位还是利用,这是不确定的。

总结:

跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在

位段的应用

我们平时在网络上,电话上,社交平台上发送与接收消息都是一对一的,而不是某个人发个消息,全世界人的都知道.这归功于对信息的加密.

而网络加密常用的有 比如源IP地址(32位) 目的IP地址(32)位 网络协议(8位) 偏移(13位) 等,会发现各个东西需要的位是不一样的,如果我们只能每次一个字节或者4个字节的开劈方式,将会浪费极大空间,而位段就完美解决了.需要多少,就用多少.

下面是网络加密需要用的一些不同位需求的表:

在这里插入图片描述


2.枚举

仔细品味这个词语,枚举,什么是枚举呢?顾名思义,就是列举

把可能的所有值,一一列举出来:

比如:

性别,只有两种: 男生 女生

月份,只有12种:1月 2月 3月 4月 5月…11月 12月

星期,只有7天:1天 2天 3天…7天

十进制一位数数字,只有10个 :0 1 2 3 … 8 9

这里就可以使用枚举了


2.1枚举类型的语法定义

enum tag
{
  list1,    //末尾是逗号
  list2,
  list3...
};

例如 星期

enum weeks
{
     Mon,
     Tues,
     Wed,
     Thur,
     Fri,
     Sat,
     Sun
};

例如 颜色

enum Color//颜色
{
     RED,
     GREEN,
     BLUE
};

例如 性别

enum Sex
{
     MALE,
     FEMALE
}

以上定义的 enum weeksenum Colorenum Sex 都是枚举类型, {}中的内容是枚举类型的可能取

值,也叫 枚举常量 。他们的值是按照顺序从0开始默认递增的.

比如下面:

enum weeks
{
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};

int main()
{
	printf("%d %d %d %d %d %d %d", Mon,Tues,Wed,Thur,Fri,Sat,Sun);
	return 0;
}

在这里插入图片描述

但是我们也可以修改值,但是还是按照一定顺序;

enum weeks
{
	Mon = 100,
	Tues,
	Wed,
	Thur = 300,
	Fri,
	Sat,
	Sun
};

int main()
{
	printf("%d %d %d %d %d %d %d", Mon,Tues,Wed,Thur,Fri,Sat,Sun);
	return 0;
}

在这里插入图片描述

2.2枚举的优点

为什么使用枚举?

我们可以使用 #define 定义常量,为什么非要使用枚举? 枚举的优点:

  1. 增加代码的可读性和可维护性

  2. #define定义的标识符比较,枚举有类型检查,更加严谨。

  3. 防止了命名污染(封装)

  4. 便于调试

  5. 使用方便,一次可以定义多个常量

第二条信息请看2.3枚举

2.3枚举的使用

enum Color//颜色
{
     RED=1,
     GREEN=2,
     BLUE=4
};

enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。

只有以上的这种写法才是正确的.而如果直接赋值,会有类型差异,比如: clr = 100,这种写法不合法


3.联合(共用体)

3.1联合类型的定义

联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块

空间(所以联合也叫共用体)。

联合的声明,变量定义:

//联合类型的声明结构
union tag
{
	member1;
	member2;
	member3;
    ...
};

比如:
union Un
{
     char c;
     int i;
};


//联合变量的定义
union tag name;
比如:
union Un func;


以上是关于自定义类型:结构体(内存对齐),枚举,联合的主要内容,如果未能解决你的问题,请参考以下文章

C语言进阶自定义类型详解(结构体+枚举+联合)

自定义结构类型:结构体枚举联合

c语言篇 +自定义类型(枚举联合结构体)以及位段

C语言☀️自定义类型(结构体+位段+枚举+联合体)建议收藏

自定义类型:结构体,枚举,联合

自定义类型~结构体~位段~枚举~联合~超详解~一遍就会