阅读导航:
1.位段
1.1什么是位段
位段的声明和结构体是类似的,但是有以下的几个不同:
1.位段的成员类型必须是:int 、unsigned int 、signed int 、char ,不能是其他的变量类型,但是结构体没有的成员变量可以是任意类型。
2.位段的成员名后面有一个冒号和数字(位段的“位”表示的意思是二进制位,这个数字表示的是占了多少个二进制位,也就是占了多少个比特位)
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
A就是一个位段类型。
对于任意的变量类型,函数…… 我们都能够从地址,大小,使用方法,内存占用去慢慢了解它。
那么A的大小是多少呢?
int main()
{
printf("%d",sizeof(struct A));
return 0;
}
为什么是8呢?
理由:
a b c d一共占用了2+5+10+30=47个比特位,1byte=8bit,那么至少申请了4个字节的空间(32比特位),还剩下的15(47-32=15)个比特位的数据没有存,那么只好再向内存申请4个字节的空间(因为a b c d 都是int类型的),因此占用了8个字节的空间。
那么问题来了,假如我把位段改一下呢?
看代码:
struct A
{
int a ;
int b ;
int c : 10;
int d : 10;
};
int main()
{
printf("%d",sizeof(struct A));
return 0;
}
那么A到底是结构体还是位段呢?
A的大小应该怎么算呢?
其实是位段!!!只要成员的后面跟上了冒号和数字就是位段。
先看答案:
答案是A占用了12个字节的空间。
其实可以这样分析:
对于a b 都是int 类型的,并且a b后面并没有跟冒号和数字,那么就把a b堪称结构体的成员,a b占用的空间为8个字节。
对于c d,c d后面跟了冒号个数字,那么可以看成位段的成员,c d一共占用了20个比特位,又因为c d的类型都是int类型的,因此c d只需要向内存申请一个int类型的空间就足够存下20个比特位了。
因此A的大小是12个字节。
一般情况下,位段的成员里面是不能混用 char和int 类型变量的,是不建议使用的。
struct A { char a:1 ;//不要这样使用位段 char b: 1 ;//里面既有char又有int int c : 7;//这样不好确定位段占用空间的大小,不利于设计 int d : 8; }; int main() { printf("%d",sizeof(struct A)); return 0; }
1.2位段的内存分配
1.位段的成员是:int 、unsigned int、signed int、char类型
2.位段上的空间是需要按照4个字节(int)或者一个字节(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;
printf("%d",sizeof(struct S ));
return 0;
}
分析:
因此:
第一个char里面存的数据是:0110_0010 换成十六进制为:0x62
第二个char里面存的数据是:0000_0011 换成十六进制为:ox03
第三个char里面存的数据是:0000_0100 换成十六进制为:ox04
不妨调试,验证一下猜想,F11走起来:
验证正确,位段确实如同我们猜想的那样去存的。
但是这里也存在一个疑问:
为什么位段里面的数据会按照从右到左的顺序存呢?为什么要这样设计呢?
为什么位段里面的数据不是从左到右存呢?这两者的差异是什么?(后面会提到)
1.3位段的跨平台问题
为什么要写可移植程序应当避免少出现位段呢?
原因可能如下:
1.int位段被机器当成有符号数还是无符号数是不确定的
2.位段中的最大位的数目不确定,比如:早期的16位机器最大为16,32位机器最大为32,但是写成27的话在16位机器中就会出现问题。
3.位段中的成员存储方式在内存中到底是从左到右开辟空间,还是从右到左开辟空间尚未定义(能够解释前一段的疑问)
4.当一个结构包含两个位段,第二个位段的成员比较大,无法容纳于第一个位段剩余位时,舍弃剩余位还是利用剩余位,这是不确定的。
2.4位段的应用
位段适合用于对于网络数据的传输,用于封装一些数据包,在网络空间中传输。
举个例子:
当你用微信和朋友聊天的时候,你输入了一个“你好”,请问机器是怎么知道你要把你发的信息精确的送到你朋友的微信窗口上去的?万一你朋友把你删除了,你朋友的手机是怎么识别你被删除的,从而不接受你的信息呢?
其实当我们发消息的时候,我们传输的数据会被打包成一个包裹(数据包),这个包裹会包含了数据的发送方,接收方,发送方的地址,接收方的地址等等……,就相当于寄送一个快递一样,需要知道收发方的姓名,电话,地址等等。
但是对于数据传输,应当使得传输的数据尽可能的小,这样在网络空间上传输数据就不会拥挤,传输的速度就会越快,那我们在标记我们要传输的数据的时候,使用位段去标记占用的空间会比int char类型要小的多。
比如我需要标记数据的四个特征 ,那我只需要两个比特位就行(00 01 10 11),但是用char的话就能够标记太多了(char大小是一个字节,是8个比特位),而我们现实生中传递信息并不需要如此复杂的标记,因此在传输数据的时候用位段来标记数据是一种简单快速的方法。
2.枚举
顾名思义,枚举的意思就是一一列举,把可能的取值都一一列出出来。
比如在我们的生活中,一周从周一到周日是可以列举出来的,性别,男 女是可以列举出来的,一年的月份是可以列举出来的。
2.1枚举的定义
请看代码:
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED = 1,
GREEN = 4,
BLUE =7
};
对于枚举类型做几点说明:
1.枚举类型是一种自定义类型,它也是一种类型,里面存的是常量(枚举常量)
2.枚举里面的成员是枚举的可能取值,每个成员的可能取值是常量,且不可修改。
3.枚举的成员都是有值的(成员都是常量,常量肯定会有一个值啊),默认从0开始,一次递增1(这里就默认枚举里面的成员是int型的了,都是整数0开始递增了,你就说是不是int类型吧),
当然在定义的时候也能够赋初值。
看代码:
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED = 1,
GREEN = 2,
BLUE = 3
};
int main()
{
printf("%d\n", RED);
printf("%d\n", GREEN);
printf("%d\n", BLUE);
printf("%d\n", MALE);
printf("%d\n", FEMALE);
printf("%d\n", SECRET);
return 0;
}
输出结果:
假如我将MALE 赋值成4,后面的FEMALE是不是也是会按照前面的MALE递增呢?
试试看:
enum Sex//性别
{
MALE = 4,
FEMALE,
SECRET
};
int main()
{
printf("%d\n", MALE);
printf("%d\n", FEMALE);
printf("%d\n", SECRET);
return 0;
}
结果:
确实是的!!!
2.2枚举的大小
直接看代码:
enum Sex//性别
{
MALE = 4,
FEMALE,
SECRET
};
int main()
{
printf("%d", sizeof(enum Sex));
return 0;
}
输出结果是:
但是为什么不是12呢?里面明明存的是3个整形啊,那不就12嘛?凭什么是4呢?
其实Sex里面并没有存MALE FEMALE SECRET 三个常量。而是存的是这三个常量中的某一个取值,换句话说,就是枚举里面没有存这3个,而是这三个中的某一个的取值。因此enum Sex的大小是4个字节。
但是C语言里面并没有明确规定此类标准,但是我们平常使用的机器是32位或者64位的,这两种机器上的int类型都是4位,因此枚举类型所占的空间就是4个字节。
2.3枚举的优点:
我们可以使用 #define MALE 4 去把MALE声明成4,那我们为啥还要用枚举呢?
枚举的优点如下:
1.增加代码的可读性和可维护性
2.和#define定义的标识符比较,枚举有类型检查,更加严谨(#define MALE 4 MALE只是4,但是不知道是int类型的4还是char类型的4)
3.防止命名污染(封装)
4.便于调试
5.方便使用,可以一次定义多个常量
2.4枚举的使用
直接看代码:
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5; //ok??
可以用枚举常量GREEN直接给枚举变量clr赋值,这个时候赋值的双方都是枚举类型
那么能不能够用int 类型的5给clr赋值呢?
答案是可以的,反正都是4个字节,编译器能够跑过去,但是不严谨。
那么其他类型的能赋值过去吗?答案也是可行的。
事实上,我们可以将枚举类型近似的看成一个int类型的常量,char类型的数据当然能够强行赋值给int类型,但是不够严谨,有时候会带来错误的结果,因此最好用强制类型转换之后再赋值。
3.联合(共用)
3.1联合的定义
联合也是一种特殊的自定义类型,这种类型定义的是变量,变量可包含一系列的成员。
联合的特征是,这些成员共用一块空间(所以联合体也叫共用体)。
看代码:
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un ;
un.c = 1;
un.i = 1;
//计算连个变量的大小
printf("%d\n", sizeof(un));
printf("%p\n", &un);
printf("%p\n", &un.c);
printf("%p\n", &un.i);
return 0;
}
打印结果:
由上述的代码可以知道,联合体的声明和结构体类似,只是更改了关键字。
那么联合体的大小怎么计算呢?
un的大小是4个字节。 un un.c un.i的地址是同一个地址。
3.2联合的特点
联合的成员是共用的一块内存空间,那么这样的一个联合变量的大小,至少是最大成员的大小(因为联合体至少得有能力放下最大的那个成员)
分析代码:
union Un
{
char c;
int i;
};
int main()
{
union Un un;
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);//现在的i改变了吗??
return 0;
}
%x——是输出控制符,输出的是无符号的十六进制整数
结果:
分析:
因为联合体共用一块空间,如果改变c,那么i也会改变。如果改变i,c也会随之改变。因此使用联合体的时候,尽量只调用联合体里面的一个变量。
3.3判断计算机当前的大小端存储
一个数值,只要在储存中需要占用的空间超过一个字节,就需要考虑到储存的顺序问题。
那么存储顺序分为两类,一个是大端字节序存储,一个是小端字节序存储。
大端字节序存储:数据的低位放在高地址处,数据的高位放在低地址处(正着存)
小端字节序存储:数据的低位放在低地址处,数据的高位放在高地址处(反着存)大多数计算机使用的都是小端字节序存储。
举例:对于a = 0x 11 22 33 44, b= 0x 00 00 00 01 分别用两种方式存。
看图:
两种方法判断计算机储存是否为小端存储:
1.法1:
int JugeStorage() { int i = 1; return *(char*)&i; } int main() { int reg = JugeStorage(); if (1 == reg) { printf("小端\n"); } else { printf("大端\n"); } return 0; }
2.法二:(使用联合体)
union XXX { char i; int c; }; int JugeStorage(union XXX* bb) { (bb->c) = 1; return bb->c; } int main() { union XXX bb; int reg = JugeStorage(&bb); if (1 == reg) { printf("小端\n"); } else { printf("大端\n"); } return 0; }
3.4联合体大小的计算
1.联合的大小至少是最大成员的大小
2.当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
看代码:
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
int main()
{
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
}
输出结果:
分析:
对于Un1,char c[5]的大小是5 ,对齐数是1,int i的大小是4 ,对齐数是4,所以Un1的大小至少是5,又因为联合的大小要对齐到最大对齐数的整数倍数,因此Un1的大小是8。
对于Un2,short c[7]的大小是14,对齐数是2,int i的大小是4 ,对齐数是4,所以Un2的大小至少是14,又因为联合的大小要对齐到最大对齐数的整数倍数,因此Un2的大小是16。