C语言自定义类型:<结构体,枚举,联合>一招破

一章教会你自定义类型:结构体,枚举,联合

大家好我是枫晨~,今天就来总结一下C语言中的所有自定义类型:结构体,枚举,联合,以及一般书上提都不会提的位段,扩大你的知识储备。


img

一、结构体

之前我们学过int,char,short,float等等类型,但是这些类型无一例外都不能描述复杂对象,何为复杂对象?一本书,它包括书名,书号,书的价格,这些都无法简简单单用单一的int,char等类型描述,但是我们可以使用结构体类型来将它们组合起来

※1.1认识结构体

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

※1.2结构体的声明

struct tag |tag:结构体变量的名字
{
    memmber-list;     | memmber-list 成员列表
}variable-list;       |variable-list 成员列表变量列表  一定不能少了最后的";""

例:定义一种学生结构体

struct student
{
    char name[20]; //学生名字
    int age;       //学生年龄
    double score;  //学生成绩
}s1;//s1:student的成员变量,这里可写可不写,但是";"必须写

※1.3特殊声明 —匿名结构体类型

在声明结构体的时候可以不完全声明

例:

struct 
{
  char name[20];
  char id[12];
}s1={"zhangsan","MALE"}; //这里的s1必须创建,而且对于匿名结构体初始化也要在这一步完成,匿名结构体只能使用一次

匿名结构体必须在创建的时候在“‘ ; ’”前创建结构体变量,且匿名结构体只能使用一次

问:如果两个匿名结构体成员都一样,是否是两个相同的类型?

例:

struct 
{
    int a;
    char b;
}x;

struct
{
    int a;
    char b;
}a[20],*p;

int main()
{
    p = &x;  //  ---->编译器会报错
    return 0;
}

结论:匿名结构体的成员如果一样,在编译器看来也是不同类型的结构体

※1.4结构体的自引用

结构体的自引用简单来说就是结构体里包含同类型结构体

这里提前介绍一下数据结构中的链表来帮助我们理解自引用:
capture-2022-03-27-17-42-48

如何存储2的节点呢?

方式1:

struct Node
{
    int data;
    sturct Node n;
};

方式2:

struct Node
{
    int data; //数据
    struct Node *next;//指针
};

结论:方式1是错误的自引用方式,方式二是正确的

方式二在存储的时候分为了数据域和指针域,数据域存储当前数据大小,指针域存储下一个数据的节点位置

capture-2022-03-27-17-51-51

typedef在结构体指针上的应用

typedef:类型重命名,将复杂类型的名字重新定义,方便用户使用
例:

typedef struct Node
{
    int data;
    struct Node* next;
}Node;//此时Node就是struct Node的重定义名字

//使用:
int main()
{
    Node data1;//定义了一个结构体成员变量data1。
    return 0;
}

值得提的一点是,绝对不能出现先有鸡还是先有蛋这样的逻辑性错误
例如:

typedef struct Node
{
    int data;
    Node* next;//还没将struct Node 重定义就开始使用重定义的名字,是绝对不可以的
}Node;//从这里开始才真正重定义完成

※1.5定义结构体变量

struct Book
{
	char name[20];
    float price;
    char id[12];
}b1; //1.直接在末尾创建结构体变量

int main()
{
    struct Book b2={"c study","22.0f","1122"};//在main函数内定义结构体变量同时初始化变量;
    struct Book b3;//定义一个结构体变量:
    b3={"c study","22.0f","1122"};//初始化结构体变量;
    return 0;
}

结构体嵌套初始化:

struct Point
{
	int x;
	int y;
};
struct Node
{
	int data;
	struct Node* next;
};
struct P3
{
	int i;
	struct Point s1;
	struct Node s2;
}s4 = { 1,{2,3},NULL};

※1.6结构体内存对齐-重要!!!

计算一个结构体的大小和计算int,char,shor等类型一样么?是将结构体内的成员大小全部简单的叠加起来?
例:

struct s1
{
    char c1;
    int i;
    char c2;
};
//结构体s1的大小是1+4+1=6么?
int main()
{
    printf("%d\n",sizeof(struct s1));
    return 0;
}

