C语言入门学习——结构体进阶(自定义类型 枚举 联合体)

本文详细探讨了结构体内存对齐的重要性,包括节省空间、提高性能和跨平台问题。介绍了内存对齐的四个基本规则,并通过实例解析了结构体大小受成员顺序影响的原因。同时,讲解了位段、枚举、联合(共用体)的特性及使用注意事项,强调了结构体传参时传地址的高效性。此外,还讨论了如何通过#pragma指令修改对齐数以优化内存使用。
摘要由CSDN通过智能技术生成

结构体内存对齐

结构体内存对齐问题是结构体中非常重要的知识点
请看代码:

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

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

int main()
{
	struct s1 S1;
	struct s2 S2;
	printf("%d %d\n",sizeof(S1),sizeof(S2));
	return 0;
}

在这里插入图片描述
为什么成员顺序不一样,结构体的大小就不一样呢?
结构体的对齐规则说明:

1 第一个成员在与结构体变量偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数:系统默认最大对齐数或该成员的大小,二者之中的较小值

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

在这里插入图片描述
图片中,是S2的结构体内存对齐,按照上述方式,来分析一下S1:
在这里插入图片描述
(1)首先C1在0偏移量地址处;
(2)C2大小为1字节与VS默认的8对齐数相比选小值,所以C2的对齐数是1的整数倍,1即可;
(3)i的大小为4字节,相比VS默认的8对齐数,选小值,所以 i 的对齐数是4的整数倍,而当前C2在1偏移量处,需要浪费2、3偏移量地址,才是4的整数倍,所以是4、5、6、7处的偏移量地址
(4)结构体的总大小,是所有对齐数里(1、1、4中)最大的对齐数的整数倍,而此时的字节总大小刚好是8个,所以结构体的大小就是0-7偏移量处的地址

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

struct S3
{
 double d;
 char c;
 int i;
}s3;
//练习-结构体嵌套问题
struct S4
{
 char c1;
 struct S3 s3;
 double d;
};
int main()
{
	struct S4 S4;
	printf("%u %u\n",sizeof(S4),sizeof(s3));
	return 0;
}

在这里插入图片描述
S3的自己的最大对齐数是8,总大小是16
在S4中,char c1在0处;
S3的对齐数倍数从8开始到23,共16个字节大小;
double d的对齐数是8,从24开始到31共8个字节大小;
S4的总大小就是成员里最大对齐数的整数倍,就是8的整数倍,当前在31处,刚好是32个字节大小,是8的整数倍
在这里插入图片描述

以上4条及其示例就是结构体内存对其的基本规则解释

结构体内存对其的基本规则

1. 第一个成员在与结构体变量偏移量为0的地址处
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数:系统默认最大对齐数或该成员的大小,二者之中的较小值
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数 倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
5. 没有默认对齐数时,自身大小就是对齐数

计算偏移量

利用 offsetof - 宏,头文件<stddef.h>,可以计算结构体成员,相对起始位置的偏移量,代码如下:

#include <stddef.h>

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

int main()
{
	printf("%d %d\n",offsetof(struct S3,i),offsetof(struct S3,d));
	return 0;
}

在这里插入图片描述
可以对比上述内存对齐的输出结果进行比较

为什么会有内存对齐?

  1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
    例如32位的平台,每次操作4个字节,若没有内存对齐,char型后面接int,当想要读取到int的内容,需要读取两次(char和int共5个,第一次读前4个,第二次往后读4个),才能完全读完;而使用内存对齐,int在char后面找其整数倍对齐,刚好一次就读到了int的内容
    所以结构体的内存对齐是拿空间来换取时间的

所以在设计结构体的时候,让占用空间小的成员尽量集中在一起。
例如最上面的 struct S1,就是前面两个char型挨着,减少了内存浪费
还可以修改默认对齐数,来达到减少内存浪费

修改对齐数

#pragma 这个预处理指令,可以修改对齐数

#pragma pack(8) 设置默认对齐数为8
#pragma pack()  取消设置,还原默认

不同的编译平台,默认对齐数是不同的

结构体的自引用
若是结构体自引用时,要使用指针,否则会循环

struct Node
{
 int data;
 struct Node next;
};
上述结构体打印它的大小时,是个未知数
struct Node
{
 int data;
 struct Node* next;
}
自引用时要使用指针

结构体传参

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

