> 文档中心 > 【C进阶】Four -> 自定义类型(结构体,位段,共用体。万字讲解,不明白,就来找我。)

【C进阶】Four -> 自定义类型(结构体,位段,共用体。万字讲解,不明白,就来找我。)


 ⭐前言

※※※大家好!我是同学〖森〗,一名计算机爱好者,今天让我们进入学习模式。若有错误,请多多指教。

👍 点赞  收藏 📝留言 都是我创作的最大的动力!


 ⭐往期真集

【C进阶】 【C进阶】Three -> 字符函数
【C进阶】 【C进阶】two -> 指针进阶练习(三)
【C进阶】 【C进阶】two -> 指针进阶(二)
【C进阶】 【C进阶】two -> 指针进阶
【C进阶】 【C进阶】 one -> 数据的存储

⭐思维导图⭐

 

⭐本章重点⭐

一、结构         1.结构体类型的声明         2.结构的自引用         3.结构体变量的定义和初始化         4.结构体内存对齐         5.结构体传参         6.结构体实现位段(位段的填充& 可移植性) 二、枚举         1.枚举类型的定义         2.枚举的优点         3.枚举的使用 三、联合        1. 联合类型的定义        2. 联合的特点         3.联合大小的计算

⭐目录⭐

目录

⭐前言⭐

⭐往期真集⭐

⭐思维导图⭐

⭐目录⭐

结构体(C语言)

1.概念

1.1结构体简介:

1.2为什么会有结构体:

2.结构体的声明: 

3. 结构体的自引用:

3.1结构体成员包含其他结构体: 

3.2结构体的自引用: 

3.3两个结构体相互包含:

4.结构体成员的定义和初始化

5. 结构体成员访问

6.结构体内存对齐

6.1结构体的对齐规则 

6.2内存对齐的三板斧 

6.3结构体嵌套

7.为什么存在内存对齐?

8.修改默认对齐数

9.结构体传参 

 位段

1.什么是位段?

2.位段的内存分配

3. 位段的跨平台问题

枚举 

1.枚举的定义

2.枚举的优点 

3.枚举的使用

联合(共用体)

1.联合的定义:

2.联合的特点 

3.面试题:


结构体(C语言)

1.概念

1.1结构体简介:

在C语言中,结构体(struct)指的是一种数据结构,是C语言中复合数据类型(aggregate data type)的一类。结构体可以被声明为变量、指针或数组等,用以实现较复杂的数据结构。结构体同时也是一些元素的集合,这些元素称为结构体的成员(member),且这些成员可以为不同的类型,成员一般用名字访

1.2为什么会有结构体:

在我们日常的生活中会有一些变量,需要有几个不同的类型修饰。比如:学生的信息(需要姓名,性别,年龄,学号,各科成就,排名等修饰)、书籍的信息(书名,作者,单价等修饰),而结构体的出现就很好地解决了这个问题。

优点:结构体不仅可以记录不同类型的数据,而且使得数据结构是“高内聚,低耦合”的,更利于程序的阅读理解和移植,而且结构体的存储方式可以提高CPU对内存的访问速度。

注意点:

  1. 结构体的声明不占用内存,只有定义结构体变量才会分配内存。
  2. 如果main函数放在结构体定义声明之前,程序将会报错感兴趣的朋友可以自行测试。
  3. 两种结构体成员相同的结构体,编译器会认为是两个不同的结构体。

2.结构体的声明: 

结构体定义:

struct tag { member-list } variable-list ; 

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

例:描述一个学生。 

struct Student    //声明结构体{    char name[20];    //名字    char sex[5];    //性别    int age;        //年龄    int score;        //分数};//注意分号不能少。

 结构体的3种常见声明

