自定义类型:结构体,枚举,联合(1)

目录

前言:

1、结构体:

1.1 结构体的基础知识:

1.2 结构体的声明:

         1.3 结构体的特殊声明:

         1.4 结构体的自引用: 

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

         1.6 结构体内存对齐 :

     1.6.1 结构体内存对齐方法一:

     1.6.2 结构体内存对齐方法二:

     1.6.3 为什么要存在内存对齐呢?:

1.7 修改默认对齐数 :

1.8 结构体传参 :

1.9 通过结构体实现位段 (位段的填充&可移植性):

1.9.1 什么是位段:

1.9.2 位段的内存分配:

1.9.3 位段的跨平台问题 :

1.9.4 位段的应用:


前言:

     所谓自定义类型即指:自己定义的类型,包括:结构体,枚举,联合,其中,结构体部分的内容主要包括:结构体类型的声明、结构的自引用、结构体变量的定义和

初始化 、结构体内存对齐、结构体传参、结构体实现位段(位段的填充&可移植性)枚举部分的主要内容包括:枚举类型的定义、枚举的优点、枚举的使用 联合部

的内容主要包括:联合类型的定义 、联合的特点、联合大小的计算

1、结构体:

1.1 结构体的基础知识:

     结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。数组是一种相同类型元素的集合,结构体也是一些值的集合,,但是不同的

是,结构的每个成员的类型可以是不同的。

1.2 结构体的声明:

     自定义类型:结构体,枚举,联合(1)_第1张图片

struct是结构体关键字tag是结构体名字,也称为结构体标签名struct tag整体称为结构体类型,,struct tag下面的{ }内部的内容都是结构体成员变量,这些成员变量

一般不需要初始化它的值,只需要定义一下类型即可,要注意{ }后面的分号;不要省略掉;

其中{ }后面的 variable-list 也是一个结构体变量,且是一个全局变量,结构的作用就是用来描述一个复杂对象的。

#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
	//例如描述一个学生
	struct Stu
	{
		char name[20];//名字
		int age;//年龄
		char sex[5];//性别
		char id[20];//学号
	}; //分号不能丢
	return 0;
}

1.3 结构体的特殊声明:

     在声明结构的时候,可以不完全的声明,此时的不完全的声明,指的就是匿名结构体类型,比如:

//匿名结构体类型
struct
{
 int a;
 char b;
 float c;
}x;

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

该匿名结构体类型省略了结构体名字,即,结构体标签名,这就称为是匿名结构体类型,即结构体的不完全声明。

//匿名结构体类型的指针
struct
{
    int a;
    char b;
    float c;
}* ps;

此时{ }后面的*和ps不是一起的,和前面的匿名结构体类型组合成为匿名结构体类型的指针,用该指针类型创建了一个全局变量叫做ps,由上述两个图我们发现,匿名结

构体类型和匿名结构体类型的指针中的结构体成员变量都是一样的,那能不能把结构体变量x的地址存放在结构体指针变量ps中呢,即:

//匿名结构体类型
struct
{
    int a;
    char b;
    float c;
}x;


//匿名结构体类型的指针
struct
{
    int a;
    char b;
    float c;
}* ps;

int main()
{
    ps=&x;
    return 0;
}

经过编译可知:

自定义类型:结构体,枚举,联合(1)_第2张图片

会报警告,在编译器看来,虽然两者中的结构体成员变量都是一样的,但是两种类型仍不相同,如果两者类型不同的话,该结构体指针ps中就不能存放该结构体变量x的

地址,所以这种写法是错误的,不合理的,编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。

不完全声明的结构,即匿名结构体类型创建好了之后,只能使用一次,后面就不可以使用该匿名结构体类型去创建变量了,并且只能使用在创建全局变量的时候使用那

么一次,这一次使用可以创建>=0个全局变量,然后在后面创建局部变量的时候,就不能再使用了,,因为不知道结构体名字,即不知道结构体标签名,就够不成一个完

整的结构体类型,所以该匿名结构体类型具有一定的局限性。

1.4 结构体的自引用:

     其作用主要是用来实现像链表这样的结构,结构体里面可以包含结构体变量的,即,一个结构体里面包含另外一个结构体类型的变量作为它的成员,这是没有问题

的,比如:

自定义类型:结构体,枚举,联合(1)_第3张图片

但是,要注意的是,虽然,结构体里面可以包含结构体变量,即,一个结构体里面包含另外一个结构体类型的变量作为它的成员,是可以的,但是,一个结构体里面

能包含他自身的结构体类型的变量作为他的成员,比如: 

自定义类型:结构体,枚举,联合(1)_第4张图片

我们先假设这是可以的,现在拿着结构体类型struct N 去创建一个局部变量sn,,如果让求该结构体变量sn所占内存空间的大小的话,我们就发现,当求该结构体所

占内存空间大小的时候,会出现死递归的现象,,所以这是不允许的,是错误的,所以对于结构体来说,自己里面不能包含自己,这是错误的, 

