2024年C语言进阶:自定义类型_自定义函数类型

本文探讨了C语言中内存对齐、位段、结构体和联合体的使用,强调了它们在提升性能和节省空间方面的策略,以及在不同平台间的移植挑战。特别是位段的内存分配和跨平台问题,以及联合体的特性,如共享内存空间和判断机器存储方式。
摘要由CSDN通过智能技术生成
  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个字节。

//星期
enum Day {
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};
//性别
enum Sex {
	FAMALE,
	MALE,
	SECRET
};
//颜色
enum Color {
	RED,
	GREEN,
	BLUE
};

上述定义的enum Day,enum Sex,enum Color都是枚举类型。{}内是枚举类型的可能取值,即枚举常量。

枚举常量取值默认从0开始,依次递增。也可进行(完全或不完全)初始化对其赋初值,所初始化常量之前的常量取值不受影响,之后的常量仍然依次递增。

当然常量只能进行初始化,而不能进行赋值操作。

//1.
enum Color c = GREEN;
//2.
enum Color c = 1;

上述操作为创建枚举类型的变量,赋值为GREEN

C语言对语法的检测没有那么严格。所以1和2都行。在C++中认为1是字面常量而GREEN为枚举常量。二是不相等的,所以不能赋值。

枚举的优点
  1. 提高代码可读性和可维护性

#define定义的常量不如枚举常量有意义,且枚举常量是具有类型的更严谨。

  1. 防止命名污染

#define定义的常量属于全局常量,易冲突。

  1. 便于调试

#define定义的常量在预编译期间就已经被替换,该常量已不复存在。而枚举类型一直存在有值有类型便于调试。

  1. 使用方便

一次可定义多个常量,且便于管理。

枚举的使用
/\*
\* 计算器
\* 使用枚举常量
\* \*/
enum Option {
	EXIT,//0
	ADD,//1
	SUB,//2
	MUL,//3
	DIV,//4
};

int Add(int x, int y) {
	return x + y;
}
int Sub(int x, int y) {
	return x - y;
}
int Mul(int x, int y) {
	return x \* y;
}
int Div(int x, int y) {
	return x / y;
}
void Calc(int (\*pf)(int, int)) {
	int a = 0;
	int b = 0;
	printf("请输入操作数:>");
	scanf("%d %d", &a, &b);
	printf("%d\n", pf(a, b));
}

void menu() {
	printf("\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\n");
	printf("\*\*\*\* 1.ADD 2.SUB \*\*\*\*\n");
	printf("\*\*\*\* 3.MUL \*\*\* 4.DIV \*\*\*\*\n");
	printf("\*\*\*\*\*\*\* 0.exit \*\*\*\*\*\*\*\*\n");
	printf("\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\n");
}
int main()
{
	int input = 0;

	do {
		menu();
		printf("请选择\n");
		scanf("%d", &input);
		switch (input) {
		case ADD:
			Calc(Add);
			break;
		case SUB:
			Calc(Sub);
			break;
		case MUL:
			Calc(Mul);
			break;
		case DIV:
			Calc(Div);
			break;
		case EXIT:
			break;
		default :
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}

联合体

联合是一种特殊的自定义类型,同样包含一系列成员,特殊在于这些成员共用同一块空间。所以联合体也叫共用体。

联合的定义
union Un {
	char c;//1
	int i;//4
};
int main()
{
	union Un u = { 0 };
	printf("%d\n", sizeof(u));
	return 0;
}

算出该联合体变量大小为4个字节,可一个整型和字符型变量最少也要5个字节,为什么会这样呢?

联合的特点
printf("%p\n", &u);//00EFF934
printf("%p\n", &u.c);//00EFF934
printf("%p\n", &u.i);//00EFF934

从上述代码可以看出c,i共用4个字节。

  • 改变i就会改变c,改变c就会改变i。故使用时仅可以使用1个成员,另一个成员也会被修改。
  • 联合的成员共用一块空间,故联合体变量的大小是至少是最大成员的大小。
Example

利用联合体判断当前机器的大小端存储。

int check\_sys() {
	union U {
		char c;
		int i;
	}u;
	u.i = 1;
	return u.c;
}
int main()
{
	if (check\_sys() == 1) {
		printf("小端存储\n");
	}
	else {
		printf("大端存储\n");
	}
	return 0;
}

联合大小的计算

而联合体也存在内存对齐。这个内存对齐相对结构体来说就简单一些了。

//1.
union Un1 {
	char c[5];
	int i;
};
//2.
union Un2 {
	short c[7];
	int i;
};
int main() {
	printf("%d\n", sizeof(union Un1));
	printf("%d\n", sizeof(union Un2));
	return 0;
}

  • 联合体变量大小至少是最大成员的大小。
  • 当最大成员大小不够最大对齐数的整数倍时,对齐到最大对齐数的整数倍处。

因为联合体所有成员共用一块空间,故算出最大成员大小后,只在最后需要再浪费几个字节的空间以对齐到最大对齐数的整数倍。

原大小应为5个字节,最大对齐数应为4,则要对齐到8个字节。

最后

不知道你们用的什么环境,我一般都是用的Python3.6环境和pycharm解释器,没有软件,或者没有资料,没人解答问题,都可以免费领取(包括今天的代码),过几天我还会做个视频教程出来,有需要也可以领取~

给大家准备的学习资料包括但不限于:

Python 环境、pycharm编辑器/永久激活/翻译插件

python 零基础视频教程

Python 界面开发实战教程

Python 爬虫实战教程

Python 数据分析实战教程

python 游戏开发实战教程

Python 电子书100本

Python 学习路线规划

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

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

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

  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值