C语言中除了内置类型外,还有自定义类型,在我们日常使用时,自定义类型使用也是比较多的,今天我们来了解下C语言的自定义类型都有哪些,以及怎么使用的。
目录
一、结构体
1、结构体类型的声明
首先来看结构体,结构体实质上是为了对生活中多数情况更好描述应用的,大家知道内置类型都有个特点,就是某种类型的集合,而生活中很多东西都不是一个单一的类型能描述的,而结构体特点就是 是一些值的集合,这些值称为成员变量,而每个成员变量可以是不同类型的变量
//结构体结构
struct stu
{
member - list; //成员列表
}varible-list; //用该结构生成的变量列表
下面看一些常见的结构体定义方式:
这种方式创建的结构体,在使用时,需要 struct Stu s; 这样创建一个结构体变量——局部变量
//结构体
struct Stu
{
char name[20]; //名字
...................
char id[20]; //学号
}; //分号不能丢struct Stu s;
另一种方式就是在创建结构体时,直接创建变量,即在分号前添加变量名——全局变量
//结构体
struct Stu
{
char name[20]; //名字
.............
char id[20]; //学号
} S ; //分号不能丢
除此之外,还有种特殊的定义方式——匿名结构体
这种方式忽略了名字,所以在使用时无法用结构体创建变量,需要在定义时就创建好变量
struct
{
int a;
char b;
float c;
}x,a[20], *p;
需要注意,当下面这样定义时,这两个结构体虽然成员一样,但是编译器会认为是两种不同的结构体。匿名结构体一般只用一次
struct
{
int a;
char b;
float c;
}x; //结构体
struct
{
int a;
char b;
float c;
}* p;//结构体指针
p = &x;
2、结构的自引用
了解自引用之前,我们先来了解下数据结构里的链表
链表是不同与顺序表的,它在内存中不是连续的,而又需要彼此间能找到,这时需要类型链条的关系将前后关联起来,所以需要结构体来组成这样的一个个节点,既能存放数据又能找到下一个节点,而下一个节点跟他的类型是一样的,就有了自引用。
自引用的意思就是自己调用自己,下面这种情况看看对不对?
struct Node
{
int data;
struct Node next;
};
乍一看,确实是自己调用自己,但是仔细分析,当求这个结构体大小sizeof时,会发现陷入了循环,每次求next大小时,又会去求一个结构体大小,这样一直循环下去
正确的自引用方法,应当用结构体指针,用这个指针存放下一个节点的地址,这样就能找到下一个节点
struct Node
{
int data; //数据域
struct Node* next; //指针域
};
下面两种常见的自引用错误示范
//typedef 重定义的名字,注意顺序,先有结构主体才能重定义,不能在内部使用外部重定义的名字
typedef struct Node
{
int data;
STNode* next;
}STNode;
struct Node
{
int data;
Node* next;
};
3、结构体变量的定义和初始化
① 定义
struct tag
{
int a[20];
int size;
char age;
}x; //声明类型同时定义变量
struct tag s1; //用创建结构体tag定义结构体变量s1
② 初始化
struct tag
{
int a[20];
int size;
char age;
}s1 = { "Li Si", 20 ,'w'} ; //在定义时初始化struct tag s2 = { "Li Si", 20 ,'w'}; //在使用时初始化
③ 结构体嵌套初始化
struct Node{
int data;
struct tag s3;
struct Node* next;
}p1 = {10, {"wang wu", 20 ,'x' }, NULL}; //定义时嵌套初始化
struct Node p2 = {10, {"wang wu", 20 ,'x' }, NULL}; //使用时嵌套初始化
4、结构体内存对齐
我们先来看一段代码:
struct s1
{
char c1;
int i;
char c2;
};
struct s2
{
int i;
char c1;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct s1));
printf("%d\n", sizeof(struct s2));
return 0;
}
这段代码观察看是求这个结构体的大小,那我们分析一下,char c1是一个字节,int i 是4个字节,char c2又是一个字节,那printf 输出就是 两个 6,这样对吗?
可以看到,两个都不是6,并且因为位置不同,两个结果存在差异,这是为什么呢?
这就引出了结构体的一个重要知识点:内存对齐
我们先用 offsetof 这个宏来观察下每个成员对于起始位置的偏移量
按照偏移量画图分析
由上图看,也才9个字节,原来输出的是12,来了解内存对齐的规则就知道了
对齐规则:
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(成员的对齐数)的整数倍的地址处。
3. 结构体总大小为最大对齐数(所有成员中对齐数最大的值)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS默认对齐数是 8
由于9个字节不是三个变量中最大对齐数(i 的最大对齐数 4)的整数倍,所以它需要浪费3个字节,凑够12个字节,所以结构体s1的总大小是12。
而 s2 由于变量位置不同,所以就会有所不同
s2 浪费2个字节,凑够8个字节
内存对齐为什么会存在呢?
有两个方面原因:
1. 平台原因(也叫移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
由下图看,用一个简单的结构体可了解到,因为访问时不能随意从任意位置开始访问,所以需要对齐来减少访问时间,而这种方法也是一种 空间换时间的方法。 让占用空间小的成员尽量集中在一起,就能节约空间
注:结构在对齐方式不合适的时候,可以自己更改默认对齐数。
指令:
#pragma pack(8) //设置默认对齐数为8
#pragma pack() //取消设置的默认对齐数,还原为默认
5、结构体传参
结构体传参也就无非是传值和传址两种方式,下面我们看看两者的区别
struct S
{
int data[100];
int num;
};
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
struct S s = { {1,2,3,4}, 100 };
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
可以看到,两种方式都可以访问结构体的成员,只是访问的符号 传值是 ”.“ 操作符,传址是 ” ->“操作符
一般使用时用第二种传址的,因为函数传参时,会压栈,如果传的结构体太大,传参时会在时间和空间上都会产生很大的开销,导致性能降低,而传址的方式就不会。
6、结构体实现位段(位段的填充&可移植性)
首先了解什么是 “位段”
位段跟结构体是类似的,有两个不同
1.位段的成员必须是 int、unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字。
这就是结构体实现的一个位段,其中 :2 就表示 _a 这个变量占2个二进制/bit位,所以叫位段。
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
位段的存在是为了一些只需要几个bit位就能处理的情况,这样能有效节约内存
那这样的位段占多大空间呢,按照上面说的,是表示的bit位,那么加起来是47个bit位,一个整形4个字节,32个bit位,那2个整形 8个字节应该够了,接下来运行看
结果果然是8个字节,那事实是如此吗?
这里就要了解位段的分配规则:
1. 位段的成员可以是 int ,unsigned int, signed int 或者是 char (属于整形家族)类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。意思是成员是 int 类型的话,一次开辟4个字节,成员是 char 的话,一次就开辟1个字节。 所以上面8个字节是这样开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
针对跨平台的不确定性,来分析下位段在内存里的存储
如上图,假设如果成立,那么这个位段占3个字节,下面验证一下
跟假设一样,看来确实是这样的,当开辟的空间不够时,重新开辟,并且不使用剩下的
下面看看内存里究竟是怎样存储的。
用一个简单的赋值来观察,假设左边是低地址,右边高地址
再来观察内存,可以看到,和假设一样,存放的是 0x 62 03 04
但是注意的是,这些只是在当前编译环境下是一致的,而位段最大的问题就是跨平台问题
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32)
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。
总结就是:位段可以达到跟结构体一样的效果,还可以节省空间,但是不能跨平台
位段的应用场景:IP数据包格式
二、枚举
枚举就是一 一列举
针对的是生活中能够穷尽的取值,比如:一周是周一到周天,月份是1-12月这些情况
下面看看枚举是如何定义的:
enum Day//星期
{
Mon, //后面带逗号 ,默认第一个取值为0
Tues, //后面取值依次+1
Wed, // 2
Thur, // 3
Fri,
Sat,
Sun //结束不带逗号
};
枚举中的内容是枚举类型的可能取值,也叫枚举常量 。
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。 例如:
enum Color//颜色
{
RED = 1,
GREEN = 2,
BLUE = 4
};
//也可以这样
enum Sex//性别
{
MALE, //未赋值默认 0
FEMALE = 20,
SECRET //未赋值,默认为前面取值+1,这里 == 21
};
枚举和 #define 定义的常量对比有以下优点:
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量
这些优点在日常使用多了就会慢慢有所了解,这里不过多赘述
三、联合体
联合体也是一种特殊的自定义类型
它和结构体一样包含成员,特点是这些成员共用同一块空间,因此也叫(共用体)
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
我们创建了一个联合体Un,并用它创建了一个变量 un ,可以看到整个联合体大小刚好是最大成员i 的大小,而每个成员的起始地址也都一样,其内存上就类似图上,c占第一个字节,i占四个字节,两者有公共区域,成员间的改变相互影响,因此,联合体在同一时间,只能使用一个成员变量 联合也存在内存对齐:当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
看下面代码,Un1应该是最大成员c[5] ,5个字节,由于内存对齐,int 成员 是最大默认对齐数,所以需要对齐到 int 的整数倍,因此只能取 8 ,Un2同上。
联合体存在的意义:他的特点就是他的意义,当需要在不同时间用不同类型的值来描述同一块内存时,就有了实际价值
实际应用场景就是验证计算机的存储方式究竟是大端存储还是小端存储
将 un.i 赋值一个1,假设内存中存放 为 0x 0100000 ,那么,un.c 为char类型,一次只能访问一个字节,而访问都是从低位向高位访问,如果un.c == 1,说明un.i是将低字节放低位,高字节放高位,符合小端存储模式,反之就是大端存储模式。
好了,这期自定义类型到这里就了解完了,后面会持续深入了解C的其他知识点,持续关注,下期更精彩。