每一个不曾起舞的日子,都是对生命的辜负。
自定义类型:结构体,枚举,联合
1. 结构体
1.1 结构体内存对齐
1.1.1 练习一:
1.1.2 练习二:
1.1.3 练习三:
1.1.4 练习四——结构体嵌套问题:
1.1.5 修改默认对齐数:
1.1.6 offosetof
1.1.7结论:
1.2 结构体传参(用到函数栈帧)
2. 位段(位段的填充&可移植性)
2.1 什么是位段:
2.2 位段求结构体大小的计算方法:
2.3 位段的内存分配
2.4 位段的跨平台问题
3. 枚举
3.1 枚举的使用:
3.2 枚举的优点:
4. 联合(共用体)
4.1 联合类型的定义
4.2 联合的特点
4.3 联合大小的计算
5. 总结:
1. 结构体
结构是一些值的某些集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.1 结构体内存对齐
这是建立在我们已经掌握结构体的基本使用之上,并且深入探究的一个问题:计算结构体的大小。即:结构体内存对齐(常考)
1.1.1 练习一:
//练习一 struct S1 { char c1; int i; char c2; }; int main() { struct S1 s; printf("%d\n", sizeof(s)); }
在我们开始入手这个之前,我们知道,char,int 分别是一个字节和四个字节,那么,这个结构体大小就是6个字节了吗?当然,在提出这个问题的时候就代表它一定是不对的,具体看一下运行结果:
既然答案不是6,而是12,那么12又是如何得来的呢?
通过上面的结构体,我们发现创建的顺序分别是c1->i->c2,那么内存的开辟也是按照这个顺序进行开辟的,char->int->char。在char已经开辟了一个字节之后,int如果接着下一个字节进行开辟,那么结果一定是6,故int一定不是接着char的下一个字节进行开辟的,通过反推我们发现:int在第五个字节开辟,即前四个字节中的第二三四个字节没有被使用,故我们知道了一个这样的规则:第一个成员变量在与结构体变量为0的地址处开辟,即char占用了0到1之间的字节。之后的成员变量要对齐到该成员变量占有字节大小的整数倍的位置上:
但是即便这样,仍然是9个字节,而不是12个字节,因此,还有一个这样的规则,结构体的大小为最大成员变量的整数倍。在这个结构体中,最大的成员类型为int,占四个字节,故在9个字节基础之上我们还应该加上3个字节,即该结构体占用了12个字节大小。
需要注意的是: 每一个成员的对齐数 = 编译器的默认对齐数与该成员对齐数的较小值,因此,在上述逻辑规则中,我们缺少了一部比较的步骤,int的对齐数需要与编译器默认的对齐数进行比较,选择小的那个,(以VS为例,VS中默认值为8),4<8,故此步骤对计算对齐数没有影响,但是仍然需要注意。
1.1.2 练习二:
通过练习一的讲解,我相信大概都懂得怎么进行计算了,那么我们变换一下顺序:
//练习二 struct S2 { char c1; char c2; int i; }; int main() { struct S2 s; printf("%d\n", sizeof(s)); }
第一个char无疑是在首字节上,第二个char大小为一个字节,1<8,故对齐数为1的倍数,所以接着第二个字节即可,第三个成员大小为int,占四个字节,4<8,故其对齐数应该为4的倍数,因此需要再跳过两个字节,在第五个字节开始开辟四个字节,故现在共占用了8个字节,8为最大对齐数4的整数倍,故此结构体的大小为8个字节。
1.1.3 练习三:
那么我们改变一下类型继续练习:
//练习三 struct S3 { double d; char c; int i; }; int main() { struct S3 s; printf("%d\n", sizeof(s)); }
从上到下依次计算,首先是double ,8<=8,从0开始占8个字节,ch然后是char,1<8,对齐数为1,故1的倍数即可,即接着第八个之后开辟一个字节,现在是9个字节,最后是int,4<8,对齐数为4,故我们需要在4的倍数开始创建Int,在9个字节的基础之上再跳过三个字节,然后开辟4个字节,9+3+4 =16个字节,全都开辟完成之后,我们知道结构体的大小必须是最大对齐数的倍数,16是8的倍数,故此结构体大小为16.
//
1.1.4 练习四——结构体嵌套问题:
//练习四 struct S3 { double d; char c; int i; }; struct S4 { char c1; struct S3 s3; double d; }; int main() { struct S4 s; printf("%d\n", sizeof(s)); }
通过练习三我们得知struct S3占16个字节,计算S4,从char 开始,1<8,取1在首位置,struct S3 为16 ,16>8,故取小的,对齐数为8,即跳过7个字节开辟,double 8<=8,对齐数为8,故此时对齐数为1+7(跳过字节数)+8+8 = 24,由于结构体大小为最大内部成员的对齐数的倍数,因此,为16的倍数,则为32。
//
1.1.5 修改默认对齐数:
上述提到VS默认对齐数为8,也就是说,不同的编译器的默认对齐数可能是不同的,因此我们引入pragma pack(),其能够将默认对齐数进行修改。
#pragma pack(4) struct S { int i;//4 4 4 0~3 //4 double d;//8 4 4 4~11 }; #pragma pack() int main() { struct S s; printf("%d\n", sizeof(s)); }
括号内部为修改之后的默认对齐数,在结构体结尾处的pragma pack()的作用是截止修改,意思就是之后的结构体不会受到这里的影响,仍为8(VS).
那么修改之后计算得到的应该是4+8= 12,而double的对齐数仍为4,故最大对齐数为4,12为4的倍数,故结果仍为12。
1.1.6 offosetof
以上的练习相信大家已经掌握这种问题的计算,那么如何验证我们计算的对齐数的方法是正确的呢?
通过offsetof调用我们能知道每一个内部成员对齐的位置,因此就能够验证出我们所计算的方法是否正确。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<stddef.h> struct S1 { char c1;//1 int i;//4 char c2;//1 }; struct S2 { char c1;//1 char c2;//1 int i;//4 }; struct S3 { double d; char c; int i; }; struct S4 { char c1; struct S3 s3; double d; }; int main() { printf("%d\n", offsetof(struct S1, c1)); printf("%d\n", offsetof(struct S1, i)); printf("%d\n", offsetof(struct S1, c2)); printf("%d\n", offsetof(struct S2, c1)); printf("%d\n", offsetof(struct S2, c2)); printf("%d\n", offsetof(struct S2, i)); return 0; }
由此证明,我们的计算方法是正确的。
1.1.7结论:
如何计算:
第一个成员在与结构体变量偏移量为0的地址处。
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
vs中默认的值为8
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐处的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐处)的整数倍。
为什么存在内存对齐:
平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间换取时间的做法.
1.2 结构体传参(用到函数栈帧)
- 直接上代码:
struct S { int data[1000]; int num; }; void print1(struct S ss) { int i = 0; for (i = 0; i < 3; i++) { printf("%d ", ss.data[i]); } printf("%d\n", ss.num); } void print2(const struct S* ps) { int i = 0; for (i = 0; i < 3; i++) { printf("%d ", ps->data[i]); } printf("%d\n", ps->num); } int main() { struct S s = { {1,2,3}, 100 }; print1(s); //传值调用 print2(&s); //传址调用 return 0; }
上面的print1和print2函数哪个好些?
答案是: 首选print2函数。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能下降。
结论:
结构体传参的时候,要传结构体的地址。