//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c//同时又声明了结构体变量s1//这个结构体并没有标明其标签,不能再次定义新的结构体变量了struct {    int a;    char b;    double c;} s1;//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c//结构体的标签被命名为SIMPLE,没有声明变量struct SIMPLE{    int a;    char b;    double c;};//用SIMPLE标签的结构体,另外声明了变量t1、t2、t3struct SIMPLE t1, t2[20], *t3;//也可以用typedef创建新类型,即类型重命名。typedef struct{    int a;    char b;    double c; } Simple2;//现在可以用Simple2作为类型声明新的结构体变量Simple2 u1, u2[20], *u3;

思考:t3 = &s1;合法吗?

警告:编译器会把这两种结构体当做两种不同的类型,虽然他们的结构体成员是一样的,所以t3 = &s1是非法的。

3. 结构体的自引用:

结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表和树等。

3.1结构体成员包含其他结构体: 

struct Student//声明结构体{char name[20];//名字char sex[5];//性别int age;//年龄int score;//分数};//注意分号不能少。struct Node{int data;struct Student n;};

3.2结构体的自引用: 

 有没有小伙伴会这样写:

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

若说的是你的话,那请你考虑下,sizeof(Node);会是多少?

你细看就会发现,你创建一个struct Node 类型的变量,就会一直创建struct Node,不会停止。最终程序会崩溃。

而正确的自引用方式是用指针。

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

如上所示:结构体的自引用应该是指针。在结尾是指针赋值为NULL。

3.3两个结构体相互包含:

如果两个结构体互相包含,则需要对其中一个结构体进行不完整声明

struct B;//对结构体B进行不完全声明。struct A//结构体A中包含指向结构体B的指针{struct B* part;//……};struct B//同时结构体B中也包含结构体A的指针{struct A* part;//……};

4.结构体成员的定义和初始化

#includestruct student{char name[20];char sex[5];int age;float score;}stu1 = { "小明", "男", 18, 85.5};//方式一:在声明结构体的同时定义结构体变量并初始化int main(){struct student stu2 = { "小红", "女", 17, 90.0 };//方式二:结构体变量与结构体声明分开,结构体变量在main函数内部定义。return 0;}

 注:结构体的声明不占用内存,只有定义结构体变量才会分配内存。

思考:结构体的嵌套该怎么定义变量并初始化。 

例:定义结构体Node变量并初始化。

struct student{char name[20];char sex[5];int age;float score;}stu1 = { "小明", "男", 18, 85.5};//方式一:在声明结构体的同时定义结构体变量并初始化struct Node{struct student stu;struct Node* p;};

答案

5. 结构体成员访问

结构体成员依据结构体变量类型的不同,一般有2种访问方式,一种为直接访问,一种为间接访问直接访问应用于普通的结构体变量,间接访问应用于指向结构体变量的指针。直接访问使用结构体变量名.成员名,间接访问使用(*结构体指针名).成员名或者使用结构体指针名->成员名。相同的成员名称依靠不同的变量前缀区分。

例:

#includestruct Birthday{int year;int month;int day;};typedef struct student{char name[20];char sex[5];int age;float score;struct Birthday birthday;}student, * Stu;int main(){student stu1;Stu p = &stu1;printf("请依次输入姓名,年龄,性别,分数\n");scanf("%s%d%s%f", p->name, &p->age, p->sex, &p->score);//间接访问printf("请依次输入出生年,月,日\n");scanf("%d%d%d", &p->birthday.year, &p->birthday.month, &p->birthday.day);printf("姓名:%s\t性别:%s\n", stu1.name, stu1.sex);//直接访问printf("出生日期:%d年%d月%d日\t年龄:%d\n", stu1.birthday.year, stu1.birthday.month     , stu1.birthday.day, stu1.age);printf("分数:%.2f\n", stu1.score);}

 输出:

6.结构体内存对齐

猜测下列代码的输出结果:

#includestruct S1{char c1;int i;char c2;};struct S2{char c1;char c2;int i;};int main(){struct S1 s;struct S2 s2;printf("s = %d\n", sizeof(s));printf("s2 = %d\n", sizeof(s2));return 0;}

 结果是否如你所愿?

