自定义类型分为结构体,枚举与联合体类型。
结构体:
结构体是一些数据的集合,这些数据可以是不同或相同类型的数据,这些数据被称为成员变量。
结构体的声明:
struct tag{
member-list;(成员列表)
}variable-list;(定义的变量列表)
如:struct stu{
int age;
char sex[2];
}s1;
特殊的声明:
如匿名结构体类型
struct{
int a;
int b;
}x;
因为匿名结构体无法表示,所以只能在声明时创建变量。
注意:在同一项目中创建两个成员相同的匿名结构体,系统会将这两个结构体自动分辨为不同类型的结构体。这就代表着在使用&或者其他操作时不能相通。
结构体的自引用:
struct Node
{
int a;
struct Node next;
}
这样的代码是错误的,在定义时无法直接自引用。
拓展知识:数据结构中的链表:链表中包含多个元素,每个元素都有两部分,一个叫数据域(存放计算作用的数据),另一个是指针域(该指针指向链表中另一元素),通过指针域使得链表相邻元素间建立联系,链表中最后一个元素的指针域可以为空指针。
正确的自引用:
struct Node
{
int a;
struct Node* next;(将结构体类型写成结构体类型的指针)
}
若要将匿名结构体重命名为Node时:
typedef struct
{
int a;
Node* next;
}Node; 这样的写法错误,因为在重命名之前是没有Node这个标识符的,而匿名体在定义时使用了这个标识符,会报错。
正确的写法:
typedef struct Node
{
int a;
struct Node* next;
}Node;(定义了个寂寞,说明只有非匿名体可以被重命名)。
结构体变量的定义与初始化
struct Point{
int a;
int b;
}p1;(定义类型时创建变量)
struct Point p2;(使用整体类型创建变量)
struct Point p3={2,3};(创建变量时根据成员变量顺序初始化)
基本初始化:使用大括号括起初始化内容,使用逗号分开成员初始化内容。
嵌套定义如:
struct po{
struct Point s;
int a;
};
struct po a={{2,3},3};{}中的{}(简称内花括)表示该结构体中有一个成员的类型是结构体类型。
结构体在内存中存放时会遵循结构体对齐规则
结构体内存对齐规则:
1.第一个成员在与结构体变量偏移量为0的地址处存放
偏移量为当前地址与结构体变量首地址的差
2.其他成员变量要对齐到对齐数的整数倍的地址处
对齐数为编译器默认对齐数与该成员大小中的较小值
VS编译器默认对齐数为8,Linux环境下没有默认对齐数
3.结构体总大小为所有成员中最大对齐数的整数倍
4.如果嵌套了结构体,嵌套的结构体存放时对齐到自己最大对齐数的整数倍,外层结构体的整体大小就是所有最大对齐数(包括嵌套的结构体的对齐数)的整数倍。
根据规则可知结构体在内存中存放时一般会“浪费空间”。
举例:
struct s{
char c1;
int i;
char c2;
}s1; sizeof(s1)的结果为12
按照对齐规则
1+3(空出)+4+1=9,而9不是最大对齐数4的倍数,所以还要扩大到12。
offsetof是一个宏(可当做函数),用来计算结构体成员对于结构体变量的偏移量的,包含头文件stddef.h。用法:offsetof(struct 变量名,成员名);返回一个整数
为什么要结构体对齐存放:
保护数据:某些硬件平台只能在特定的地址上取出内容。
提高性能/效率:处理器在访问未对齐的内存时需要两次访问,而对齐时只需要一次访问。
在结构体设计时既要满足对齐,又要节省空间,所有尽量将占空间小的结构体成员放在一起。
修改默认对齐数:
#pragma pack(n)将系统的默认对齐数修改为n,若改为1则存放按照原始方法(相当于无对齐规则),若改为0则改动无效果。
传参方面:结构体传参要比结构体地址传参浪费空间。
因为函数传参时,参数要压栈,结构体参数的大小肯定要大于一个结构体地址参数的大小。否则栈区开销过大,效率降低。
位段:
1.位段的成员必须是si/unsi的int,不仅如此,整型家族的成员都可以使用位段。
2.定义位段的成员名时后面有一个冒号和一个数字
如:经过位段处理的int _a : 2;表示_a这个成员数据储存只使用两个bit位,其储存的数组范围也被限定为0-3或者无符号的0-4.在知道变量数值的取值范围情况下可以用位段规定空间。
注意:位段不具有跨平台性。举例:
struct std{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
根据位段的储存规则:轮到d时还需要30个bit位,但是在第一个字节中我已经用了17个,还剩十五个,所以不管怎么样,内存都会给d再开辟一个字节的空间(32bit位),至于d到底是存一半在前一个字节还是全部都存在后一个字节这个问题并没有说明,而且前面的成员也不一定是满打满算存放的,根据大小端储存方式以及数组二进制码的转换,每四个bit位输出一个数值可以使用调试地址查看具体的存放方式,这也是位段不具有跨平台性的原因。
此外
1.int位段被当成有符号还是无符号数是不确定的
2.位段中最大位的数目不能确定
3.位段中成员在内存中从左向右分配,还是从右向左分配不确定。
4.当一个结构包含两个位段时,第二个位段成员比较大无法容纳于第一个位段剩下的位时,是满打满算存放还是存放到别处是不确定的。
枚举:把可能的取值一一列举出来
如:
enum Sex
{
MALE;
FEMALE;
SECRET (不带分号)
};
初始化如:enum Sex s=MALE;
注意:枚举类型在创建时每个变量都被系统赋过值(默认赋值为第一个成员为0,往下依次加1,若将一个成员赋值(定义时赋值),其之下的值根据被赋值成员的值依次加1得值),在main中可以使用%d将该值打印出来。如printf("%d",MALE);
一般情况下,使用#define与枚举是一样的。
枚举相较于#define的优点:
1.增加代码的可读性与可维护性
2.枚举类型有类型检查,更为严谨
3.防止命名污染(封装)
4.便于调试
5.使用方便,一次定义多个常量
类型检查如:
enum Color
{
RED=5;
YEL=8
BLUE
}
在main中enum Color c=5;时c的第一个元素被赋值为5
但此写法在cpp中无法实现,因为cpp中枚举常量有类型检查
正确写法:enum Color c=YEL;YEL在之前就已经被定义,相当于#define定义的常量。
便于调试:在代码编译阶段,#define定义过的常量会被替换成其他符号,在调试中不便于找到原来的值。使用枚举常量可以避免这种情况。
枚举常量:在main外定义,定义中写上可能取值,在main中可根据成员对应值来赋值。(适用于C)
联合体:
union un
{
char c;
int i;
}
在联合体u中有char与i成员,理应来说union un u; u最少也要耗费五个字节空间来存放全部成员,但是实际上c共用了i储存的第一个字节,所以联合体也叫共用体。联合体u和其成员c与i的储存首地址是相同的。
u.c代表u中的c成员。因为是共用体,所以在使用时同一时间只能使用一个成员,联合变量的大小至少是其最大成员的大小,保证有能力储存所有的成员。
对成员赋值:u.i=1;
因为成员共用一个存储空间,我们可以将整型成员赋值为1,然后根据字符类型成员的值判断大小端储存方式。
联合体大小的计算:
1.联合体大小至少是最大成员的大小
2.当最大成员大小不是最大对齐数的整数倍时,就要增加对齐到最大对齐数的整数倍。
例如:
sizeof(un1);的结果为8,由5变为4*2。
sizeof(un2);的结果为16.,由14变为4*4。
最大对齐数计算:un1中有两种类型,short与int,较大的为int,再用int(4)与VS默认对齐数(8)比较,4<8,最后确定最大对齐数为4,un1大小为8(离最大成员5最近的最大对齐数的倍数)。un2中两种类型,同理最大对齐数还是4,un2大小为16。