速通C语言第十站 自定义类型

系列文章目录

 速通C语言系列

 速通C语言第一站 一篇博客带你初识C语言        http://t.csdn.cn/N57xl

 速通C语言第二站 一篇博客带你搞定分支循环   http://t.csdn.cn/Uwn7W

 速通C语言第三站  一篇博客带你搞定函数        http://t.csdn.cn/bfrUM

速通C语言第四站  一篇博客带你学会数组          http://t.csdn.cn/Ol3lz

 速通C语言第五站 一篇博客带你详解操作符      http://t.csdn.cn/OOUBr

速通C语言第六站 一篇博客带你掌握指针初阶   http://t.csdn.cn/7ykR0

速通C语言第七站 一篇博客带你掌握数据的存储 http://t.csdn.cn/qkerU

 速通C语言第八站 一篇博客带你掌握指针进阶    http://t.csdn.cn/m95FK

速通C语言第八.五站 指针进阶题目练习               http://t.csdn.cn/wWC2x

速通C语言第九站 字符相关函数及内存函数       http://t.csdn.cn/7rCu0                                           
 

感谢佬们支持!


文章目录

  • 系列文章目录
  • 前言
  • 一、结构体
  •          1 结构体的特殊声明
  •          2 结构体的自引用
  •             补充:数据结构
  •          3 结构体的内存对齐
  •          4 offsetof
  •          5 位段
  •            位段的内存分配
  •            位段的意义
  • 二、枚举
  •        1 结构
  •        2 枚举的优点
  • 三、联合体(共用体)
  •        1 初始化
  •         例:判断大小端
  •        2 联合体大小的计算
  • 总结

前言

  上篇博客我们学习了字符相关及内存函数,这篇博客给大家带来自定义类型,其中的struct对之后数据结构和C++的学习会起到接轨的作用,而剩下两个大家只要了解即可


一、结构体

1 结构体的特殊声明

struct tag
{
	member_list;//成员列表

}variable_list;//变量列表

我们在声明结构体时,可以进行不完全的声明,在原来的基础上,我们可以省略tag,进行所谓匿名结构体的声明。

例:

struct
{
	char c;
	int i;
	char ch;
	double d;
}s;

但是如果你用匿名结构体创建了一个指针ps,再用ps存s的地址

struct
{
	char c;
	int i;
	char ch;
	double d;
}*ps;

int main()
{
  ps=&s;
}

这个时候编译器会报一个警告

因为虽然两个结构体成员相同,编译器仍然认为这是两个不同的类型,所以认为 ps=&s 不合理。


2 结构体的自引用

结构体的成员中可以包含结构体

例:

struct A
{
	int i;
	char c;
};

struct B
{
	char c;
	struct A sa;//结构体
	double d;
};

在上面这个例子中,结构体B的一个成员就是由结构体A创建的变量。


这个时候我们再思考一下,一个结构体的成员能否是自己呢?

例:

struct N
{
	int d;
	struct N n;
};

int main()
{
	struct N sn;
}

我们不妨从结构体的大小这个角度来看

虽然我们还没学如何计算,但是我们可以大致猜测一下 

N中有两个成员,一个是int类型,他的大小是4,而另一个成员是结构体N,N的两个成员一个是int,一个又是N……

显然,这波是个套娃,我们永远也无法算出N的大小,所以可以得出

结构体的成员不能是该结构体


啊但是!

结构体的成员能否是该结构体的指针呢?

我们依然从结构体的大小这个角度来看

由于任何指针的大小均为4个字节,所以我们可以算出结构体的大小

所以结构体的成员可以是该结构体的指针

那么这个时候就要引申出一个很重要的东西了


补充:数据结构

我们在内存中存数据时,通常是用数组来存储,即连续存储,下面我们引出第二种方式 - > 链式存储

相比于顺序存储,链式存储每个节点的位置是随机的,每个节点由两部分组成,一部分叫数据域,用于存放数据,另一部分叫指针域,用于存放下一个节点的地址(指针)。

以结构体来看是这样的

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

3 结构体的内存对齐

现在我们来着手计算一个结构体的大小,先不急着计算,我们先来看几个例子

例;

struct s
	{
		int i;
		char c;
	};
	struct s s1 = { 0 };
	printf("%d\n", sizeof(s1));

i是int类型,4个字节;c是char类型,1个字节,哪s1的大小是否是5给个字节呢?

运行一波


再给一个例子

struct s2
	{
		char c1;
		int i;
		char c2;
	};
	struct s2 s3 = { 0 };
	printf("%d\n", sizeof(s3));

运行以后

 所以可以得知,结构体的大小计算不是简单的加,这波其实涉及到了一个规则

内存对齐规则

1 第一个成员在与结构体变量偏移位置为0的位置存放

解释一波偏移量

结构体变量开辟后下面的第一个空间偏移量为0,再下面的一个偏移量为1,以此类推,如图

由于 c1为char类型,一个字节,所以我们直接把c1放到偏移量为0的位置