那结构体如何实现自引用呢?

数据结构即,数据在内存中存储的结构,比如:

现在有一组数字,1,2,3,4,5 如果想把这一组数字存储在内存里面,可以怎么样存储呢?

1.

给一块连续的空间,把数字存放进去,这就是使用了数组,,像这种,能够在内存中找到一块连续的空间把该数字存储起来的情况, 这种数据结构我们称之为:顺序

表,即,按照一定的顺序把数据存储在内存中。

2.

另外一种存储方式就是,把数据任意存储在内存中,并不是按照一定的顺序进行存储的,就是随机存储的,虽然是随机任意存储的,但是,我们可以找得到,1找2,2找

3,..4找5,等5找到之后,就不需要再找了,停下来即可,,所以,只要能够知道1的位置,就可以通过后面来把所有的数字全部找到,像这种用链条一样的形式把每一

个元素都找,并且每个元素在内存中并不是连续存放的情况时,,我们这种数据结构为:链表。

顺序表和链表统称为线性数据结构,即:

自定义类型:结构体,枚举,联合(1)_第5张图片

其次还有树形数据结构,典型的就是二叉树,

我们把链表每个存储数据的东西叫做节点,如何通过节点把每个数据串起来呢,,那么此时的节点应该怎么设计呢?

节点分为数据域和指针域,前一部分是数据域,后一部分是指针域,为了使得通过前面一个数据找到后面一个数据,,我们可以把后面一个数据的地址存放到前面一个

数据的指针域中去,对于数据5来说,没有下一个节点了,,所以就在存放数据5的节点中的指针域里面放置一个空指针NULL,就可以满足要求了,数据域里面存放的就

是该节点要存放的数据,而指针域是用来存放下一个节点中数据的地址的,如果把下一个节点中数据的地址存放在前一个节点的指针域中的话,就需要指针来存储,那

么就需要的是结构体指针。

struct Node
{
	int date;
	struct Node* next;
};

对于指针变量的大小,是可以计算的,4byet或者8byte,,上图整体就是一个节点,该节点中的数据可以找到与自己同类型的下一个节点中的数据,这就称为是结构体

的自引用,在此假设节点中数据的类型是结构体类型。

所以结构体的自引用的实现过程中,结构体里面不是包含一个同类型的结构体变量,而是包含一个同类型的结构体指针。

正确的结构体自引用的方式即为:

//代码2
struct Node
{
 int data;
 struct Node* next;
};

通过上图我们可以联想,是否能够改写成匿名结构体类型,即:

//代码2
struct 
{
 int data;
 struct Node* next;
};

就是把结构体名字,即结构体的标签名省略掉,,这样也是不可以的,,因为,如果再这里省略掉了结构体标签名,那么结构体成员变量中的 struct Node* next是怎么

来的呢,,所以,这样是不行的,错误的。

我们知道typedef是用来重命名的,既然上图不行,那我们能不能把该匿名结构体类型重新命名为一个新的名称呢,比如:

自定义类型:结构体,枚举,联合(1)_第6张图片

该图就表示,把这个匿名结构体类型加上typedef重新命名为红色框里面的Node,然后再拿着这个重命名后的Node结构体类型去定义指针变量next,这也是不行的,这

是因为,,你要产生新的Node,前提是你必须要有这个结构体类型,即下图蓝色的匿名结构体类型才可以

自定义类型:结构体,枚举,联合(1)_第7张图片

但是,我们观察蓝色框中发现,Node* next目前还不知道是什么意思呢,,因为,首先要有该匿名结构体类型才可以去重命名Node,但是,该匿名结构体类型中有参数

是不确定的,因为现在还没命名出来新的Node,,所以,Node* next 系统是不认识的,所以就不能进行重命名,更不用说去定义指针next了,,所以这也是不行的,就

是,我们首先要明确这个匿名结构体类型之后才可以去重命名Node,,然后再拿着Node去定义指针变量next,,但是,现在第一步中的匿名结构体类型就不明确,所

以,就无法得到重命名后的Node,更不用说再拿着Node去定义指针变量next了,,前后顺序出现错误,这种写法错误,

自定义类型:结构体,枚举,联合(1)_第8张图片

即使在C++中,也是不对的,所以只能写成:

typedef struct Node
{
    int date;
    struct Node* next;
}Node;

 这样的话,就是先有了结构体类型,并且参数都明确,然后再加上typedef再去重命名为Node,,这样才是可以的。

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

有了结构体类型,那如何使用该结构体类型定义结构体变量,其实很简单:

自定义类型:结构体,枚举,联合(1)_第9张图片

关于变量的初始化和定义以及访问:

自定义类型:结构体,枚举,联合(1)_第10张图片

1.6 结构体内存对齐 

     已知,一个int整型所占内存空间大小为4byte、一个char字符类型所占内存空间大小为1byte、一个float单精度浮点型所占内存空间大小为4byte、一个short短整型所

