2024年C语言进阶:自定义类型_自定义函数类型,Python面试中常问的MMAP到底是啥东东

在这里插入图片描述

感谢每一个认真阅读我文章的人,看着粉丝一路的上涨和关注,礼尚往来总是要有的:

① 2000多本Python电子书(主流和经典的书籍应该都有了)

② Python标准库资料(最全中文版)

③ 项目源码(四五十个有趣且经典的练手项目及源码)

④ Python基础入门、爬虫、web开发、大数据分析方面的视频(适合小白学习)

⑤ Python学习路线图(告别不入流的学习)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

如上述代码所示,使用同样的匿名结构体创建一个结构体指针ps,并存入s1的地址。

乍一看貌似可以,也就是说两个匿名结构体虽然成员相同,但编译器默认为两个结构体类型。二者属于两种类型的变量,所以会提示指针类型不兼容。

结构体自引用

结构体中该如何引用自身类型的变量呢?或者说,在定义结构体时包含自身类型的成员变量是否可行呢?

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

如果可行的话,如何计算该结构体所创建的变量的大小呢?自身嵌套一个同类型的结构体变量,如果一直嵌套下去是无法计算出大小的。所以显然是错误的。

Example 2
struct Node 
{
    //数据域
	int data;
	//指针域
    struct Node\* next;
};

类似于数据结构中链表的使用方法,使用指针存入下一个节点的地址。如图:

data被成为数据域,结构体指针next被成为指针域。这才是结构体自引用的正确方法。

注意
typedef struct {
	int data;
	Node\* next;

}Node;

typedef将结构体重命名为Node,但这在定义结构体之后才能生效,所以在结构体定义中,编译器无法识别该类型名。

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

上述方案才是正确方法,当然这里的typedef重命名对于结构体定义部分来说仍然是无意义的,所以说匿名结构体的功能非常鸡肋,不用也罢。

结构体变量的定义
struct Point {
	int x;
	int y;
}p2 = { 3,3 }, p3 = { 4,4 };
struct Point p4 = { 1,2 };

struct S {
	double d;
	struct Point p;
	char n[10];
};

int main()
{
	struct Point p1 = { 3,4 };
	struct S s = { 3.14, {1,1}, "zhangsan"};
    printf("%lf %d,%d %s\n", s.d, s.p.x, s.p.y, s.n);
	return 0;
}

  • p1,p2p3,p4分别采用不同的定义和初始化方式。

p1main函数内部定义的局部变量,p2p3是在结构体变量列表内定义的全局变量,p4是直接用结构体类型定义的全局变量。

  • 结构体s内部嵌套了一个p变量。
结构体传参

在C语言初阶时,就已经介绍过结构体传参的两种方式:传值调用和传值调用。如:

struct S {
	int data[1000];
	int num;
};
void Print1(struct S tmp) {
	for (int i = 0; i < 10; i++) {
		printf("%d ", tmp.data[i]);
	}
	printf("\n%d\n", tmp.num);
}
void Print2(struct S\* ps) {
	for (int i = 0; i < 10; i++) {
		printf("%d ", ps->data[i]);
	}
	printf("\n%d\n", ps->num);
}
int main()
{
	struct S s = { {1,2,3,4,5,6,7,8,9,10},100 };
	Print1(s);
	Print2(&s);
	return 0;
}

通过结构体内存对齐,可以看出结构体传值调用时,不止开辟了表面大小的空间。结构体过大,参数压栈的系统开销就大,导致性能下降。结构体传参还是选择传址调用。

结构体内存对齐

在掌握结构体的基本使用后,进一步深入探讨结构体的大小,所占的内存空间。那就要研究一个不可避免的问题:内存对齐。而内存对齐在结构体这里体现的尤为明显,所以也叫结构体内存对齐。

struct S1 {
	char c1;
	int a;
	char c2;
};
struct S2 {
	char c1;
	char c2;
	int a;
};
int main()
{
	struct S1 s = { 'x',100,'y' };
	printf("%d\n", sizeof(struct S1));//12
	printf("%d\n", sizeof(struct S2));//8
	return 0;
}

上述代码可以,看出结构体的成员变量位置不同,结构体大小也不同。至于原因,且看下文分解:

内存对齐的规则
  1. 结构体第一个成员永远位于结构体距起始位置偏移量为0的位置。

即首个成员一定放在为结构体所开辟的内存空间的第一个位置。

  1. 从第二个成员开始,各自放在偏移量为该变量对齐数的整数倍处。对齐数为变量自身大小和编译器默认对齐数的较小值。

Linux环境下无默认对齐数,Windows环境下对齐数为8。而一般无变量类型所占字节大于8,故对齐数一般为变量的自身大小。

  1. 结构体的总大小必须为所有成员变量的对齐数的最大值的整数倍。

