C语言自定义类型

前言

我们先来看看C语言内置类型:
在这里插入图片描述
long long和Bool类型是从C99开始引入的。
而自定义类型有三个:
结构体:struct
枚举:enum
联合体:union

结构体的声明

结构体基础知识

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

结构的声明

这里我们直接看例子,假如我们要用一个结构体描述一个学生:

struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢

这里要注意的就是分号不能省略。

特殊的声明

//匿名结构体类型
struct
{
	int a;
	char b;
	float c;
}x;
struct
{
	int a;
	char b;
	float c;
}a[20], * p;

这种声明是结构体的匿名类型,上面的两个结构在声明的时候省略掉了结构体标签。
这匿名类型的结构体创建变量只能创建全局变量,若创建了局部变量则会报错如下图:
在这里插入图片描述

struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;

如上两个结构体他们内部完全相同,那么请问p是否和&x相等?
答案是否定的,因为编译器会把上面的两个声明当成完全不同的两个类型。
所以是非法的。

结构体的自引用

再说自引用时我们首先来介绍一下一种数据结构(后面会细说):链表。
数据结构的定义:数据在内存中存储的结构。
我们这里说两种数据结构一种是线形数据结构(顺序表,链表),另一种是树形数据结构(二叉树),这里我们就简单说一下线形数据结构作为了解。
首先是顺序表,就是数据在内存中顺序存储类似于数组,例如1,2,3,4,5、顺序存储如下:
在这里插入图片描述
另一种则是链式存储(链表结构)即在计算机中用任意一组存储单元存储数据,这组存储单元可以连续可以不连续。如下图所示:
在这里插入图片描述
这里就需要结构体自引用了,首先举一个错误示例:

struct Node
{
int data;
struct Node next;
};

这样自引用结构体是否可行?
答案显而易见是不可以的,如果这样自引用那么sizeof(struct Node)将是无限大。
下面这一种是正确的的自引用:

struct Node
{
int data;
struct Node* next;
};

结构体自引用指针类型,指针内部存放要用到的结构体的地址,当指针为NULL时便停止了自引用。
而后期我们每次定义结构体变量都比较麻烦(因为结构体比较长),我们是否可以给他起一个“小名”?
答案是可以的,这里就要用到一个关键字为typedef,这个关键字的作用是为一种数据类型定义一个新名字。
代码示例如下:

typedef struct Node
{
	int data;
	struct Node* next;
}Node;

以前我们要定义一个结构体类形需要struct Node sN;而使用typedef关键字后我们只需要
Node sN;即可。
这里还有一个错误的代码示例:

typedef struct
{
	int data;
	Node* next;
}Node;

这样的代码是错误的,因为typedef还没有对结构体类型完成简化,所以在其内部不可用Node代替struct Node。

结构体变量的定义和初始化

这里比较简单直接以代码为模板举例,不做过多赘述:

struct Point
{
	int x;
	int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = { x, y };
struct Stu //类型声明
{
	char name[15];//名字
	int age; //年龄
};
struct Stu s = { "zhangsan", 20 };//初始化
struct Node
{
	int data;
	struct Point p;
	struct Node* next;
}n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化
struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始化

结构体内存对齐

其实这里主要计算结构体的大小,而在计算中内存对齐是目前面试的热门考点。
我们先看一组代码示例:

#include <stdio.h>
struct S1 {
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	printf("%zd\n", sizeof(struct S1));
	printf("%zd\n", sizeof(struct S2));
	return 0;
}

在这里插入图片描述

结构体内存对齐的规则:
1.结构体的第一个成员直接对齐到相对于结构体变量起始位置为0的偏移处。
2.从第二个成员开始,要对齐到其自身【对齐数】的整数倍的偏移处。
对齐数:结构体成员自身大小和默认对齐数的较小值
(vs编译环境默认对齐数大小是8)
Linux环境默认不设对齐数(对齐数是结构体成员的自身大小)。
3.结构体的总大小,必须是最大对齐数的整数倍。
每个结构体成员都有一个对齐数,其中最大的对齐数就是最大对齐数。
例如struct s1的大小如下图所示:
在这里插入图片描述
还有第四条内存对齐规则(用在结构体嵌套的时候):
4.如果嵌套了结构体的情况
嵌套的结构体对齐到自己的最大对齐数的整教倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
代码示例如下:

#include <stdio.h>
struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
int main()
{
	printf("%zd\n", sizeof(struct S4));;
	return 0;
}

在这里插入图片描述

在这里插入图片描述
那么我们明明可以使内存紧挨着存放,为什么还要用内存对齐呢?
原因有二:
1、平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。(相当于牺牲了空间,但节省了内存运行时间)
这里就相当于你去商店用100元买了22元的商品,若没有内存对齐那么老板要找你78元凑这8元零钱比较费时间,而你实行内存对齐让老板找你70那么老板很快就能凑齐钱数。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到?
让占用空间小的成员尽量集中在一起。

struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};

