结构体内存对齐的意义
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 - 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
在32位机器中 有字长的概念,字长的长度是32位bit ,也就是4个字节,也就是从计算机中读取一次就读取4个字节
假设结构体没有内存对齐,就会可能造成一些情况,一个数据要读取两次才能读取完整
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
如果我们要结构体既满足对齐又节约空间的方法:
- 让占空间小的成员尽量靠在一起
修改默认对齐数
vs编译器默认的对齐数是8,如果我们要修改这个对齐 数就要使用#pragma pack()来设计
#include<stdio.h> #include<stddef.h> #pragma pack(1)//设计对齐数为1 struct S3 { double d; char c; int i; }; #pragma pack()//取消设计的对齐数 struct S4 { double d; char c; int i; }; int main() { printf("%d\n", sizeof(struct S4)); printf("%d\n", sizeof(struct S3)); return 0; }
- #pragma pack(1)//设计对齐数为1
- #pragma pack()//取消设计的对齐数,还原vs默认对齐数
结构体传参
传参有两种一种是传值,一种是传地址,
传值
#include<stdio.h> #include<stddef.h> struct S { int a; int arr[10]; }; void print(struct S s) { int i = 0; for (i = 0; i < 9; i++) { printf("%d ", s.arr[i]); } } int main() { struct S s = { 5,{1,2,3,4,5,6,7,8,9} }; print(s); return 0; }
传地址
#include<stdio.h> #include<stddef.h> struct S { int a; int arr[10]; }; void pri(struct S* p) { int i = 0; for (i = 0; i < 10; i++) { printf("%d ", p->arr[i]); } } int main() { struct S s = { 5,{1,2,3,4,5,6,7,8,9} }; pri(&s); return 0; }
两种差异,假设结构体的大小很大,传值的时候又会开辟一块一模一样的临时空间(压栈),,而传值调用,虽然也会压栈,但是开辟的空间只有4个字节,所以我建议结构体尽量使用传地址,
如果我们不想修改结构体成员的值,我们可以使用const修饰参数,或者使用传值调用。
传地址效率会很好
位段
位段是为了节省空间的,但也会浪费空间。
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是 int、unsigned int 或signed int 。
- 位段的成员名后边有一个冒号和一个数字。
- 位段的位指的是二进制位,bit
- 在c99以后,位段的成员也可以是其他类型,但基本上是int 和char
为啥要有位段呢?
我们可以想想我们有时候存储一些数据的时候,会有一些空间被浪费掉,只是用了其中的一个或者几个比特位
#include<stdio.h> #include<stddef.h> struct A { int a : 2;//占2个bit int b : 4;//占4个bit int c : 6;//占6个bit }; struct B { int a; int b; int c; }; int main() { printf("%d\n", sizeof(struct A)); printf("%d\n", sizeof(struct B)); return 0; }
位段的内存分配
1.位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
2.位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
在我们使用位段的时候,规定了一个成员占几个bit,当开辟好空间后,这些成员怎么存,从哪边开始使用,我们是不知道的,
我以下面为例
这是一个字节,如果我们要把a存储进去,是要存放在左边还是右边或者是中间呢?我们是不知道的,但是我们是可以探究的
以vs编译器为例
#include<stdio.h> #include<stddef.h> struct A { char a : 3; char b : 4; char c : 5; char d : 4; }; int main() { struct A s = { 0 }; s.a = 10; s.b = 12; s.c = 3; s.d = 4; return 0; }
我们知道,连续的地址会有高低地址,在vs编译器中,位段的存储是先使用高地址进行存储的,上图代码,中先开辟一个字节进行存储a,然后再存储b如果剩下的空间足够b存储,就不会开辟新的空间,
a = 10 补码二进制位为:1010,但是a的大小只有3个bit,所以会丢失数据只存储了010
b = 12 补码二进制位为:1100 b的大小为4bit,所以不会丢失数据
c = 3 补码二进制位为:011,因为c的大小为5bit 所以 会存储 00011
d = 4 补码二进制位为:100 ,因为大小为4bit 所以会存储0100
最终结果为011000100000001100000100,换算成十六进制62 03 04
结果是一模一样的
位段跨平台的问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。 - 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
- 总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
枚举
在前面我们在定义常量的时候就运用到了enum
常量:
- 字符常量
- const 修饰的常变量
- #define定义的
- enum的枚举常量
枚举的定义
枚举的作用是把一些情况一一列举出来
enum Day { MON,//枚举常量 TUES, WED, THUR, FRI, SAT, SUN };
这种写法是使用默认值
而下面这种写法可以自己定义值
enum Day { MON = 1, TUES = 2, WED = 3, THUR = 4, FRI = 5, SAT = 6, SUN = 7 };
我们在使用枚举时可以定义枚举类型变量进行接收
枚举常量是有类型,类型不是int ,在C语言中可以使用整形变量接收枚举常量,但是在c++却不行
枚举的优点
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较,枚举有类型检查,更加严谨
- 便于调试
- 使用方便,一次可以定义多个变量
联合体
联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。和结构体的定义相似
联合体的关键字: union
联合体的定义
```sql #include<stdio.h> #include<stddef.h> union Un { char a; int b; }; int main() { printf("%zd\n", sizeof(union Un)); union Un aa; printf("%p\n", &(aa.a)); printf("%p\n", &(aa.b)); return 0; }
可以看到联合体成员公用一块空间
那怎么计算联合体的大小呢?
我们可以想想,既然要共用一块空间,那么这块空间就要有最大成员空间的大小,这样才能保证成员能正常存储
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
如果我们要使用联合体进行赋值可以这样赋值
#include<stdio.h> #include<stddef.h> union Un { char a; int b; }; int main() { union Un aa; aa.a = 'a'; aa.b = 0xffffff12; return 0; }
如果使用union Un aa = {‘b’};就相当于全部成员都初始化了,因为成员之间公用一块空间
但是我们还是不知道共用了空间,一旦修改会变动哪一部分
前面我们知道,内存的存储是分大、小端存储的,我们还写了判断大、小端存储的方法
#include<stdio.h> #include<stddef.h> int check_sys() { union Un { char a; int b; }; union Un te; te.b = 1; return te.a; } int main() { int a = 1; char* p = (char*)&a; //int a = check_sys(); if (*p) { printf("小端存储"); } else { printf("大端存储"); } return 0; }
图中定义的函数,里面的联合体就是利用了这一特性
这里是vs存储1的情况是小端存储,
联合体大小的计算
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
#include<stdio.h> #include<stddef.h> union Un1 { char arr[7]; int a; }; union Un2 { int arr[7]; long long a; }; int main() { printf("%d\n", sizeof(union Un1)); printf("%d\n", sizeof(union Un2)); return 0; }
这个代码就可以很清楚的明白了
联合体的应用
#include<stdio.h> #include<stddef.h> struct prize { int stock_number;//库存 double price;//定价 int item_type;//商品类型 union { struct { char title[20];//书名 char author[20];//作者 int ngnum_pages;//页数 } book; struct { char design[30];//设计 }mug; struct { char design[30];//设计 int colors;//颜色 int sizes;//尺寸 }shirt; } item; }; int main() { //书 struct prize Book; //杯子 struct prize Cup; //被子 struct prize blanket; return 0; }
总结
结构体、枚举、联合体的声明都有匿名声明,如果有不懂可以私聊我