C语言字节对齐、位域、枚举、联合体

C语言字节对齐


什么是字节对齐

现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。


为什么要字节对齐

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

总结:实际就是用空间时间,为了达到更快的对变量的访问速度。


字节对齐的四个关键概念

基本数据类型的自身对齐值。
程序的指定对齐值:即#pragma pack(value)时的指定对齐值value。(默认#pragma pack(8),其中value=2^n)
自定义类型的自身对齐值:结构体或类的成员中自身对齐值最大的值。
自定义类型的有效对齐值:自定义类型的自身对齐值和程序指定对齐值中较小的值。min=(程序指定对齐值,自定义类型对齐值)

注:自定义类型的有效对齐值最关键。

#pragma pack(4)
struct test
{
    
    
	char a;	// 1
	short b;// 2
	char c; // 1
};
//上述结构体test的自身对齐值为2(b的自身对齐值),而指定对齐值为4(32位编译器默认值),故最终的有效对齐值为2.

当字节没有对齐时,我们会在内存中添加一些字节(不使用)来达到字节对齐的效果。添加要点:

当前类型的字节对齐值+添加字节+之前所有类型大小总和=下一个基本数据类型的自身对齐值与程序的指定对齐值中的最小值的倍数(min=(下一个基本数据类型,程序指定的对齐值))
对于最后一个类型的字节对齐值+添加字节+之前所有类型大小总和=自定义类型的有效对齐值的倍数


【举例】

在这里插入图片描述

S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。

//举例说明
struct S1 {
    
    
    char c1; //1+3 注:当前类型(char)自身对齐值为1要是下一个类型的有效值4(min=(#pragma pack(8),int))的倍数所以最少要补3个字节
    int i;	 //4 注:因为当前类型自身对齐值为4是下一个类型的有效值1(min=(#pragma pack(8),char))的倍数所以不用补字节
    char c2; //1+3 注:要满足S1的整体大小满足自定义类型的有效对齐值(4=min(8,4)) 默认#pragma pack(8)的倍数所以还需要补3个字节
};

struct S2 {
    
    
    char c1;//1 注:当前类型(char)自身对齐值为1是下一个类型(char)的自身对齐值1的倍数所以不用添加
    char c2;//1+2 注:当前类型自身对齐值为1+之前所有类型大小1+添加字节总和是下一个类型的自身对齐值4的倍数所以最少要补2个字节
    int i;//4
};

int main() {
    
    
    printf("%d\n",sizeof(struct S1));//12
    printf("%d\n", sizeof(struct S2));//8
    system("pause");
    return 0;
}

内存分配效果:
在这里插入图片描述

结论:让占用空间小的成员尽量集中在一起。


修改默认对齐数

#pragma  pack(1)

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

在这里插入图片描述

结论:结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。当指定#pragma pack(1),结构体的不管怎么排列有效值都为1,这时结构体大小就是各个类型的字节大小相加。

【练习】

在这里插入图片描述


struct S3
{
    
    
    double d;//8
    char c;  //1+3  注:自身类型(char)对齐值为1+之前类型大小8=下一个类型的有效值4(min=(int,默认#pragma pack(8)))的倍数所以添加3个字节
    int i;   //4    注:当前类型(int)对齐值4+之前所有类型12+添加字节=自定义有效值对齐值(8)的倍数,因此不用添加
};
//自定义类型的自身对齐值:结构体或类的成员中自身对齐值最大的值。
struct S4
{
    
    
    char c1;//1+7 注:自身类型(char)对齐值1要是下一个自定义类型的对齐值(S3成员中的自身对齐值最大的(double)=8)的倍数所以需要补7个字节的空间。
    struct S3 s3;//16
    double d; //8
};

int main()
{
    
    
    //输出的结果是什么?
    printf("size S3=%d\n", sizeof(struct S3)); //16
    printf("size S4=%d\n", sizeof(struct S4)); //32
    return 0;
}

结论:自定义类型的自身对齐值:结构体或类的成员中自身对齐值最大的值。

在这里插入图片描述

