C语言-自定义类型-结构体(详解结构体内存对齐)

文章目录

  • 前言
  • 一、结构的声明
    • 结构体的基础知识
    • 1.结构体的声明
  • 二、特殊的声明
  • 三、结构自引用
    • 解决方案 1:
    • 解决方案 2:
  • 四、结构体变量的定义和初始化
  • 五、*结构体内存对齐
  • 六、修改默认对齐数
  • 总结


前言

本文会详解C语言中自定义类型中结构体的使用、声明、自引用、结构体变量的定义和初始化、*结构体内存对齐、结构体传参等详细说明


一、结构的声明

结构体的基础知识

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

1.结构体的声明

C语言-自定义类型-结构体(详解结构体内存对齐)_第1张图片
上图所示,MyStruct是属于结构体标签,根据个人实际需求来自行更改;大括号里面的称为成员列表(成员变量),里面可以有一个也可以有多个,每个成员可以为不同类型的变量;
有了结构体类型,接着就要创建结构体变量了,如下
C语言-自定义类型-结构体(详解结构体内存对齐)_第2张图片


二、特殊的声明

在声明结构的时候,可以不完全声明。如:
C语言-自定义类型-结构体(详解结构体内存对齐)_第3张图片
则此时创建变量就不能像上面所示的写法创建了
C语言-自定义类型-结构体(详解结构体内存对齐)_第4张图片
那么应该怎么创建变量呢?如图
C语言-自定义类型-结构体(详解结构体内存对齐)_第5张图片
该写法称为匿名结构体类型。意思就是标签名字省略不写,但是你想用这个类型创建变量,只能把变量写在全局变量的位置,否则不能创建变量,这个匿名结构体类型只能使用一次。
那如果在两个成员一模一样的情况下,写一个匿名结构体类型,再写一个匿名结构体的指针,那么这两个结构体在编译器看来是不是一样的?
C语言-自定义类型-结构体(详解结构体内存对齐)_第6张图片
我们在main函数中测试一下,将s1的地址赋值给p变量,看看能否正常赋值
C语言-自定义类型-结构体(详解结构体内存对齐)_第7张图片
在这里插入图片描述
可以发现编译器给出了警告,从“ * ”到“ * ”类型不兼容,也就是说编译器认为两边的类型不相同的,所以在匿名结构体类型里面虽然成员是一样的,但是编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的!


三、结构自引用

在结构中包含一个类型为该结构体本身的成员是否可以呢?
C语言-自定义类型-结构体(详解结构体内存对齐)_第8张图片
如果像这样去设计,那么sizeof(struct Node)有多大,int类型是4个字节,那么n是多大呢?那么这个结点里还有一个data,还有一个n,就会一直循环下去,所以没办法确定,所以该写法是错误的。
当一个结点要找一个同类型的下一个结点的时候,叫做自引用C语言-自定义类型-结构体(详解结构体内存对齐)_第9张图片
如上图链表所示,一个结点既要保存1这个数,又要有能力找到下一个结点,所以我们可以认为这一个结点就相当于一个复杂对象,我们把它设计成一个结构体。
每一个结点都会存上下一个结点的地址,所以每个结点我们可以分为两个部分,一部分存数据,一部分存地址
C语言-自定义类型-结构体(详解结构体内存对齐)_第10张图片

解决方案 1:

使用指针。由于指针的长度是确定的(在32位及其上指针长度为4)所以编译器能够确定该结构体的长度

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

struct Node* next在结构体内找到同类型的下一个结点的地址,就是结构体的自引用。使用该结构体,结构体的类型就是struct Node

解决方案 2:

使用typedef

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

该方案的目的是使用typedef为结构体创建一个别名。


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

变量的创建:
C语言-自定义类型-结构体(详解结构体内存对齐)_第11张图片
变量的初始化:
创建的同时进行初始化
C语言-自定义类型-结构体(详解结构体内存对齐)_第12张图片
C语言-自定义类型-结构体(详解结构体内存对齐)_第13张图片


五、*结构体内存对齐

