【C语言】自定义类型:结构体,枚举,联合


1.结构体

1.1什么是结构体

结构体是一些值的集合,这些值叫做成员变量,成员变量可以是不同类型。

1.2结构体的声明

struct stu//标签/类型名
{
	char name[20];//成员变量
	int age;//成员变量
};//分号不能丢

结构体的特殊声明

struct     //没有标签
{
	char name[20];
	int age;
}s1;//s1是在声明时定义的全局变量
  1. 以上的这种结构体声明叫做匿名结构体类型,即没有名字。
  2. 这种类型只能在创建时直接创建变量。在这里插入图片描述
  3. 既然没有了名字,那下面的代码合法吗?
struct
{
	int a;
	char b;
	float c;
}x;
struct
{
	int a;
	char b;
	float c;
}a[20], * p;
int main()
{
	p = &x;
	return 0;
}

错误的。p指向的结构体类型已经和x的结构体类型不同。在这里插入图片描述
也就是说这种匿名结构体类型只能使用一次,当你再次使用时就是不同的类型了。

1.3结构体的自引用

以下两种做法方式哪种正确?

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

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

乍一看好像都没问题,但是如果要求结构体的大小,那就有漏洞。第一种方式是错误的,因为自身引用自身,无限套娃,是不能得出大小的,而第二种方式利用同类型的指针,指向下一个结构体,存放下一个结构体的地址,这样就可以把数据串联起来。

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

struct stu
{
	char name[20];
	int age;
}s1;//这是在声明的同时定义的变量(全局变量)
struct stu s2;//这是结构体的定义
struct people
{
	struct stu p;
	char sex[5];
	float score;
};
int main()
{
	struct stu s3;//这是在函数内定义的局部变量
	struct stu s4 = { "zhangsan",20 };//这是结构体的初始化
	struct stu s5 = { .age = 20,.name = "zhangsan" };//这是结构体的乱序初始化
	printf("%s %d\n", s5.name, s5.age);//这是结构体的打印
	struct people s6 = { {"lisi",19},"nan",99.5f };//结构体的嵌套初始化
	printf("%s %d %s %f\n", s6.p.name, s6.p.age, s6.sex, s6.score);
}

结果
zhangsan 20
lisi 19 nan 99.500000

注意

typedef struct stu
{
	char name[20];
	int age;
}Stu;//Stu不是变量,而是重命名的类型
int main()
{
	Stu s = { "zhangsan",20 };
	return 0;
}

1.5结构体的内存对齐

我们先来猜测下面两个结构体的大小

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));
}

结果
12
8

疑惑
为什么会出现两个不同的结果?难道不是两个char加上一个int的大小,一共6个字节吗?
这就关系到结构体的内存对齐问题了。不是简单安排自身大小来连续存放,是有一定的对齐规则。
对齐规则

  1. 结构体的第一个成员对齐到结构体在内存中存放位置的0偏移处。以struct S1 s为例。
    在这里插入图片描述

  2. 从第二个成员开始,每个成员都要对齐到一个对齐数的整数倍数。对齐数:结构体成员自身大小和默认对齐数的较小值。VS的默认对齐数是8,Linux gcc没有对齐数,对齐数就是结构体成员自身大小。以struct S1 s为例,i的大小是4,默认对齐数是8,对齐数就是4,要对齐到4的整数倍,c2的大小是1,默认对齐数是8,对齐数就是1,要对齐到1的整数倍。
    在这里插入图片描述

  3. 结构体大小必须是所有成员的对齐数中最大对齐数的整数倍。以struct S1 s为例,当前在计算过程中,结构体的大小是9个字节,而所有成员中最大对齐数是4,9不是4的倍数,所以继续占用空间。

在这里插入图片描述
我们可以利用offsetof函数来验证下这种对齐规则。offsetof返回偏移量,第一个参数是类型名,第二个参数是成员名,头文件是stddef.h。

struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	struct S1 s;
	printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));
	printf("%d\n",offsetof(struct S1,c1));
	printf("%d\n",offsetof(struct S1 ,i));
	printf("%d\n",offsetof(struct S1, c2));
}

结果
12
8
0
4
8

  1. 如果结构体中,嵌套了结构体,要将嵌套的结构体对齐到自己的成员中最大对齐数的整数倍处。结构体的总大小必须是最大对齐数的整数倍,这里的最大对齐数是包含嵌套结构体成员中的对齐数、的所有对齐数的最大值。
//练习
struct S3
{
	char c1;
	int i;
	char c2;
}s3;
struct S4
{
	char c;
	struct S3 s3;
	double d;
}s4;

s3的最大对齐数是4,要对齐到4的整数倍。double的对齐数是8,要对齐到8的整数倍处。在这里插入图片描述
练习

struct S3
{
double d;
char c;
int i;
};
struct S2
{
char c1;
char c2;
int i;
};
  1. 通过以上练习的计算,在设计结构体的时候,如果我们既要满足对齐,又要节省空间让占用空间小的成员尽量集中在一起。
  2. 如果结构体成员含有数组,如何对齐存放?把数组看成相同类型的不同变量,例如char a[3]看出char a1,char a2,char a3。

为什么会存在结构体对齐
来自网上参考资料

  1. 平台原因(移植原因):
    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
    在这里插入图片描述

1.6修改默认对齐数

#pragma pack(1)//设置默认对齐数为1
struct S
{
	char c1;
	int i;
	char c2;
};
#pragma pack(1)//设置默认对齐数为1
int main()
{
	struct S s;
	printf("%d", sizeof(s));
	return 0;
}

结果
6