在这里插入图片描述
输出结果一样:
但是当结构体传参时,参数会有压栈的过程,是会产生时间和空间开销(形参是实参的一份临时拷贝,在栈区临时创建一份相同的,结束时销毁),假如结构体过大,那么传参大效率就会很低;
若是传的地址,则只是创健一个“临时的一级指针”(32位上为4字节大小)来保存传过来的结构体地址,然后通过地址就可以间接操作原结构体成员,在空间和时间上节省很多,若是只想读取,可在开头加上const即可
所以:结构体传参尽量传结构体的地址

位段

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

  1. 位段的成员必须是整型家族的数据类型

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

    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(位)
    };
    
struct S {
 char a:3;
 char b:4;
 char c:5;
 char d:4;
};

struct S s = {0};
int main()
{
	s.a = 10; 
	s.b = 12; 
	s.c = 3;
	s.d = 4;
	return 0;
}

在这里插入图片描述
请看内存中的现象,解释如下:
第一个字节
a在 0000 0000 中占3位,a是10,就是1010,取三位就是 0000 0010
b在 0000 0 中占4位,b是12,就是1100,取四位就是 0110 0010
c在 0 中占5位,而第一个字节剩一位,则再新申请一个字节
第二个字节
则c在 0000 0000 占5位 ,c是3,就是0011 取5位就是 0000 0011
d在 000 中占4位,剩余3位,则再次申请一个字节
第三个字节
d在 0000 0000 占4位,d是4,就是 0100 取四位就是 0000 0100
第一个字节 0110 0010 = 0x62,第二个字节 0000 0011 = 0x03,第三个字节 0000 0100 = 0x04
所以共占3个字节,小端存储 62 03 04 与输出结果一致

位段的设计就是为了节省空间,而内存对齐是牺牲空间,所以位段就是为了不内存对齐

位段不可跨平台:

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

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

跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在

枚举

枚举enum是一组被命名的整型常量的集合,是C语言中的一种基本类型,是把可能的取值一 一列举出来
枚举的定义

enum Day//星期
{
 Mon,
 Tues,		注意这里是 , 逗号
 Wed,
 Thur,
 Fri,
 Sat,
 Sun
};			结尾是;分号
int main()
{
	printf("%d %d %d\n",Mon,Tues,Wed);
	return 0;
}

若没给枚举初始化,则第一个常量被默认为0,后面的常量会递增 1,2,3…
在这里插入图片描述
若只给其中指定成员初始化会什么结果?请看代码:

enum Day//星期
{
 Mon = 1,
 Tues,
 Wed,
 Thur = 9,
 Fri,
 Sat,
 Sun
};
int main()
{
	printf("%d %d %d\n",Tues,Thur,Fri);
	return 0;
}

在这里插入图片描述
输出结果可知:其他未初始化的成员,会在初始化成员的后面接着递增

枚举的优点:

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
#define Mon	#define则没有enum Day c = Mon; 会提示有多个相同
  1. 防止了命名污染(封装)

  2. 便于调试

     程序编译的过程是:预编译 -> 编译 -> 汇编 -> 链接 -> .exe.....
     假如是#define Mon,在预编译时,就把代码里所有Mon替换为一个常量并删除Mon了,当需要调试时,虽然写着是Mon但实际已经是一个常量了,与真实的代码相比看不出变化,不便于调试;
     用枚举,就不会完全替换了,读到Mon,就是Mon,便于调试
    
  3. 使用方便,一次可以定义多个常量

联合(共用体)

联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。 比如:
在这里插入图片描述
对联合体赋值会怎样?请看代码:

//联合类型的声明
union Un
{
 char c ;
 int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小


int main()
{
	un.c = 1;
	printf("%d\n", un.i);
	return 0;
}

在这里插入图片描述
说明,int i 的4个字节与 char c 的有一个字节是共用的,具体分析如下:
c在 00 00 00 00 00 00 00 00只占最后两个(1字节),当赋值为1时,内存是:00 00 00 00 00 00 00 01,i打印是,打印4个字节,所以也是1,同时证明了是小端存储

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

union Un1
{
 char c[5]; 0~4
 int i;		最大对齐数45不是4的整数倍
};			浪费3个内存,结果为8
union Un2
{
 short c[7];	0~13
 int i;			最大对齐数414不是4的整数倍则浪费2个内存 结果为16
};

printf("%d\n", sizeof(union Un1));	8
printf("%d\n", sizeof(union Un2));  16

在这里插入图片描述

总结

  1. 要注意结构体的对齐规则
  2. 在设计结构体的时候,让占用空间小的成员尽量集中在一起。
  3. 使用位段时,不要对齐,但不可跨平台
  4. 结构体传参尽量传结构体的地址
  5. 枚举是常量,要注意枚举初始化的(常量之间递增1)
  6. 联合体也需要内存对齐
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值