C语言中的自定义类型之结构体与枚举和联合详解

2022-11-13 10:11:21 枚举 自定义 详解

1.结构体

1.1结构的基础知识

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

1.2结构的声明

struct tag
{
	member-list;
}variable-list;

例如:

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

注意:声明结构体的时候,它的成员是不能被初始化的,只有当创建变量的时候才可以对结构体变量中的成员进行初始化。

1.3特殊的声明

在声明结构体的时候,可以进行不完全的声明,也就是在声明的时候省略掉结构体类型中的标签。

例如:

struct
{
	int a;
	char b;
	float c;
}a;
struct
{
	int a;
	char b;
	float c;
}*b;

这样的结构体称为匿名结构体类型。

那么下面这个代码合法吗?

b = &a;

如果在编译器中运行,会发现编译器给出一个警告

这说明编译器会将上面两个匿名的结构体类型当成完全不同的两个类型。

就算是这么写:

struct
{
	int a;
	char b;
	float c;
}a, *c;
c = &a;

也是不行的。

而如果我们给这个匿名结构体重命名,接下来使用这个新的类型名,编译器就会将它们当成是同一个类型了,如下代码:

typedef struct 
{
	int a;
	char b;
	float c;
}new;
int main()
{
	new a = { 0 };
	new* b = &a;
	return 0;
}

这时编译器就不会有警告了。

1.4结构的自引用

在结构中包含一个类型为该结构本身的成员是否可以呢?

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

如果这个代码可行的话,那么sizeof(struct Node)为多少呢?

可以发现这么写的话,结构体中有结构体,结构体中又有结构体,这样就会像没有限制条件的递归一样一直循环下去。

所以结构的自引用的正确写法应该是这样:

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

通过指针来找到下一个结构体。

数据在内存中存储的结构有顺序表和链表

顺序表:

链表:

其中链表可以通过这种自引用的方式找到下一个结构体,而最后一个结构体中的结构体指针给上一个NULL空指针就可以了。

注意:

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

这个代码这么写是不行的,因为代码是一行一行往下读的,用新的类型名给创建结构体成员是,这个新的类型名还未被定义出来。

所以应该这么写,如下代码:

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

相当于是声明了结构体后再对结构体类型重命名了。

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

struct Point
{
	int x;
	int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
struct Point p3 = { 1, 2 };//定义结构体变量p3的同时赋初值,简称初始化
struct Point
{
	int x;
	int y;
}p1 = {1, 2}; //声明类型的同时初始化
struct Point
{
	int x;
	int y;
};
struct Node
{
	int data;
	struct Point p;
}n1 = {1, {1, 2}};//结构体嵌套初始化
struct Node n2 = { 2, {3, 4} };//结构体嵌套初始化

1.6结构体内存对齐

知道结构体怎么声明,怎么定义变量之后,还要学会计算结构体的大小。

首先需要掌握结构体的对齐规则:

1.第1个成员在与结构体变量偏移量为0的地址处

2.其他成员变量要对齐到自身对齐数的整数倍的地址处

对齐数:取编译器默认的一个对齐数和该成员的大小其中的较小值,VS中默认的对齐数为8

3.结构体总大小为成员中最大对齐数的整数倍

4.如果是嵌套了结构体的情况,嵌套的结构体对齐到自己的成员中最大对齐数的整数倍的地址处,结构体的总体大小就是所有对齐数(包含嵌套结构体的对齐数)的整数倍

练习1:

struct s1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d\n", sizeof(struct s1));
	return 0;
}

可以画图来算:

其中空白的空间没有被使用,对应的颜色为对应成员变量所占的空间,都是根据对齐规则来排放的,要注意成员的变量的对齐数是要取自身大小和默认对齐数两者之一的最小值的,最终计算结构体总大小是取这些对齐数中最大的对齐数的整数倍。

利用这个方法,来做一下下面几道题吧:

练习2:

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

练习3:

struct s3
{
	double d;
	char c;
	int i;
};
int main()
{
	printf("%d\n", sizeof(struct s3));
	return 0;
}

练习4:

struct s3
{
	double d;
	char c;
	int i;
};
struct s4
{
	char c1;
	struct s3 s3;
	double d;
};
int main()
{
	printf("%d\n", sizeof(struct s4));
	return 0;
}

其中成员s3的大小为16

为什么会存在内存对齐?

1.平台原因:

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则会出现硬件异常

2.性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界上对齐

原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问只需要一次访问。

例如,在32位平台上,处理器一次访问4个字节的数据

这是没对齐的情况:

这是对齐的情况:

从对齐的位置开始一次访问就拿到了int类型的数据了

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

如果想要既要满足对齐,又要节省空间的话,可以这么做,让结构体中占用空间小的成员尽量集中在一起。

例如:

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

s1和s2的类型成员一模一样,但是s1和s2所占空间的大小不同,s2所占空间明显要小。

1.7修改默认对齐数

#pragma是个预处理指令,可以修改默认对齐数

#pragma pack(4)//修改默认对齐数为4
struct s1
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//修改默认对齐数为1
struct s2
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
	printf("%d\n", sizeof(struct s1));
	printf("%d\n", sizeof(struct s2));
	return 0;
}

由于代码是一行一行往下读的,所以后面取消设置的默认对齐数对前面已经声明的结构体类型不会产生影响,结构体的对齐数在结构体声明的时候已经定下来了。

注意:不要随便修改默认对齐数。

结构在对齐方式不合适的时候,可以自己更改默认对齐数

1.8结构体传参

如下代码:

struct s
{
	int num;
};
void print1(struct s s)
{
	printf("%d\n", s.num);
}
void print2(struct s* p)
{
	printf("%d\n", p->num);
}
int main()
{
	struct s a = { 0 };
	a.num = 123;
	print1(a);//传结构体
	print2(&a);//传结构体地址
	return 0;
}