①最终打印结果答案是12,为什么不是叠加起来的结果呢?
再看第二例:

struct s2
{
    char c1;
    char c2;
    int i;
};
//结构体成员没有变,只是顺序变了,答案还是原来是12么?
int main()
{
    printf("%d\n",sizeof(struct s2));
    return 0;
}

②打印结果答案是8,明明结构体成员没有变,为什么既不是12又不是6呢?

这里就涉及到了结构体内存对齐这个知识点!

深入探讨计算结构体大小(结构体内存对齐):

如何计算?首先得了解结构体的对齐规则:
1.第一个成员在与结构体变量偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值

  • VS中默认的值为8

3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

例:

struct s1
{
    char c1;
    int i;
    char c2;
};
//求struct s1的结构体大小

image-20220327195902113

offsetof函数 -返回/计算结构体相对于起止位置偏移量

函数头文件:<stddef.h>
image-20220327193758270

#include <stddef.h>
int main()
{
	printf("%d\n",offsetof(struct s1,c1));
	printf("%d\n",offsetof(struct s1,i));
	printf("%d\n",offsetof(struct s1,c2));
    return 0;
}

image-20220327194122551

和我们刚刚在内存中分析的一样,分别偏移了0,4,8

回到问题②,为什么成员类型一样,但是只是成员位置不同,结构体大小就从12变为了8呢?同样也是结构体对齐的原因,大家可以仿照struct s1画一下内存图

capture-2022-03-27-20-13-29

刚刚还有一个第四点一直没有提到,那就是如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

为什么存在内存对齐?

平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总体来说:

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

总结:设计结构体的时候,既要忙住对齐,又要节省空间:让占用空间小的成员集中在一起

1.7修改默认对齐数

首先vs下默认对齐数为8,Linux的gcc编译器下无对齐数

修改方法:#pragma -预处理指令
修改默认对齐数:#pragma pack(2^n)

一般括号内都填写2^n关系的数

其中#pragma pack(1)是取消对齐数*

总结:结构体在对齐方式不合适的时候可以自己更改默认对齐数

1.8结构体传参

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

问:print1函数好还是print2函数好?

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

关于栈帧空间在C语言中的细节剖析我也会出一章来详细讲解

二、位段

2…1什么是位段?

位段的声明和结构体是类似的语法,但是存在两个不同
1.位段的成员必须是 int,unsigned int,signed int或char类型
2.位段的成员后边是冒号和一个数字

例:

struct A
{
	int a : 2;
	int b : 5;
	int c : 10;
    int d : 30;
};

冒号以及数字得含义是什么呢?
冒号后面的数字代表a这个成员只占2个bit,b只占5bit

而后又引出一系列问题,如果a只占2个bit,那a的取值有哪些呢?

a取值二进制取值
00 0
10 1
21 0
31 1

对比一下结构体:

struct AA
{
    int a;
    int b;
}

a是一个整形,占4个字节,共32个bit位,最大可取到INT_MAXimage-20220330155328822

总结:
位段的作用:按需求使用内存,节省了空间!
位段无对齐。

2.2位段内存分配

1.位段的成员可以是in, unsigned int, int signed int或者是char类型
2.位段的空间上是按照需求以4个字节或者1个字节的方式来开辟的;
3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

如何理解第二条:位段的空间上是按照需求以4个字节或者1个字节的方式来开辟的?
以struct A为例:

struct A
{    
    int a : 2;
    int b : 5;
    int c : 10;
    int d : 30;
};

我们可以理解成:位段第一个成员类型是int类型,则开辟4个字节也就是32个bit位的空间,以 struct A为例,开辟32个bit后分配2个bit供A使用,分配5个供b使用,分配10个供c使用,问题就在于第一次开辟的4个字节的空间经过几次开辟以后还剩下15个bit位,但是d需要30个bit,那必然是要再开辟4个字节的空间分配给d,但是之前多余的15个bit位d成员会不会使用呢?这是不确定的,在不同编译器下情况有所不同,这也是为什么位段不跨平台原因之一!

图片理解:image-20220330161515376

