为什么要内存对齐?
讲了这么久的内存对齐,那我们不好奇为啥要内存对齐导致浪费那么多空间呢?这样设计的意义是什么?
查阅资料是这么说的:
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常.(这个我牛牛见识少,没咋遇见过,不过可以理解)
- 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问.
举例:16位机器一次只能读取四个字节的数据.
总结:内存对齐就是一种以时间换空间的方法,不要小看一次和两次的区别,在大量数据读取时,作用是很大的.
1.4 查看偏移量与修改默认对齐数
在c语言中,有一种宏定义,可以帮助我们查看结构体中成员的偏移量.
这里我们只需要了解它是如何使用的就行.
#include <stdio.h> #include <stddef.h> typedef struct S2 { char c1;//0 char c2;//1 int i;//4 }s2; typedef struct S3 { int a;//0-3 char c1;//4 int i;//8-11 double b;//16-23 }s3; typedef struct S4 { char c1;//1 struct S3 s3;//8-31 int d;//32-35 }s4; int main() { printf("%d\n", offsetof(s2, c1)); printf("%d\n", offsetof(s2,c2)); printf("%d\n", offsetof(s2, i)); printf("\n%d\n", offsetof(s3, a)); printf("%d\n", offsetof(s3, c1)); printf("%d\n", offsetof(s3, i)); printf("%d\n", offsetof(s3, b)); printf("\n%d\n", offsetof(s4, c1)); printf("%d\n", offsetof(s4, s3)); printf("%d\n", offsetof(s4, d)); return 0; }
运行结果:
0 1 4 0 4 8 16 0 8 32
在vs环境下修改默认对齐数:
#pragma pack(4)//设置默认对齐数为4
在vs环境下还原默认对齐数:
#pragma pack()//取消设置的默认对齐数,还原为默认
为什么要修改默认对齐数?
我们知道内存对齐是以空间换时间的方法.
大多数情况下都是这样的,但是有的特殊时候,空间很有限,这时我们不想牺牲空间,即以时间换空间.此时就要使用修改默认对齐数了,使其不对齐.
二、位段篇
你听过"位段"吗?不是段位哦,嘘~~偷偷告诉你,牛牛王者是最强王者段位哦!
位段其实和结构体很相似,我们可以先观察一个位段先.
#include <stdio.h> typedef struct A { int a : 2; int b : 5; int c : 10; int d : 15; }A; int main() { printf("%d", sizeof(A)); return 0; }
位段长得与结构体很相似,但是他与"富哥"结构体不一样,位段就十分节省了,字节都是省着用,一个字节都要按比特位数着用!是出了名的"吝啬鬼".
位段的使用要求:
1.位段的成员必须是 int、unsigned int 或signed int 或者char类型
2.位段的成员名后边有一个冒号和一个数字。(冒号后面的数字不得超过前面类型的大小.)
位段的位就是指"比特位",一个字节占8比特位.
位段空间使用规则是:
1.先申请该类型所占的字节个数个的字节空间(如:int占四个字节,则先申请四个字节的空间)
2.从低位向高位使用":"(冒号)个比特位的空间.
3.如果字节不够了,就继续申请新的类型大小数目的字节空间.
比如示例1:
typedef struct A { int a : 2; int b : 5; int c : 10; int d : 15; }A;
由于int占四个字节,则先申请四个字节空间.
a:2,表示a申请占用2个比特位,
b:5,表示b申请占用5个比特位,
…
是的你没有听错,就是比特位,经过计算,2+5+10+15=32,四个字节刚好够用,那他真的是占四个字节吗?
是的,不相信的小伙伴,可以复制示例1运行一下,结果就是四个字节.
那超出四字节会怎样申请新的空间?
#include <stdio.h> typedef struct B { int a : 20; int b : 5; int c : 10; int d : 15; }B; int main() { a=1; b=2; c=3; d=5; printf("%d", sizeof(B)); return 0; }
分析:
int占四个字节,前申请四个字节的空间.
a占用了其中的20个比特位,b又占用了5个比特位.(还剩7个比特位)
c需要10个比特位,显然是不够的,
这时,又向内存申请了四个字节的空间,
假设,我们大方一点,这7个比特位不要了,直接使用新的内存空间,d又占用15个比特位,八个字节,足够了.
通过调试,我们发现,确实占用了八个字节大小.
我们可以通过调试,在内存窗口观察内存中实际是怎样存放的.
计算转化为16进制:
调试验证:(图中是小端存储,所以是反过来的).
但是,我们只是假设那7个剩余的比特位不要了,有的编译器可没有这么大方,这取决于编译环境,所以这也是位段的一个缺点!
位段总结:
- 在位段中,int是有符号还是无符号是未知的.
- 虽然说位段中":"(冒号)后面的数字不得超过该成员类型所占字节数所换算的比特位,但是在不同的平台,类型的大小是不确定的.
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。(我们在vs是从左向右分配的)
- 当第一个位段的剩余的内容无法存储第二个位段时,要开辟新的空间,那之前剩余的空间是否被利用取决于平台,也没有规定.
总结,虽然位段节省了大量的空间,但是时间效率,跨平台可移植性都堪忧.
三.枚举(enum)
在c语言初阶时,我们在讨论c语言类型时,其实也提到过这个名词"枚举".
那么今天就来真正学懂"枚举"吧!
"枚举"其实就是列举的意思.
比如:
三原色: “红”,“绿”,“蓝”.
性别: “男”,“女”.
星期:" 星期一",“星期二”……
当一件事物可以一 一列举出来,我们可以使用枚举将他们表示出来.
枚举类型中的成员只有在定义时可以更改(因为常量也要有值不是吗?)
他们都是常量,定义之后是不允许更改的.
枚举的定义:
🌰栗子
#include <stdio.h> enum Day//星期 { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }; enum Sex//性别 { MALE=6, FEMALE, }; enum Color//颜色 { RED, GREEN=7, BLUE=10 }; int main() { printf("Day;"); printf("%d ", Monday); printf("%d ", Tuesday); printf("%d ", Wednesday); printf("%d ", Thursday); printf("%d ", Friday); printf("%d ", Saturday); printf("%d ", Sunday); printf("\n"); printf("%d ", MALE); printf("%d ", FEMALE); printf("\n"); printf("%d ", RED); printf("%d ", GREEN); printf("%d ", BLUE); return 0; }
运行结果:
Day;0 1 2 3 4 5 6
Sex;6 7
Color;0 7 10’’
对于未被初始化的枚举常量,默认是从0开始的.
枚举的优点
当我们在使用case语句,或者其它选择语句时,数字1,2,3这类并没有指向性.
这时我们可以用枚举常量代替他们,使得代码可读性极大提高.
示例:
请设计一个程序,要求:
输入:一个整数date范围是(0-7)
输出:打印星期一到星期天.
enum Day//星期 { Monday=1, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }; #include <stdio.h> int main() { int date = 0; scanf("%d", &date); switch (date) { case Monday: printf("星期一"); break; case Tuesday: printf("星期二"); break; case Wednesday: printf("星期三"); break; case Thursday: printf("星期四"); break; case Friday: printf("星期五"); break; case Saturday: printf("星期六"); break; case Sunday: printf("星期天"); break; default: printf("超出范围了兄弟!!!\n"); } return 0; }
这个例子可能有些牵强,但是希望友友们能理解,很多情况,枚举常量还是能够可以帮助我们更好的提升代码可读性的.例如,通讯录那里有好的例子.
- 增加代码的可读性和可维护性
- #define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
四、联合体(unio)
联合体是一种很特殊的自定义类型,他与结构体一样可以同时定义多个变量.
但是最主要的特点是,这些成员变量使用的都是同一块内存空间.
所以,他还有另外一个名字—共用体.
我们看一个联合体的例子.
union Un { char a; int b; double c; };
联合体的定义有了前面结构体的基础,还是很简单理解的.牛牛不过多介绍了.
联合体的应用:
#include <stdio.h> typedef union test { int a; char b; }test; int main() { test t1; printf("%p\n", &(t1.a)); printf("%p\n", &(t1.b)); t1.a = 0xaabbccdd; t1.b = 0xaa; printf("%x\n", t1.a); return 0; }
运行结果:
0000004B5DFCFC64
0000004B5DFCFC64
aabbccaa
联合体的特点是其中的成员变量都在使用同一块内存空间,所以打印出来的地址是一样的.
但是这样就产生了一个问题,如果我们同时使用这里的多个成员,那内存地址中存放谁的值呢?
所以联合体中的成员变量不能同时使用.
这也就是为什么修改了b,导致a的一个字节的数据也被修改的原因.
联合体的大小计算
很明显,由于他们都是使用同一块空间,所以大小是由最大成员变量所决定的,但是要注意的是,联合体也是要讲究内存对齐的.
练习一下吧!
#include <stdio.h> union test1 { char a[9]; int b; double c; }; union test2 { short a[7]; int b; char c; }; int main() { printf("%d\n", sizeof(union test1)); printf("%d\n", sizeof(union test2)); return 0; }
答案:
16
16
分析:
test1:最大元素是a[9],但是需要内存对齐,对齐数是取double的8,所以占16字节.
test2:最大元素是a[7],占14个字节,但是对齐数是四个字节的b,所以也要内存对齐为16字节.