#pragma pack(4)
unsigned short* pucCharArray[10][10];
typedef struct unRec
{
    
    
    unsigned long ullndex;	//4	注:自身类型(long)对齐值4是下一个类型有效值(min=(short,#pragma pack(4)))的倍数因此不用添加字节
    unsigned short usLevel[7]; //14
    unsigned char ucPos; //1 + 1 注:要达到结构体有效值4(min=(#pragma pack(4),long))的倍数所以还需要补一个字节。
}REC_S;
REC_S stMax, * pstMax;

int main()
{
    
    
    //输出的结果是什么
    printf("%d\n", sizeof(pucCharArray));//400 注:一个二维数组,这个数组有100个short *的指针,每个指针大小是4
    printf("%d\n", sizeof(stMax));	//20
    printf("%d\n", sizeof(pstMax));	//4
    printf("%d\n", sizeof(*pstMax));//20
    return 0;
}

在这里插入图片描述

#pragma pack(4)
struct tagAAAA{
    
    
	struct
	{
    
    
		char ucFirst;   //1+1 注:自身类型(char)的对齐值1要是下一个类型有效值的2(min=(#pragma pack(4),short))倍数 因此补一个字节
		short usSecond; //2	注:自身类型(short)的对齐值2+之前所有的类型大小(2)+添加字节=下一个类型有效值的1(min=(#pragma pack(4),char))倍数 所以不用添加字节
		char ucThird;  	//1+1 注:自身类型(char)的对齐值1+添加字节+之前所有类型的大小4=该结构体有效值对齐值2(min=(short,#pragma pack(4)))的倍数 所以还需要添加一个字节
	}half;
	short kk; 	//2 注: 自身类型(short)的对齐值2+之前所有类型的大小6+添加字节=外层结构体有效值2(min=(short,#pragma pack(4)))的倍数所以不用添加字节
}number;
struct tagBBBB{
    
    
	char ucFirst;	//1+1
	short usSecond; //2
	char ucThird;   //1+1
	short usForth;  //2
}half;
struct tagCCCC{
    
    
	struct
	{
    
    
		char ucFirst; //1+1
		short usSecond; //2
		char ucThird; //1+1
	}half;
	long kk;	//4+2 注: 自身类型(long)的对齐值4+之前所有类型的大小6+添加字节=外层结构体有效值4(min(max(long,short),#pragma pack(4)))的倍数所以所以还需要添加两个字节
};
int main()
{
    
    
    //输出的结果是什么?
    printf("%d\n", sizeof(struct tagAAAA));	//8
    printf("%d\n", sizeof(struct tagBBBB)); //8
    printf("%d\n", sizeof(struct tagCCCC)); //12
    return 0;
}

C语言位域

位域是指信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。


在结构内声明位域的形式如下

struct
{
    
    
  type [member_name] : width ;
};

下面是有关位域中变量元素的描述:

元素 描述
type 只能为 int(整型),unsigned int(无符号整型),signed int(有符号整型)或者是 char (属于整形家族) 四种类型,决定了如何解释位域的值。
member_name 位域的名称
width 位域中位的数量,宽度必须小于或等于指定类型的位宽度。

在这里插入图片描述

上段代码在内存中的存储形式是:

在这里插入图片描述


位域存储需要注意

一个位域必须存储在同一个字节中,不能跨两个字节
不能跨类型存储,若跨类型需要考虑字节对齐。这就违背了原本节约空间的本意了
位域的长度不能大于指定类型固有长度,比如说int的位域长度不能超过32,char的位域长度不能超过8
位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的


不能跨字节存储

typedef struct Test {
    
    
    char a : 1;
    char b : 6;	
    char c : 3;	//存放在下一个字节字节
}Test;
Test test;
sizeof(test)=2;

不能跨类型存储,若跨类型存储要考虑字节对齐

在这里插入图片描述

typedef struct Test {
    
    
    char a : 1;
    int b : 1;
}Test;
int main()
{
    
    
   
    Test test;
    printf("%d\n", sizeof(test));//8
    return 0;
}