在vs中,经过测试,是不会使用剩余内存的,所以可以这么画

2.3位段跨平台问题

1.int位段被当成有符号数还是无符号数是不确定的。
2.位段中最大位的数组不能确定。(16位机器最大16,32位机器最大32,如果一个占27bit位的数放在16位机器中会出问题。
3.位段的成员在内存中从左向右还是从右向左分配标准未定义
4.当一个结构体包括两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时。是舍弃剩余的位还是利用这是不确定的

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

2.4位段的运用

这部非暂且不提及,以后如果有机会我会好好的讲一下这部分的运用。

三、枚举

3.1枚举定义

枚举内定义的是常量
枚举关键字:
作用:顾名思义一一列举

enum sex
{
	MALE,    
	FEMALE,
	SECRET,
};

int main()
{
    enum sex s = MALE;
    enum sex s2 = FEMALE;
    return 0;
}

其中enum定义的叫做枚举常量,main函数中定义的s和s1称为枚举变量
值得一提的是,枚举在定义时便初始化了基本值

enum定义的常量基本值
MALE0
FEMALE1
SECRET2

如果你想更改枚举在定义时的基本值,可以这样定义:

enum sex
{
    MEALE = 5,//并非赋值操作,常量不可以赋值,这里的=含义是初始化
    FEMEALE,
    SECRET,
};
enum定义的常量基本值
MALE5
FEMALE6
SECRET7

3.2枚举优点

我们在学习C的前期讲过使用#define来定义常量,那为什么非要使用枚举定义常量呢?

枚举优点:
1.增加代码的可读性和可维护性
2.和#define定义的标识符比较具有类型检查,更加严谨(在c++中体现)
3.防止了命名污染
4.便于调试
5.使用方便,一次可以定义多个常量

3.3枚举的使用

枚举一般定义同一类的变量,如刚刚提到的性别,还有一周的星期,颜色等等

四、联合体

4.1联合类型的定义

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

联合体关键字:

union un
{
    char c,
    int i,
};

4.2联合体特点

联合体的成员共用一块内存空间,这样一个联合变量的大小,至少是最大成员的大小

用一段代码来感受一下共用内存空间:

union Un
{
    char c;
    int i;
};
int main()
{
    union Un u;
    printf("%d\n",sizeof(union Un));
    printf("%s\n",&u);
    printf("%s\n",&u.i);
    printf("%s\n",&u.c);
    
}

结果:image-20220330171707653

联合体的大小为4,正常我们想,一个char类型和int类型加起来不应该是5个字节嘛?怎么是四个字节?还有就是3个方式取出的地址都相同,该怎么去理解呢?
image-20220330172529346

有了这一个特点,我们可以尝试用联合体来判断机器大小端问题:

4.3联合体判断机器大小端

union U
{
    char c;
    int i;
};
int main()
{
    union U u;
    u.i = 1;
    if(1 == u.c)
    {
        printf("小端\n");
    }
    else
    {
        printf("大端\n");
    }
    return 0;
}

4.4联合体大小的计算

1.联合体大小至少是最大成员的大小;
2.当最大成员不是最大对齐数的整数倍时候,就要对齐到最大对齐数的整数倍

例:

union un
{
    char arr[5]; //大小:5    对齐数 1
    int i;       //大小:4    对齐数:4
}

联合体un中,最大对齐数是4,而最大成员大小为5,不是最大对齐数的整数倍,则联合体大小继续增加,直到到8时,是对齐数4整数倍,所以un大小为8.

例2:

union uc
{
    short arr[7]; //大小:14    对齐数 2
    int i;       //大小:4    对齐数:4
}

联合体uc中,最大对齐数是4,而最大成员大小为14,不是最大对齐数的整数倍,则联合体大小继续增加,直到到16时,是对齐数4整数倍,所以un大小为8.

我想说的话:
关于c语言中自定义类型的内容基本上已经总结完,还剩下一些小知识点在后面我们学习过程中再慢慢提及。这一次也是购入了平板用于做笔记和画图来增加文章观感,如果有更多好建议欢迎提出!!!

  • 42
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 75
    评论
评论 75
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

XY枫晨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值