当结构在对齐方式不合适的时候,我们可以修改默认对齐数。

1.7结构体传参

函数传参包括传值还有传地址,对于结构体哪种方式更好?先看例子

struct stu
{
	char name[20];
	int age;
};
void print1(struct stu s)
{
	printf("%s ", s.name);
	printf("%d\n", s.age);
}
void print2(struct stu* s)
{
	printf("%s ", s->name );
	printf("%d\n", s->age );
}
int main()
{
	struct stu s = { "zhangsan",20 };
	print1(s);
	print2(&s);
	return 0;
}

函数传参时,都需要在栈区上为形参开辟空间,如果形参太大,需要开辟的空间就越大,函数在执行过程中需要花费的时间和空间也就越大。将结构体本身作为参数传过去,可能需要开辟大片空间,若作为指针传过去则开辟的空间就小的多。
结论
所以结构体传参的时候,要传结构体的地址。


2.位段

2.1什么是位段

位段是一种特殊的结构体,但与结构体有两个不同的地方:一是位段的成员必须是 int、unsigned int 、signed int或者char;二是位段的成员名后边有一个冒号和一个数字。

struct S//S就是一个位段
{
	int a : 2;
	int b : 4;
	int c : 10;
	int d : 20;
};

那么冒号和后面的数字有什么含义?位段的大小是多少?位段会不会对齐,还是有其他存储方式?不急,慢慢来。
(1)冒号和后面的数字表示,这个成员变量占多少比特位,例如a占两个比特位,b占4个比特位,c占10个比特位。并且这个数字不能超过这个成员本身大小,char类型的不能超过8,int类型的不能超过32。
(2)sizeof(struct S)的结果是8,为什么是8?这就涉及到位段的内存分配。

2.2位段的内存分配

例子

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

空间是如何分配的?

  1. 结构体的第一个成员a是char类型,开辟一个8比特的空间。将10赋给a,首先将10转换成二进制,由于a占3个比特位,所以对10的二进制进行截断,将后3位交给a,放在空间的右边。在这里插入图片描述
  2. b占剩余空间的4个比特位,将12的二进制进行截断,按照上面的方法放入这4个比特位中。在这里插入图片描述
  3. c占5个比特位,开辟的空间只剩1个比特位,所以重新向内存申请一个字节的空间。旧的空间剩下的1个比特位就丢掉不用,在新空间占5个比特位。剩下的步骤就不再赘述。在这里插入图片描述
    结果大小是3个字节,通过这个例子我们也就能明白位段的存储方式。而且相比于结构体位段好像更加节省空间,但是位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

2.3位段的跨平台问题

  1. int 位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。比如在一些机器,int的大小是32比特,在另一些机器上可能就是16比特,无意间可能就超过了其本身大小。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。我所举的例子是基于VS这个编译器的,其成员在内存中的分配是从右向左分配的。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。在VS编译器中,剩余的空间不足以容纳接下来的位段时会舍弃。

3.枚举

3.1什么是枚举

枚举顾名思义就是一一列举,把可能的取值一一列举。比如课目、亲人、月份都可以一一列举。

3.2枚举的声明

enum Sex//枚举类型
{
	MALE,//枚举常量
	FEMALE,//枚举常量
	SECRET,//枚举常量
};//这些枚举常量是枚举的可能取值
int main()
{
	enum Sex s = MALE;//s是基于枚举类型创建的变量,其可能取值只能是上面3个
	return 0;
}

这些可能取值从上到下依次递增,未对其初始化时从上到下,从0开始,依次加1。在这里插入图片描述

3.3枚举的优点

  1. 增加代码的可读性和可维护性。利用枚举将一些值赋予意义,像性别,不直接用数字表示,而用其英文表示,增加其可读性;当这些枚举常量需要修改时,只需在其声明处修改即可,不用去刻意寻找,增加可维护性。
  2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
  3. 防止了命名污染(封装)。枚举把这些常量集中起来。
  4. 便于调试。#define定义的常量不能调试,因为调试的时候常量已被调换。
  5. 使用方便,一次可以定义多个常量。

3.4枚举的大小

在这里插入图片描述
为什么是4?一个枚举类型的变量只可能是其中一个常量,这些常量是整形,所以其大小是一个整形(4)。

4.联合体(共用体)

4.1什么是联合体

联合体也是一些值的集合,这些值也是其成员,但特点是这些成员共用一块内存,所以也叫共用体。

4.2联合体的定义

union UN//联合体的声明
{
	char c;
	int i;
};
int main()
{
	union UN u;//联合体的定义
	printf("%d",sizeof(u));
	return 0;
}

结果
4

疑惑
为什么是4?
我们可以查看联合体成员变量的地址在这里插入图片描述
可以发现其地址相同,这也验证了联合体成员共用一块内存,说明联合体的大小至少是最大成员的大小,这样才能容纳其他成员。这意味着两个变量不能同时使用,不让会发生冲突。这些变量不需要同时存在,在一定程度上也节省了空间。
在这里插入图片描述

练习
数据的存储这节中,有一道笔试题 :判断当前计算机的大小端存储。现在介绍另一种做法。

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

在这里插入图片描述

4.3计算联合体的大小

  1. 联合的大小至少是最大成员的大小。
  2. 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
    例子
union Un1
{
	char c[5];//最大成员大小是5
	int i;//最大对齐数是4
};
union Un2
{
	short c[7];//最大成员的大小是14
	int i;//最大对齐数是4
};
printf("%d",sizeof(union Un1));//8
printf("%d",sizeof(union Un2));//16

猜你喜欢

转载自blog.csdn.net/Zhuang_N/article/details/128841326