位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。

struct k
{
    
    
    int a : 1;
    int   : 2;	/*该2位不能使用*/
    int b : 3;
    int c : 2;
};

【练习】

练习一:
在这里插入图片描述

#pragma pack(4)
struct tagAAA
{
    
    
	unsigned char ucld : 1;
	unsigned char ucPara0 : 2;	//1 注: ucld和ucPara0总共占一个字节
	unsigned char ucState : 6;	//1
	unsigned char ucTail : 4;	//1
	unsigned char ucAvail;		//1
	unsigned char ucTail2 : 4;	//1
	unsigned char ucData;		//1
}AAA_S;

int main()
{
    
    
   
    printf("%d\n", sizeof(AAA_S));	//6
    return 0;
}

练习二:

#pragma pack(4)
struct tagPIM
{
    
    
	unsigned char ucPim1;
	unsigned char ucData0 : 1;
	unsigned char ucData1 : 2;
	unsigned char ucData2 : 3;
}*pstPimData;
int main()
{
    
    
	int a = 1;
	char* b = &a;
	printf("%p\n", &a);
	printf("%d\n", *b);
	unsigned char puc[4];
	pstPimData = (struct tagPIM*)puc;
	memset(puc, 0, 4);
	pstPimData->ucPim1 = 2;
	pstPimData->ucData0 = 3;
	pstPimData->ucData1 = 4;
	pstPimData->ucData2 = 5;
	printf("%d %d %d %d\n", puc[0], puc[1], puc[2], puc[3]);//2 41 0 0
	return 0;
}

在这里插入图片描述


在这里插入图片描述


我们在取数据时,只取我们在位域中设定的位数。把多的位数去掉。

练习三:

struct S
{
    
    
	char a : 3;
	char b : 4; //1 注:a和b占一个字节
	char c : 5; //1 注:无法装下c c占一个字节
	char d : 4; //1 
};

int main()
{
    
    
	struct S s = {
    
     0 };
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
	printf("size s=%d",sizeof(s));//3
	return 0;
}

内存存储
在这里插入图片描述
解释:
在这里插入图片描述


枚举

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

枚举的定义

枚举类型的定义
enum Day//星期
{
    
    
	Mon,	
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};//其中各个变量用逗号隔开,而结构体中的每个变量同分号隔开。
//以上定义的 enum Day是枚举类型。 {}中的内容是枚举类型的可能取值,也叫枚举常量。

枚举类型的两个特点

枚举类型中的可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
枚举类型的大小是4个字节

【说明】

typedef enum Color//颜色
{
    
    
	RED,
	GREEN,
	BLUE=10,
	YELLOW,
	BLACK
}Color;
int main()
{
    
    
	printf("%d\n",RED);
	printf("%d\n", GREEN);
	printf("%d\n", BLUE);
	printf("%d\n", YELLOW);
	printf("%d\n", BLACK);
	return 0;
}

在这里插入图片描述

总:可能取值若定义了则取定义值。若未定义则是上一个值加1。第一个值若未定义则为0。


联合(共用体)

共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。

联合类型的定义

//联合类型的声明
union Un
{
    
    
	char c;
	int i;
};//共用一个空间

联合的特点

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

//联合类型的声明
union Un
{
    
    
	int i;
	char c;
};
int main()
{
    
    
	union Un un;
	// 下面输出的结果是一样的吗?
	//下面输出的结果是什么?
	un.i = 0x11223344;
	un.c = 0x55;
	printf("%p\n", &(un.i));
	printf("%p\n", &(un.c));
	printf("%x\n", un.i);	//0x11223355
	return 0;
}

在这里插入图片描述
程序执行的过程:

在这里插入图片描述


联合的计算

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

union Un1
{
    
    
	char c[5];
	int i;	
};
union Un2
{
    
    
	short c[7];
	int i;	
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1)); //12
printf("%d\n", sizeof(union Un2)); //20

小伙伴觉得写得还不错的来个三连!!!

猜你喜欢

转载自blog.csdn.net/weixin_44627813/article/details/110352439