C语言自定义类型
结构体
结构体类型的声明
定义
结构体是一些值的集合,这些值被称为成员变量。结构体的每个成员可以是不同类型的变量
声明
-
语法
struct 自定义类型名 { 成员列表; }变量列表;
-
案例
定义一个学生类型,学生的属性要求包含名字,电话,性别,年龄:
struct Student { char name[20];//姓名 char tele[12];//电话 char sex[10]; //性别 int age; //年龄 }std3,std4,std5; //std3,std4,std5是在结构体定义时就创建好的全局结构体变量 struct Student std2; // 创建结构体全局变量 int main() { struct Student std1; // 创建结构体变量 return 0; }
说明:
- std1是局部变量,std2、std3、std4、std5是全局变量
- std3,std4,std5是在结构体定义时就创建好的全局结构体变量,如果不需要定义结构体及创建变量,可以省略std3,std4,std5。注意:变量列表可以省略,但是结尾的分号不能省略(易错)。
结构体的自引用
结构体中不能包含一个类型为该结构体本身的成员变量,但是可以包含一个类型为该结构体指针的成员变量。
-
错误案例:
struct Student { int age; struct Student std; // 包含一个类型为该结构体本身的成员变量 }; int main() { printf("%d", sizeof(struct Student)); // 思考,如果可以包含相同类型,那么sizeof的计算结果是多少 return 0; }
-
正确案例:
struct Student { int age; struct Student* nextStd; }; int main() { printf("%d", sizeof(struct Student)); // int类型占4字节,指针类型占4字节,所以sizeof的计算结果是8字节 return 0; }
结构体的重命名
使用typedef关键字可以对结构体类型进行重命名,例如下列代码段,将struct Student
类型重命名为了Std
类型。重命名后,使用原结构体名和新结构体名都可以创建结构体变量:
typedef struct Student
{
int age;
char name[20];
}Std;
int main()
{
struct Student std1; // 使用结构体名创建变量
Std std2; // 使用结构体别名创建变量
return 0;
}
结构体变量的定义和初始化
-
定义
-
方式一:声明结构体的同时,定义结构体变量
-
方式二:先声明结构体,再定义结构体变量
/* 案例代码 */ struct Student { int age; char name[20]; }std1; //方式一:声明结构体的同时,定义变量std1 int main() { struct Student std2; // 方式二:先声明结构体Student,再定义结构体变量std2 return 0; }
-
-
初始化
-
方式一:定义结构体变量的同时,进行初始化
-
方式二:先定义结构体变量,再进行初始化
/* 案例代码 */ struct Student { int age; char name[20]; }; int main() { struct Student std1 = {10,"zhangsan"}; // 方式一:定义结构体变量的同时,进行初始化 struct Student std2; // 方式二:先定义结构体变量,再进行初始化 std2.age = 20; std2.name = "lisi" }
-
结构体内存对齐
-
结构体对齐规则:
-
第一个成员在于结构体变量偏移量为0的地址处。
-
其他成员变量要对齐到某个数字(对齐数)的整数倍处。
对齐数 = 编译器默认的一个对齐数与该成员比较,取较小值(如果成员是数组,则用数组中元素类型进行比较)
例如:假设编译器默认的对齐数是8,一个int类型的大小是4,两者相比较,得出该int类型的对齐数是4
vs编译器默认的对齐数是8
gcc编译器没有默认的对齐数
-
结构体的总大小为最大对齐数(每个成员变量都有自己的对齐数 )的整数倍。
-
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
-
-
案例:
-
案例1-请分析以下代码段的输出结果:
/*假设当前使用的是vs编译器, 默认的对齐数是8*/ struct S1 { char c1; // 成员自己的对齐数为1,编译器对齐数为8,两者比较取较小值为1 int a; // 成员自己的对齐数为4,编译器对齐数为8,两者比较取较小值为4 char c2; // 成员自己的对齐数为1,编译器对齐数为8,两者比较取较小值为1 } struct S2 { char c1; char c2; int a; } int main() { struct S1 s1 = {0}; printf("%d ", sizeof(s1)); struct S1 s2 = {0}; printf("%d\n", sizeof(s2)); return 0; }
输出结果为:12 8
S1内存分析:
- 结构体S1的c1变量存储在偏移量为0的地址处,占用1个字节。(第一个成员在于结构体变量偏移量为0的地址处)
- 结构体S1的a变量存储在偏移量为4的地址处,占用4个字节。(成员变量要对齐到对齐数的整数倍处,a的对齐数是4)
- 结构体S1的c2变量存储在偏移量为8的地址处,占用1个字节。(变量a占用了偏移量4~8的地址)
- 根据上述分析S1已经占用了偏移量0~9的地址,根据结构体对齐规则3,结构体的总大小为最大对齐数的整数倍,最大对齐数是变量a的4字节,所以向上取整占用12字节地址空间
S2内存分析:
- 结构体S2的c1变量存储在偏移量为0的地址处,占用1个字节。
- 结构体S1的c2变量存储在偏移量为1的地址处,占用1个字节。
- 结构体S1的a变量存储在偏移量为4的地址处,占用4个字节。(成员变量要对齐到对齐数的整数倍处,a的对齐数是4)
- 根据上述分析S1已经占用了8个字节的地址空间,已经是最大对齐数a的4字节整数倍,无需再向上空间取整。
-
案例2-请分析以下代码段的输出结果:
/*假设当前使用的是vs编译器, 默认的对齐数是8*/ struct S3 { double d; char c; int i; } struct S4 { char c1; struct S3 s3; double d; } int main() { struct S4 s = {0}; printf("%d\n", sizeof(s)); return 0; }
输出结果为:32
S4内存分析:
- 首先明确S3的大小为16,最大对齐数为8
- 结构体S4的c1变量存储在偏移量为0的地址处,占用1个字节。
- 结构体S4的c2变量存储在偏移量为8的地址处,占用16个字节。(根据结构体对齐规则第4条,嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处)
- 结构体S4的d变量存储在偏移量为24的地址处,占用8个字节。
- 根据上述分析S4已经占用了32个字节的地址空间,已经是最大对齐数d的8字节整数倍(参考结构体对齐规则第4条),无需再向上空间取整。
-
-
为什么存在内存对齐?
-
平台原因:不是所有硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
-
性能原因:数据结构(尤其是栈)应该尽量在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器可能需要进行2次访存才能拿到一个完整的成员变量。而访问对齐的内存,处理器只需要进行1次访存,即可拿到一个完整的成员变量。
总体来说,内存对齐是一种拿空间换时间的做法。
-
-
设计结构体时,怎样节省空间?
-
原则:让占用空间更小的成员尽量集中在一起
-
案例:
请分析以下代码段的输出结果
/*假设当前使用的是vs编译器, 默认的对齐数是8*/ struct S1 { char c1; int a; char c2; }; struct S2 { char c1; char c2; int a; } int main() { printf("%d, %d\n", sizeof(S1), sizeof(S2)); return 0; }
输出结果为:12 8
说明:S1和S2类型的成员一摸一样,但是S1和S2所占的空间大小有了一些区别。
-
-
修改默认对齐数
C语言提供了#pragma这个预处理命令,可以修改默认对齐数
语法:
#pragma pack(默认对齐数)
案例:
请分析以下代码段的输出结果
/*假设当前使用的是vs编译器, 默认的对齐数是8*/ #pragma pack(4) // 设置默认对齐数 struct S1 { char c1; double d1; }; #pragma pack() // 取消默认对齐数 struct S2 { char c1; double d1; }; int main() { printf("%d, %d\n", sizeof(S1), sizeof(S2)); return 0; }
输出结果为:12 16
S1内存分析:
- 结构体S1的c1变量存储在偏移量为0的地址处,占用1个字节。(第一个成员在于结构体变量偏移量为0的地址处)
- 结构体S1的d1变量存储在偏移量为4的地址处,占用8个字节。(默认对齐数是4,double类型对齐数是8,两者比较取较小值4)
- 根据上述分析S1已经占用了12个字节的地址空间,已经是最大对齐数4的整数倍,无需再向上取整。
S2内存分析:
- 结构体S2的c1变量存储在偏移量为0的地址处,占用1个字节。(第一个成员在于结构体变量偏移量为0的地址处)
- 结构体S2的d1变量存储在偏移量为8的地址处,占用8个字节。(默认对齐数是8,double类型对齐数是8,两者比较取较小值8)
- 根据上述分析S2已经占用了16个字节的地址空间,已经是最大对齐数4的整数倍,无需再向上取整。
结论:当结构体对齐方式不适用业务场景时,我们可以自己更改默认对齐数。更改时需要注意,默认对齐数要设置成2的整数倍。
-
计算成员变量在结构体中的偏移量
使用C语言中的offsetof宏可以计算成员变量在结构体中的偏移量
语法:
offsetof(结构体类型, 成员变量名)
头文件:使用offsetof宏,需要导入<stddef.h>头文件
案例:
请分析以下代码段的输出结果
struct Std { char c1; int a; double d; }; int main() { printf("%d, %d, %d", offsetof(Std, c1), offsetof(Std, a), offsetof(Std, d)); return 0; }
输出结果:0,4,8
结构体传参
结构体既可以值传递,也可以地址传递
-
值传递
struct Std { char c1; int a; double d; }; void Init(struct Std std1) { std1.c1 = 'b'; std1.a = 10; std1.d = 10.0 } int main() { struct Std std1 = {'a', 1, 2.0}; Init(std1); printf("%c, %d, %f", std1.c1, std1.a, std1.d); return 0; }
输出结果:a, 1, 2.0
值传递会在内存中拷贝一份相同的结构体类型传递给形参,形参被修改不会影响实参的值
-
地址传递
struct Std { char c1; int a; double d; }; void Init(struct Std* std1) { std1.c1 = 'b'; std1.a = 10; std1.d = 10.0 } int main() { struct Std std1 = {'a', 1, 2.0}; Init(&std1); printf("%c, %d, %f", std1.c1, std1.a, std1.d); return 0; }
输出结果:b, 10, 10.0
地址传递形参和实参指向了内存中的同一个结构体变量,修改形参,实参的值也会改变。
-
总结
- 使用结构体值传递方式时,如果结构体过大,参数压栈的系统开销比较大,会导致程序性能下降。
- 所以结构体传参时,推荐使用地址传递方式。如果期望该结构体不要在传递过程中被修改,可以将形参用const关键字进行修饰。
结构体实现位段(位段的填充&可移植性)
-
什么是位段
位段的声明和结构体是类似的,有2个不同点:
- 位段的成员必须是整型(int、 unsigned int 、signed int、short、char …)。
- 位段的成员名后有一个冒号和数字。
-
位段的声明
/* 位段的声明案例 */ struct S { int a : 2; int b : 5; int c : 10; int d : 30; }; int main() { struct S s; return 0; }
-
位段位的说明
- 位段的成员名后冒号紧接着的数字,表示这个整型占几个比特位。例如:
- 结构体S中
int a : 2;
表示变量a占用2个bit位(a的值在0~2^2之间) - 结构体S中
int b : 5;
表示变量a占用2个bit位(a的值在0~2^5之间) - 结构体S中
int c : 10;
表示变量a占用2个bit位(a的值在0~2^10之间) - 结构体S中
int c : 30;
表示变量a占用2个bit位(a的值在0~2^30之间)
- 结构体S中
- 位段的成员名后冒号紧接着的数字,表示这个整型占几个比特位。例如:
-
位段的内存分配规则
- 位段的成员可以是int、unsigned int、signed int或者是char(属于整型家族)类型
- 位段在空间上是按照4字节(int)或1字节(char)的方式来开辟的
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
-
案例
请分析以下代码段的输出结果
struct S { int a : 2; int b : 5; int c : 10; int d : 30; }; int main() { printf("%d\n", sizeof(S)); return 0; }
输出结果:8
说明:按照位段的内存分配规则2,结构体S的空间是按4字节为单位开辟的,首先开辟一个4字节空间(031bit),元素a占用01bit,元素b占用26bit,元素c占用716bit。由于剩余的空间不够分配给元素d(元素d需要占用30个bit),所以需要再开辟一个4字节的空间,前0~29bit用于存放变量d。所以,结构体S总共占用8字节。
-
位段的跨平台问题
- int位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,假设占用bit数写成27,在16位机器上运行会出问题)
- 位段中的成员在内存中是从左向右分配,还是从右往左分配,标准尚未定义。
- 当一个结构体包含2个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余位还是利用,这是不确定的。
-
位段的应用
思考,既然位段既对数据类型有严格限制,由不支持跨平台,这么难用的数据类型,到底有什么应用场景那?
答案:大家知道IP数据报的首部都是以4字节的整数倍进行填充的,这种数据类型就非常适合使用位段进行填充。
枚举
枚举顾名思义就是把可能的值一 一列举
比如我们现实生活中:
一周的星期只有七天
性别只有男女
月份只有12个月
这些有限的取值范围代表特定的含义,就可以使用枚举类型
枚举类型的定义
enum Day // 星期
{
Mon,
True,
Wed,
Thur,
Fir,
Sat,
Sun
};
enum Sex // 性别
{
MALE = 0,
FEMALE = 2,
SECRET = 4
};
以上定义的Day和Sex都是枚举类型。{ }中的内容是枚举的可能取值,也叫枚举常量。使用枚举值时,需要注意以下事项:
- 枚举常量间用逗号隔开,最后一个枚举常量后不需要符号。(易错)
- { }后需要以分号结尾。(易错)
- 枚举常量都是有值的,默认从0开始,一次递增1。
- 当某个枚举常量被赋初值,后续的枚举常量如果没有被赋初值,会接着上一个枚举常量的值递增1。
枚举的优点
- 使用枚举可以增加代码的可读性和可维护性。
- 和#define定义的标识符比,枚举有类型检查,更加严谨。
- 使用枚举可以防止命名污染(封装)
- 便于调试
- 使用方便,一个枚举类型中,一次可以定义多个枚举常量
枚举的使用
enum Color // 颜色
{
RED = 1,
GREEN = 2,
BLUE = 4
};
int main()
{
enum Color clr = RED; // 只能拿枚举常量给枚举赋值
clr = (enum Color)1; // 整数值赋值必须在枚举常量的取值范围内,否则可能会导致不可预测的结果。
return 0;
}
联合(共用体)
联合类型的定义
联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间
union Un // 联合体的声明
{
char c;
int 1;
};
int main()
{
union Un u; // 联合体的定义
printf("%d\n", sizeof(u)); // 计算联合体的大小
return 0;
}
联合的特点
-
特点:
联合体的成员是共用同一块内存空间的,这样的联合变量的大小,至少是最大成员的大小(因为联合体至少得有能力保存最大的那个成员)。
-
案例:
假设结构体
union Un
在内存中的起始地址是0x000000,请分析以下代码段的输出结果union Un { char c; int 1; }; int main() { union Un u; printf("%d\n", sizeof(u)); printf("%p\n", &u); printf("%p\n", &(u.c)); printf("%p\n", &(u.i)); return 0; }
输出结果:
4
0x000000
0x000000
0x000000
内存分析:
因为联合体占用的是同一块内存空间,所以
char c
和int i
的起始地址都是0x000000。 因为联合体至少要能装的下最大成员,所以Un至少需要4字节的空间,才能装下
int i
。
联合大小的计算
-
大小计算原则
- 联合体的大小至少是最大成员的大小
- 当最大成员大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍
-
案例:
请分析以下代码段的输出结果
union Un { int a; char arr[5]; }; int main() { union Un u; printf("%d\n", sizeof(u)); return 0; }
输出结果:8
内存分析:结构体如果想存下
char arr[5]
成员,至少需要5个字节。而Un的最大对齐数是4字节,所以根据大小计算原则2,结构体需要对齐到最大对齐数的整数倍,5字节向上取整,刚好是8字节。