携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情
前言
C语言中除了诸如int、char等等的内置类型,还有自定义类型,主要是这几种:结构体、联合、位段和枚举。
本文就来分享一波作者对自定义类型的学习心得与见解。本篇属于第二篇,主要介绍联合、位段和枚举。
笔者水平有限,难免存在纰漏,欢迎指正交流。
结构体数组与传参
关于结构数组
本质上还是数组,只不过存放的数据类型为结构体。
结构数组的声明,如:struct book library[5];
结构数组中的成员的访问,像是结合了数组与结构体一般,结构体本身不能被访问,访问的对象一般是其中的成员,比如:library[0].value。
关于结构体传参
函数传参有两种方式:传值与传址,即直接传值和传递指针。
struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
上面的 print1 和 print2 函数哪个好些?
答案是:首选print2函数。
若是直接传整个结构体的值,则需要另外开辟空间存储结构体值的副本,也不能对原结构体操作;而传递指针的话无论原结构体占用内存多大,只需要再开辟一固定空间存储指针即可,同时,通过解引用还能改变原结构体内成员的值,一举多得。
函数传参时,参数是需要压栈的,会有时间和空间上的系统开销。
如果传递一个结构体对象时,结构体过大,参数压栈的系统开销会比较大,会导致性能的下降。
所以,结构体传参的时候要传结构体的地址。
位段
位段的声明和结构体是类似的,但有两个不同:
1.位段的成员必须是char,int,unsigned int,或signed int。
2.位段的成员名后有一个冒号和数字。
struct o
{
int A;
float B;
char C;
};//结构体
struct p
{
int A:2;
unsigned int B:6;
int C:11;
};//位段
那么位段有什么独特之处呢?
我们先看一下它的内存占用情况:
struct p
{
int A : 2;
int B : 9;
int C : 11;
};
int main()
{
printf("%d", sizeof(struct p));
return 0;
}
结果是4,为什么?
冒号后的数字实际是是bit位占用安排,比如说int A:2就是为A分配两个bit位,但是不能超过原数据类型的上限。
有什么用呢?比方说现在我定义的一个结构体内有一个成员需要存储性别信息,那性别信息有几种状态?这里取男,女,保密三种,那我只需要2bit位就足以存储了吧,如果我还是用int32bit位的话就会浪费30bit位。
为位段分配内存时,先根据成员类型,比如说这里都是int(这里是都相同的情况,不同的情况先不做讨论),那就以4个字节(32bit)为一个单位分配内存,不够时再开辟一个单位。位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
位段涉及很多不确定因素,是不垮平台的,注重可移植的程序应该避免使用位段。
问题探究:
struct p
{
int A : 2;
int B : 9;
int C : 11;
int D : 20;
//22+20>32,需要再开辟32bit,那么问题来了,
//D的20bit是全部放到新的空间去了还是说先放10bit在原来的空间再把剩下的放在新空间呢?
//不同的平台可能有不同的方式
};
还有就是,A只占2bit对吧,那A是从低位开始放还是从高位开始放呢?不同平台的情况也可能各不相同。
我们来探究一下上面两个问题在VS下的情况。
struct p
{
char A : 2;
char B : 4;
char C : 4;
char D : 5;
};
int main()
{
struct p o = {0};
o.A = 10;
o.B = 6;
o.C = 8;
o.D = 4;
}
假设在VS上从低位存起并且超出原空间大小的变量全部放到新空间去。
首先,编译器会根据位段的大小一次开辟足够大小的空间,也就是开辟三个字节,为什么不是两个字节呢?第一个字节放入A和B之后还剩两位,放不下C,所以需要第二个字节,C就全部放在了第二个字节里,还剩4位,放不下D,所以需要第三个字节,D就全部放在了第三个字节里。
可能有人就会说了,你这A赋的是两位,怎么放得下10(二进制1010)呢?放不下的会发生截断,从低位向高位截断,所以给o.A赋值10会只保留10。o.B赋值6(二进制110),不够4位发生补位,则为0110。o.C赋值8(二进制1000),则为1000。o.D赋值4(二进制100),不够5位发生补位,则为00100。
这样一来的话,这三个字节对应十六进制数为1a 08 04,到底会不会是这样呢?我们进入调试后在内存窗口看一看。
发现果真如此:
事实证明:位段在VS上从低位存起并且超出原空间大小的变量全部放到新空间去
位段跨平台问题总结:
1.int在位段中被当成有符号还是无符号不确定
2.位段中最大位数目不能确定。(16位机器最大16,32位机器最大32)
3.位段中成员在内存中从低位向高位还是从高位向低位没有标准。
4.当一个结构体中的成员较大,超出原空间时,是将该成员全部放入新空间去还是一部分放在旧空间而剩余部分放在新空间去不确定。
总结一下就是,位段在结构体的基础上可以按需求分配空间以进一步节省空间,不过要注意跨平台问题。
枚举
什么是枚举
顾名思义就是把可能的值一一列举。
比如我们现实生活中:
一周的星期一到星期日是有限的7天,可以一一列举。
性别有:男、女、保密,也可以一一列举。
月份有12个月,也可以一一列举
通过使用enum来声明枚举类型 ,例如:
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
以上定义的 enum Day , enum Sex都是枚举类型。
{}中的内容是枚举类型的可能取值,也叫 枚举常量。
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
enum Color//定义Color类型,该类型的变量可能取值为RED,GREEN和BLUE(都是常量)
{
RED = 1,//若是不赋初值则默认为0
GREEN,//2
BLUE//3 值一个一个递增下去
};
int main()
{
enum Color a = Color::BLUE;//要注意不要使a = 2,a是Color类型而2是int类型,注意 :: 可以限定BLUE是Color类型的枚举常量
printf("%d",RED);//0
printf("%d",GREEN);//1
printf("%d",BLUE);//2
return 0;
}
结构体里的是成员,是变量,而枚举里的是该类型变量的可能取值,是常量。
为什么要用枚举类型
1.增加代码的可读性和可维护性。
枚举常量具有自描述性,也就是说可以不借助注释通过变量名或常量符号就能清楚地get到数据对应的意义,远比直接用数值直观的多。
2.和#define定义的标识符相比枚举具有类型检查,更加严谨(C中不够严格,但是在C++中要求比较严格)。
3.防止了命名污染(封装)。
4.便于调试。
5.使用方便,一次可定义多个强相关性常量。
关于联合(共用体)
共用体的使用与特点
联合也是一种特殊的自定义类型,也包含一系列的成员,特征是这些成员共用一块空间。
实际上,共用体和结构体很相像,区别在于共用体各成员共用同一块空间。
union p //声明共用体
{
char A;
char B;
char C;
int D;
};
int main()
{
union p u; //定义共用体
printf("%p\n",&u);
//访问共用体成员,打印成员地址
printf("%p\n",&(u.A));
printf("%p\n",&(u.C));
printf("%p\n",&(u.B));
printf("%p\n",&(u.D));
printf("%d", sizeof(u));
}
发现成员的地址和共用体的地址是一样的,则有以下情况。
所以一个共用体变量的大小至少是最大成员的大小( 因为它至少得有能力保存最大的那个成员)。
因为共用空间,所以在同一时间下只能存储和使用其中的一个成员。
例如:
那这样就会有一些问题:
变量的地址是其最低地址。这里a的地址就和x整体的地址相同。虽然栈区中变量创建先使用的是高地址,但是正如数组中的元素从低地址向高地址排列一个道理,联合体中的成员先使用的是低地址,也就是说联合体所有成员的地址其实都和联合体整体的初始地址相同。
联合体内的成员共同使用同一块空间,每个成员都认为自己是唯一使用该块空间的,也就是每一个成员其实都是第一个元素!每一个成员都从共用空间的低地址处开始向高地址处根据类型开辟空间。
利用共用体判断存储字节序大小端
有一种之前博客中介绍过的方法:
int main()
{
int a = 1;//0x00000001
printf("%x",*(char*)&a);//强制转换成char截取第一个字节的数
return 0;
}
根据这种思路其实还可以使用共用体:
union un
{
char b;
int a;
};
因为x.a,x.b共用一块空间嘛,取出x.b的值就相当于取出x.a第一个字节的值。
共用体大小的计算
1.联合大小至少是最大成员的大小。
2.当最大成员大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍。
union p
{
char o[5]; //对齐数按char来看也就是1,占用空间5
int i; //对齐数为4,占用空间4
};
上述例子中最大对齐数为4,p占用内存为8。
以上就是本文全部内容了,感谢观看,你的支持就是对我最大的鼓励~