一.前言
C语言还有一个重要的自定义类型:结构体,不了解的可以看一下我之前写的文章:C语言结构体
二.位段的理解
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
struct student
{
char name[20];
int age;
float score;
};
位段和结构体的相同点:
- 声明都是struct这个关键字来表示
- 都是用{ }来表示并且结束时跟上一个’ ; ’
- 每个成员后面都需要添加一个’ ; '来作为结束标志
- 两者在定义和初始化时也类似。
位段和结构体的不同点:
- 位段的成员名后边有一个冒号和一个数字。
- 位段的成员必须是 int、unsigned int、 signed int和char类型。
三.位段的内存分配
3.1位段成员的理解
看到位段的表示,有人肯定会想位段里面成员中数字是用来做什么的?是赋值吗?
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
当然不是,数字是代表这个成员在内存中占用多少比特位,就像_a本来是int类型,但是我们只为他开辟两个比特位的空间。当然既然设置好了每个成员的类型,我们就不能随意设置大小。比如一个整型它最大只有32个比特位,你不能设置一个比他还要大的数,像这样:
struct A
{
int a : 4;
int b : 5;
int c : 6;
int d : 33;
};
这样就会报错:
3.2位段的大小
struct A
{
int a : 4;
int b : 5;
int c : 6;
int d : 30;
};
printf("%d\n", sizeof(struct A));
经过之前计算结构体大小的折磨,我们再来计算一下位段的大小。我们先看结果: 咦?这怎么会是8呢?要想想8代表8个字节也就是两个整型的大小,但是两个字节是怎么算的呢?
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
也就是说,按照A这个位段来讲,先开辟一个整型也就是32个比特位的大小a占去4个还剩28个,b占5个,c占6个还剩17个,但是d占了30个,发现剩的位置不够,所以就又为他开辟了一个整型的空间。最后总大小就是两个整型,8个字节啦。同理,如果位段全是char类型的话,就每次开辟一个字节的空间。 但是这里我们遗留了几个问题:比如d虽然占30个比特位,之前遗留了17个比特位,我们这遗留的比特位是继续用呢?还是直接丢掉呢?我们a,b,c在这32个比特位中是从哪里开始存的呢?,我们带着问题继续往下看。
如果我们在一个位段里既有char类型,又有int类型的话,他是怎么开辟的呢?这个问题不好回答,因为,位段本身就有很多的不确定性。我们一般不会这样写。
3.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\n", s.a);
printf("%d\n", s.b);
printf("%d\n", s.c);
printf("%d\n", s.d);
return 0;
}
经过计算我们可以知道整个位段是占3个字节的大小也就是24个比特位 看到这里我们可以猜一下我们之前留下了的疑问。 我们假设是从右边低地址到左边高地址,像这样先存放a: 我们继续存b: 现在我们看到第一次开辟的一个字节的空间已经存不下c的五个比特位了,所以我们必须再开一块空间,当然也是从低向高开始存: 最后是d,我们发现d需要4个比特位,我们是将剩下的放到一起,还是从新开辟一块空间?当然是在开辟一块空间了,把剩下的放到一起还是挺怪的,我们就大方一点: 好了,a,b,c,d的位置我们都放好了,接下来我们把每个人的值放进去,来验证我们的猜想是否正确:
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
首先,数据在内存中是以二进制补码的形式存放的,所以我们先把10,12,3,4变成二进制数。,但是整数的补码和原码是一样的,我们只需要转成原码就可以了
10的二进制应该是1010,但是a只能取前三位是010。同理12是1100,3是0011但是我们为3开辟了5个比特位,所以前面再补一个0,4是0100.将他们放到该放的地址中: 因为struct S s = { 0 };我们已经提前初始化了,所以空位全部为0: 现在就一目了然了,a,b,c,d在s中应该是这样放的。我们把s用16进制形式展示出来 我们在内存中看到的应该就是62 03 04.接下来我们通过调试来看结果: 发现和我们猜想的结果是一样的。好现在我们来总结一下:
- 如果位段里的成员是char,就每次开辟一个字节的空间,数据是从这个字节中从低位到高位(右到左)开始存储的
- 如果存完一次后,剩下的空间不够存下一个成员,则把剩下的舍弃,从新开辟一个空间,直至最后。
但是这就是位段存储的规则吗?不是,这只是vs编译器的规定,其他的编译器不一定是这个标准。之前我也说过位段有很多的不确定性:
- int 位段被当成有符号数还是无符号数是不确定的
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
这时候肯定会有人讲,既然这么麻烦为什么还要用呢?我们通过学习结构体对齐的知识会发现,位段需要的内存要比结构体要小。
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
}s;
struct S1
{
char a;
char b;
char c;
char d;
}s1;
int main()
{
printf("%d\n", sizeof(s));
printf("%d\n", sizeof(s1));
return 0;
}
看结果: 在这里位段要比结构体少一个字节,你可能会觉得这没什么,但是如果代码写的多了,你会发现位段其实帮你节省了不少空间。这是在我这里体现的不是那么明显。 所以,跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台(不确定性)的问题存在。这也就是所谓的高风险高回报,在不同的时候去选位段,还是结构体可以让你的代码变得更高效。
四.枚举
看完位段,我们该了解一下枚举类型了。
4.1枚举类型的声明和定义
先看代码:
enum Day
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex
{
MALE,
FEMALE,
SECRET
};
enum Color
{
RED,
GREEN,
BLUE
};
以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。enum是枚举的关键字。{}中的内容是枚举类型的可能取值,也叫枚举常量。记住这和结构体不同,结构体里的内容可以叫成员变量,而这个是常量。而且这些常量都是有自己的大小的。默认从0开始,一次递增1
enum Day
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
int main()
{
printf("%d\n", Mon);
printf("%d\n", Tues);
printf("%d\n", Wed);
printf("%d\n", Thur);
printf("%d\n", Fri);
printf("%d\n", Sat);
printf("%d\n", Sun);
return 0;
}
看结果:
但是如果你不希望这些常量是这些值,你可以自己修改
enum Day
{
Mon=1,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
int main()
{
printf("%d\n", Mon);
printf("%d\n", Tues);
printf("%d\n", Wed);
printf("%d\n", Thur);
printf("%d\n", Fri);
printf("%d\n", Sat);
printf("%d\n", Sun);
return 0;
}
再看结果: 但是我们这样修改可以吗?
enum Day
{
Mon = 1,
Tues,
};
int main()
{
Mon = 2;
printf("%d\n", day);
return 0;
}
答案是不行,既然他是常量,就不能直接修改
这里要注意定义时的细节,每个常量后面都要跟一个逗号,最后一个的不需要, 你也可以这样写:enum Day { Mon=1,Tues,Wed,Thur,Fri,Sat,Sun };
4.2枚举变量的定义
和结构体类似:
enum Day
{
Mon = 1,
Tues,
}day;
enum Day
{
Mon = 1,
Tues,
};
enum Day day=Mon;
4.3枚举的作用
枚举定义是定义好了,但是有什么用呢?
enum Day
{
Mon=1,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
int main()
{
printf("%d\n", Fri);
return 0;
}
看看打印的结果: 这里我们会发现,系统似乎是直接把Fri替换成5打印出来了。那我们试试这样写代码可不可以:
enum Day
{
Mon = 1,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
int main()
{
enum Day day = 5;
int day1 = Mon + Tues;
printf("%d\n", day);
printf("%d\n", day1);
return 0;
}
我们在这里直接把day的之改成5,Mon和Tues值直接加起来存到Int类型中 看打印的结果: 可以看到这也是可以的,说明在C程序里完全可以容忍int类型与enum Day类型的转换。但如果我们将文件改成C++类型的呢? 来看看结果: 发现会报错,但是int day1 = Mon + Tues;这句话依然是没问题的,这就说明在这里运算的时候编译器将Mon,Tues当成int来计算了。但我们最好不要这样写。不同类型直接加上强制类型转换最好,养成一个良好的习惯。
既然这样我们直接用define来定义常量不行吗?
#define Mon 1 是预处理指令,就是将后面写的Mon全部替换成1
接下来我们通过枚举类型的优点来看比define好在哪里:
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨
- 防止了命名污染(封装)
- 便于调试
define处理的,在预编译环节就已经将需要替换的值替换掉了。所以在调试的时候你已经看不到Mon这些东西了,你看到的就是替换后的1这个数字
4.4枚举的大小
我们最后在研究一下枚举的大小。看着一段代码,猜一下答案:
enum Day
{
Mon = 1,
Tues,
Wed,
};
int main()
{
printf("%d\n", sizeof(Day));
return 0;
}
按理说,他有Mon,Tues,Wed这三种取值,应该有12个字节的大小吧,其实呢 这个枚举类型的大小只有4个字节,因为Mon,Tues,Wed只是说明他未来的可能取值。看到这里你们可能又有一个问题,他为什么是一个整型的大小而不是浮点型,char类型的呢?
枚举类型大小一般是由所编译器规定的int整数类型大小。也就是默认是int类型的大小
五.联合体(共用体)
5.1联合体定义
union Un
{
char c;
int i;
};
union Un un;
这样看上去和结构体基本一样只是联合体的关键字是uion。但是共用体有什么特殊之处呢?我们先看这一段代码:
union Un
{
int i;
char c;
};
int main()
{
union Un un;
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
printf("%p\n", &un);
return 0;
}
再看结果: 你会发现联合体,联合体里面的两个值他们三个的地址是一样的。这也就是联合体(共用体)的特殊之处了。
- 共用体各个成员公用一块地址
- 每个成员都是从这个共用地址的首地址开始存储的
- 共用体的大小至少是这些成员的最大值。也就是说你这个共用体至少也要有能装这些成员所需的最大值。
- 既然共用体公用一块内存,你对这个成员做了改变,也会对其他成员产生影响
5.2联合体在内存中的存储
union Un
{
int i;
char c;
};
int main()
{
union Un un;
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
return 0;
}
现在我们来看一下这个代码,在VS中数据的存储是小端字节序存储,所以在内存中i是这样存储的:
字节序存储:在数据的存储中,数据是以二进制补码的形式存储的,但是数据的存储过程中分为两种存储方法。 小端字节序存储:数据的低位存到高地址中,数据的高位存到低地址中。 大端字节序存储:数据的低位存到低地址中,数据的高位存到高地址中。
这就是un.i = 0x11223344;在内存中存储的样子。又因为un.c = 0x55,并且c是char类型,一次只能访问一个字节,所以c把联合体首地址的位置变成了55. 于是最后打印出来的结果是: 这段代码表明了,联合体里面的成员会相互影响。当然我们可以用这个特点来做一个题目。刚才我为大家解释了小端字节序存储和大端字节序存储。接下来我们要写一个代码来说明我们使用的编译器是什么类型的存储。 我先为大家理一下思路: 我们定义一个int型变量1,他以十六进制表示00 00 00 01,如果是大端字节序存储,他在内存中以十六进制表示也应该是00 00 00 01,但如果以小端字节序存储,他应该是01 00 00 00.所以我们只要判断他的第一个字节是00还是01就可以了。 我们不妨这样想定义一个联合体,里面的成员放两个int类型,char类型。当我们把int类型的值设为1.然后判断char类型的变量是多少不就可以了吗?因为他们两个公用一块内存,首地址也相等,是不是可以说这个char类型就是int类型值的第一个地址。这样我们就可以把代码写出来了。
int check_sys()
{
union Un
{
int i;
char c;
}u;
u.i = 1;
return u.c;
}
int main()
{
int ret = check_sys();
if (ret == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
我们再来看看结果:
5.3联合体的大小
刚才我们讲过,联合的大小至少是最大成员的大小。但这是至少,不代表他的大小就是最大成员的大小。他的判断方法和结构体有一点类似:当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。 我们先看这个代码:
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
再来看看结果: 如果看过我之前讲结构体的文章对这个最大对齐数可能不会这么陌生
对齐数:是指这个类型的大小与默认对齐数相比的最小值 最大对齐数:所有类型的对齐数的最大值 VS里默认的对齐数是8
也可以通过下面这幅图来了解:
在Un1中
union Un1
{
char c[5];
int i;
};
这个联合体大小至少能装下五个char类型的空间,char c[5]虽然是一个数组,但他的类型还是char类型,所以他的对齐数是1,而Int类型的对齐数是4,所以这个联合体的大小应该是4的整数数。而他至少应该是5个字节,但是5不是4的整数倍呀,所以往后又开辟了三个字节的空间,所以总共是8个字节的大小。
union Un2
{
short c[7];
int i;
};
一个short是两个字节,其对齐数是2,int的对齐数是4,所以Un2的大小应该是4的整数倍,但是这个联合体的最小值应该能装下一个含有7个short类型的数组,也就是14个字节的大小,但14不是4的整数倍,所以应该在开辟两个空间。最后这个结构体的大小是16字节。
六.结束
作者我虽然是个小白,但还是希望这篇文章能帮到大家
|