目录

​编辑

一、结构体的基本使用

1.什么是结构体

2.结构体的一般声明

3.结构体的特殊声明

4.结构体的自引用

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

二、结构体内存对齐

1.内存对齐规则

3.为什么会存在内存对齐

4.修改默认对齐数

5.结构体传参

三、位段

1.概述

2.位段的内存分配

3.位段的跨平台问题

四、枚举

1.枚举的定义

2.枚举的优点

3.枚举的使用

五、 联合(共用体)

1.联合类型的定义

2.联合体大小的计算


一、结构体的基本使用

1.什么是结构体

结构体是C语言中重要的知识点,结构体使C语言有能力描述复杂的类型

比如描述一个学生,一个学生包含: 名字+年龄+性别+学号。

我们用基本的数据类型没有办法描述这样的复杂对象,这里就只能使用结构体来描述了。

2.结构体的一般声明

struct Stu//struct表示创建结构体,Stu表示结构体标签,数据的类型为struct Stu{char name[20];//名字int age; //年龄char sex[10]; //性别char id[15]; //学号}s;//这个s就是一个结构体变量//当这个结构体定义在main函数内时,s是局部变量;在mian函数外,是一个全局变量

3.结构体的特殊声明

我们也可以省略结构体的标签

struct{int a;char b;float c;}x;struct{int a;char b;float c;}a[20], *p;//a[20]是一个有二十个结构体的数组,p是指向这样一个结构体的指针变量p = &x;//在上面的结构体虽然没有标签,但是这依旧是两个不同的结构体,所以不能互认

4.结构体的自引用

首先我们引入链表的概念,就像寻宝游戏中的线索一样,你有了一条线索,然后沿着这条线索寻找,你会找到下一条线索,沿着找到的条线索寻找,你又会找到下一条线索,这样层层递进最后就能找到宝物,所有的线索也都被串联起来了。

你如果在上一层的数据中存放下一层的信息,那么这一层层的信息就可以不断向下寻找,我们的数据就也被串联起来了,这就是一种链表的简单概括。

类似于链表的思想,结构体可以引用自己。

struct Node{int data;struct Node next;};//但是这样是不可以的,这个程序会不断地向内部寻找struct node nest,sizeof无法计算//修改struct Node{int data;struct Node* next;};//用指针则是正确的,指针变量的大小是固定的
typedef struct{int data;Node* next;}Node;//错误,因为类型重定义没有读全就使用了typedef struct Node{int data;struct Node* next;}Node;//正确

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

struct Point{int x;int y;}p1; //声明类型的同时定义变量p1struct Point p2; //定义结构体变量p2struct Point p3 = {x, y};//初始化:定义变量的同时赋初值。struct Stu {char name[15];//性别int age;//年龄};struct Stu s = {"zhangsan", 20};//初始化struct Node{int data;struct Point p;struct Node* next;}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

二、结构体内存对齐

1.内存对齐规则

对于任何一种类型的数据我们都关系它的大小,结构体也不例外。

  • 但与不同的数据类型不同,结构体存在自己的内存占用方式,也就是内存对齐。
  • 第一个成员在与结构体变量偏移量为0的地址处。
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的值为8
  • 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

2.举例分析

#includestruct A{int a;// 4/8->40 1 2 3short b;// 2/8->24 5int c;// 4/8->48 9 10 11char d;// 1/8->112};struct B{int a;// 4/8->40 1 2 3short b;// 2/8->24 5char c;// 1/8->16int d;// 4/8->48 9 10 11};int main(){printf("%d %d", sizeof(struct A), sizeof(struct B));return 0;}

这个程序的运行结果为:16 12,由此证明结构体的储存不是数据的简单堆叠

接下来,我们进行分析:

首先我们分析struct A:

(1)第一个元素储存在偏移量为0的地址处,int类型的变量占4字节,它就会占据偏移量为0、1、2、3这四个字节。

