今天我们就接着上次,继续来较为深入的了解自定义类型。
目录
自定义类型
位段
位段的概念
位段相信大家都还是比较陌生的,那么什么是位段呢?
位段就是为了节省空间而进行创造的,它是基于结构体进行实现,因此它的声明也与结构体类似,但是有两点不同:
1.位段的成员必须是 int、unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字。这又是什么意思?我们下面对位段的代码进行分析:
struct A { int _a : 2; int _b : 5; int _c : 10; int _d : 30; };
这就是一个位段类型的代码,若是我们普通的创建一个结构体:
struct A { int _a; int _b; int _c; int _d; };
我们就会发现第二点所说的不同,那么位段在结构体成员的基础上在后方加上冒号以及数字又是什么意思呢?这其实是说明该变量申请的bit位。比如说int _a:2;表示_a占用两个bit位的空间,_b是占用5个bit位的空间,以此类推。
那么这样创建又有什么样的好处呢?就好比说_a我只需要赋予它两个bit位大小的值,但是我如果使用结构体的话,会直接申请32个bit位,这样就会大大的浪费空间。这样也变相论证了位段是为了节省空间而创造的。
位段的内存分配
既然位段是为了节省空间而进行的,那么它是如何进行内存分配的呢?
我们注意以下几点:
1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型。
也就是说,我们在使用位段的时候最好用int和char来进行创建
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
说明位段是基于需求来进行创建的,不是一次性的开辟较大的空间
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。那么什么是不跨平台呢?我们接下来举例来进行说明。
就比如这个位段:
struct A { int _a : 2; int _b : 5; int _c : 10; int _d : 30; };
int类型是一次开辟4个字节:
这样就会出现图中的问题:那么_a是从左向右进行开辟,还是从右向左进行开辟的呢?这个不同的编译器可能会有不同的方式,所以位段是不跨平台的。
即便是这样,但我们使用的是VS,我们可以在VS环境下看看它是如何使用的,下面进行举例:
struct S { char a : 3; char b : 4; char c : 5; char d : 4; };
这个位段是如何分配空间的呢?我们画图进行分析。
我们假设其是从右往左进行开辟,那么a和b的分配如下:
这时我们就会发现剩下了一个bit位,但是c是占5个bit位的,那么是c的先占用一个bit位,再在新开辟的一个字节里面占4个bit位,还是直接在新开辟的一个字节里占5个bit位呢?
倘若先占一个bit位,那么d就可以直接放进第二个字节里:
倘若不是,那么就会再开辟一个字节:
那么真正的大小是多少呢?我们进行验证:
#include<stdio.h> struct S { char a : 3; char b : 4; char c : 5; char d : 4; }; int main() { printf("%zd\n", sizeof(struct S)); return 0; }
运行结果如下:
我们发现是第二种情况,也就是说,位段也是会有少量的空间浪费的。
那么,如果我们进行赋值,那么位段又是如何分配空间的?
我们如下进行赋值:
#include<stdio.h> 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; return 0; }
位段既然是以二进制进行存放的,那么我们先进行分析:
a的赋值是10,10的二进制位是00001010,但是3只有3个bit位,因此存放010;b的赋值的二进制位是00001100,那么存放的是1100;c的赋值的二进制位是00000011,存放的00011;d的赋值二进制位是00000100,存放的是0100。
位段虽然是二进制进行存放,但是16进制会更方便我们进行判断,如果是16进制,我们就会看到6 2 0 3 0 4:
我们在内存中进行查看:
位段的跨平台问题
那我们总的来分析一下位段的跨平台的问题:
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。总的来说,跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
枚举
枚举,顾名思义,就是一一列举,那么什么时候我们会想到枚举?
比如说我要列举一周七天,需要列举一年的月份,只要是数量不多,并且取值可以被一一列举的,都可以使用枚举。那么枚举该如何定义呢?
枚举的定义
枚举的定义,我们只需要写上enum+自定义类型名,之后再写上可能的取值就可以了 :
enum Day { //枚举的可能取值 Mon, Tues, Wed, Thur, Fri, Sat, Sun };
enum Day是枚举类型,Mon等成员是枚举的可能取值,也叫枚举常量,枚举的可能取值默认是从0开始,依次递增1的:
#include<stdio.h> enum Day { //枚举的可能取值 Mon, Tues, Wed, Thur, Fri, Sat, Sun }; int main() { printf("%d\n", Mon); printf("%d\n", Tues); printf("%d\n", Wed); return 0; }
运行结果如下:
当然,在定义的时候也是可以赋初值的:
#include<stdio.h> enum Day { //枚举的可能取值 Mon=1, Tues=2, Wed=4, Thur, Fri, Sat, Sun }; int main() { printf("%d\n", Mon); printf("%d\n", Tues); printf("%d\n", Wed); return 0; }
运行结果如下:
枚举的优点
我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
1. 增加代码的可读性和可维护性。
这个就很好理解了,一眼看过去就知道是枚举常量,而#define定义的值需要去头文件中查看。
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
#define是对值进行替换,只是进行替换,不会有类型检查。
3. 防止了命名污染(封装)。
4. 便于调试。
调试时枚举的代码不会进行替换。
5. 使用方便,一次可以定义多个常量 。
枚举的使用
那么枚举应该如何继续使用?
使用枚举的时候我们需要拿枚举常量给枚举变量赋值,这样才不会出现类型的差异:
#include<stdio.h> enum Color//颜色 { RED = 1, GREEN = 2, BLUE = 4 }; int main() { enum Color clr = GREEN; return 0; }
若是这里让clr = 5;,在较为严格比如C++下运行是会报错的。所以我们应该赋予枚举变量。
联合
联合的定义
什么是联合呢?联合其实是一种特殊的自定义类型。
联合也是包含一系列的成员,但是这些成员共用一块空间,因此,联合体也叫共用体。
联合的关键字是union,既然我们知道了关键字,又知道联合的成员是共用一块空间的,那我们就创建一个联合并计算它的大小。#include<stdio.h> union Un { char i; int j; }; int main() { printf("%zd\n", sizeof(union Un)); return 0; }
运行结果如下:
我们可以看到,明明包含了char和int两种类型,却只占了4个字节,这也论证了它们是占用同一块空间的,那么它们的地址也是一样的吗?那我们就来编写代码:
#include<stdio.h> union Un { char i; int j; }; int main() { union Un un; printf("%p\n", &un); printf("%p\n", &(un.i)); printf("%p\n", &(un.j)); return 0; }
为了方便确认,我们在x86的环境下运行:
我们可以看到,它们的地址都是相同的,也就是说,它们的起始地址也是相同的:
这么看来,联合体在同一时间只能使用一个成员,不然一个值会修改另一个值。
联合体大小的计算
联合体大小的计算我们需要注意以下两点:
1.联合的大小至少是最大成员的大小。
2.当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。这两点该如何理解呢?我们举例说明:
#include<stdio.h> union Un { char c[5]; int i; }un; int main() { printf("%zd\n", sizeof(un)); return 0; }
那么这个联合体大小是多少呢?我们结合上面进行分析:
char c[5]占用5个字节,而int i占用了4个字节但是它的最大对齐数是4。如果只申请5个字节的话不是最大对齐数的整数倍,因此我们推测大小应该是8:
小结
那么对于自定义类型的深入了解就告一段落了,接下来就是对动态内存管理的认识了。咱还是得多多努力,让更多的机会把握在自己手中。好啦,我们下次再见!