C语言:五种自定义类型

目录

一、数组

二、结构体

1. 结构的声明

2. 结构的自引用

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

4. 结构体内存对齐

5. 修改默认对齐数

6. 结构体传参

三、位段 

四、枚举

五、联合(共同体)


 

一、数组

第一种是最常见的数组,数组也是自定义类型,前面文章有介绍过。这里就只详细总结后面的四种自定义类型。

二、结构体

结构体里面存的可以是不同类型的成员变量。

1. 结构的声明

结构体有两种声明方式:一是普通声明;二是特殊声明(也叫不完全声明)。

1. 结构体声明
1.1 普通声明
struct tag   结构体标签
{
	member_list;   存放成员列表
}variable_list;   存放变量列表
1.2 特殊声明/不完全声明(输出即是匿名结构体类型)
struct
{
	int a;
	char b;
}x;

普通声明是中规中矩地声明,也是用得比较多的一种;而特殊声明,也就是匿名结构体类型是会把struct后面的结构体标签给省略掉,这也意味着匿名结构体类型声明后就只能使用一次,就算是两个一模一样的结构体类型,其中一个存放指针变量并指向另一个结构体变量,再将这个指针存放某个东西,也会造成非法访问的,所以说匿名结构体类型在实际编写代码中会用的比较少。

2. 结构的自引用

错误示范1:

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

写出这样的代码是看着好像没问题,跟递归有点像,但是这样写是错的,不能在结构体中直接把其中一个成员写成与本结构体相同的类型,这样只会造成死循环使程序崩溃,正确的修改如下:

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

应该将结构体struct Node类型改成struct Node*类型,这就类似于数据结构中的链表,都是通过指针指向的位置来进行寻找的。

那如果是用typedef来定义结构体类型进而来实现自引用应该怎么办呢?

错误示范二:

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

在现实中会有很多初学者的代码是这样子的,其实这样的代码是不正确的。虽然注意到了应该用指针来进行访问,但因为是用typedef来定义结构体的,而该代码是在整个结构体的结尾才声明成Node的,这样的话中间部分(也就是结构体内部)的Node是未定义的。正确的修改应该如下:

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

把整个结构体类型定义成struct Node,结构体内部就可以使用这个结构体类型指针来调用,最后再声明成Node(也就是将结构体类型变为Node,以方便下面使用时的调用等)。

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

其实这方面的知识之前的文章有提到过,比较简单,这里再做一下的整理:

结构体定义情况如下:

struct Point
{
	int x;
	int y;
}p1;

struct Point p2;

一是声明类型的同时定义变量,如p1;二是在外面定义结构体变量,如p2。这两种定义的都是全局变量,而如果是在一个函数中定义的结构体变量,那么这个结构体变量就是局部的。

结构体初始化最常见的有两种:

定义结构体变量的同时初始化,如:

struct Point
{
	int x;
	int y;
};

struct Point p = { 1,2 };

也有一种是结构体嵌套初始化,就像下面的n1和n2:

struct Point
{
	int x;
	int y;
};

struct Point p = { 1,2 };

struct Node
{
	int data;
	struct Point pp;
	struct Node* next;
}n1 = { 10,{4,5},NULL };

struct Node n2 = { 20,{7,8},NULL };

4. 结构体内存对齐

首先我们要清楚的一点是:在了解完结构体内存对齐后的最终目标是能够计算结构体的大小。


接下来就是结构体对齐规则:
1. 结构体的第一个成员,存放在结构体变量开始位置的0偏移处
2. 从第二个成员开始,都要对齐到对齐数的整数倍的地址处
        对齐数:成员自身大小和默认对齐数相比,取较小值(VS环境的一个默认对齐数是8;Linux环境没有默认对齐数->对齐数就是成员自身大小)
3. 结构体的总大小,必须是最大对齐数的整数倍(最大对齐数是指所有成员的对齐数中最大的那个)
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍


那么这时候很多人就有一个疑问了:为什么会存在内存对齐呢?

1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则会跑出硬件异常。

2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,CPU处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总的来说:结构体的内存对齐是拿空间来换取时间的做法。


那么,这种拿空间来换取时间的方法随着运行时间的缩短,有时候务必会浪费很大的一块空间。所以,这时候就引出了一个设计结构体既满足对齐又节省空间的方法,那就是让占用内存小的成员尽量集中在一起。巧妙地利用结构体对齐规则,在内存中未存放内容的位置塞进一些占用内存小的成员,大大节省了空间。就比如下面的例子:

struct s1
{
	char c1;
	int i;
	char c2;
};

struct s2
{
	char c1;
	char c2;
	int i;
};

这两个结构体s1和s2类型成员是一样的,但是通过对齐规则来计算,就会很快地发现结构体s1的大小是12字节,而结构体s2的大小是8字节,又再次证明了让占用内存小的成员尽量集中在一起可以节省空间。

5. 修改默认对齐数

对于修改默认对齐数,可以使用#pragma预处理指令来实现。

例如:

1. 

#pragma pack(6)   //设置默认对齐数为6

2. 

#pragma pack()   //取消默认对齐数,还原为默认

6. 结构体传参

先说结论:结构体传参的时候,要传结构体的地址。

那么为什么呢?接下来通过一个简单的代码来解释。

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* s)
{
	printf("%d\n", s->num);
}

int main()
{
	print1(s);   //传结构体
	print2(&s);  //传地址
}