S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
这里代码留给大家去运行。

修改默认对齐数

前面我们讲解时是以VS为模板讲解,其默认对齐数是8,那么我们在日常中也可以更改其默认对齐数,这就我们就要用到 #pragma 这个预处理指令。
代码如下

#include <stdio.h>
#pragma pack(4)//设置默认对齐数为4
struct S1
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认对齐数8

当然,我们一般是不会更改其默认对齐数的,若要更改一般会更改为2的n(0,1,2…)次方。

结构体传参

我们直接用代码示例:

#include <stdio.h>
struct S
{
	int data[1000];
	int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
void print1(struct S s)
{
	printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
	printf("%d\n", ps->num);//结构体指针调用
}
int main()
{
	print1(s); //传结构体
	print2(&s); //传地址
	return 0;
}

上面的 print1 和 print2 函数哪个好些?
答案是:首选print2函数。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。而这里我们传递指针那么其空间固定为4或8,会提高计算机性能。
所以结构体传参的时候,最好要传结构体的地址。

位段

位段与结构体相似,但又有许多不同。

什么是位段

位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字。

struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

我们看一下这个位段的大小:
在这里插入图片描述
4个整形变量在结构体中应该是16个字节,但在位段中却是8个字节。

位段的内存分配

如int _a:2;这事一个位段中定义的变量,其冒号后的数字表示对此变量开辟2个比特位的内存来存储此变量(注意是比特位,不是字节,1字节等于8个比特位)。那么根据以上所述,这个位段的大小应该是6个字节(2+5+10+30=47比特位=6个字节)。下面是对位段内存分配的一段总结:

  1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
    就是说上述位段,编译器看到第一个变量为int类型后会先开辟4个字节的空间,而这4个字节的内存用完后或者说剩余的内存不足以存放下一个变量,若下一个变量还是int类型,那么编译器还会在开辟4个字节。

那么位段在内存中是高位开始存储还是从低位开始存储,1个字节剩余的比特位不足以存储下一个变量,是开辟新空间后直接在新空间存储,还是在这一字节剩余空间存储一部分,剩余部分在新开辟的空间空间存储呢?接下来我以VS为模板来展示。

我们先看一段代码:

#include <stdio.h>
struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

int main()
{
	struct S s = { 0 };
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
	printf(" % d\n", sizeof(struct S));
	return 0;
}

在这里插入图片描述
我们可以看到在VS上运行结果是3个字节。这里我们推测在VS上,若一个字节不够用,系统开辟新的空间,那么下一个变量会直接存储在下一个空间,而放弃前一个字节剩余的比特位。
如上位段是char类型,那么系统首先会开辟1字节(8比特位),前两个变量用了7个比特位第三个变量需要5个比特位,若第三个变量一个比特位存储在系统开辟的第一个字节的空间,那么后两个变量需要8个比特位,最后这个结构体只需要2个字节。但执行程序却需要3个字节,这就证明位段会直接抛弃剩余的比特位,变量会直接在新的空间中存储。
我们进行下一个猜测,位段会从开辟的空间中高位向低位存储,如上程序第一个变量需要3个比特位,但是赋值却是10,而10的二进制是1010这里就需要截断变成010(二进制)在系统内存存储显示为16进制为2,第二个变量分配了4个比特位且赋值为12,所以二进制存储为1100,这里不需要截断,4个比特位刚好存储,16进制存储则是6,而第一个字节是8个比特位那么前一个别特为补0(这里记录一下案例,后面要说)。第三个变量五个比特位,二进制位011,这里只用到了3个比特位,那么剩余的两个比特位补0(后面也要说),因为第二个字节只有这一个变量,所以16进制是03,第四个变量仍是独占一个字节,所以16进制存储为04。
如下图所示:
在这里插入图片描述
我们来调试看一下内存:在这里插入图片描述
可以见得在VS上我们的猜测都完全正确,但有很多小伙伴可能有疑问,位段所占的空间比结构体要小很多,那为什么大多数情况我们只用结构体不用位段?下面我们讲一下位段的缺点:

位段跨平台问题

我么在上面对位段描述中,都会说前提是以VS为模板,上述这些位段的存储方式,内存大小,在其他IDE(集成开发环境)都可能不适用,还有在上面所注意的两点,位段中的int类型,系统是不确定其有无符号的,在VS中可能会补0,但在其他IDE中补1也是可能的,而且位段的最大位数也是不确定的,例如现在1字节是32位,但在以前的16位机器中1字节是16位的。以下为对其跨平台使用问题的总结:

