一章教会你自定义类型:结构体,枚举,联合
大家好我是枫晨~,今天就来总结一下C语言中的所有自定义类型:结构体,枚举,联合,以及一般书上提都不会提的位段,扩大你的知识储备。
文章目录
![img](https://i-blog.csdnimg.cn/blog_migrate/a9e11fa78439cde7b648e295002afc81.gif)
一、结构体
之前我们学过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结构体的自引用
结构体的自引用简单来说就是结构体里包含同类型结构体
这里提前介绍一下数据结构中的链表来帮助我们理解自引用:
如何存储2的节点呢?
方式1:
struct Node
{
int data;
sturct Node n;
};
方式2:
struct Node
{
int data; //数据
struct Node *next;//指针
};
结论:方式1是错误的自引用方式,方式二是正确的
方式二在存储的时候分为了数据域和指针域,数据域存储当前数据大小,指针域存储下一个数据的节点位置
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的结构体大小
offsetof函数 -返回/计算结构体相对于起止位置偏移量
函数头文件:<stddef.h>
#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;
}
和我们刚刚在内存中分析的一样,分别偏移了0,4,8
回到问题②,为什么成员类型一样,但是只是成员位置不同,结构体大小就从12变为了8呢?同样也是结构体对齐的原因,大家可以仿照struct s1画一下内存图
刚刚还有一个第四点一直没有提到,那就是如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
为什么存在内存对齐?
平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
总结:设计结构体的时候,既要忙住对齐,又要节省空间:让占用空间小的成员集中在一起
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取值 | 二进制取值 |
---|---|
0 | 0 0 |
1 | 0 1 |
2 | 1 0 |
3 | 1 1 |
对比一下结构体:
struct AA
{
int a;
int b;
}
a是一个整形,占4个字节,共32个bit位,最大可取到INT_MAX
总结:
位段的作用:按需求使用内存,节省了空间!
位段无对齐。
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成员会不会使用呢?这是不确定的,在不同编译器下情况有所不同,这也是为什么位段不跨平台原因之一!
图片理解:
在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定义的常量 | 基本值 |
---|---|
MALE | 0 |
FEMALE | 1 |
SECRET | 2 |
如果你想更改枚举在定义时的基本值,可以这样定义:
enum sex
{
MEALE = 5,//并非赋值操作,常量不可以赋值,这里的=含义是初始化
FEMEALE,
SECRET,
};
enum定义的常量 | 基本值 |
---|---|
MALE | 5 |
FEMALE | 6 |
SECRET | 7 |
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);
}
结果:
联合体的大小为4,正常我们想,一个char类型和int类型加起来不应该是5个字节嘛?怎么是四个字节?还有就是3个方式取出的地址都相同,该怎么去理解呢?
有了这一个特点,我们可以尝试用联合体来判断机器大小端问题:
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语言中自定义类型的内容基本上已经总结完,还剩下一些小知识点在后面我们学习过程中再慢慢提及。这一次也是购入了平板用于做笔记和画图来增加文章观感,如果有更多好建议欢迎提出!!!
![](https://i-blog.csdnimg.cn/blog_migrate/81235150d6541f4495cb9b88251edcd9.jpeg)