占内存空间大小为2byte、一个long长整型所占内存空间大小为4byte、一个long long长长整型所占内存空间大小为8byte、一个double双精度浮点型所占内存空间大小为

8byte、除此之外,还知道数组所占内存空间的大小可以根据该数组元素个数乘以该数组每个元素的类型所占内存空间大小就可以知道整个数组所占内存空间的大小为多

少, 那么在这里,怎么计算一个结构体所占内存空间的大小呢?

例题:

struct S
{
	char c1;//1byte
	int i;//4byte
	char c2;//1byte
};
int main()
{

	struct S s = { 0 };//不完全初始化
	printf("%d\n", sizeof(s));//6 ?
	//答案不是6byte,而是12byte,这是为什么呢?
	return 0;
}

当计算一个结构体所占内存空间大小的时候,不能简单的把每个结构体成员变量的类型的大小进行累加,这是不对的,因为计算结构体所占内存空间大小的时候,会涉

及到内存布局的问题,即内存对齐的问题结构体成员变量在内存中到底是怎么放置的呢?

1.6.1 结构体内存对齐方法一:

一、结构体成员变量都是变量的基本类型:

总体上遵循两个原则: 

(1)、整体空间是占用空间最大的成员(的类型)所占字节数的整数倍。 

(2)、数据对齐原则---内存按结构体成员的先后顺序排列,当排到该成员时,其前面已摆放的空间大小必须是该成员类型大小的整数倍,如果不够则补齐,依次向后类推。

例1、

struct  A
{
   char  a;
   double  b;
   int  c;
   char  d;
};

那么,这个结构体占用多少字节呢?

1、它的第一个成员是char 类型的 a,占 1 个字节,放入结构的0地址处:

自定义类型:结构体,枚举,联合(1)_第11张图片

2、下来要存储的是第二个成员是double 类型的 b,占 8 个字节,它该怎么存放呢?

我们回想到原则(2)中说,存储到某个成员时,前面已经存放的所有成员所占的总空间大小是该成员大小的整数倍,不够则补齐!

也就是1图中显示的下一个存储成员起始地址应该是 8 的整数倍,而上图中显示的起始地址为 1 ,并不是 8 的整数倍,所以需要补齐。向后增加地址数到 8 (补齐操

作),这时 8 是 8 的整数倍,可以存储。则将成员 b 存放到 8 地址处:

自定义类型:结构体,枚举,联合(1)_第12张图片

3、下来要存储的是第三个成员是 int 类型的 c,占 4 个字节,由上图所知,下一个存储成员的起始地址是16,刚好是 4 的倍数。所以不用补齐,直接用 4 个字节来存储成员 c:

自定义类型:结构体,枚举,联合(1)_第13张图片

4、最后存储的是第四个成员是 char 类型的 d,占 1 个字节。由上图所知,下一个存储成员的起始地址是20,刚好是 1 的倍数。所以不用补齐,直接用 1 个字节来存储 成员 d: 

自定义类型:结构体,枚举,联合(1)_第14张图片

5、把所有成员都存储到位,这就完了吗? 

   No!我们还要考虑最后一个要素:原则(1),整体空间是占用空间最大的成员(的类型)所占字节数的整数倍!而这个结构的占用空间最大的成员是 double 类型的

b,占用 8 个字节。而现在整体空间由上图所知是 21 个字节,并不是 8 的倍数,所以仍需要补齐!补多少呢? 比 21 大的最小的 8 的倍数是 24,所以就补到 24 字节:

自定义类型:结构体,枚举,联合(1)_第15张图片

所以最终算下来,结构体 A 的大小是 24字节!

6、测试:

 自定义类型:结构体,枚举,联合(1)_第16张图片

 

二、结构体成员变量中包含另外一个结构体时:

我们把本结构体称为父结构体,把成员结构体变量称为子结构体

(1)、整体空间是子结构体与父结构体中占用空间最大的成员(的类型)所占字节数的整数倍。

(2)、数据对齐原则---父结构体内存按结构体成员的先后顺序排列,当排到子结构体成员时,其前面已摆放的空间大小必须是该子结构体成员中最大类型大小的整数

倍,如果不够则补齐,依次类推。

(3)、数据对齐原则---内存按结构体成员的先后顺序排列,当排到该成员时,其前面已摆放的空间大小必须是该成员类型大小的整数倍,如果不够则补齐,依次向后类

推。

例2、

struct  A        //子结构体
{
  char  a;
  double  b;
  int  c;
  char  d;
};

struct  B        //父结构体
{
  char a;
  struct  A  b;
  int  c;
}; 

那么,结构体 B 占多少字节呢?通过之前分析,我们已知结构体 A 的大小为 24 字节:

1、首先存储成员 a ,占用 1 个字节,下一个存储地址是 1:

