自定义数据类型:结构体,枚举,联合

之前我们已经了解过结构体,这篇文章再来深入学习的一下,然后再学习其他自定义数据类型,枚举和联合

目录

1.结构体

1.1 结构体类型的声明

1.2 结构体的自引用

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

1.4 结构体内存对齐

1.5 结构体传参

1.6 结构体实现位段(位段的填充和可移植性)

2.枚举

2.1 枚举类型的定义

2.2 枚举的优点和使用

3.联合

3.1 联合类型的定义

3.2 联合的特点

3.3 联合大小的计算


1.结构体

1.1 结构体类型的声明

结构体的基础知识:

内置数据类型有char short int long long float double 等,而C语言也允许我们自己创建类型。

数组是一组相同类型元素的集合,而结构体可以是不同类型成员的集合。

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

结构的声明

struct tag//tag为结构体名称,可以自己设置
{
	member-list;//成员列表
}variable-list;//变量列表

例如声明一个学生结构体

struct Student//结构体名称是Student, 可以自己起不同的名称
{
	//成员变量
	char name[20];//姓名
	int age;//名字
	float weight;//体重
};//最后要加分号

特殊的声明:匿名结构体类型

//匿名结构体类型
struct
{
	char c;
	int a;
	double d;
}s1,s2;//只能在这里创建变量,后面不能再使用,相当于一次性

struct
{
	char c;
	int a;
	double d;
}*ps;//结构体指针

int main()
{
	ps = &s1;
	//虽然*ps的类型与s1的类型内容相同
	//但他们都是匿名结构体变量,编译器会认为它们不同
	//会有警告
	return 0;
}

警告:编译器会认为两个匿名结构体是完全不同的两个类型。

1.2 结构体的自引用

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

错误的示范
struct Node1
{
	int data;
	struct Node1 n;
};
结构体内部有自己,那它的大小是什么,自己大小+data大小,自己大小会无限嵌套

正确的自引用方法

struct Node
{
	int data;//4
	struct Node* next;//指针 4/8
};

int main()
{
	struct Node n1;
	struct Node n2;
	n1.next = &n2;//n1的下一个数据是n2
	return 0;
}

通过这个结构体指针,我们就可以找到下一个结构体单元,使数据链接起来。

typedef类型重命名

typedef struct
{
	int data;
	char c;
}S;//有typedef这里S是类型,不是结构体变量

typedef struct
{
	int data;
	Node* next;//这样是错误的,还没有定义Node 报错
}Node;

解决方法:
typedef struct Node
{
    int data;
    struct Node* next;
}Node;

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

结构体变量的定义:

有了结构体类型,我们来定义一下结构体变量

struct S
{
	int a;
	char c;
}s1;//1.声明时创建,是全局变量

struct S s2;//2.通过类型创建,全局变量

int main()
{
	struct S s3;//3.通过类型创建,是局部变量
	return 0;
}

结构体变量的初始化

#include<stdio.h>
struct S
{
	int a;
	char c;
}s1;

struct B
{
	float f;
	struct S s;
};

int main()
{
	//创建变量时初始化
	struct S s2 = { 100,'a'};//1.按顺序初始化
	struct S s3 = { .c = 'r', .a = 100 };//2.自己指定顺序初始化

	struct B sb = { 3.14f,{200,'w'} };

	printf("%f %d %c\n", sb.f, sb.s.a, sb.s.c);

	return 0;
}

1.4 结构体内存对齐

这是结构体知识中比较重要的内容

我们已经掌握了结构体的基本使用,现在我们深入讨论一个问题,如何计算结构体的大小

#include<stdio.h>
struct S1
{
	int a;
	char c;
};
struct S2
{
	char c1;
	int a;
	char c2;
};

struct S3
{
	char c1;
	int a;
	char c2;
	char c3;
};


int main()
{
	printf("%d\n", sizeof(struct S1));//8
	printf("%d\n", sizeof(struct S2));//12
	printf("%d\n", sizeof(struct S3));//12

	return 0;
}

看到这个结果,大家肯定会有疑惑,结构体的大小究竟是怎么算的。

如果计算大小,首先要知道结构体对齐规则

1.结构体的第一个成员永远都放在0偏移处

2.从第二个成员开始,以后的每个成员都要对齐到某个对齐数的整数倍数。

  这个对齐数时:成员自身大小和默认对齐数的较小值

  备注:VS环境下默认对其数是8

        gcc环境下没有默认对齐数,没有默认对齐数就是成员自身的大小

3.当成员全部存放进去后,

  结构体的总大小必须是:所有成员的对齐数中最大对齐数的整数倍 如果不够,则浪费空间对齐

4.如果嵌套了结构体,嵌套的结构体要对齐到自己成员的最大对齐数的整数倍,整个结构体的大小,必须是最大对齐数的整数倍,最大对齐数包含嵌套的结构体成员中的对齐数

我们拿这个结构体来举例计算一下

struct S3
{
	char c1;
	int a;
	char c2;
	char c3;
};

 c1是第一个成员放在0偏移处,a的大小是4个字节,与默认对齐数相比,它的对齐数是4,放在4的倍数处,也就是4偏移量处,c2和c3大小都是1,偏移量都是1的倍数,所以直接在后面存放。

这个结构体的大小就是 最大对齐数4 的 整数倍 ,12

可以再做些练习

#include<stdio.h>

struct S2
{
	char c1;
	char c2;
	int a;
};
struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;//16
	double d;
};

int main()
{
	printf("%d\n", sizeof(struct S2));//8
	printf("%d\n", sizeof(struct S3));//16
	printf("%d\n", sizeof(struct S4));//32
	return 0; 
}

为什么存在内存对齐

大部分的参考资料都是这样说的

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