笔者猜测是为了凑齐读取域宽的整数倍,不至于让之后创建的变量紧随其后而造成不必要的麻烦。

  1. 若结构体嵌套,内嵌结构体对齐到其成员最大对齐数的整数倍处,整体结构体总大小须为其成员最大对齐数的整数倍。

由第3条可推得,内嵌结构体和整个结构体同样都是结构体,都要对齐到各自成员变量对齐数的最大值的整数倍处。而一般整个结构体的最大成员变量都是内嵌结构体。

偏移量:距起始位置的字节个数,相当于下标位置。如第1个字节的偏移量为0,第2个字节的偏移量为1。

现在再来看上面的例子:

//1.
struct S1 {
	char c1;
	int a;
	char c2;
};
//2.
struct S2 {
	char c1;
	char c2;
	int a;
};

  • c1放在偏移量为0的地址处,a的对齐数为4则放在偏移量为4的地址处浪费3个字节,次之c2对齐数为1偏移量为任意位置故紧随其后。共占9个字节,但总大小需为4的倍数,所以再浪费3个字节,共12个字节。
  • c1c2对齐数都为1,随后a对齐数为4故放在偏移量为4的地址处,正好一共8个字节。

Example

求出下列结构体所创建的变量的大小。

//3.
struct S3 {
	double d;
	char c;
	int a;
};
//4.
struct S4 {
	char c1;//1
	struct S3 s;//8
	double d;//8
};

  • 嵌套结构体所占内存为16个字节,但其最大对齐数为8,所以整个结构体的成员变量对齐数的最大值即为8。
存在内存对齐的原因

这样的内存对齐的机制,显得又浪费空间又使得计算繁琐,但是它的存在是有很必要的,虽没有官方明确的解释,但是也可总结为以下两点:

  1. 移植原因

不是所有硬件平台都能任意的读取地址上的任意数据。某些平台只能在特定的地址处以特定的方式读取特定的数据。如只在地址为4的倍数处读取,且每次读取4个字节的数据。平台之间移植性差。

  1. 性能原因

数据应尽可能地存储在地址的自然边界上并对齐,以防止同一块空间的数据要作两次访问,提升读取数据的效率。

总结就是内存对齐是为了牺牲空间复杂度降低时间复杂度,以空间换取时间。当然我们要做的就是尽己所能既节省空间又节省时间

结构体中不同的变量放在不同的位置,结构体所占的大小不同。让占用空间小的成员集中在后面,可以是实现一定程度上的节约空间。

默认对齐数的修改
//设置默认对齐数
#pragma pack(n);

struct Tag {
  member_list;  
};

//恢复默认对齐数
#pragma pack();

默认对齐数是可以被修改的,使用前设置,使用后取消。当认为结构体的默认对齐数不适当时,可自行设置。同时对齐数

n

n

n 一般都设置为

2

n

2^n

2n 。

Example

实现宏计算结构体中某变量相对于首地址的偏移量。

#include <stddef.h>
struct S1 {
	char c1;
	int a;
	char c2;
};
int main()
{
	printf("%d\n", offsetof(struct S1, c1));
	printf("%d\n", offsetof(struct S1, c2));
	printf("%d\n", offsetof(struct S1, a));
	return 0;
}

位段
位段的定义

位段的声明和结构体类似,但又两点不同。

  1. 类型不同:位段的成员必须是整型变量,如char,int,unsigned int等。
  2. 写法不同:位段的成员名后使用:数字来规定分配的空间。如:
struct A {
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

计算位段A的大小得8,而4个整形变量最小占16个字节。说明位段一定程度上可以节省空间

位段中的“位”表示二进制位,而:后的数字代表系统分配给该变量的比特位数。

在描述对象时,属性变量中的所有位数不一定全部使用,使用位段可以规定系统分配给变量的空间。当然数据过大仍会溢出。

位段的内存分配
  • 系统按成员变量类型来为位段开辟空间,一次性开辟一个变量类型大小的空间。

如该成员为int型,则一次开辟4个字节,若不够则再开辟4个字节。若为char类型,则开辟1个字节。