2、其次存储成员 b,而 b 是结构体 A 的变量,结构体A中最大类型大小double,占8 个字节,所以存储该变量地址应是 8 的倍数,所以需要补齐地址到 8 ,然后存储大

小为 24 字节的变量 b,因为b是结构体A的变量,所以要存储A所占的空间大小,即24byte,而不是存储double  b里面的8byte,所以,当遇到子结构成员(b)的时候,

往里存放的应该是整个子结构所占空间的大小;

自定义类型:结构体,枚举,联合(1)_第17张图片

3、最后存储成员 c,成员 c 类型是 int,占用 4 个字节。由上图可知 c 的存储始地址是 32 ,刚好是 4 的倍数,并不用补齐,所以直接存储:

自定义类型:结构体,枚举,联合(1)_第18张图片

4、最后也要看整体空间大小是否满足原则(1) :

我们发现,当前结构体总大小是 36 ,而子结构体和父结构体中最大类型是double,要注意,这时候要把struct A b这个数值24byte排除,他不能算入,所以占 8 个字

节。而 36 并非是 8 的整数倍,所以需要补齐为 8 的整数倍。即  40 字节。

自定义类型:结构体,枚举,联合(1)_第19张图片

所以最后,结构体B的大小是 40 字节。

5、测试: 

自定义类型:结构体,枚举,联合(1)_第20张图片

三、结构体成员变量中包含数组类型时:

之前的理解了,那么这个也就不难理解!

只要不把数组看作数组,而把它看成一个个类型相同的成员变量序列!然后用前面的计算方法计算即可。

struct Stu
{
	char name[20];
	int age;
};
int main()
{
	printf("sizeof(struct Stu) = %d\n", sizeof(struct Stu));
	return 0;
}

比如,这里是一个char类型的数组,我们可以不把他看成一个数组,而是看成20个char类型的变量,即:

struct Stu
{
	char 1;
    char 2;
    ..
    char 20;
    int age;
};
int main()
{
	printf("sizeof(struct Stu) = %d\n", sizeof(struct Stu));
	return 0;
}

首先存储的是成员1,占用1byte,下一个存储地址是1,我们发现结构体里面没有结构体成员是另外一个结构体,即没有子结构体和父结构体的概念,所以我们就要用第

一种方法的规律来做,再往下走,前面摆好的空间大小是1,是我char 2结构体成员变量的所占的1byte的整数倍,即1/1=1,是整数倍,所以不需要补,直接存储就行,

再往下走,前面摆好的空间大小是2,是我char 3结构体成员变量的所占的1byte的整数倍,即2/1=2,是整数倍,所以不需要补,直接存储就行.......一直等到把20个char

类型存放完,然后就开始存放int整型,,我们前面摆好的空间大小是20byte,他是我,int age结构体成员变量所占的4byte的整数倍,即:20/4=5,是整数倍,,所以不

需要补,直接存储,再存进去4byte,所以整体空间就是24byte,然后,在整个结构体中,占空间最大的成员就是age,类型是int,所占空间为4byte,,我们的整体空间

是24byte,又是我占用空间最大的成员age,类型为int的所占空间大小为4byte的整数倍,即:24/4=6,是整数倍,所以就不需要补,,最后所占空间就是24byte;

测试:

自定义类型:结构体,枚举,联合(1)_第21张图片

四、包含位域成员的结构体大小计算:

  使用位域的主要目的是压缩存储,其大致规则为:

(1)、如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止。

(2)、如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍。

(3)、如果相邻的位域字段的类型不同,则各编译器的具体实现有差异。VC6采取不压缩方式,gcc采取压缩方式。

(4)、如果位域字段之间穿插着非位域字段,则不进行压缩。

(5)、整个结构体的总大小为最宽的基本类型成员大小的整数倍。

例题1:

//在Linux + gcc 环境下,以下结构体的sizeof值是多少? 
struct A
{
	int f1 : 3;
	char b;
	char c;
};

解析: sizeof( struct A ) 的值为 4, 由于 f1 只占用了 3 个位,不足一个字节,假设 f1 摆放在地址 0 处,则地址1可用,故 b 摆放在地址 1 处,则 c 摆放在 2 处, 0 ~ 2

共 3 个字节不是 int 的整数倍(整个结构体的总大小为最宽基本类型成员int大小的整数倍),故地址 3 处补齐,共四个字节。如下图:

自定义类型:结构体,枚举,联合(1)_第22张图片

例题2、

//如果在VC环境下,sizeof(struct A)的值为 8!
struct B
{
	char f1 : 3;
	char f2 : 4;
	char f3 : 5;
};

解析: 按照之前规则,第一个字节仅能容下f1 和 f2,所以 f2 被压缩存储到 第一个字节中,而f3 只能从下一个字节开始存储,所以sizeof( struct B )的值为 2 ;如下图:

自定义类型:结构体,枚举,联合(1)_第23张图片

例题3、

