导言:
c语言中有很多的数据类型,这些数据类型一般分为两类,基本数据类型和自定义的数据类型。本文旨在介绍一些常用的自定义类型和他们的使用细节,其中主要对结构体中的内存对齐进行介绍,以及对枚举和联合进行简单的说明。
目录
正文:
一.结构体
1.结构的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。利用struct关键字可以对结构体进行一些操作:
结构的声明:
struct tag
{
member-list;
}variable-list;
结构体变量的定义和初始化:
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct 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};//结构体嵌套初始化
简单了解了结构体后,我们来讨论一下结构体的大小为多少。这就涉及到了内存对齐的知识。
2.结构体内存对齐
结构体内存对齐是指编译器在分配结构体变量内存时,按照一定的规则将结构体成员变量存放在内存中的过程。由于硬件的特性和效率考虑,结构体成员变量的存储并不是按照其定义的顺序进行的,而是按照一定的对齐规则进行存储。
我们先来看一段代码:
#include<stdio.h>
struct S1{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main() {
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
运行结果如下:
这里可以发现两个结构体的成员变量除了定义的顺序不一样其它的完全一样,但即使这样两个的大小却不尽相同。这里就要先介绍内存对齐的规则了。
结构体内存对齐的规则
- 对齐原则:结构体成员变量的起始地址必须是其大小的整数倍。
- 对齐大小:结构体成员变量的对齐大小是根据其数据类型来确定的。一般情况下,基本数据类型的对齐大小为其大小本身,即char为1字节,short为2字节,int为4字节,double为8字节等。对于结构体中的嵌套结构体或指针类型,其对齐大小一般为4字节或8字节。
- 结构体对齐规则:结构体的对齐大小是其所有成员变量中最大的对齐大小。编译器会在结构体成员变量之间插入空隙,以保证对齐要求。这些空隙的大小取决于前一个成员变量的大小和对齐大小。
- 总大小规则:结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
光看文字还是比较难以理解的,下面进行画图分析,
我们先看S1,假设右边是S1的内存图,右边的数字是它们的地址,c1的对齐大小是1,i的对齐大小是4,c2的对齐大小是1。由规则1可知,c1要从0偏移量开始,紧接着是地址为2的地方,但2不是i的对齐大小4的倍数,所以这块空间不用来存i,结果就是浪费掉了,一直到4的地方,i才开始存入,而存完i之后的8是c2对齐大小1的倍数,所以c2就直接存入了8的位置。这样一共的大小就是9,但是由规则4可知,总的对齐数必须是最大对齐数的整数倍,所以9要一直到12的大小才结束,而中间多余的空间只能浪费掉了。
再来分析一下S2,c1要从0偏移量开始,紧接着是地址为2的地方,2又是c2对齐大小1的倍数,所以地址为1的地方存下了c2,紧接着是2,不是i的对齐大小4的倍数,所以一直自增,直到4的地方,4是i对齐大小4的倍数,所以在4的位置存下了i。总共加起来大小为8个字节,而8恰好又是最大对齐数4的倍数,所以总大小就是8。
根据上述的分析,我们应该已经知道了结构体大小的计算方式了,但是我们需要注意的是上述的案例是在vs进行的,在vs编译器环境下默认的对齐数是8,而数据的对齐数的大小是成员本身的大小和编译器默认的大小两者取最小值。
事实上我们可以通过#pragma来自定义默认的对齐数,我们可以测试更改默认值后的大小。
#include<stdio.h>
#pragma pack(2)//设置默认对齐数为2
struct S1{
char c1;//1
int i;//4
char c2;//1
};
#pragma pack()//取消默认的对齐数,还原为默认
#pragma pack(2)//设置默认对齐数为2
struct S2
{
char c1;//1
char c2;//1
int i;//4
};
#pragma pack()//取消默认的对齐数,还原为默认
int main() {
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
运行结果:
可以发现与第一次的又不一样了,可见默认对齐的大小更改后对内存的影响还是比较大的。所以结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。
那这种设计方式究竟有什么用呢?实际上,这是空间换时间的一种做法。
内存对齐的主要作用是提高内存访问的效率和性能。具体来说,内存对齐有以下几个用途:
提高内存访问速度:内存对齐可以使得结构体成员变量在内存中按照规则存放,减少了内存对齐不足的情况。这样可以使得CPU在访问内存时更加高效,减少了内存访问的时间和开销。
减少内存碎片:内存对齐可以使得结构体成员变量按照对齐规则存放,避免了因为成员变量大小不同而导致的内存碎片。这样可以充分利用内存空间,减少内存浪费。
避免内存访问错误:内存对齐可以保证结构体成员变量的存放位置正确,避免了因为对齐不足而导致的内存访问错误。这样可以提高程序的稳定性和可靠性。
平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
总的来说,内存对齐可以提高内存访问的效率,减少内存碎片,避免内存访问错误,从而提高程序的性能和稳定性。因此,在使用结构体时,我们需要了解内存对齐的规则,并根据实际需求进行合理的内存对齐设置
二.枚举
- 概念:枚举是一种用户自定义的数据类型,用于定义一组相关的常量。枚举常量的取值只能是预定义的枚举成员,不允许取其他值。
- 语法:枚举的定义使用关键字enum,其基本语法为:enum 枚举名 { 枚举成员列表 };
- 好处:1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量- 示例代码:
enum Weekday { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY };
枚举常量可以用于表示一周的星期几、月份、状态等,通过枚举可以提高代码的可读性和可维护性。
三.联合
- 概念:联合是一种特殊的数据类型,它可以在同一块内存空间中存储不同类型的数据。联合的成员变量共享同一块内存,只能同时存储其中一个成员的值。
- 语法:联合的定义使用关键字union,其基本语法为:union 联合名 { 成员列表 };
- 大小的计算:联合的大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。- 示例代码:
union Data { int i; float f; char str[20]; };
联合常用于节省内存空间,当多个成员变量不会同时使用时,可以使用联合来共享内存。联合还可以用于类型转换,通过改变联合的成员变量可以实现不同类型的数据转换。
四、枚举和联合在程序设计中的作用
- 提高可读性和可维护性:枚举常量可以用于表示一组相关的常量,通过枚举可以使代码更加易于理解和维护。
- 节省内存空间:联合可以在同一块内存中存储不同类型的数据,节省内存空间并提高程序的效率。
- 实现类型转换:通过改变联合的成员变量,可以实现不同类型的数据转换,提高程序的灵活性和扩展性。
总结:
本文主要对结构体,枚举和联合进行了一个较为简单的介绍。主要对结构体中的内存对齐进行了研究,结构体内存对齐是C语言中一个重要的概念,它可以提高内存访问的效率,并避免由于对齐不足而导致的内存访问错误。在使用结构体时,我们需要了解结构体内存对齐的规则,并根据实际需求在实际开发中进行灵活运用。枚举可以用于定义一组相关的常量,提高代码的可读性和可维护性;联合可以在同一块内存中存储不同类型的数据,节省内存空间并实现类型转换。希望本文能帮助到你。