(2)第二个元素的储存就需要考虑默认对齐数,VS默认对齐数为8,short类型的变量占2字节,对齐数为2,2和8相比2更小,这个short类型的变量就会从偏移量为默认对齐数的整数倍也就是偏移量为4(2×2)的位置开始储存,占据偏移量为4、5这两个字节。

(3)第三个元素的储存,VS默认对齐数为8,int类型的变量占4字节,对齐数为4,4和8相比4更小,这个int类型的变量就会从偏移量为默认对齐数的整数倍也就是偏移量为8(4×2)的位置开始储存,占据偏移量为8、9、10、11这四个字节。

(4)第四个元素的储存,VS默认对齐数为8,char类型的变量占1字节,对齐数为1,1和8相比1更小,这个char类型的变量就会从偏移量为默认对齐数的整数倍也就是偏移量为12(1×12)的位置开始储存,占据偏移量为12这一个字节。

(5)所有的元素储存完毕,整个结构体的大小是所有变量对齐数的最大值的整数倍(除去系统默认对齐数)。在这里变量的默认对齐数有int ->4,

short ->2 ,int ->4 ,char ->1,其中最大值为4,整个结构体数据储存共占据13个字节,只有16(4×4)可以涵盖所有的用于储存变量的13个字节,最后结构体大小为16个字节。

接着我们分析struct B:

(1)第一个元素储存在偏移量为0的地址处,int类型的变量占4字节,它就会占据偏移量为0、1、2、3这四个字节。

(2)第二个元素的储存就需要考虑默认对齐数,VS默认对齐数为8,short类型的变量占2字节,对齐数为2,2和8相比2更小,这个short类型的变量就会从偏移量为默认对齐数的整数倍也就是偏移量为4(2×2)的位置开始储存,占据偏移量为4、5这两个字节。

(3)第三个元素的储存,VS默认对齐数为8,char类型的变量占1字节,对齐数为1,1和8相比1更小,这个char类型的变量就会从偏移量为默认对齐数的整数倍也就是偏移量为6(1×6)的位置开始储存,占据偏移量为6这一个字节。

(4)第四个元素的储存,VS默认对齐数为8,int类型的变量占4字节,对齐数为4,4和8相比4更小,这个int类型的变量就会从偏移量为默认对齐数的整数倍也就是偏移量为8(4×2)的位置开始储存,占据偏移量为8、9、10、11这四个字节。

(5)所有的元素储存完毕,整个结构体的大小是所有变量对齐数的最大值的整数倍(除去系统默认对齐数)。在这里变量的默认对齐数有int ->4, short ->2 ,int ->4 ,char ->1,其中最大值为4,整个结构体数据储存共占据12个字节,只有12(4×3)可以涵盖所有的用于储存变量的13个字节,最后结构体大小为12个字节。

在上图中我们可以清晰地观察到数据的存储状态,但是中间也有许多空间被浪费掉了。

3.为什么会存在内存对齐

  • 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  • 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
  • 总体来说:结构体的内存对齐是拿空间来换取时间的做法。
struct S1{char c1;int i;char c2;};//12struct S2{char c1;char c2;int i;};//8

在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:让占用空间小的成员尽量集中在一起。S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。

4.修改默认对齐数

之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数

#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));//12printf("%d\n", sizeof(struct S2));//6return 0;}

5.结构体传参

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

这两个函数都可以完成任务,但是函数传参的时候,参数需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,会导致性能的下降。而且传值变量不能改变变量的内容,我们不能在需要的时候改变原结构体。

结论:结构体类型传参只传递地址。

三、位段

1.概述

类似于结构体,不过后面的数字规定了定义的内部变量所占的比特位的数量。

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

  • 位段的成员必须是 int、unsigned int 或signed int 。(这个不用框的太死,其实任何整形数据都可以位断,只是其他数据不常用)
  • 位段的成员名后边有一个冒号和一个数字。