//相邻位域类型不同的情况
struct  C
{
         char  f1:3;
         short  f2 : 4;
         char f3 : 5;
};

解析: 由于相邻位域类型不同,在VC中不压缩,所以sizeof值为 6 ,而在gcc中压缩,其值为 2 。 

例题4、

  struct  D
  {
         char  f1:3;
         short  f2 : 4;
         char f3 : 5;
  };

解析:非位域字段穿插其中,不会产生压缩,在VC和gcc中sizeof大小均为 3 。

1.6.2 结构体内存对齐方法二:

一、结构体成员变量都是变量的基本类型:

1. 第一个成员 要从 与结构体变量偏移量为0,即相对于起始位置的偏移量为0的地址 处进行存放,第一个成员变量永远都从该位置开始往后存放。
2. 其他成员变量 要放从与结构体变量偏移量的值是 对齐数 整数倍 的地址处开始存放,即其 他成员变量 要从与 相对于起始位置的偏移量值是对齐数的整数倍的地址处进
行存放 ,,其中, 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值 ,VS中默认的对齐数的值为8 ,Linux中,gcc编译器下没有默认对齐数的概念, 本质上就
是以该成员所占内存空间的大小作为对齐数的。
3. 结构体 总大小 最大对齐数(每个成员变量都有一个对齐数 ) 整数倍 。  
例题1、
struct S
{
	char c1;//1byte
	int i;//4byte
	char c2;//1byte
};
int main()
{

	struct S s = { 0 };//不完全初始化
	printf("%d\n", sizeof(s));//6 ?
	//答案不是6byte,而是12byte,这是为什么呢?
	return 0;
}

1、第一个成员要从与结构体变量偏移量为0,即相对于起始位置的偏移量为0的地址处进行存放。

第一个成员是char类型的c1,他要从相对于起始位置的偏移量为0的地址进行存放,由于c1是char类型的,所以占1byte,

自定义类型:结构体,枚举,联合(1)_第24张图片

2、 其他成员变量要从与结构体变量偏移量的值是对齐数整数倍的地址处开始存放,即其他成员变量要从相对于起始位置的偏移量值是对齐数的整数倍的地址处开始

存放,,其中,对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值,VS中默认的对齐数的值为8 ,Linux中gcc编译器下没有默认对齐数的概念,本质上就是以

该成员所占内存空间的大小作为对齐数的。

其他成员变量即指,除了第一个成员变量之外的成员变量,接下来就是其他成员变量中的整型变量i,,该变量的大小是4byte,VS下默认的对齐数是8,所以取两者较小
值得到的对齐数就是4,所以存放整型变量i的话,按理说应该从字符变量c1后面的地址开始存放,但是,该地址与起始位置的偏移量是1,1不是4的倍数,所以不能从这里
开始存放整型变量 i,再看要求,是指,把整型变量i存放到与结构体变量偏移量的值是对齐数的整数倍处,即整型变量i要放在相对于起始位置的偏移量的值是对齐数的
整数倍处,这里的对齐数就是4,所以整型变量i要从与起始位置的偏移量为4的地址处开始存放,又因整型变量i的类型是int类型,,占4byte,则有:

自定义类型:结构体,枚举,联合(1)_第25张图片

再看字符变量C2,,C2所占内存空间的大小就是1byte,VS下默认的对齐数就是8,,所以取较小值得到的就是1,所以,字符变量C2要从相对于起始位置的偏移量的值

是1的倍数的地址处进行存放, 而在int整型变量 i 存放在内存后的下一个位置相对于起始位置的偏移量是8,8又是1的倍数,所以,字符变量C2就可以从与起始位置偏移

量为8的地址处进行存放,C2是char类型,占1byte,则有:

自定义类型:结构体,枚举,联合(1)_第26张图片

此时,从相对于起始位置的偏移量为0的位置到相对于起始位置的偏移量为8的位置,两者中间的空间,就分配给了C1,i,C2了,,一共占据9byte,但是9byte并不是结

构体所占内存空间的总大小,还要满足第三个条件

3. 结构体总大小最大对齐数(每个成员变量都有一个对齐数 )整数倍。  

目前来说,结构体的大小是9byte,成员变量C1的对齐数就是1,成员变量 i 的对齐数是4,成员变量 C2的对齐数是1,所以所有的成员变量的对齐数的最大值就是4,

而,结构体的总大小应该是最大对齐数的整数倍,,现在结构体的大小是9byte,不是4的整数倍,所以不可以作为结构体的总大小,所以要再浪费3个字节,即,浪费3

个空间,即得,结构体的总大小是12byte。自定义类型:结构体,枚举,联合(1)_第27张图片 

由上图可知,从偏移量为0的地方到偏移量为11的地址,都开辟给了结构体变量,所以该结构体变量所占内存空间的大小就是12byte。

4、测试:

自定义类型:结构体,枚举,联合(1)_第28张图片

例题2、