对于print1来说,因为函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。那么,从长远来看,如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。

而对于print2来说,因为它传递的是地址,就没有这个问题。

所以总的来说,结构体传参首选还是传地址。

三、位段 

什么是位段?

其实位段说到底就是结构体的一种特殊实现,位段的声明和结构体是类似的,但有两个不同点:一是位段的成员必须是int、unsigned int、char类型;二是位段的成员名后边有一个冒号和一个数字。


知道了什么是位段, 位段又是如何进行内存分配的呢?又有哪些值得注意的点呢?

1. 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的(易遗忘!!!)

2. 位段的成员应是整型家族(int, unsigned int, signed int, char)

3. 位段涉及很多不确定因素,位段与结构体的最大区别是:位段节省了内存空间,但不跨平台,因此注重可移植的程序应该避免使用位段


关于位段跨平台问题应该注意的一些点:

1. int位段被当成有符号数还是无符号数是不确定的

2. 位段中最大位的数目不能确定(16位机器最大是16,32位机器最大是32,如果冒号后面跟的数字是25,在32位机器跨到16位机器上的时候是会出问题的)

3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义(在VS环境下是从右向左分配的)

4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不能确定的(在VS环境下是舍弃的)

总结:位段相较结构体来说,虽然也是可以达到同样的效果,也比结构体更能节省空间,但是有跨平台问题存在。


对于位段来说,有跨平台问题就真的一无是处吗?其实不是的,下面就列举一个用位段实现的例子(数据包):

数据包在两个用户之间传递数据的时候,是会将4位版本号、8位服务类型、8位协议这一系列的东西附加在要发送的数据上面,形成数据包并一起传输过去的。那么就可以试想,一些只有4个比特位的如果用char类型存储起来或者一些是16个比特位的用int类型存储起来的时候,就会照成大量的空间浪费,那么传输起来的速度将会比较慢,所以这时候就会用位段来对这些东西进行存储,节省空间,就可以提高传输效率等。

四、枚举

enum Sex
{
	MALE,
	FEMALE,
	SECRET
};

enum Color
{
	RED,
	GREEN,
	BLUE
};

例如上面的两个例子,定义的Sex和Color都是属于枚举类型。 { }中的内容是枚举类型的可能取值,也称为枚举常量。

其中,这些枚举常量都是有值的,默认是从0开始,一次递增1,当然在定义的时候也可以赋初值,就比如下面的例子:

enum Color
{
	RED=1,
	GREEN=2,
	BLUE=4
};

还有三点值得注意的是:

1. 枚举类型中存的是可能取值,一次调用时只能够取出其中的一个,所以计算该枚举类型的大小的时候会发现它的大小只是一个int类型的大小

2. 枚举类型和结构体类型的定义形式是相似的,都是可以用typedef来进行定义并声明

3. 给枚举变量赋值的时候,只能拿枚举常量来赋给它;如果是直接用一个整数来对枚举变量来进行赋值,则会警告说出现类型差异,就像下面这个例子:

#include <stdio.h>
typedef enum Color
{
	RED=1,
	GREEN=2,
	BLUE=4
}Color;

int main()
{
	Color clr = GREEN;   //正确
	//clr = 5;           //错误,出现类型差异
	printf("%d", clr);
	return 0;
}

那么,使用枚举类型又有什么优势呢?不可以直接全部都用#define来定义全局变量吗?

这里就总结了几个枚举的优点:

1. 增加代码的可读性和可维护性

2. 和#define定义的标识符比较枚举有类型检查,更加严谨

3. 防止了命名污染(封装)

4. 便于调试

5. 使用方便,一次可以定义多个常量

五、联合(共同体)

联合也是一种特殊的自定义类型,这种类型也包含一系列的成员,最明显的特征是这些成员共用同一块空间(所以联合体也叫共用体)。

其次,联合体大小也有自己的计算规则:

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

2. 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍


拓展:判断当前机器的大小端存储

其实这道题在之前已经有做过了,但是之前的方法是将整型地址强制类型转换为字符地址,再访问该地址的内容判断是否与原来的数相等,若相等,则是小端字节序存储;若不相等,则是大端字节序存储。(如果还不了解大小端是什么可以看看之前的文章)

//判断大小端
#include <stdio.h>

//方法一
int cheak_sys()
{
	int a = 1;
	if (*(char*)&a == 1)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

//方法二
int cheak_sys()
{
	int a = 1;
	return *(char*)&a;
}

int main()
{
	int ret = cheak_sys();
	if (ret == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

之后也知道了有一个库函数:memcmp这个库函数是对内存进行比较的,也是同样可以实现大小端判断的。

但是今天介绍一种新的方法,这种方法比较巧妙,是运用到了联合体的知识:

#include <stdio.h>

int cheak_sys()
{
	union Un
	{
		char c;
		int i;
	}u;
	u.i = 1;
	return u.c;
}

int main()
{
	int ret = cheak_sys();
	if (ret == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

这种联合体的方法巧妙之处就在于联合的成员c和i是共用一块内存空间的,我们先让内存中存的是i,由于i是int类型,所以会开辟4个字节,将它存入一个值,但是我们最后只是返回c(也就是char类型),其只在同一块内存中占用一个字节的内存(开始的第一个字节),就可以判断是大端还是小端了。

  • 6
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蔡欣致

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值