2 其他成员要对齐到某个数字(对齐数)的整数倍地址处。

对齐数=编译器默认的一个对齐数与该成员大小的较小值

(VS默认的对齐数为8,而Linux则没有这个概念,因为它直接就把自身的大小当对齐数了)

int为4,而默认对齐数为8,显然其较小者为4,所以下个成员应从4的整数倍地址开始存

 再存c2,对齐数为1,所以直接往下存就行


3 结构体的总大小为最大对齐数(每个变量的对齐数的最大值)的整数倍

c1为1,i为4,c2为1,所以其最大值为4,所以最后的大小应为4的整数倍,所以我们再浪费4个字节到12

 所以最终这个结构体变量的大小为12.


我们交换一下c2和i的顺序

struct s2
	{
		char c1;
		char c2;
		int i;
	};
	struct s2 s3 = { 0 };
	printf("%d\n", sizeof(s3));

运行一波

所以在设计结构体时,我们既要节省空间,又要满足对齐,如何做到?

让占用空间小的成员尽量集中在一起


4 如果有嵌套结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有的对齐数(含嵌套结构体的对齐数)的最大值的整数倍

例:

struct s4
	{
		double d;
		char c;
		int i;
	};

	struct s5
	{
		char c1;
		struct s4 s;
		double d;
	};
	struct s5 s3 = { 0 };
	printf("%d\n", sizeof(s3));

运行一波

 由于结构体s4中对齐数为8,所以s4存储时应对齐到8的整数倍上,由计算的出s4占16字节

接下来正好是偏移量24的位置,正好存放d,d占8个字节,8+24=32,而32正好是8的整数倍,所以最终的大小就为32

如图:


5 如果含数组,在求对齐数时以类型为标准而非元素个数

例:

char c[5]; //对齐数为1

补充:我们可以修改默认对齐数

例:比如将默认对齐数改为2

#pragma pack(2)

我们再用上面的例子测试一下

struct s2
	{
		char c1;
		int i;
		char c2;
	};
	struct s2 s3 = { 0 };
	printf("%d\n", sizeof(s3));

大家可以自己画画图,就能得出结果

 比原结果少了4个字节。

所以结构体在对齐不合适时,我们可以自己更改默认对齐数。


4 offsetof

offsetof是个宏,可以用来计算某个成员相对于首地址的偏移量,第一个参数是结构体类型,第二个是成员名。需引头文件<stddef.h>

例:我们还是用刚刚的例子

struct s2
	{
		char c1;
		int i;
		char c2;
	};
	struct s2 s3 = { 0 };

	printf("%d\n", (int)offsetof(struct s2, c1));
	printf("%d\n", (int)offsetof(struct s2, i));
	printf("%d\n", (int)offsetof(struct s2,c2));

运行一波

等我们学到宏之后,我们将会模拟实现一波offsetof,期待的话请多多为我投票吧


5 位段

位段和结构体是类似的,只有2个不同

1 位段的成员必须是int、unsigned int、signed int或char

2 位段的成员名后有一个冒号和一个数字,数字用于表示该成员所占空间大小。

例:

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

2+5+10+30=37比特位

我们来计算一波大小

我们在给出对应的结构体再计算一波大小

struct B
	{
		int _a;
	    int _b; 
		int _c;
		int _d;
	};
	struct B s2 = { 0 };
	printf("%d\n", sizeof(s2));

 那位段具体是如何分配大小的呢?


位段的内存分配

位段上的空间是以四个字节(int、unsigned int、signed int)和一个字节(char)来开辟的

具体如何使用?

我们以char为例再给一个例子

struct s
		{
			char a : 3;
			char b : 4;
			char c : 5;
			char d : 4;
		};

由于是char,所以我们先开一个字节

从a开始,a占3个比特位,问题来了,先占左边还是先占右边?

我们假设先占右边

 

 下来是b,b占4个比特位,接着往下存

 到c的时候就不够了,所以我们再开一个字节

现在问题又来了,是浪费掉上面那个剩下的比特位直接在这个里存还是用掉

我们假设浪费掉

到d之后又不够了,所以我们再开一个字节,按上面的猜测,我们浪费掉第二个字节剩下的比特位

 我们计算一下大小

(正好是3)

 为了验证我们的猜想,我们赋一波值

struct s s1 = { 0 };
		s1.a = 10;
		s1.b = 12;
		s1.c = 3;
		s1.d = 4;

我们给a赋值为10--------》1010,由于a有3个字节,所以只能放010上去

           b赋值为12--------》1100,由于b有4个字节,所以只能放1100上去

           c赋值为3--------》  011,  由于c有5个字节,所以凑成00011上去

           d赋值为4--------》 100,由于d有4个字节,   所以凑成0100上去

按照我们刚才的猜想,我们将值写到三个字节上,并转化为16进制显示

即为  

 我们再来调试一波

 确实如此,所以我们的猜测是对的

因此我们得出结论

