C语言结构体大小的计算及位域


c结构体大小的计算


首先来看一个例子:

#include<stdio.h>
#include<stdlib.h>

struct s
{
	char a;
	int b;
	char c;
};
int main()
{
	printf("sizeof(s)= %d\n",sizeof(struct s));
	
	system("pause");
	return 0;
}

在这里插入图片描述

如果不存在内存对齐这个问题,这个结构体应该占1+4+1=6个字节;
然而事实上它占了12个字节。

一。内存对齐

1.什么是内存对齐
结构体内存对齐:
元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。
从结构体存储的首地址开始,每个元素放置到内存中时,它都会认为内存是按照自己的大小来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始。
2.为什么存在内存对齐

  • 平台原因(移植问题):
    一些资料上是这样说的,
    “不是所有的硬件平台都能访问任意地址上的任意数据;
    某些硬件平台只能在某些特定地址处取某些特定的数据,否则就会抛出硬件异常”。
    也就是说在计算机在内存读取数据时,只能在规定的地址处读数据,而不是内存中任意地址都是可以读取的。
  • 性能问题:
    正是由于只能在特定的地址处读取数据,所以在访问一些数据时,对于访问未对齐的内存,处理器需要进行两次访问;
    而对于对齐的内存,只需要访问一次就可以。
    3.偏移量

二。 结构体的大小

举例1:

#include<stdio.h>
#include<stdlib.h>

struct s
{
	int a;
	char b;
	double c;
	char d;

};
int main()
{
	printf("sizeof(s)= %d\n",sizeof(struct s));
	
	system("pause");
	return 0;
}

在这里插入图片描述

分析:
int占4个字节
char占1个字节
float占4个字节
double占8个字节
a在偏移量为0的地址处,即它对齐到0号地址处的位置,占四个字节,即占用0,1,2,3;

第二个元素的大小为char型,占1个字节,要求对齐到自己对齐数的整数倍处,4是1的倍数,故而,b占用4偏移;

接下来可用偏移为5偏移,接下来存double c;
由于5不是8的倍数,所以向后偏移5,6,7,都不是8的倍数,偏移到8时,8是8的倍数,故而c从8处开始存储,占用8,9,10,11,12,13,14,15偏移;

现在可用偏移为16偏移,最后该存char d ;
因为16是1的倍数,故d占用16偏移,

接下来在整体向后偏移一位,现处于17偏移,min(默认对齐参数,类型最大字节数)=8;因为17不是8的倍数,所以继续向后偏移18…23都不是8的倍数,到24偏移处时,24为8的整数倍,
故而,该结构体大小为24个字节。
如图:
在这里插入图片描述

举例2:

#include<stdio.h>
#include<stdlib.h>

struct S1
{
	char c1;
	char c2;
	int i;
};
struct S2
{
	char c1;
	struct S1 s3;
	double d;
};
int main()
{
	printf("sizeof(S1)= %d\n",sizeof(struct S1));
	printf("sizeof(S2)= %d\n", sizeof(struct S2));
	system("pause");
	return 0;
}

在这里插入图片描述
分析:

结构体S1的大小为1(char)+1(char)+2(偏移量)+4(int)=8,S1的对齐数就是其结构体成员中最大的对齐数,即4;

在结构体S2中,对齐数分别是0、4、8;
首先 c1 对齐在0号地址的位置,其占用1个字节,
根据规则,嵌套的结构体S1要对齐到自身对齐数4的整数倍处,所以S1对其在4号地址处,其大小是8个字节;
d 的对齐数为8,所以它要对齐到16号地址处,
所以此时计算出S2的大小是1(char)+3(偏移量)+8(struct S1)+4(偏移量)+8(double)=24,
结构体S2中个成员的对齐数分别为0、4、8,因为24是8的倍数,符合规则。
所以S2的总大小就是24个字节。

计算规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处。

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数=编译器默认的一个对齐数与该成员大小的较小值,在VS环境下默认值为8,在Linux环境下默认值为4。

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

  4. 如果嵌套了结构体的情况,被嵌套的结构体对齐到其自身对齐数的整数倍处(结构体的对齐数就是其内部成员中最大的对齐数),此时结构体的整体大小就是所有最大对齐数(含被嵌套结构体的对齐数)的整数倍。

举例三:

#include<stdio.h>
#include<stdlib.h>

struct S1
{
	char a;
	int i;
	char b;
};
struct S2
{
	int a;
	char b;
	short c;
	double d;
	int f;
	struct S1 s;
};
struct S3
{
	char c1;
	struct S2 s;
	double d;
};
int main()
{
	printf("sizeof(S1)= %d\n",sizeof(struct S1));
	printf("sizeof(S2)= %d\n", sizeof(struct S2));
	printf("sizeof(S3)= %d\n", sizeof(struct S3));
	system("pause");
	return 0;
}

在这里插入图片描述
(具体分析就不列举了,自己思考哦)


位域


一。位域的定义

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

1.位域的定义和位域变量的说明位域定义与结构定义相仿,其形式为:

struct 位域结构名 
{ 位域列表 }; 

其中位域列表的形式为:
类型说明符 位域名:位域长度

例如:

struct bs     
{     
int a:8;     
int b:2;     
int c:6;     
};  