为什么我们需要6个字节就能解决的问题,会花8个或12个字节呢?接下来让我们揭开它的面纱。 

是什么在里面作怪呢?

没错,它是我们常说的内存对齐.你可别小看它,他可是面试的常客

6.1结构体的对齐规则 

 1. 第一个成员在与结构体变量偏移量为0的地址处。(结构体的首地址对齐到0处)

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

对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值 VS 中默认的值为 8 3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。 4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

6.2内存对齐的三板斧 

 每个方格代表一个字节

第一斧:

通过比较每个结构体的成员大小和编译器的默认对齐数,取其较小值作为该成员变量的对齐数

 vs编译器的默认对齐数是8,Linux系统下没有默认对齐数。

 第二斧:

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

c1(首个成员)对齐的地址是0偏移量,占1字节。

i的对齐数是4,而偏移量1的位置不是4的倍数,所以i不能放在这里,只能放在偏移量为4的位置。占4字节

c2的对齐数是1,8是1的倍数,可以c2放在偏移量为8处。占1个字节。

第三斧:

由上可知,结构体s占9个字节,而最大对齐是4(i)。9不是4的倍数。所以9不是最终结构体s的大小。12才是结构体s的倍数。 

 聪明的你是否懂了?让我们来练一练吧!

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

答案是 8

若你还是不放心的话?这里我们用offsetof来测试一下 

offsetof

offsetof (type,member)

头文件:

功能:返回成员偏移量

此具有函数形式的宏返回数据结构或联合类型类型中成员成员的偏移值(以字节为单位)。

返回的值是类型为 size_t的无符号整数值,其中包含指定成员与其结构开头之间的字节数。

参数:

type:type应为结构或联合类型。

member:类型的成员。

返回值:类型size_t的值,具有类型成员的偏移值。

#include#includestruct S2{char c1;char c2;int i;};int main(){struct S2 s2;printf("s2中c2的偏移量:%d\n", (int)offsetof(struct S2, c2));printf("s2中i的偏移量:%d\n", (int)offsetof(struct S2, i));return 0;}

 输出:


6.3结构体嵌套

猜下列代码的输出结果 

#includestruct S3{double d;char c;int i;};struct S4{char c1;struct S3 s3;double d;};int main(){printf("%d\n", sizeof(struct S4));return 0;}

 输出结果:32

 

  

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

结构体的最大对齐数是8,而32正是8的倍数,所以S4的大小是32.


7.为什么存在内存对齐?

 1. 平台原因(移植原因)

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

8.修改默认对齐数

#include #pragma pack(8)//设置默认对齐数为8struct S1{char c1;int i;char c2;};#pragma pack()//取消设置的默认对齐数,还原为默认#pragma pack(1)//设置默认对齐数为1struct 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;}

 输出:


9.结构体传参 

读下面代码,思考print1好还是print2优?

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

print2较好点,

因为函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降 所以,结构体传参时,尽可能的要用结构体指针

 位段

 1.什么是位段?

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

例: 

struct S{int c : 3;//占3个bit位int i : 5;//占5个bit位int z : 10;//占10个bit位};

 由于位段占的内存比较小,所以存储的数据也就比较小,但是节约了空间。在满足使用条件下使用位段。

注意:

1、位段占的二进制位数不能超过该基本类型所能表示的最大位数,如在VS中int是占4个字节,那么最多只能是32位;

2、不能对位段进行取地址操作;

3、若位段占的二进制位数为0,则这个位段必须是无名位段,下一个位段从下一个位段存储单元(这里的位段存储单元经测试在VC环境下       是4个字节)开始存放;

4、对位段赋值时,最好不要超过位段所能表示的最大范围,否则会数据错误。

5、位段不能出现数组的形式。

6、无名位段不能被访问,但是会占据空间;

int :6;    无名位段,即,没有成员变量名称。

