人一能之,己百之;人十能之,己千之。 ——《中庸》

目录

一.结构体

1.结构的基础知识

2.结构体的声明

3.结构体成员的类型

4.结构体变量的定义和初始化:

5.结构体成员的访问:

6.结构体传参

7.结构体内存对齐:结构体的大小

8.为什么要有内存对齐?

二.结构体的位段

1.什么是位段:

2.位段的内存分配:

3.位段的跨平台问题

4.位段的应用:

三.枚举

1.枚举的定义

2.枚举的优点:

四.联合(共用体)

1.联合体等定义:

2.联合体的大小


一.结构体

1.结构的基础知识

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

2.结构体的声明

结构体的定义如下所示,struct为结构体关键字,tag为结构体的标志,member-list为结构体成员列表,其必须列出其所有成员;variable-list为此结构体声明的变量。

struct tag {member-list} variable-list ; //这个;不能少

在结构体中,必须有struct tag和member-list是必须要有的,而variable-list是可以没有的,variable-list只是一个结构体变量,而且是全局变量,这里可以不写也行。

struct tag {member-list} ;

3.结构体成员的类型

结构体成员可以是标量,数组,指针,甚至是其他结构体。当我们想定义一个人时,我们可能会想到他的身高,体重,名字等我们就用float来定义,体重用int,名字用char。