  1. int 位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
    器会出问题。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
    舍弃剩余的位还是利用,这是不确定的。
    总结:
    跟结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
    但是万物都是存在即合理,位段存在当然也有自己特殊的应用。

位段的应用

当我们聊天,假如有A,B两个同学A同学给B同学发了一个呵呵,这个发送消息的底层操作就需要包装(这里仅当了解,设计了网络协议的知识)。
如下图:
在这里插入图片描述
这种包装若使用结构体就会耗费大量的内存,所以在计算机发展的过程中就选择使用了位段。

枚举

枚举顾名思义就是一一列举。
把可能的取值一一列举。
比如我们现实生活中:
一周的星期一到星期日是有限的7天,可以一一列举。

枚举类型的定义

在这里插入图片描述
如上图所示,都是枚举类型。枚举类型其实也可以看做常量如enum Day中的Mon自动赋值为0,Tues赋值为1,每一个枚举类型顺序加一。当然枚举类型也是可以初始化的,如下代码:

#include <stdio.h>
enum Day//星期
{
	Mon=2,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};
int main()
{
	printf("%d", Mon);
	return 0;
}

在这里插入图片描述
当然,初始化之后,后续的枚举变量会在此基础上加一,如Mon为2,Tues则为3。
在VS2022中枚举变量是可以赋值的:
如下图所示:
在这里插入图片描述

枚举的优点

我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
  3. 防止了命名污染(封装)
  4. 便于调试
  5. 使用方便,一次可以定义多个常量

联合(共用体)

联合类型的定义

联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
联合如下图所示:
在这里插入图片描述

联合的特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。

#include <stdio.h>
union Un
{
	char c;
	int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小
int main()
{
	printf("%d\n", &(un.i));
	printf("%d\n", &(un.c));
	return 0;
}

在这里插入图片描述
我们可以看到两个变量的地址是一样的。

#include <stdio.h>
union Un
{
	char c;
	int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小
int main()
{
	printf("%d", sizeof(union Un));
	return 0;
}

在这里插入图片描述
如下图我们可以看到,其内存空间为4个字节,那联合体的大小是怎么计算的呢?

联合大小的计算

联合的大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
这里的对齐数与结构体大小的对齐数是一样的。

union Un1
{
char c[5];
int i;
};

如这个共用体以VS为模板,对齐数为8,char c【5】的对齐数为1,int的对齐数是4。而char c【5】的内存五个字节已经足够i使用了,但是其最终内存还必须是对齐数的倍数,所以其内存大小为8,如下图所示。
在这里插入图片描述
那么共用体和结构体有什么区别呢?
因为共用体是其变量使用同一块内存,所以不能同时使用共用体变量,但结构体则没有限制,在大多数情况下共用体所使用的内存要少于结构体。
最后制作不易,期待你的三连,若有错误欢迎私信指出。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值