 2.位段的内存分配

 先分析一波刚刚的结构体s;

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

是否还是不太明白呢?让我们来看看接下来的 例题:

#includestruct S {char a : 3;char b : 4;char c : 5;char d : 4;};int main(){struct S s = { 0 };printf("%d\n", sizeof(s));s.a = 10;s.b = 12; s.c = 3; s.d = 4;return 0;}

首先,先开辟一个字节,即8bit位

a占3个bit位,00000000,假如是先从地位开始占位的,还剩5个bit位。

b占4个bit位,00000000,蓝色代表b所占的bit位。还剩1个bit位。

c占5个bit位,因为还剩1个bit位。而c需要5个bit位,所以我们再开辟一个字节,假如我们从从新开辟的字节的地位存储c

即:00000000 00000000,绿色代表c所占的bit位。还剩3个bit位。

d占4个bit位,因为还剩3个bit位,不够存放d,所以我们再来开辟一个字节,00000000 00000000 00000000

 共占3个字节,到底对不对呢?

看来vs编译器是bit位不够放的时候,下一个变量直接占用新开辟的空间 

 我们再来看一下赋值。

a =10;1010,而a只占3bit位,只保存后3位。010

b = 12;1100,b占4bit位,保存 1100

c= 3; 11,c占5bit位,在前面补0,保存 00011

d = 4;100;d占4bit位,保存0100;

所以总共就是        01100010 00000011 00000100        变成16进制位        62 03 04

结果和我们的一样,看来我们的推算是正确的。 

关于位段的内存分配你懂了吗?

3. 位段的跨平台问题

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

枚举 

什么是枚举呢?顾名思义,就是一一列举 

例:

每周可以分为星期一到星期日。

每年可以分为1月到12月。

1.枚举的定义

enum Day//星期{ Mon, Tues, Wed, Thur, Fri, Sat, Sun};

{} 中的内容是枚举类型的可能取值,也叫 枚举常量 这些可能取值都是有值的,默认从 0 开始,一次递增 1 ,当然在定义的时候也可以赋初值。

2.枚举的优点 

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

3.枚举的使用

 

#includeenum{Mon,Tues,Wed,Thur,Fri,Sat,Sun}day1,day2;int main(){day1 = Sun;day2 = Wed;printf("day1 = %d\tday2 = %d", day1, day2);return 0;}

联合(共用体)

 1.联合的定义:

联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体

 

//联合类型的声明union S{    int i;    char c;}//联合变量的定义union S s;
#include//联合类型的声明union S{int i;char c;};int main(){//联合变量的定义union S s;printf("%d\n", sizeof(s));printf("%p\n", &s);printf("%p\n", &s.i);printf("%p\n", &s.c);return 0;}

2.联合的特点 

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

 

#include//联合类型的声明union S{int i;char c;};int main(){//联合变量的定义union S s;s.c = 0X55;s.i = 0;printf("%d\n", sizeof(s));printf("%p\n", &s);printf("%p\n", &s.i);printf("%p\n", &s.c);printf("s.i = %x, s.c = %x", s.i, s.c);return 0;}

 

 

 我们发现他们的地址都是一样的。

这也就说明s.c和s.i共用了一个字节。

我们发现s.i  和s.c 都为0,我们不是给s.c赋值为0x55吗?

没错,s.i的值把s.c的值覆盖了。

3.面试题:

 判断当前计算机的大小端存储。

方法一:int类型数据强制转换为char类型:

#includeint main(){int i = 1;char c = (char)i;if (c == 1)printf("小端\n");elseprintf("大端\n");return 0;}

输出:

 

方法二:联合体 

#includeint test_sys(){union S{char c;int i;}s1;s1.i = 1;return s1.c;}int main(){int i = 0;i = test_sys();if (i == 1)printf("小端");elseprintf("大端");return 0;}

输出:

 

在线造句网