  • 位段使用时涉及很多不确定因素,程序可移植性差,故位段是不跨平台的。

如图所示,先开辟4个字节的空间,a占用2bit,b占用5bit,c占用10bit。这4个字节还剩15个bit不够d的存放,必然要在开辟4个字节的空间。这就是算出来的8个字节。

问题是d接着一半存放在第一个字节一半存放在第二个字节,还是全部存放在新开辟的空间内?

不同的编译环境下可能会产生不同的结果,这是C标准中未规定的内容。笔者在此仅考虑Windows环境的情况,请看接下来的例子。

struct S {
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
struct S s = { 0 };
int main() {
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
	return 0;
}

对位段变量进行赋值操作,就又带来了一个问题单个字节内先使用高地址还是低地址?这也是标准未规定的。

我们先进行假设:位段中先使用高地址再使用低地址,同时剩余空间不足则将其抛弃并重新开辟。如果vs中的最后结果和预期一致,则假设正确。

我们按照假设写出位段的内存情况:

(

0110

0010

0000

0011

0000

0100

)

2

(

6

2

0

3

0

4

)

10

(\ 0110\ 0010\ 0000\ 0011\ 0000\ 0100\ )_{2} \(\quad 6 ;\quad 2 \qquad 0 \qquad 3 \qquad 0 \quad; 4\quad)_{10}

( 0110 0010 0000 0011 0000 0100 )2​(620304)10​

vs显示结果和我们的假设完全相符。故假设正确。所以可以得出结论,在vs环境下:

  1. 每次开辟空间所开辟的字节个数,由需开辟空间的成员变量的类型所决定。
  2. 内存使用时,先使用低字节再使用高字节,单个字节内从高位到低位使用。
  3. 所开辟内存空间不足时,抛弃剩余内存,重新开辟类型大小的空间。

由于这些规则C标准并未明确规定,因而这些结论因编译器而异。所以位段的平台移植性差。

位段的跨平台问题
  1. int位段的最高位是否被当做符号位不确定。
  2. 位段中成员类型的所占比特位数目不确定。

早期16位机器int占2个字节共16个比特位,而变量分配bit位数目不得多于最大值。

  1. 位段成员在内存中先使用高地址还是低地址不确定。
  2. 所开辟内存空间不足时,是否抛弃剩余内存重新开辟还是接着使用剩余内存不确定。
位段的应用

和结构相比,位段可达到同样的效果,可以节省空间,但是需使用小心且跨平台性差。而位段可以应用到网络协议中,不至于浪费大量的空间,网络传输协议中每几个比特位成一组用于传输不同的数据。

枚举类型

枚举顾名思义一一列举,有很多数据可以列举出来,如:性别,月份,颜色等。

枚举的定义
enum Tag {
    con1,
    con2,
    ...
    con3
};

  • enum是枚举关键字,Tag是枚举对象名;
  • con1,con2,...,con3是枚举常量列表。

同时枚举就相当于整形常量,故所有枚举常量都是4个字节。

做了那么多年开发,自学了很多门编程语言,我很明白学习资源对于学一门新语言的重要性,这些年也收藏了不少的Python干货,对我来说这些东西确实已经用不到了,但对于准备自学Python的人来说,或许它就是一个宝藏,可以给你省去很多的时间和精力。

别在网上瞎学了,我最近也做了一些资源的更新,只要你是我的粉丝,这期福利你都可拿走。

我先来介绍一下这些东西怎么用,文末抱走。


(1)Python所有方向的学习路线(新版)

这是我花了几天的时间去把Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。

最近我才对这些路线做了一下新的更新,知识体系更全面了。

在这里插入图片描述

(2)Python学习视频

包含了Python入门、爬虫、数据分析和web开发的学习视频,总共100多个,虽然没有那么全面,但是对于入门来说是没问题的,学完这些之后,你可以按照我上面的学习路线去网上找其他的知识资源进行进阶。

在这里插入图片描述

(3)100多个练手项目

我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了,只是里面的项目比较多,水平也是参差不齐,大家可以挑自己能做的项目去练练。

在这里插入图片描述

(4)200多本电子书

这些年我也收藏了很多电子书,大概200多本,有时候带实体书不方便的话,我就会去打开电子书看看,书籍可不一定比视频教程差,尤其是权威的技术书籍。

基本上主流的和经典的都有,这里我就不放图了,版权问题,个人看看是没有问题的。

(5)Python知识点汇总

知识点汇总有点像学习路线,但与学习路线不同的点就在于,知识点汇总更为细致,里面包含了对具体知识点的简单说明,而我们的学习路线则更为抽象和简单,只是为了方便大家只是某个领域你应该学习哪些技术栈。

在这里插入图片描述

(6)其他资料

还有其他的一些东西,比如说我自己出的Python入门图文类教程,没有电脑的时候用手机也可以学习知识,学会了理论之后再去敲代码实践验证,还有Python中文版的库资料、MySQL和HTML标签大全等等,这些都是可以送给粉丝们的东西。

在这里插入图片描述

这些都不是什么非常值钱的东西,但对于没有资源或者资源不是很好的学习者来说确实很不错,你要是用得到的话都可以直接抱走,关注过我的人都知道,这些都是可以拿到的。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值