struct A{int _a:2;//a虽然是一个整型变量,但是只用两个比特位储存int _b:5;//余下同理int _c:10;int _d:30;};

2.位段的内存分配

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

举个例子:

struct S{char a:3;char b:4;char c:5;char d:4;};struct S s = {0};s.a = 10;s.b = 12;s.c = 3;s.d = 4;

首先,我们需要了解这几个变量在内存中如何分配空间:

(1)先开辟一个字节的空间:00000000

(2)a变量储存在后三个比特位:00000 000->a

(3)b变量储存在从第二个比特位开始后四个比特位:0 0000->b 000->a

(4)c变量的储存需要五个比特位,这个字节的比特位不够了,需要再次开辟一个字节的空间:0 0000->b 000->a 00000000

(5)c变量储存在后五个比特位:0 0000->b 000->a 000 00000->c

(6)d变量的储存需要四个比特位,这个字节的比特位不够了,需要再次开辟一个字节的空间:0 0000->b 000->a 000 00000->c 00000000

(7)c变量储存在后五个比特位:0 0000->b 000->a 000 00000->c 0000 0000->d

(8)最后这个结构体大小为三个字节,虽然有些比特位被舍弃了,但是相比原来还是大大节省了空间

然后,我们对其赋值

(1)10转化为二进制:1010,但内存只给了三个比特位,所以a只能储存010也就是2

(2)12转化为二进制:1100,给了四个比特位,b正常储存

(3)3转化为二进制:11,但内存给了五个比特位,c正常储存富余的位置补零

(4)4转化为二进制:100,但内存只给了四个比特位,d正常储存富余的位置补零

最终:0 1100->b 010->a 000 00011->c 0000 0100->d(地址从左到右递增)

3.位段的跨平台问题

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

总结:跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。由于位段节省空间的优势,它可以用于网络数据的信息传输。

四、枚举

1.枚举的定义

枚举顾名思义就是一一列举,把可能的取值一一列举。

比如:一周有七天,性别有两种,一年有十二个月等等。

enum Day{Mon,Tues,Wed,Thur,Fri,Sat,Sun};//星期enum Sex{MALE,FEMALE,SECRET};//性别

以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。{}中的内容是枚举类型的可能取值,也叫枚举常量

这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。

enum Day{Mon=1,Tues,Wed,Thur,Fri,Sat,Sun};//把第一个枚举常量定义为1,下面的枚举常量也会改变

2.枚举的优点

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

3.枚举的使用

enum Color//颜色{RED=1,GREEN=2,BLUE=4};enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。clr = 5; //这样也是可以的

五、 联合(共用体)

1.联合类型的定义

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

//联合类型的声明union Un{char c;int i;};//联合变量的定义union Un un;

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

#includeunion Un{int i;char c;};int main(){union Un un;printf("%p\n", &(un.i));printf("%p\n", &(un.c));printf("%d\n", sizeof(union Un));return 0;}//结果:005FF918//005FF918//4

由于内部变量共用部分空间,所以要避免同时使用内部变量,也就是上面的char c与int i不能同时使用。

2.联合体大小的计算

  • 联合大小的计算联合的大小至少是最大成员的大小。
  • 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

例如:

#includeunion Un1{char c[5];//可以理解为五个字符变量,5bytes,数据类型为char,对齐数为1int i;//4bytes,数据类型为int,对齐数为4};//类似于结构体,联合体也存在内存对齐。//所以它的大小是int对应的对齐数的整数倍,也就是8union Un2{short c[7];//可以理解为七个变量,14bytes,数据类型为short,对齐数为2int i;//4bytes,数据类型为int,对齐数为4};//所以它的大小是int对应的对齐数的整数倍,也就是16int main(){printf("%d\n", sizeof(union Un1));//8printf("%d\n", sizeof(union Un2));//16return 0;}

结构体、枚举和联合结束