用print2函数会好一点,函数在传参的时候,参数是需要压栈的,会有时间和空间上的开销,如果传递一个结构体的时候,结构体过大,参数压栈的系统开销就会比较大,会导致性能上的下降。

所以在进行结构体传参的时候,要传结构体的地址。

2.位段

结构体可以实现位段的能力

2.1什么是位段

位段的成员和结构体是类似的,但有两个不同:

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

2.位段的成员名后面有一个冒号和一个数字(区别于结构体)

例如:

struct s
{
	int _a : 2;
	int _b : 5;
	int _c : 8;
	int _d : 10;
};

s就是一个位段类型

又比如:

struct x
{
	short _a : 3;
	short _b : 4;
};

x也是一个位段类型,要区别于结构体

那么,位段的大小是多少呢?

2.2位段的内存分配

1.位段的成员需要时整型家族类型的,如long long、int,short、char这些

2.位段上的空间是按照需要以4个字节(int)或者1个字节(char)来开辟的

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

如下代码:

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

位段是如何在内存中开辟空间的呢?

这里画个图来讲解一下:

首先我们假设在一个字节空间里面如果有放不下的位空间就会新开辟一个字节空间来存放位空间,比如成员a和成员b一共占了7个位,那么成员c需要占5个位,这时一个字节空间放不下了,就需要新开辟一个字节空间来存放c的这5个位,而d也需要5个位,这个时候就又需要再开辟一个新的字节空间了。

接下来对数据进行存储,如果数据大于对应的位空间,就要发生截断,而如果小于,那就要高位补0直到补满位空间

这样对应的字节上的数据转化成16进制位就是44、07、09

此时可以进入调试观察内存中的值来验证假设

这三个 内存空间的值可以看到和计算出来的是一样的,那么说明在MSVC这个编译器下位段就是这么开辟空间和存储数据

那么位段究竟有什么用呢?比如当一个数据的值只需要它的范围在0~3的时候,这个时候就可以用到位段了

2.3位段的跨平台问题

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

2.位段中最大位的数目不能确定(16位机器最大16,32位机器最大32,写成27,就会在16位机器中出现问题)

3.位段中的成员在内存中从左向右分配,还是从右向左分配,C语言的标准尚未定义

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

总结:和结构相比,位段可以达到同样的效果,可以很好的节省空间,但是又跨平台的问题存在

2.4位段的应用

这个功能可以用位段来实现的

3.枚举

枚举就是列举,把可能的值一 一列举出来

比如:

一周的星期一到星期天是有限的7天,可以一 一列举

性别有男、女、保密,可以一 一列举

月份有12个月,也可以一 一列举

对于这些例子,就可以使用枚举了

3.1枚举类型的定义

如下代码:

enum Day
{
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};
enum Sex
{
	MALE,
	FEMALE,
	SECRET
};
enum Color
{
	RED,
	GREEN,
	BLUE
};

以上定义的enum Mon, enum Sex, enum Color都是枚举类型,{}中的内容是枚举类型的可能取值,也叫枚举常量。

这些可能的取值都是有初值的,默认第一个成员为0,然后递增1,也可以在定义枚举类型的时候对其中的内容进行初始化,注意,这是初始化,不是赋值。

enum Color
{
	RED = 2,
	GREEN = 5,
	BLUE = 8
};

要是其中的一个枚举常量改了初值,那么在这个常量开始往后的常量若是没有改初值的话,都是在其基础上递增1,比如:

3.2枚举的优点

可以使用#define来定义常量,为什么还要使用枚举常量呢?

枚举的优点:

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

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

3.防止命名污染

4.便于调试

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

从test.c文件到test.exe文件中间要经过预编译,编译,反汇编和链接等过程,其中在预编译阶段,用#define定有的标识符常量会被转化成字面常量,这导致了在调试过程中看到的标识符常量和程序运行中是不一样的,不方便进行调试,而且用标识符常量是不能给枚举类型定义的变量初始化的

3.3枚举的使用

enum Color
{
	RED = 2,
	GREEN = 5,
	BLUE = 8
};
enum Color clr = RED;

只能拿枚举常量给枚举变量初始化,才不会出现类型上的差异

4.联合

联合体也叫做共用体

4.1联合类型的定义

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

//联合类型的声明
uNIOn Un
{
	char c;
	int i;
}a;
union Un un;//联合变量的定义

计算一下联合变量的大小:

可以看到联合变量只占4个字节的大小,它其中的成员c和成员i是公用一个空间的

如图:

它们在内存中的第一个字节空间是共用的

4.2联合的特点

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

可以看到,成员c和成员i的起始地址是一样的

而在这个代码里,因为成员的空间是共用的,所以改变c的值的时候,i也相应的发生了变化(机器上是小端字节序存储)

利用联合的这个特点,可以用来判断联合的大小端存储模式

如下代码:

union Un
{
	char c;
	int i;
};
int main()
{
	union Un un;
	un.i = 1;
	if (1 == un.c)
	{
		printf("小端");
	}
	else
	{
		printf("大端");
	}
	return 0;
}

4.3联合大小的计算

1.联合的大小至少是最大成员的大小

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

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

Un1的最大对齐数是4,数组的对齐数根据数组元素的对齐数来定,Un1的最大成员大小是5,不是最大对齐数的整数倍,所以要进行对齐,Un1的最终大小为8

Un2的最大对齐数是4,最大成员大小是14,所以要对齐,最终大小是16

关于自定义类型的内容就到这里了,今后也会不定期更新

到此这篇关于C语言中的自定义类型之结构体与枚举和联合详解的文章就介绍到这了,更多相关C语言自定义类型内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

相关文章