C语言(2.4 自定义数据类型)
目录
一、结构体
结构体:一些值的集合,这些值称为成员变量,每个成员变量可以是不同类型
关键字:struct
1. 声明和定义
1.1 结构体声明和定义
声明结构体变量时,每个语句后以分号结束,大括号后的分号不要忘记。
- 声明
struct Stu
{
int a;
char b;
};
- 声明+定义
struct Stu
{
int a;
char b;
} stu,*s;
1.2 匿名结构体类型
省略结构体名称,只能使用一次,并且必须定义变量
struct
{
int a;
char b;
} stu, *s;
1.3 结构体自引用
声明结构体类型时,结构体中的成员变量,有指向自己类型的指针
struct Node
{
int data;
struct Node *next; //指向自己类型的指针变量
};
错误情况:
- 声明结构体类型时,结构体中成员变量,不能存在自己本类型的变量
struct Node
{
int data;
struct Node next; //错误,含有自己类型的变量
};
- 声明结构体类型时,使用typedef重命名时,不能使用重命名的名字自引用
typedef struct Node
{
int data;
Node next; //错误,使用重命名的名字定义变量
} Node;
2. 初始化
- 声明+定义+初始化
struct Stu
{
int a;
char b;
}stu = {1, 'A'};
- 定义+初始化
struct Stu stu = {1, 'A'};
- 嵌套初始化
struct Peo
{
int a;
struct Stu b;
char c;
}peo;
struct Peo peo = {3, {1,'A'}, 'S'};
3. 结构体的使用
3.1 成员变量的访问
- 结构体变量访问成员变量,使用"."操作符访问
peo.a = 100; //访问peo的成员变量a
- 结构体指针访问成员变量,使用"->"操作符访问
struct Peo* pc = peo;
pc->a = 100; //访问pc指针指向的结构体变量的成员变量a
(*pc).a = 100; //指针的解引用访问方式,是->操作符的原理
3.2 结构体传参
结构体传参有两种方式,分别是传值和传址
void function(struct S1 s1); //传值
void function(struct S1 *s1); //传址
传值传参是将结构体变量复制一份压入栈中,函数对变量的副本进行操作,复制过程会消耗大量内存和时间
传址传参是将结构体变量的地址压入栈中,函数通过地址对变量进行操作
总结:结构体传参的时候尽量选择传址传参,为避免直接修改到变量,可以使用const修饰
4. 内存对齐
为什么存在内存对齐:
- 平台原因:不是所有的硬件都能访问任意地址上的任意数据,某些硬件平台只能在某些地址取某些特定类型的数据,否则抛出硬件异常
- 性能原因:数据结构应该尽可能在自然边界上对齐,访问未对齐的内存,处理器需要两次内存访问,对齐内存只需要一次访问
4.1 内存对齐的计算
对齐数 = 默认对齐数 和 成员变量大小 中的较小值
- 第一个成员变量:在结构体变量偏移量为0的地址处
- 其他成员变量:的偏移量是对齐数的整数倍
- 结构体总体大小:为最大对齐数的整数倍
- 有嵌套结构体的:对齐到嵌套结构体内部的最大对齐数,结构体整体大小就是最大对齐数(含嵌套)的整数倍
设计结构体时,应让占空间小的成员变量尽量集中在一起
struct S1
{
char a; //在偏移量为0的位置,占0号位置的空间
int i; //对齐数为4,在偏移量为4的位置,占4~7号位置的空间,1~3号位置浪费
char b; //对齐数为1,在偏移量为8的位置,占8号位置的空间,9~11号位置浪费
}; //最大对齐数为4,总体大小为4*3=12个字节
struct S2
{
char a; //在偏移量为0的位置,占0号位置的空间
char b; //对齐数为1,在偏移量为1的位置,占1号位置的空间
int i; //对齐数为4,在偏移量为4的位置,占4~7号位置空间,2~3号位置浪费
}; //最大对齐数为4,总体大小为4*2=8个字节
struct S3
{
char c; //在偏移量为0的位置,占0号位置的空间
struct S1 s1; //嵌套结构体中的最大对齐数是4,在偏移量为4的位置,占4~15号位置的空间,1~3号浪费
char d; //对齐数为1,在偏移量为16的位置,占16号位置的空间
}; //最大对齐数为嵌套结构体中的4,4*5=20个字节
4.2 修改默认对齐数
VS2019的默认对齐数为8,其他大多数编译器没有默认对齐数
结构体在对齐方式不合适时,我们可以自己更改默认对齐数(一般都设置为 2 n 2^n 2n)
#pragma pack(2) //设置默认对齐数为2
#pragma pack() //取消设置默认对齐数,还原为默认值
4.3 offsetof 宏
offsetof 是计算结构体中成员变量的偏移量的宏(不是函数),使用时需要引头文件<stddef.h>
offsetof(type, member-designator)
- 参数:
- type - 结构体的数据类型名
- member-designator - 成员变量名
使用:
int n = offsetof(struct S1, a); //计算struct S1中的a的偏移量,并赋值给n
5. 位段
位段:将结构体中的成员变量按照bit位进行分配内存,达到节省空间的效果
5.1 位段的声明
- 位段的成员变量类型:只能是 int、unsigned int、signed int 和 char 类型
- 位段的空间:按照4个字节(int)或1个字节(char)的方式开辟空间
struct A
{
int _a : 2; //2个bit位,int类型32位剩下30位
int _b : 5; //5个bit位,int类型32位剩下25位
int _c : 10; //10个bit位,int类型32位剩下15位,不够下一个用,浪费15个bit的空间
int _d : 30; //30个bit位,第一个int剩下的15位不够,开辟第二个int空间,占用30位,浪费2个bit位
}; //总大小2个int类型,8字节/64位
位段的初始化和定义和结构体一样,不再赘述
5.2 内存分配
struct S1
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
} s1;
s1.a = 10; //1 010
s1.b = 12; //1100
s1.c = 3; //00011
s1.d = 4; //0100
- 开辟第一个char空间,a占低位3位,剩5位,a赋值为010
- b占4位,接着a的位置,剩1位,b赋值为1100
- c占5位,第一个char空间剩的不够,开辟第二个char空间,c占低位5位,剩3位,c赋值为00011
- d占4位,第二个char空间剩的不够,开辟第三个char空间,d占低位4位,剩4位,d赋值为0100
总共开辟了3个char空间,位段占3个字节
5.3 跨平台问题
- int 类型被当成有符号还是无符号是不确定的
- 位段中最大位的数目不确定(16位机器的int只有2个字节,32位有4字节)
- 位段中的成员在内存中是从左到右还是从右到左没有标准定义(VS2019一个字内从低位到高位,字之间从低地址到高地址)
- 当一个结构体包含2个位段,第二个成员比较大,无法容纳于第一个剩余的位,是舍弃还是利用不能确定(VS2019舍弃)
总结:位段可以节省空间,但是跨平台问题多
5.4 位段的应用
如:IP数据报的字段就是用位段实现
二、联合体(共用体)
联合体:每个成员变量共用同一块空间
关键字:union
1. 声明、定义和使用
声明联合体时,成员变量的创建语句后加分号,大括号后不要忘记加分号
1.1 联合体声明和定义
- 声明
union Un
{
int i;
char c;
};
- 声明+定义
union Un
{
int i;
char c;
} un;
1.2 匿名联合体类型
跟结构体一样,声明时必须定义变量,且只能定义一次
union
{
int i;
char c;
} un;
1.3 联合体的使用
联合体变量可以接受成员变量类型的数据
un = 12; //int类型
un = 'a'; //char类型
2. 内存大小的计算
- 联合体大小最小是最大成员变量的大小
- 最大成员变量大小不是对齐数的整数倍数时,就要对齐到最大对齐数的整数倍
- 数组、嵌套联合体等复合类型的成员变量的最大对齐数是其内部的最大对齐数
union Un
{
int i; //对齐数为4,是最大的
char c;
} un; //所以该联合体大小为4
union Un
{
int i; //对齐数为4,是最大的
char c[5]; //内存空间为5,对齐数为1
} un; //联合体大小是最大对齐数的倍数,所以不是5,而是8
三、枚举类型
枚举:将可能取到的值一一列举
枚举常量:可能取到的值
关键字:enum
1. 声明和定义
枚举常量的后面是逗号,最后一个常量不加标点,大括号后别忘了分号
枚举常量默认从0开始,依次递增1
- 声明
enum Color //三原色
{
RED, //0
GREEN, //1
BLUE //2
};
- 声明+定义
enum Color //三原色
{
RED,
GREEN,
BLUE
} color; //定义变量color
- 设置枚举常量值
枚举常量总是以上一个常量值递增
enum Color //三原色
{
RED = 2, //2,设置值为2
GREEN, //3,沿着上一个递增
BLUE //4
};
enum Color //三原色
{
RED, //0,默认从0开始
GREEN = 3, //3,设置值为3
BLUE //4,沿上一个递增
};
enum Color //三原色
{
RED = 6, //6
GREEN = 7, //7
BLUE = 8 //8
};
2. 枚举的使用
- 枚举常量可以单独作为右值使用
int a = RED; //枚举常量直接使用
- 枚举常量可以赋值给枚举类型的变量使用
enum Color c = RED; //枚举常量赋值给枚举变量
- 枚举变量可以被赋整型数值(不建议)
enum Color c = 0; //枚举常量中包含的整型值赋值给枚举变量
enum Color d = 99; //枚举常量中不包含的整型值赋值给枚举变量
注意:在C语言中可以实现,其他语言不一定能运行通过,但是该使用方法非常不推荐
3. 枚举类型的原理
为什么使用枚举类型:
#define
指令定义太多宏常量,会导致代码松散,阅读体验差,枚举类型增加了代码的可读性和可维护性#define
指令在预处理期直接对标识符进行替换,没有类型检查,枚举类型在编译器对标识符进行替换,会进行类型检查,更严谨- 枚举类型将常量封装起来,防止命名污染
- 枚举类型使用方便,一次可以定义多个常量
枚举类型的原理:
枚举常量并不专门开辟空间存储在堆、栈、静态区等内存里,而是保留在代码中被存储于代码区,代码在编译期将名字替换成对应的值。
所以枚举常量是一个常量,不能被赋值,也**不能被取地址&**操作。