结构体内存对齐
当我们创建一个结构体变量时,内存就会开辟一块空间,那么在创建结构体变量时内存到底是怎么开辟空间的呢?会开辟多大的空间呢?我们来看一下下面的代码
struct S { int i; char c; char b; }; struct G { char c; int i; char b; }; int main() { struct S u; struct G g; printf("%d\n", sizeof(u)); printf("%d\n", sizeof(g)); return 0; }
在这个代码中,我们创建了两个结构体类型,并用这两个类型创建了两个结构体变量,当变量被创建的时候,内存就会为这些变量开辟空间,而在这两个变量中,我们都创建了两个char类型的成员和一个int类型的成员,不同的是这三个成员的排列顺序不同,然后我们来打印一下这两个变量所占的字节,来看一下有什么不同
我们发现,这两个结构体变量虽然里面的元素类型一样,但是他们的大小并不一样,而他们之间的区别就是结构体成员的排列顺序不同,所以我们可以知道,结构体成员的排列顺序是会影响结构体的大小的,那么他到底是怎么影响的呢,这就和结构体的创建规则有关。
结构体在创建时要进行内存对齐,对齐的规则是:
1.第一个成员在与结构体变量偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为83.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
下面我们来根据对齐规则来算一个下上面的两个结构体的大小:
struct S { int i; char c; char b; };
首先,我们先看struct S,第一个成员是int类型,占4个字节,它的对齐数就是4,第二个是char类型,占一个字节,对齐数是1,一个放在1的整数位上,也就是可以接着往下放,那就是在int的4个字节后面紧跟着来存放,第三个也是char类型,对齐数也是1,那么也可以接着放在后面,三个成员的最大对齐数是4,根据对齐规则,结构体的大小是最大对齐数的整数倍,而我们刚刚算完这三个占的大小已经是6个字节了,所以为了对齐,我们要浪费两个字节的空间,使这个结构体的大小为8个字节,是最大对齐数的2倍。
struct G { char c; int i; char b; };
我们再来看第二个,这里,我们的第一个元素变成了char类型,占一个字节,但是第二个成员是int类型,对齐数为4,根据对齐规则要对齐到4的整数倍处,那就只能放在结构体开始储存的位置往后4个字节的地址处,加上它本身占4个字节,这时候我们在内存中就使用了8个字节,然后在存入第三个成员char类型,对齐数是1,可以直接在后面存放,最大对齐数还是4,但是我们已经使用了9个字节,所以这时候的大小应该是12个字节,4的3倍。
以上的结果也是符合刚刚的运行结果的。
下面我们来看一个结构体里嵌套了一个结构体时的例子:
struct G { char c; int i; char b; }; struct S { char a; int i; struct G c; };
我们在结构体struct S中嵌套了一个结构体struct G,根据对齐规则,我们嵌套的结构体一个对齐到自己的最大对齐数的整数倍上,那就是4的整数倍,而第一个成员是char,第二个是int,int对齐到4的整数倍上,那前两个成员就占了8个字节,而struct G正好对齐到int的后面,大小我们刚刚算出来是12个字节,加起来就是20个字节,下面我们来验证一下
与运行结果相同,那么我们的计算就没有问题。
那么我们结构体在储存时为什么要进行内存对齐呢?主要有以下两个原因。
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
跟主要的可能是第二种原因,为了增加处理器的访问效率,我们选择了用浪费空间的方式,来使我们的处理器可以一次性的访问到我们需要的元素,用空间来换时间。
根据我们的对齐规则我们也可以发现,如果我们想要在创建结构体变量时节省空间,我们应该尽量让小的成员集中在一起,这样可以减少空间的浪费,节省空间。
在规则中我们看到,计算每个成员的对齐数时要选择默认对齐数与该成员大小的较小值,在vs编译器中这个默认对齐数是8,而在有的编译器中没有默认对齐数,如gcc编译器的默认对齐数就是成员自身的大小,当然,这个默认对齐数也是可以该的,而我们如何来修改默认对齐数呢,我们看下面的代码
struct H { char c1; double d; char c2; }; int main() { struct H h; printf("%d\n", sizeof(h)); return 0; }
根据我们的对齐规则,这个结构体的大小应该是24个字节,我们来运行一下看看结果
如果我们要把它的默认对齐数改为4,那么我们再来重新计算一下,首先第一个char类型占一个字节,然后double类型占8个字节,但是对齐数为4,对齐到4的倍数,就可以在第4个字节的位置开始存储,这时候前两个只占12个字节,最后一个char占一个字节,最大对齐数为4,大小为4的倍数,应该为16,我们来验证一下
根据运行的结果我们可以看到,确实是改变了默认对齐数
修改默认对齐数的方法就是在结构体类型前加上#pragma pack(n),n表示修改后的默认对齐数的值(一般都是2的次方数,当改为1时,表示不存在对齐),在结构体类型的后面加上#pragma pack()表示取消修改。
#pragma pack(4) struct H { char c1; double d; char c2; }; #pragma pack()
如上面的代码,表示我们只把#pragma pack(4)~#pragma pack()之间的结构体类型的默认对齐数改为了4。
结构体传参
在学习函数的时候我们曾经学到,函数在调用时有两种方法,一种是传值调用,一种是传址调用C语言–函数,我们来看下面的代码。
struct S { int data[1000]; int num; }; //结构体传参 void print1(struct S s) { printf("%d\n", s.num); } //结构体地址传参 void print2(struct S* ps) { printf("%d\n", ps->num); } int main() { struct S s = { {1,2,3,4}, 1000 }; print1(s); //传结构体 print2(&s); //传地址 return 0; }
在这个代码中,我们的print函数的目的是打印结构体变量中的一个成员,print1传参时传的就是结构体变量的值,print2传的就是结构体变量的地址,他们的不同点在于传值的时候,我们的形参是实参的一份临时拷贝,也就是说,当我把结构体变量的以传值的方式传参给函数时,当我们调用这个函数,内存就会把这个结构体拷贝一份,当我们的结构体变量比较小时还好,但是当这个结构体变量里的成员非常多,占据的空间非常大时,就会导致系统开销比较大,性能下降,而如果我们使用传址的方式,我们一个地址的大小也就4或者8个字节,就没有上面的问题,所以在结构体传参时,我们尽量要传地址,既可以节省时间,也可以节省空间。
结构体实现位段
什么是位段
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 char、int、unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字。
如
struct A { int a : 2; int b : 5; int c : 10; int d : 30; };
这里的A就是位段,每个成员后面的数字代表他们需要的二进制位。
位段在内存中的存储
1.位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
2.位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
我们来举个例子
struct S { char a : 3; char b : 4; char c : 5; char d : 4; }; int main() { struct S s = { 0 }; s.a = 10; s.b = 12; s.c = 3; s.d = 4; return 0; }
我们来研究这个位段在内存中是如何储存的
首先,我们看成员a,它占3个二进制位,是一个char类型,我们需要创建出一个字节的空间,也就是8个二进制位,a要走了3个,我们假设在存储是,我们存储的顺序是与小段存储的方式类似,也是从右向左存储的,那么a在内存中存储的位置应该是后面的三个二进制位
低地址->高地址 ******** aaa
假设一个代表内存中的一个二进制位,这八个代表一个字节,这3个a上面对应的*就是a在这一个字节中所占的空间。
b需要4个二进制位,而我们刚刚创建的一个字节中还有5个二进制位,所以我们把b的4个二进制位放在a的后面。
低地址->高地址 ******** bbbbaaa
这就是a与b在内存中的存储情况,而这是c需要个二进制位,我们只剩下一个二进制位了,而c又是一个char类型,所以我们需要再创建一个字节的空间,而当我们创建好了新的空间,c的个二进制位应该怎么储存呢,我们是接着第一个字节把剩下的二进制位用完还是在我们新创建的字节里重新储存呢,假设内存在存储时选择直接浪费掉哪个二进制位,在新的空间进行储存,这时内存中的存储分布应该是这样
低地址->高地址 ******** ******** bbbbaaa ccccc
存储d时,d需要4个二进制位,第二个字节中的二进制位也不够,所以我们再创建一个字节,存储d
低地址->高地址 ******** ******** ******** bbbbaaa ccccc dddd
如果我们的假设没错的话,这一个就是a,b,c,d这4个成员在内存中的储存位置,然后我们又对这4个成员进行了赋值
a=10=>1010(二进制数)=>010(3个二进制位)
b=12=>1100(二进制数)=>1100(4个二进制位)
c=3=>11(二进制数)=>00011(3个二进制位)
d=4=>100(二进制数)=>0100(4个二进制位)
当我们用上面的数据对我们刚刚的位段进行赋值,那么这个位段在内存中存储的内容应该是这样的
低地址->高地址 0bbbbaaa 000ccccc 0000dddd 01100010 00000011 00000100(二进制) 62 03 04 (十六进制)
根据我们的计算,我们发现,如果位段按照我们刚刚的假设来存储,那么在内存中存储的内容应该是62 03 03,那我们现在来调试一下看看
结果与我们推断的一样,那么就说明当前的编译器位段的存储是按照我们假设的方式来存储的。
位段的问题
但是位段在C语言中的规定又有许多不确定的地方,
1.int 位段被当成有符号数还是无符号数是不确定的。
2.位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机 器会出问题。
3.位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4.当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。
不同的平台可能对上述的问题有不同的规定,所以位段是不能跨平台的
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
位段的使用环境是在我们传输一个数据包时,可以使用位段使数据包在不能压缩的情况下,所占的空间最小。
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!