//练习2
struct S2
{
  char c1;
  char c2;
  int i;
};
printf("%d\n", sizeof(struct S2));
//按照上述方法得出的结果是8byte。

例题3、

//练习3
struct S3
{
 double d;
 char c;
 int i;
};
printf("%d\n", sizeof(struct S3));
//按照上述方法得出来的结果就是 16 byte、

二、结构体成员变量中包含另外一个结构体时:

1. 第一个成员 要从 与结构体变量偏移量为0,即相对于起始位置的偏移量为0的地址 处进行存放,第一个成员变量永远都从该位置开始往后存放。
2. 其他成员变量 要放从与结构体变量偏移量的值是 对齐数 整数倍 的地址处开始存放,即其 他成员变量 要从与 相对于起始位置的偏移量值是对齐数的整数倍的地址处进
行存放 ,,其中, 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值 ,VS中默认的对齐数的值为8 ,Linux中,gcc编译器下没有默认对齐数的概念, 本质上就
是以该成员所占内存空间的大小作为对齐数的。
3. 结构体 总大小 最大对齐数(每个成员变量都有一个对齐数 ) 整数倍 。  
4. 如果 嵌套了结构体 的情况,嵌套的结构体,即子结构体,要从与相对于起始位置的偏移量的值是 子结构体自己的结构体成员变量最大对齐数的整数倍 处的地址进行存
放,结构体的 整体大小 就是所有结构体成员变量的 最大对齐数 的整数倍,包括 子结构体里面的成员变量 父结构体里面除了 子结构体 之外的成员变量。
//练习4-结构体嵌套问题
struct S4     //子结构体
{
	double d;
	char c;
	int i;
};
struct S5     //父结构体
{
	char c1;
	struct S4 s4;
	double d;
};
int main()
{

	struct S5 s5 = { 0 };//不完全初始化
	printf("%d\n", sizeof(s5));
	return 0;
}

由之前的题可知,sizeof(struct S4)= 16byte,,现在要在内存中放置结构体变量s4,已知:

如果 嵌套了结构体 的情况,嵌套的结构体,即子结构体,要从与相对于起始位置的偏移量的值是 子结构体自己的结构体成员变量最大对齐数的整数倍 处的地址进行存
放,结构体的 整体大小 就是所有结构体成员变量的 最大对齐数 整数倍 ,包括 子结构体里面的成员变量 父结构体里面除了 子结构体 之外的成员变量。
子结构体, 要从与相对于起始位置的偏移量的值是 子结构体自己的结构体成员变量最大对齐数的整数倍 处进行存放,而子结构体自己的成员变量有的d,它的对齐数是
8,还有c,它的对齐数是1,还有 i ,它的对齐数是4, 三者中最大对齐数就是8 ,所以, 子结构体要从与相对于起始位置的偏移量的值是子结构体自己的结构体成员变量
中最大对齐数8的整数倍处开始存放 ,则有:
自定义类型:结构体,枚举,联合(1)_第29张图片

最后要放置的变量是双精度的double b,它的大小是8byte,VS下默认的对齐数也是8,所以,取较小值作为对齐数就是8,,现在双精度类型的

浮点型变量b要从与相对于起始位置的偏移量值是8的整数倍的地址处行存放,,因为24是8的倍数,所以直接从偏移量是24的位置处开始存放双

精度浮点型变量b,,则有:

自定义类型:结构体,枚举,联合(1)_第30张图片

现在结构体所占内存空间的大小就是32byte,,又因为:结构体的整体大小就是所有结构体成员变量的最大对齐数整数倍,包括子结构体里面的成员变量父结构体

里面除了子结构体之外的成员变量。

此时,子结构体里面的成员变量的最大对齐数是8,父结构体里面,除了子结构体之外的成员变量的最大对齐数是、也是8,所以综合在一起,就是所有的结构体成员变

量,除了父结构体里面的子结构体之外的所有的结构体成员变量的最大对齐数就是8,而32又是8的整数倍,所以,该结构体总大小就是32byte。

1.6.3 为什么要存在内存对齐呢?:

因为通过上述可知,如果存在内存对齐的话,,就避免不了会浪费内存空间,那为什么还要存在内存对齐呢?

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

2. 性能原因: 数据结构( 尤其是栈 ) 应该尽可能地在自然边界上对齐。 原因在于,为了访问 未对齐的内存 ,处理器需要作 两次 内存访问;而 对齐的内存 访问仅需要 一次 访
问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
自定义类型:结构体,枚举,联合(1)_第31张图片

