目录
在C语言中,除了标准规定的内置的整型、浮点型...等等这些类型外,还有一些特殊的类型:结构体类型、位段、枚举类型以及联合体,下面让我们一起来看一下它们的具体形式。
结构体
像int、char、double 这些类型只能表示单一的属性,像int整型,能表示人的年龄这种单一的属性,但如果想表示一个人的信息,就需要用到结构体类型了。一个结构体里面可以设计多个元素,比如表示一个的信息,可以设计姓名、年龄、身份证号等等元素。
具体表示形式:
struct man //这里的struct man是结构体类型名
{
char name[20];
int age;
char ID[20];
}s; //这里的s 是结构体变量名
如果s放在main函数外,创建的就是结构体全局变量,放在main函数里面,就是局部变量。
struct man
{
char name[20];
int age;
char ID[20];
};
int main()
{
struct man s;
return 0;
}
特殊声明的结构体类型
匿名结构体类型:不写明结构体类型名,只能使用一次。
struct //没写出结合体类型名
{
char name[20];
int age;
char ID[20];
}s;
注意:匿名结构体没有类型名,如果有两个内容完全一样的结构体,其实它们是不同的。
编译器会把它们当成完全不一样的两个结构体,如下图:
结构体的自引用
链表的实现就需要用到结构体自引用,也就是在结构体内部引用自身结构体指针。
struct man
{
char name[20];
int age;
char ID[20];
struct man* next;
};
为了简便,通常我们使用typedef重命名结构体类型,这样以后struct man就可以写成man了。
typedef struct man
{
char name[20];
int age;
char ID[20];
struct man* next;
}man;
但注意不能写成如下形式:
typedef struct
{
char name[20];
int age;
char ID[20];
man* next;
}man;
这里省略了结构体类型名,将其重定义为man,但在重定义之前又在结构体里面自引用了man,编译器此时会报错,不认识 " man"。
结构体嵌套初始化
结构体是可以嵌套使用的,那么如何初始化呢?
struct score
{
double max;
double avg;
double min;
}p;
struct man
{
char name[20];
int age;
char ID[20];
struct score p;
}s;
int main()
{
struct man s = { "zhangsan",20,"123",{100.0,60.0,80.0} };
return 0;
}
这struct man中嵌套了struct score,初始化时要再用个 { } 对其初始化。
结构体内存对齐
结构体如何计算其大小,这就涉及到了内存对齐,我们来看一下规则:
这里我们采用VS2022环境测试,看几个例子帮助理解:
实例一:
首先解释一下规则里的相关术语:
偏移量:结构体类型在内存中开辟空间,开辟的这块空间是连续的,每个大小为1字节,第一个字节相对于起始位置偏移量是0,第二个是1..... 依次往下。
对齐数:编译器默认的一个对齐数(VS底下是8,其他编译器可能是4,也有的就是结构体成员的大小)和该结构体成员的大小的最小值。
S1中,首先是char类型的c1,按照上述规则第一个结构体成员放在偏移量为0处;
第二个int类型的 i 要从偏移量为4的地方放起(规则二),往下放4个字节,所以char s1和int i
占了8个字节,而这里就有3个字节浪费了,char c2从偏移量为8的位置放起,占1个字节,这时整个结构体占了9个字节,但规定三说:结构体总大小为最大对齐数(4)的整数倍,也就是12,所以整个结构体大小为12,浪费6个字节。
同样的,struct s2里面:
实例二:
计算sizeof(S4)
这里嵌套了结构体,要用到规则四。 struct S3:
struct S4:
struct S4大小是32字节。
既然结构体内存对齐存在浪费,为什么还会有这种规定呢?
解释一下性能原因:比如:
struct S
{
char a;
int b;
};
在32位平台下,一次处理32个比特位,也就是4个字节,如果没有内存对齐,先处理char a,处理了1个字节,剩下int b 还有4个字节就要分两次处理,而内存对齐的情况下就会第二次再处理int b,只需要处理一次,保证一次处理完整的数据。所以内存对齐其实是一种用空间换时间的方法。
当然,其实两种方式各有优劣,不同情形下用不同的处理方式。
为了尽量减少内存对齐的空间浪费,我们一般将小的、零碎的空间放在前面。
修改最大对齐数
#pragma pack(n)
#pragma pack( )
可以修改最大对齐数,n就表示修改的数值。
位段
位段是用结构体来实现的一种类型,它是为了节省空间而设计的。
(例如有些变量只可能是1或0,不需要那么多空间)
struct s
{
int _a : 4;
unsigned int _b : 1;
char _c : 2;
};
位段的成员必须是整型家族的成员,如:int 、char 、unsigned char......
这里的s就是一个位段类型,int _a:4 代表大小是4个比特位(不是字节)。
计算位段大小
由2知:struct s里面的成员都是int类型的,所以每次开辟4个字节的空间,a要2个比特位,b要5个比特位,c要10个,加起来17个,还没满32个比特位(4个字节),要是加上d的30个就超过了32个比特位,所以d新开辟4个字节的空间进行存储,所以一共开辟了8个字节的空间。
注意无符号整型没有符号位,而是转化为计数位,所以unsigned int 要4个字节的空间,_a 和 _c加起来是4个字节,_c是一个字节,但是结构体内存对齐适用所有场景,按规定三,结构体总大小是最大对齐数的整数倍,所以大小是8个字节。
位段在内存中的存储
按照上面位段的计算,S大小应该是3个字节。
a用了3个比特位,但是存放数字10需要4个二进制位(比特位),因为10的二进制数为:1010
这里类似截断,截取最后3位,010 ;类似的,b截取 1100;c本来是011,但给了5个比特位,所以是00011,;d:0100 .
在一个字节内部,我们从低位向高位使用。
第一个字节里: 先放a, 010 再放b 1100
第二个字节里:放c 00011
第三个字节里:放d 0100
转化为十六进制就是620300,这就是位段的存储方式。
位段的跨平台问题
位段的应用
位段在网络里面应用是比较多的,因为网上信息传输存在各种各样或大或小的数据包,如果数据包太大太多,容易造成丢包,影响网络质量。这时可以用位段节省空间,减小数据包的大小,以此来加快网络流畅度。
枚举类型
枚举与穷举不同,穷举是列举出所有可能,枚举是列举某样可能性不算太多的事物的所有可能情况
比如:人的性别、一周的星期。
enum Sex
{
Man,
WOman,
Secruit
};
int main()
{
enum Sex a = Man;
return 0;
}
枚举的都是常量,第一个默认为0,第二个为1.......也可以在定义时修改。
解释一下第4条:便于调试, define定义的常量在调试时是看不出来的,因为#define是预处理指令
是在程序编译之前进行的,所以调试的时候也看不出来,在编译前就替换了。但是枚举可以。
只能拿枚举常量给枚举变量赋值,像上面的代码:enum Sex a=Man这是可以的,但是不能enum Sex a=0这是错误的。
联合体
联合体是共用开辟的内存空间的,但不能同时使用,一个在使用另一个就不能使用。
这里sizeof(u)的大小是4个字节,因为a 和c共用一块空间,按大的算。
所以下面打印的三个地址是一样的。
观察这段代码,帮助你更好的理解
union s
{
int a;
char b;
}u;
int main()
{
u.a = 0x00112233;
u.b = 0;
return 0;
}
我们看u在内存中的变化:
可以观察到给u.b赋值改变的是u.a的第一个字节。
联合体大小的计算
联合体在计算大小时也存在对齐。
sizeof(union Un)大小是8,最大对齐数是4,本来Un大小是5,因为存在内存对齐,所以是8.
有人会问,这里的char arr[5],最大对齐数不应该是5吗?
不是的,char arr[5]可以看做是char s1;char s2.....char s5,最大对齐数是1.
可以这么理解,但是这两个绝对是不等价的!