struct people{ int weight;//体重 char name[20];//数组来存名字 float height;//身高};

4.结构体变量的定义和初始化:

struct people{ int weight;//体重 char name[20];//名字 float height;//身高};int main(){ struct people man = { 55,"小陈",172 };//man就是结构体变量,是局部变量 struct people max = { .weight = 55,.name = "小陈",.height = 172 }; //这两种结构体的初始化都可以 return 0;}

typedef是在计算机编程语言中用来为复杂的声明定义简单的别名,它与宏定义有些差异。它本身是一种存储类的关键字,与auto、extern、mutable、static、register等关键字不能出现在同一个表达式中。

这里我们写struct people感觉有点麻烦,我们就可以使用typedef来给结构体定义一个简单的别名,之后我们就可以使用别名来代替结构体类型。上述初始化代码我们就可以这样写。

typedef struct people M;struct people//第一次使用时不能用M来代替{ int weight;//体重 char name[20];//名字 float height;//身高};int main(){ M man = { 55,"小陈",172 }; //这里就是用M来代替struct people M max = { .weight = 55,.name = "小陈",.height = 172 }; //这两种结构体的初始化都可以 return 0;}

当结构体的成员有结构体,我们又该如何对结构体初始化呢?

struct book{ char name[20]; struct people; int price;};struct people{ int weight; char name[10]; float height;};int main(){ struct book b1 = { "活着",{60,"富贵",170},50 };//用大括号括起来 //从结构体book里面从上往下初始化结构体成员 return 0;}

5.结构体成员的访问:

1.结构体变量.结构体成员名

2.结构体指针->结构体成员名

struct people{ int weight; char name[10]; float height;};void print(struct people* b2){ printf("%d %s %f\n", b2->weight, b2->name, b2->height);}int main(){ struct people b1 = { 55,"小陈",172}; printf("%d %s %f\n", b1.weight, b1.name, b1.height); print(&b1); return 0;}

6.结构体传参

结构体传参也有两种,一种是传结构体变量过去,还有一种就是传地址过去,这两者有什么区别吗?

struct people{ int weight; char name[10]; float height;};void print1(struct people* b2){ printf("%d %s %.1f\n", b2->weight, b2->name, b2->height);}void print2(struct people b2)//形参{ printf("%d %s %.1f\n", b2.weight, b2.name, b2.height);}int main(){ struct people b1 = { 55,"小陈",172}; printf("%d %s %.1f\n", b1.weight, b1.name, b1.height); print1(&b1);//传地址 print2(b1);//传变量,实参 return 0;}

当我们传参的时候,如果传的结构体变量,那形参就要开辟结构体成员内存的总空间来接收实参,因为函数传参的时候,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。

所以我们尽量使用传地址的形式,也就是传指针,因为我们知道指针大小就是4或者8个字节。

所以这样就比较节省空间。

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

7.结构体内存对齐:结构体的大小

C语言马上都已经学完了,我们还没学过计算结构体的大小。今天我们就将好好的学习一下,在这里我会详细的讲解如何计算结构体的大小。

struct A{int a;char c1;}s1;struct B{char c1;int a;char c2;}s2;struct C{char c1;int a;char c2;char c3;}s3;int main(){printf("%d\n", sizeof(struct A));printf("%d\n", sizeof(struct B));printf("%d\n", sizeof(struct C));return 0;}

在这里计算结构体的大小会不会是A结构体有一个int和char,大小是5个字节。B有6个字节,C有7个字节。其实想想也不会这么简单,不然我们也不会系统的学习。

很奇怪,为什么结构体的大小会是8,12,12呢?而且结构体C还比结构体B的变量多一个。

这里就要请出结构体内存对齐这个东西了。

1.结构体的第一个成员永远放在0偏移处。

2.第二个成员开始,以后每一个成员都要到某个对齐数的整数倍处,这个对齐数是:成员自身大小和默认对齐数的较小值。

注:VS环境下,默认对齐数是8。gcc环境下,没有默认对齐数,对齐数就是成员自身的大小。

3.当成员全部存放进去后,结构体的总大小是所有成员的对齐数中最大对齐数的整数倍。如果不够,则浪费空间对齐。

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

上述的话是什么意思呢?我们使用上面的结构体例子来说明一下,就懂了。

对于结构体A:

对于结构体B:

对于结构体C:

对于结构体嵌套结构体的:

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

struct A{char c1;int a;double b;};struct B{char c2;int a1;struct A s1;};int main(){printf("%d", sizeof(struct B));return 0;}

8.为什么要有内存对齐?

1. 平台原因:

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件地址处取某些特定类型的数据,否则抛出文件异常。

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


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

struct A//这个结构体的大小是8{char c1;char c2;int a;};struct B//这个结构体的大小是12{char c1;int a;char c2;};

虽然结构体成员都是一样,但是顺序不同也会使结构体的大小不同。

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:让占用空间小的成员尽量集中在一起。

二.结构体的位段

1.什么是位段:

顾名思义位段就是二进制位(比特位)。

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

1.位段的成员必须是整型家族的:int,unsigned int或者signed int,char。

2.位段成员名后边有一个冒号和一个数字。

2.位段的内存分配:

1.位段的空间是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。

2.位段涉及很多不确定的因素,位段是不跨平台的,注意可移植性的程序应当避免使用位段。

struct A{char a : 3;//这些就是位段char b : 4;char c : 5;char d : 4;}S;int main(){S.a = 10;S.b = 12;S.c = 3;S.d = 4;return 0;}

1.我们假设分配到内存空间的比特位是从右向左的

2.分配的内存剩余的比特位不够使用时,浪费掉。

VS2022环境测试数据:

结果确实按照我们假设的一样。

3.位段的跨平台问题

1.int位段被当成有符号数还是无符号数是不确定的。
2.位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
3.位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4.当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:
跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

4.位段的应用:

在网络上传输数据的时候,可以通过位段来节省空间。

三.枚举

1.枚举的定义

枚举顾名思义就是一一给列举出来。

想我们现实中星期,性别和月份等可以一一列举出来。

枚举的可能取值默认时从0开始的,依次递增1。

enum sex{    MALE,    FEMALE,    SECRET};int main(){    enum sex s = MALE;    printf("%d\n", MALE);    printf("%d\n", FEMALE);    printf("%d\n", SECRET);    return 0;}

enum sex{    MALE,    FEMALE=5,//也可赋值,后面依次自增1    SECRET};int main(){    enum sex s = MALE;    printf("%d\n", MALE);    printf("%d\n", FEMALE);    printf("%d\n", SECRET);    return 0;}

2.枚举的优点:

为什么使用枚举” />

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

2.联合体的大小

联合体的大小至少是最大成员的大小

当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

这里的对齐规则是和结构体的内存对齐是一样的,但是联合体是公用一块空间的。

比如我们举例说明:

union un {char c;int i;};int main(){union un u;printf("%d\n", sizeof(u));printf("%p\n", &u);printf("%p\n", &(u.i));printf("%p\n", &(u.c));return 0;}

之前我们不是写了一个判断大小端的题吗?我们通过int a=1;(char*)p=&a, 强制类型转换,访问一个字节来判断。但是这里我们可以通过共用体来实现。非常的简单。

union un {char c;int a;};int main(){union un s;s.a = 1;if (s.c == 1)printf("小端\n");elseprintf("大端\n");return 0;}

全部的内容就结束了,感谢支持。