那在设计结构体的时候,我们 既要满足对齐,又要节省空间 ,如何做到:
让占用空间小的成员尽量集中在一起。
//例如:
struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
struct S3
{
	int i;
	char c1;
	char c2;
};
int main()
{
	printf("%d\n", sizeof(struct S1));//答案是12byte
	printf("%d\n", sizeof(struct S2));//答案是8byte
	printf("%d\n", sizeof(struct S3));//答案是8byte
	//S1和S2类型的结构体成员变量是一模一样,但是S1和S2所占空间的大小有了一些区别。
	//所以,在结构体成员变量一样的情况下, 如果调整各结构体成员变量的顺序,在对齐的过程中,所占内存空间的大小是可能不一样的。
	//所以,只要设计得当,是可以节省内存的,即,只要保证尽量让占用空间小的成员尽量集中在一起,就会节省空间,观察S2,S3,只要保证让
	//占用空间小的成员尽量集中在一起,看成一个整体,不需要考虑整体的顺序,都是可以节省空间的。
	return 0;
}

1.7 修改默认对齐数 

已知,在VS编译器下, 默认对齐数是8,那么该默认对齐数的数值是否能够发生改变呢?

之前我们见过了 #pragma 这个 预处理指令 ,这里我们再次使用, 可以改变我们的默认对齐数。
#include 
#pragma pack(8)//设置默认对齐数为8
			   // #pragma pack(8) === #pragma pack(),,两者都是默认对齐数是8.
struct S1
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认,原来的默认本来就是8,所以,这里的 #pragma pack 没起作用。

#pragma pack(1)//设置默认对齐数为1
struct S2
{
	char c1; //1  1 —— 1
	int i;   //4  1 —— 1    三者最大对齐数就是1,相当于是没有对齐。
	char c2; //1  1 —— 1
};
#pragma pack()//取消设置的默认对齐数,还原为默认,默认是8

#pragma pack(2)//设置默认对齐数为2
struct S3
{
	char c1; // 1  2 —— 1
	int i;   // 4  2 —— 2    三者最大对齐数就是2;
	char c2; // 1  2 —— 1
};
#pragma pack()//取消设置的默认对齐数,还原为默认,默认是8

int main()
{
	//输出的结果是什么?
	printf("%d\n", sizeof(struct S1)); //12
	printf("%d\n", sizeof(struct S2)); //6
    printf("%d\n", sizeof(struct S3));//8
    //根据上述总结的方法即可得到这些结果。
	return 0;
}

默认对齐数就是限制了结构体成员变量的最大对齐数就是该默认的数字。

当把默认对齐数设置为1的时候,就相当于是没有进行对齐,在设置默认对齐数的时候,一般不会设置成奇数,都是设置成偶数,除非在特定的情况下,比如,就是不需

要对齐的情况下,就可以把默认对齐数设置成1

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

百度笔试题:
写一个宏, 计算结构体中某变量相对于首地址的偏移 ,并给出说明:
考察: offsetof 宏的实现 ,注:这里还没学习宏,可以放在宏讲解完后再实现。
offsetof (type,member) === int offsetof (type,member)        头文件是:#include

type:结构体类型,       member:结构体成员变量的成员名,返回的是结构体成员变量相对于起始位置的偏移量,是一个整型数字。

自定义类型:结构体,枚举,联合(1)_第32张图片

1.8 结构体传参 


struct S 
{
	int data[1000];
	int num;
};
//结构体变量的值进行传参
void print1(struct S s) 
{
	printf("%d\n", s.num);
}
//结构体变量地址传参
void print2(struct S* ps)
{
	printf("%d\n", ps->num);
}
int main()
{
	struct S s = { { 1, 2, 3, 4 }, 1000 };//不完全初始化
	print1(s);   //传值调用   把结构体变量的值传过去
	//传值调用,形参就是实参的一份临时拷贝,当实参所占内存空间较大的时候,形参也要开辟一个与之大小相同的空间,传递的时候内存很大,用时较多,
	//传过去之后,形参部分还要开辟相同的空间进行存储,所以会造成时间和空间上的双重浪费,
	//改变形参不会影响实参,把数据拷贝过去之后,只能使用,不能更改实参中的数据,不好。
	print2(&s);  //传址调用   把结构体变量的地址传过去
	//只把地址传过去了,地址和指针变量所占内存空间的大小是4/8byte,所以不会浪费很多空间,并且传递的时候由于内存较小,时间也较短,效率很高
	//改变形参会影响实参,功能强大, 较好。
	return 0;
	//传址调用会更好一些,函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
	//如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
    //当使用传址调用的时候,如果不想通过形参改变实参的话,只需要在形参部分*的左边加上const即可,可以做到想改就改,不想改就不改。
	//结构体变量传参的时候,一般都是传地址。
}

1.9 通过结构体实现位段 (位段的填充&可移植性):

1.9.1 什么是位段:

位段的声明和结构体是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 或signed int,或者char类型(整形家族)  。
2.位段的成员名后边有 一个冒号和一个数字。
比如:
struct A 
{
 int _a:2;
 int _b:5;
 int _c:10;
 int _d:30;
};
A就是一个位段类型,那位段 A 的大小是多少?
struct A 
{
	int _a : 2;    //_a占2个bit位
	int _b : 5;    //_b占5个bit位
	int _c : 10;   //_c占10个bit位
	int _d : 30;   //_d占30个bit位
};
int main()
{
	printf("%d\n", sizeof(struct A));//8byte
	return 0;
}

