自定义类型
1.结构体
1.1什么是结构体
结构体是一些值的集合,这些值叫做成员变量,成员变量可以是不同类型。
1.2结构体的声明
struct stu//标签/类型名
{
char name[20];//成员变量
int age;//成员变量
};//分号不能丢
结构体的特殊声明
struct //没有标签
{
char name[20];
int age;
}s1;//s1是在声明时定义的全局变量
- 以上的这种结构体声明叫做匿名结构体类型,即没有名字。
- 这种类型只能在创建时直接创建变量。
- 既然没有了名字,那下面的代码合法吗?
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], * p;
int main()
{
p = &x;
return 0;
}
错误的。p指向的结构体类型已经和x的结构体类型不同。
也就是说这种匿名结构体类型只能使用一次,当你再次使用时就是不同的类型了。
1.3结构体的自引用
以下两种做法方式哪种正确?
struct Node
{
int data;
struct Node next;
};
struct Node
{
int data;
struct Node* next;
};
乍一看好像都没问题,但是如果要求结构体的大小,那就有漏洞。第一种方式是错误的,因为自身引用自身,无限套娃,是不能得出大小的,而第二种方式利用同类型的指针,指向下一个结构体,存放下一个结构体的地址,这样就可以把数据串联起来。
1.4结构体变量的定义和初始化
struct stu
{
char name[20];
int age;
}s1;//这是在声明的同时定义的变量(全局变量)
struct stu s2;//这是结构体的定义
struct people
{
struct stu p;
char sex[5];
float score;
};
int main()
{
struct stu s3;//这是在函数内定义的局部变量
struct stu s4 = { "zhangsan",20 };//这是结构体的初始化
struct stu s5 = { .age = 20,.name = "zhangsan" };//这是结构体的乱序初始化
printf("%s %d\n", s5.name, s5.age);//这是结构体的打印
struct people s6 = { {"lisi",19},"nan",99.5f };//结构体的嵌套初始化
printf("%s %d %s %f\n", s6.p.name, s6.p.age, s6.sex, s6.score);
}
结果
zhangsan 20
lisi 19 nan 99.500000
注意
typedef struct stu
{
char name[20];
int age;
}Stu;//Stu不是变量,而是重命名的类型
int main()
{
Stu s = { "zhangsan",20 };
return 0;
}
1.5结构体的内存对齐
我们先来猜测下面两个结构体的大小
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
}
结果
12
8
疑惑
为什么会出现两个不同的结果?难道不是两个char加上一个int的大小,一共6个字节吗?
这就关系到结构体的内存对齐问题了。不是简单安排自身大小来连续存放,是有一定的对齐规则。
对齐规则
-
结构体的第一个成员对齐到结构体在内存中存放位置的0偏移处。以struct S1 s为例。
-
从第二个成员开始,每个成员都要对齐到一个对齐数的整数倍数。对齐数:结构体成员自身大小和默认对齐数的较小值。VS的默认对齐数是8,Linux gcc没有对齐数,对齐数就是结构体成员自身大小。以struct S1 s为例,i的大小是4,默认对齐数是8,对齐数就是4,要对齐到4的整数倍,c2的大小是1,默认对齐数是8,对齐数就是1,要对齐到1的整数倍。
-
结构体大小必须是所有成员的对齐数中最大对齐数的整数倍。以struct S1 s为例,当前在计算过程中,结构体的大小是9个字节,而所有成员中最大对齐数是4,9不是4的倍数,所以继续占用空间。
我们可以利用offsetof函数来验证下这种对齐规则。offsetof返回偏移量,第一个参数是类型名,第二个参数是成员名,头文件是stddef.h。
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
struct S1 s;
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
printf("%d\n",offsetof(struct S1,c1));
printf("%d\n",offsetof(struct S1 ,i));
printf("%d\n",offsetof(struct S1, c2));
}
结果
12
8
0
4
8
- 如果结构体中,嵌套了结构体,要将嵌套的结构体对齐到自己的成员中最大对齐数的整数倍处。结构体的总大小必须是最大对齐数的整数倍,这里的最大对齐数是包含嵌套结构体成员中的对齐数、的所有对齐数的最大值。
//练习
struct S3
{
char c1;
int i;
char c2;
}s3;
struct S4
{
char c;
struct S3 s3;
double d;
}s4;
s3的最大对齐数是4,要对齐到4的整数倍。double的对齐数是8,要对齐到8的整数倍处。
练习
struct S3
{
double d;
char c;
int i;
};
struct S2
{
char c1;
char c2;
int i;
};
- 通过以上练习的计算,在设计结构体的时候,如果我们既要满足对齐,又要节省空间让占用空间小的成员尽量集中在一起。
- 如果结构体成员含有数组,如何对齐存放?把数组看成相同类型的不同变量,例如char a[3]看出char a1,char a2,char a3。
为什么会存在结构体对齐
来自网上参考资料
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 - 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
1.6修改默认对齐数
#pragma pack(1)//设置默认对齐数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack(1)//设置默认对齐数为1
int main()
{
struct S s;
printf("%d", sizeof(s));
return 0;
}
结果
6
当结构在对齐方式不合适的时候,我们可以修改默认对齐数。
1.7结构体传参
函数传参包括传值还有传地址,对于结构体哪种方式更好?先看例子
struct stu
{
char name[20];
int age;
};
void print1(struct stu s)
{
printf("%s ", s.name);
printf("%d\n", s.age);
}
void print2(struct stu* s)
{
printf("%s ", s->name );
printf("%d\n", s->age );
}
int main()
{
struct stu s = { "zhangsan",20 };
print1(s);
print2(&s);
return 0;
}
函数传参时,都需要在栈区上为形参开辟空间,如果形参太大,需要开辟的空间就越大,函数在执行过程中需要花费的时间和空间也就越大。将结构体本身作为参数传过去,可能需要开辟大片空间,若作为指针传过去则开辟的空间就小的多。
结论
所以结构体传参的时候,要传结构体的地址。
2.位段
2.1什么是位段
位段是一种特殊的结构体,但与结构体有两个不同的地方:一是位段的成员必须是 int、unsigned int 、signed int或者char;二是位段的成员名后边有一个冒号和一个数字。
struct S//S就是一个位段
{
int a : 2;
int b : 4;
int c : 10;
int d : 20;
};
那么冒号和后面的数字有什么含义?位段的大小是多少?位段会不会对齐,还是有其他存储方式?不急,慢慢来。
(1)冒号和后面的数字表示,这个成员变量占多少比特位,例如a占两个比特位,b占4个比特位,c占10个比特位。并且这个数字不能超过这个成员本身大小,char类型的不能超过8,int类型的不能超过32。
(2)sizeof(struct S)的结果是8,为什么是8?这就涉及到位段的内存分配。
2.2位段的内存分配
例子
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;
空间是如何分配的?
- 结构体的第一个成员a是char类型,开辟一个8比特的空间。将10赋给a,首先将10转换成二进制,由于a占3个比特位,所以对10的二进制进行截断,将后3位交给a,放在空间的右边。
- b占剩余空间的4个比特位,将12的二进制进行截断,按照上面的方法放入这4个比特位中。
- c占5个比特位,开辟的空间只剩1个比特位,所以重新向内存申请一个字节的空间。旧的空间剩下的1个比特位就丢掉不用,在新空间占5个比特位。剩下的步骤就不再赘述。
结果大小是3个字节,通过这个例子我们也就能明白位段的存储方式。而且相比于结构体位段好像更加节省空间,但是位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
2.3位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。比如在一些机器,int的大小是32比特,在另一些机器上可能就是16比特,无意间可能就超过了其本身大小。
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。我所举的例子是基于VS这个编译器的,其成员在内存中的分配是从右向左分配的。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。在VS编译器中,剩余的空间不足以容纳接下来的位段时会舍弃。
3.枚举
3.1什么是枚举
枚举顾名思义就是一一列举,把可能的取值一一列举。比如课目、亲人、月份都可以一一列举。
3.2枚举的声明
enum Sex//枚举类型
{
MALE,//枚举常量
FEMALE,//枚举常量
SECRET,//枚举常量
};//这些枚举常量是枚举的可能取值
int main()
{
enum Sex s = MALE;//s是基于枚举类型创建的变量,其可能取值只能是上面3个
return 0;
}
这些可能取值从上到下依次递增,未对其初始化时从上到下,从0开始,依次加1。
3.3枚举的优点
- 增加代码的可读性和可维护性。利用枚举将一些值赋予意义,像性别,不直接用数字表示,而用其英文表示,增加其可读性;当这些枚举常量需要修改时,只需在其声明处修改即可,不用去刻意寻找,增加可维护性。
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装)。枚举把这些常量集中起来。
- 便于调试。#define定义的常量不能调试,因为调试的时候常量已被调换。
- 使用方便,一次可以定义多个常量。
3.4枚举的大小
为什么是4?一个枚举类型的变量只可能是其中一个常量,这些常量是整形,所以其大小是一个整形(4)。
4.联合体(共用体)
4.1什么是联合体
联合体也是一些值的集合,这些值也是其成员,但特点是这些成员共用一块内存,所以也叫共用体。
4.2联合体的定义
union UN//联合体的声明
{
char c;
int i;
};
int main()
{
union UN u;//联合体的定义
printf("%d",sizeof(u));
return 0;
}
结果
4
疑惑
为什么是4?
我们可以查看联合体成员变量的地址
可以发现其地址相同,这也验证了联合体成员共用一块内存,说明联合体的大小至少是最大成员的大小,这样才能容纳其他成员。这意味着两个变量不能同时使用,不让会发生冲突。这些变量不需要同时存在,在一定程度上也节省了空间。
练习
在数据的存储这节中,有一道笔试题 :判断当前计算机的大小端存储。现在介绍另一种做法。
union UN
{
char c;
int i;
};
int main()
{
union UN un;
un.i = 1;
if (un.c == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
4.3计算联合体的大小
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
例子
union Un1
{
char c[5];//最大成员大小是5
int i;//最大对齐数是4
};
union Un2
{
short c[7];//最大成员的大小是14
int i;//最大对齐数是4
};
printf("%d",sizeof(union Un1));//8
printf("%d",sizeof(union Un2));//16