我们先来测试一下,将两个结构体内部顺序变换一下,问这两个结构体的大小是多少?是相等吗?还是?
C语言-自定义类型-结构体(详解结构体内存对齐)_第14张图片
程序运行得出结果:
C语言-自定义类型-结构体(详解结构体内存对齐)_第15张图片
为什么这个结果变成12和8呢?这就涉及到结构体的内存对齐了,意思就是,结构体在内存中要进行一定的对齐,放到对齐的位置上才可以。
在了解内存对齐之前,我们首先来了解一下offsetof宏,用来计算结构体成员相对于起始位置的偏移量
C语言-自定义类型-结构体(详解结构体内存对齐)_第16张图片
则由结构体成员的偏移量可以得出,S1在内存中是如何存放的,如下图
C语言-自定义类型-结构体(详解结构体内存对齐)_第17张图片
接着S2也是同理,只是位置顺序相对于S1变换了,所以内存空间如下图
C语言-自定义类型-结构体(详解结构体内存对齐)_第18张图片
C语言-自定义类型-结构体(详解结构体内存对齐)_第19张图片
可以看出,使用offsetof测试出来变量a、b、i,的起始位置的偏移量与上述S1,S2的内存空间图中,a、b、i的起始位置是一样的,一个是0,4,8,一个是0,1,4。
所以结构体内存到底是如何对齐的?

结构体内存对齐的规则:

  1. 结构体的第一个成员直接对齐到相对于结构体变量起始位置为0的偏移处。
  2. 从第二个成员开始,要对齐到某个【*对齐数】的整数倍的偏移处。(*对齐数:结构体成员自身大小和默认对齐数的较小值。VS编译器环境:8;Linux编译环境默认不设对齐数,对齐数就是结构体成员自身大小)
  3. 结构体的总大小,必须是最大对齐数的整数倍。
    每个结构体成员都有一个对齐数,其中最大的对齐数就是最大对齐数。
  4. 如果仙桃了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

满足了以上结构体内存对齐的规则,我们可以分析出,上述内存图为何这么设计。如S1为例子:
C语言-自定义类型-结构体(详解结构体内存对齐)_第20张图片
C语言-自定义类型-结构体(详解结构体内存对齐)_第21张图片
接着来探讨S2,s2中变量a本身为1个字节,从起始位置0开始,变量b自身大小为1默认对齐数为8,所以对齐数为1,b就要放在1的倍数空间上,所以直接放在空间为1的位置处,接着找i,i的自身大小为4默认对齐数为8,所以对齐数为4,所以要放在4的倍数的空间上,所以2的偏移和3的偏移用不上,所以从4开始;最后所有对齐数最大的是4,所以结构体大小就是4的倍数,而8刚好是4的倍数,后面没有浪费空间,得出最后结构体大小为8
C语言-自定义类型-结构体(详解结构体内存对齐)_第22张图片
结构体内存对齐规则的第四条规则举实例说明:

struct S3
{
	double d;
	char c;
	int i;
};

struct S4
{
	char a;
	struct S3 s3;
	double d;
};

int main()
{
	printf("%d\n", sizeof(struct S4));
	return 0;
}

C语言-自定义类型-结构体(详解结构体内存对齐)_第23张图片
由图所示,所有对齐数的里面最大的是8,32是8的倍数,不需要往后浪费空间,所以s4的大小为32。
那么为什么存在内存对齐?

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

那么在设计结构体时,既要满足内存对齐,又要节省空间,如何做到呢?
struct S2为例,让占用空间小的成员尽量集中在一起即可满足!


六、修改默认对齐数

使用#pragma这个预处理指令,来改变默认对齐数

// 设置默认对齐数
#pragma pack(1)

struct S1
{
	char a;
	int i;
	char a2;
};

// 恢复默认对齐数
#pragma pack()

int main()
{
	printf("%d\n", sizeof(struct S1));

	return 0;
}

运行结果如下:
C语言-自定义类型-结构体(详解结构体内存对齐)_第24张图片
C语言-自定义类型-结构体(详解结构体内存对齐)_第25张图片
可以看出,将默认对齐数手动设置成1,则在内存中可以看出变量不需要对齐了,直接往后依次开辟空间。


总结

以上就是对C语言自定义类型-结构体的详解了,希望该文章对大家的学习有所帮助!
制作不易,点个关注点个赞吧!

你可能感兴趣的:(C语言,c语言,学习,数据结构,c++)