总体来说︰

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

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:

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

 修改默认参数 #pragma pack 预处理指令

#include<stdio.h>
#pragma pack(1)//把默认对齐数改为1
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)
struct S
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
struct S1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d\n", sizeof(struct S));//6
	printf("%d\n", sizeof(struct S1));//12

	return 0;
}

1.5 结构体传参

结构体传参有两种方式

#include<stdio.h>
struct S
{
	int data[1000];
	int num;
};

struct S s1 = { {1,2},100 };

//结构体传参
void print1(struct S s)
{
	printf("%d\n", s.num); 
}

//结构体地址传参
void print2(struct S* s)
{ 
	printf("%d\n", s->num);
}

int main()
{
	//传结构体变量
	print1(s1);

	//传结构体指针
	print2(&s1);
}

上面的print1和print2函数哪个好些?

答案是∶首选print2函数。原因︰

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。就是再拷贝一份数据。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

结论结构体传参的时候,要传结构体的地址。
 

1.6 结构体实现位段(位段的填充和可移植性)

什么是位段

位段的声明和结构体是类似的,有两个不同

1.位段的成员是int , unsigned int ,signed int 或其他整形家组char...
2.位段的成员后边有一个冒号和一个数字

#include<stdio.h>

//位段   二进制位

struct A
{
	//_只是成员变量的名称中的下划线,没有什么特殊意义
	int _a : 2;//存放两个二进制位就能表示的数字  00 或 01 或 10 或 11
	int _b : 5;
	int _c : 10;
	int _d : 30;
};//47个bit位

//一个字节8个bit位

int main()
{
	struct A sa = { 0 };
	printf("%d\n", sizeof(sa));//8 个字节,相比16个字节已经节省空间了
	printf
	return 0;
}

上述代码中A就是一个位段类型,它的大小为sizeof(struct A)是8个字节。

为什么是8个字节,这里我们学习一下位段的内存分配

  1. 位段的成员可以是int unsigned int signed int或者是char(属于整形家族)类型
  2. 位段的空间上是按照需要以4个字节( int)或者1个字节( char )的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

 在VS平台上,段位数据的内存分配方式如下:

1.分配到的内存中的比特位是由右向左使用的,高地址到低地址
2.分配的内存剩余的比特位不够使用时,浪费掉,开辟新的空间

 我们来验证一下

#include<stdio.h>

struct S
{
	char _a : 3;
	char _b : 4;
	char _c : 5;
	char _d : 4;
};

int main()
{
	struct S s = { 0 };
	s._a = 10;//1010  截断 010
	s._b = 12;//1100
	s._c = 3;//00011
	s._d = 4;//0100
	return 0;
}

 空间开辟过程:

 调试验证:

 位段的跨平台问题

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

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

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

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

总结︰

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

2.枚举

枚举顾名思义就是一一列举。

把可能的取值一一列举。

比如我们现实生活中︰

一个的星期一到星期日是有限的7天,可以——列举。

性别有:男、女、保密,也可以——列举。

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

这里就可以使用枚举了。

2.1 枚举类型的定义

enum Sex
{
	//枚举的可能取值,默认是从0开始,递增1的
	MALE,
	FEMALE=5,
	SECRET
};

2.2 枚举的优点和使用

 枚举的优点

为什么使用枚举?

我们可以使用#define定义常量,为什么非要使用枚举﹖枚举的优点︰

1.增加代码的可读性和可维护性
⒉和#define定义的标识符比较枚举有类型检查,更加严谨。

3.防止了命名污染(封装)
4.便于调试
5.使用方便,一次可以定义多个常量

 枚举的使用:

enum Sex
{
	//枚举的可能取值,默认是从0开始,递增1的
	MALE,
	FEMALE=5,
	SECRET
};

int main()
{
	enum Sex s = MALE;//把可能的确实赋给这样的变量 
	printf("%d\n", MALE);//0
	printf("%d\n", FEMALE);//5
	printf("%d\n", SECRET);//6
	return 0;
}

 也可以给枚举变量直接赋值相应的数字,但这种方法在C++中是不行的

如:

enum Sex s = 0;//MALE

 枚举类型和变量的大小:

枚举类型成员都是int类型,大小都是4个字节

#include<stdio.h>

enum Sex
{
	MALE,
	FEMALE=5,
	SECRET
};

int main()
{
	enum Sex s = MALE;

	printf("%d\n",sizeof(enum Sex));//4
	printf("%d\n", sizeof(MALE));//4
	printf("%d\n", sizeof(s));//4
	return 0;
}

3.联合

3.1 联合类型的定义

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

#include<stdio.h>

//一个时间段只能使用一个值
union Un
{
	char c;
	int i;
};

int main()
{
	union Un u;
	printf("%d\n", sizeof(u));//4

	//下面三个都相等
	printf("%p\n", &u);
	printf("%p\n", &u.i);
	printf("%p\n", &u.c);
	return 0; 
}

3.2 联合的特点

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

 可以利用这一点判断是大端存储还是小端存储

#include<stdio.h>

union Un
{
	char c;
	int i;
};

int main()
 {
	union Un u;
	u.i = 1;
	if (u.c == 1)
	{
		printf("小端存储\n");
	}
	else
	{
		printf("大端存储\n");
	}
}

3.3 联合大小的计算

  • 联合的大小至少是最大成员的大小。
  • 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
#include<stdio.h>

union Un
{
	char arr[5];
	int n;
};

int main()
{
	printf("%d\n", sizeof(union Un));//8
	return 0;
}

这里输出结果是8,是因为在联合体中也存在对齐。数组的对齐数是按类型算,最大对齐数是4,最大对齐数的整数倍是8。

本篇结束

猜你喜欢

转载自blog.csdn.net/qq_72916130/article/details/131362055