在VS下

1 一个字节内部中,先使用低地址(右边),从右到左使用

(注意:这个规则和大小端没有关系)

2 剩余空间如果不够,会浪费掉


位段的意义

位段可以根据需求定义变量有几个比特位,起到了节省空间的作用

但是由于int只有4个字节(32个比特位),所以你不能定义32以上的数

另外,位段涉及很多不确定因素,例如位段是不跨平台的,注重可移植的程序应避免使用位段

我们再讨论一下位段的跨平台问题

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

2 位段中最大位的数目不确定,16位机器上int最大为16比特位,而32位机器上int最大为32比特位

3 位段中的成员从左向右分配/从右向左分配不确定

4  当一个结构包含两个位段,第二个位段成员较大,无法容纳第一个位段剩余的位时,使用/浪费未定义。


二、枚举

可以一一列举的东西我们可以用枚举来实现,比如月份、性别等

1、结构

enum 类型
	{
		//枚举类型的可能取值(常量)
		member1,
        member2,
        ……
	};

其中: member1默认从0开始,依次递增

例:

星期

enum Day
	{
		Mon,
		Tue,
		Wed,
		Thu,
		Fri,
		Sat,
		Sun
	};

我们可以在初始化的时候对默认开始的常量进行修改

例:

enum colour
	{
		RED=5,
		GREEN,
		BLUE
	};
	printf("%d %d %d", RED, GREEN, BLUE);

运行一波


1 枚举的优点

  1 比 #define RED 5 这样的操作更好,有类型检查

  2 增加可读性

例:

我们之前写过一个简易的计算器

在 指针进阶 的博客中(http://t.csdn.cn/YvheL) 我们采用了函数指针数组的操作,即通过下标来访问每个函数。虽然方便,但是下标的方式可读性很低,这个时候我们就可以用一波枚举

enum option
	{
		exit, //0
		Add,  //1
		Sub,  //2
		Mul,  //3
		Div   //4
};

这样下标就被替换为了这些具体的选择,非常奈斯

3 防止命名污染

4 方便调试

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


三、联合体(共用体)

联合也是一种特殊的自定义类型,这种类型定义的变量也包括一系列的成员,其特征是

这些成员共用同一块空间,所以也叫共用体

1 初始化

  union un
	{
		char c;
		int i;
	};
	union un u = { 10 };
	printf("%d %d",u.c, u.i);

由于成员使用同一块空间,所以将10赋值给u时,u的每个成员都是10,所以打印之后


那如何进行独立初始化?

union un u;
	u.i = 10;
	u.c = 100;

因为所有成员共用一块空间,你改了一个,其他的也会相应被改。

所以独立初始化是个比较鸡肋的存在。


下面我们再来研究一下联合体时如何存储的

先来看看它的大小

	printf("%d\n", sizeof(u));

 答案是4,正好是一个int的大小

再来看看地址

printf("%p\n", &u);
	printf("%p\n", &(u.c));
	printf("%p\n", &(u.i));

均为一个地址,所以i和c确实用了相同的空间,i占了4个字节,而c占了一个字节

所以我们可以得出结论

联合体的特点:联合体的成员共用一块空间,这样一个联合体的大小至少是最大成员的大小


例:判断大小端

我们在数据存储那一节中(http://t.csdn.cn/qkerU)知道了数据在内存中的存储方式有大端和小端

之前验证大小端的方式是给一个数,将其强转为char,拿到第一个字节的值,便可验证

现在学了联合体,我们可以用联合体的性质来更轻松的验证了

union un
	{
		char c;
		int i;
	};

我们给到一个包含char和int的联合体,由于c和i占用同一块空间,所以我们看char的值便可

判断大/小端了,兄弟们,赶紧把 奈斯 打在公屏上

我们将其实现为一个函数

int check()
	{
		union un
		{
			char c;
			int i;
		}u;
		u.i = 1;
		return u.c;
	}

所以,当返回1时,是小端,返回0时,是大端


  2 联合体大小的计算

联合体作为一种特殊的结构体,是否有对齐规则呢?

有的,但是鉴于联合体的特点,对齐规则只有一条了

就是当最大成员不是最大对齐数的整数倍时,要对齐到最大对齐数的整数倍处

我们给到一个例子

union un
		{
			char a[5];
			int i;
		};
		union un u;
		printf("%d\n", sizeof(u));

显然,a数组要占5个字节,但该联合体的最大对齐数为4,所以最后要对齐到4的整数倍,也就是8


 总结

 做总结,今天这篇博客带大家学习了自定义类型,总体上来讲还是细碎的规则多一些,而且相比其他章节重要性也没那么高,大家只要了解即可。但是内存对齐规则大家要重点掌握,不仅考的多,而且在C++中仍然有用到。

水平有限,还请各位大佬指正。如果觉得对你有帮助的话,还请三连关注一波。希望大家都能拿到心仪的offer哦。

每日gitee侠:今天你交gitee了嘛

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值