该结构体共占了两个字节。
因为:都为数据类型都为int型,位域a占8位,位域b占2位,位域c占6位,一个字节为8位。

2.空域:宽度为 0 的一个未命名位域强制下一位域对齐到其下一成员的数据类型边界。

struct bs
{
char a:1;
char :0;         //空域
char b:2;     //从下一个单位开始存放
char c:3;
};

该结构体共占了2个字节。

注意:

  • 如果一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域;
  • 位域的长度不能大于指定类型固有长度,比如说int的位域长度不能超过32;
  • 位域可以是无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。
struct k
{
	int a : 1;
	int : 2;       /*该2位不能使用*/
	int b : 3;
	int c : 2;
};

该结构体共占了4个字节。
从以上分析可以看出,位域在本质上就是一种结构类型, 不过其成员是按二进位分配的。

二、位域的使用

位域的使用和结构成员的使用相同。
其一般形式为: 位域变量名.位域名
#include<stdio.h>
#include<stdlib.h>
struct bs
{
	unsigned a : 1;
	unsigned b : 3;
	unsigned c : 4;
} bit, *pbit;
int main()
{
	bit.a = 1;
	bit.b = 7; //注意:位域的赋值不能超过该域所能表示的最大值,如b只有3位,能表示的最大数为7,若赋为8,就会出错   
	bit.c = 15;

	printf("%d,%d,%d\n", bit.a, bit.b, bit.c);
	pbit = &bit;
	pbit->a = 0;
	pbit->b &= 3;				//复合的位运算符"&="(详解如下)
					//相当于: pbit->b=pbit->b&3位域b中原有值为7,与3作按位与运算的结果为3(111&011=011,十进制值为 3)
	pbit->c = 1;
	printf("%d,%d,%d\n", pbit->a, pbit->b, pbit->c);
	system("pause");
	return 0;
}

执行结果:
在这里插入图片描述

按位与(&)
参加运算的两个数,换算为二进制(0、1)后,进行与运算。只有当相应位上的数都是1时,该位才取1,否则该为为0。

将10与-10进行按位与(&)运算:
0000 0000 0000 1010
1111 1111 1111 0110
-----------------------
0000 0000 0000 0010
所以:10 & -10 = 0000 0000 0000 0010

三。结构体的大小(含位域)

1.看下面的结构体:

#include<stdio.h>
#include<stdlib.h>
struct foo1 
{
	char    a : 2;
	char    b : 3;
	char    c : 1;
};

struct foo2
{
	char    a : 2;
	char    b : 3;
	char    c : 7;
};

int main()
{
	printf("第一个结构体大小:%d\n",sizeof(struct foo1));
	printf("第二个结构体大小:%d\n", sizeof(struct foo2));
	system("pause");
	return 0;
}

结果为:
在这里插入图片描述
分析:
如果按照正常的内存对齐规则, 这两个结构体大小均应该为3,那么问题出在哪了呢?
首先通过这种现象我们可以肯定的是:
带有’位域’的结构体并不是按照每个域对齐的,而是将一些位域 成员’捆绑’在一起做对齐的

以foo1为例,这个结构体中成员都是char型,而且三个位域占用的总空间为:6 bit < 8 bit(1 byte),
这时编译器会将这三个成员’捆绑’在一起做对齐,并且以最小空间作代价,故sizeof(struct foo1) = 1。

foo2这个结构体成员类型也都是char型,但三个成员位域所占空间之和为:9 bit > 8 bit(1 byte),
这里位域是不能跨越两个成员基本类型空间的,这时编译器将a和b两个成员’捆绑’按照char做对齐,而c单独拿出来以char类型做对齐, 这样实际上在b和c之间出现了空隙,但这也是最节省空间的方法了。

再看一种结构体定义:

#include<stdio.h>
#include<stdlib.h>
struct foo3
{
	char    a : 2;
	char    b : 3;
	int c : 1;
};

int main()
{
	printf("结构体大小:%d\n",sizeof(struct foo3));
	system("pause");
	return 0;
}

在这里插入图片描述
分析:

在foo3中虽然三个位域所占用空间之和为6 bit < 8 bit(1 byte),
但是由于char和int的对齐系数是不同的,是不能捆绑在一起,
那是不是a、b捆绑在一起按照char对齐,c单独按照int对齐呢?
不可以。
sizeof(struct foo3)=8。编译器把a、b、c一起捆绑起来并以int做对齐了。
不够一个类型的size时,将按其中最大的那个类型对齐。此 处按int对齐。

例题:

#include<stdio.h>
#include<stdlib.h>
struct s1
{
	int i : 8;
	int j : 4;
	int a : 3;
	double b;
};

struct s2
{
	int i : 8;
	int j : 4;
	double b;
	int a : 3;
};
int main()
{
	printf("第一个结构体大小:%d\n",sizeof(struct s1));
	printf("第二个结构体大小:%d\n", sizeof(struct s2));
	system("pause");
	return 0;
}

在这里插入图片描述

分析:
第一个结构体中,
i,j,a共占15个位,不足8个字节,按8字节对齐,共16字节

第二个结构体中,i,j共占12位,不足8字节,按8字节对齐,a也按8字节对齐,加上double共8+8+8=24个字节

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

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

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

  3. 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++,GCC采取压缩方式;

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

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

哈哈,可以说是很清晰了哦。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值