1.9.2 位段的内存分配:

1. 位段的成员可以是 int      unsigned int      signed int 或者是 char (属于整形家族)类型
2. 位段的空间上是按照需要以  4个字节( int,包括上面三种) 或者 1个字节( char ) 的方式来 开辟 的。
3. 位段涉及很多不确定因素, 位段是不跨平台的,注重可移植的程序应该避免使用位段。
struct A 
{
	//看到int,是按照4byte进行开辟的,一个整型一个整型的开辟,所以,在这里先开辟一个整型
	//4byte === 32bit
	//_a要走2个bit,还剩30bit,_b再要走5bit,还剩25bit,_c再要走10个bit,还剩15bit位,后者如果所要的bit位的个数在前面还够提供的时候
	//会先使用上一次剩余的,而不会再开辟新的,当_c要完10个bit位时,还剩15个bit,_d再要30个bit,不够用了,所以会再开辟一个整型,即再开辟4byte
	//32bit,现在出现一个问题,在新的4byte开辟之前还剩余了15个bit,那这15个bit会先使用掉呢,即新的和旧的4byte中各使用了15个bit,
	//还是直接不使用,而去使用了新开辟的4byte中的30bit位呢? —— 答案是不使用之前剩下的,而去直接使用新开辟的4byte中的30bit位。

    //C语言本身并没有规定上述实现过程,这只是在VS编译器下的实现方式,在别的编译器下不一定是直接使用新的4byte,所以说,位段涉及到了很多的不确定因素,不可以跨平台使用,如果避免不了不使用位段,就需要把各个平台的实现方式搞清楚,从而写出不同的代码来实现功能,在VS编译器下,每个字节内部的bit位使用顺序不清楚是从低到高使用还是从高到低使用,不确定因素。


	//不管怎么样,在此过程中,一共开辟了2次4个byte,所以共开辟了8byte,所以所占内存空间的大小就是8byte,结果就是8.
	int _a : 2;    //_a占2个bit位
	int _b : 5;    //_b占5个bit位
	int _c : 10;   //_c占10个bit位
	int _d : 30;   //_d占30个bit位
};
int main()
{
	printf("%d\n", sizeof(struct A));//8byte
	return 0;
}

现在问题出现了,我们知道,一个int占4byte,32bit,好好的int为什么要规定成占2,5,10,30个bit位呢

主要是为了用来节省空间,比如:假设性别共有3种情况,男,女,保密,如果使用二进制数字,即比特位来表示的话,只需要两个二进制数字,两个bit位就可

以表示出所有情况,因为两个bit位可以表示2^2=4种情况,00,01,10,11 ,这四种情况,就完全可以把上面3种性别表示出来,如果不写成位段的情况,_a就会开

4个byte,32bit,可以表示2^32种情况,只用来表示3种性别,就会造成内存空间浪费,所以,如果一些结构体成员变量取值不是很大的时候,就可以使用

段来节省空间,避免浪费,根据实际的需求,通过位段来分配所需要的内存块的大小,根据实际需求来设置位段的大小;

我们常见的通过结构体来实现的位段,结构体成员变量的类型,即位段的类型一般都是相同的,也有不相同的情况,在这里不进行讨论,默认为位段的类型都是

相同的即可。

由上面的位段来看,_a,_b,_c,_d中能存储的数据大小肯定被限制了,因为bit位数不同,存储能力也不同。

还有就是:int _d:50 这是不对的,因为,int占4byte,最多有32bit,现在要求有50个bit位,肯定是不行的,位段的大小不可以超过该类型本身占据的bit位的个

数,最多相等。

使用位段来节省内存空间的时候,在VS编译器下一个字节(char)/四个字节(int)内部的地址从低位向高位来使用的,当bit位不够用的时候,会进行开

辟新的空间,,在VS编译器下,会去直接使用新开辟的字节中的bit位,而不使用旧空间中的bit位,直接浪费掉。

对于数组来说,随着下标的增加,地址从低到高进行使用,对于结构体中的结构体成员变量来说,按照顺序从低地址往高地址进行使用,同时,监视内存中的地

址,是从左往右地址依次增加,所以,当我们绘制数组和结构体成员变量所占内存空间的时候,常默认为从左到右地址依次增加,而位段是由结构体实现的,所

以位段中的结构体成员变量内存开辟和结构体是一样的,所以在绘制图形的时候,也是从左往右进行绘制

例题:

//一个例子
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)_第33张图片

自定义类型:结构体,枚举,联合(1)_第34张图片

 1.9.3 位段的跨平台问题

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

1.9.4 位段的应用:

自定义类型:结构体,枚举,联合(1)_第35张图片

 今天的分享到底结束,如果对您有用,请点赞收藏哦~

你可能感兴趣的:(c语言)