C进阶⚡- 04自定义类型:结构体、枚举、联合

 我知道的只是  “ 肉随便加  ”和  “ 要加多少加多少  ” 这些词。    ———— 路飞

阶段2目标:

此阶段开始大量刷题,多多参加编程类竞赛,在实战中锻炼编程思维和本领,并且要在不断复习夯实初阶的基础上,刻意地进行编程思维的训练。学无止境!为了精进编程,可以去学习一切为他服务的课程!

写在前面:

在C语言中有许多自带的内置类型,如:char、 int 、 float  、double、 long int、 long   long  .....

当然,我们也可以自定义一些数据类型,就是我们今天所谈到的:结构体、枚举、联合

目录

本章重点

一、结构体

1.什么是结构体?

2.结构体的声明

3.特殊的声明形式

 4.结构的自引用

5.结构体变量的定义与初始化

 6.※结构体内存对齐  (计算结构体的大小)

7.为什么存在结构体内存对齐?

8.修改默认对齐数

百度笔试题

9.结构体传参

二、位段

1.什么是位段?

2.位段的内存分配

 3.位段的跨平台问题

4.位段的应用

三、枚举

1.什么是枚举类型

2.枚举类型的定义

3.枚举类型的优点    为什么使用枚举类型?

4.枚举的使用

四、联合(共用体)

1.联合类型的定义

2.联合体的特点

面试题

3.联合体的应用场景

4.联合体大小的计算

应用:通讯录


本章重点

结构体

  • 结构体类型的声明
  • 结构的自引用
  • 结构体变量的定义和初始化
  • 结构体内存对齐
  • 结构体传参
  • 结构体实现位段(位段的填充&可移植性)

枚举

  • 枚举类型的定义
  • 枚举的优点
  • 枚举的使用

联合

  • 联合类型的定义
  • 联合的特点
  • 联合大小的计算

一、结构体

1.什么是结构体?

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

注释:生活中往往一件东西是由多个复杂类型构成的,例如:

人👨‍💻——>名字+年龄+性别+身高+体重+身份证号码+电话......

书📕——>书名+作者+出版社+定价+书号......

2.结构体的声明

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

比较抽象,看个例子~  一本书

struct Book
{
	char name[20];
	char author[20];
	float price;
};

当然,也可以在最后加上结构体定义的变量

struct Book
{
	char name[20];
	char author[20];
	float price;
}b1, b2;

对于结构体定义变量,注意有以下几种写法,对比一下了解即可:

struct Book
{
	char name[20];
	char author[20];
	float price;
}b1, b2;//全局

struct Book b3;//全局

int main()
{
	struct Book b4;//局部

	return 0;
}

3.特殊的声明形式

在声明结构的时候,可以不完全的声明。(匿结构体)

//匿名结构体类型
struct 
{
	char name[20];
	char author[20];
	float price;
}b1, b2;

可见,在声明的时候省略掉了结构体标签(tag)也是可以的,

但是变量创建就限制了只能在结构体末尾创建,你还想在main()函数中写struct b3;是错误的。

思考一下,同样是用一个匿名结构体来声明,这样写是否可行??

p = &b1;
//匿名结构体类型
struct
{
	char name[20];
	char author[20];
	float price;
}b1, b2;


//匿名结构体类型
struct
{
	char name[20];
	char author[20];
	float price;
}* p;

int main()
{
	p = &b1;
	return 0;
}

我们看出,会出现警告,那是因为——>

警告: 编译器会把上面的两个声明当成完全不同的两个类型。 所以是非法的


 4.结构的自引用

自己能够找到自己类型的下一个节点。

拿数据结构中的链表举例:

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

对比以下写法,看是否可行?

//代码1
typedef struct
{
	int data;
	Node* next;
}Node;
//这样写代码,可行否?


//代码2
//解决方案:
typedef struct Node
{
	int data;
	struct Node* next;
}Node;

 代码1:这种写法是错误的,因为我们是先定义,后使用。而Node是在结构体后命名的,结构体中又有Node*,则是不允许的。

5.结构体变量的定义与初始化

结构体变量定义

struct Point
{
	int x;
	int y;
}p1 = { 1,1 }, p2 = {2,2};

struct Point p3 = { 3,3 };

int main()
{
	struct Point p4 = { 4,4 };

	return 0;
}

 结构体嵌套初始化

//结构体嵌套初始化
struct S
{
	double d;
	struct Point p;
	char name[20];
};
int main()
{
	struct S s = { 3.14,{5,9},"QBJ" };

	return 0;
}

 6.※结构体内存对齐  (计算结构体的大小

为啥会输出12?      8??       什么jb玩意这是??           如何计算结构体的大小??     不要着急,我们来讲一下结构体内存对齐的知识你就懂了~~

//结构体内存对齐
//12
struct S1
{
	char c1;
	int a;
	char c2;
};

//8
struct S2
{
	char c1;
	char c2;
    int a;
};
int main()
{
	struct S1 s1 = { 'x',100,'y' };
	struct S2 s2 = { 'A','Q' ,6};
	printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));

	return 0;
}

结构体对齐数的3个规则

  1. 结构体的第一个成员永远放在结构体起始位置偏移量为0的位置
  2. 结构体成员从第二个成员开始,总是放在偏移量为一个对齐数的整数倍处            对齐数 = 编译器默认的对齐数和变量自身大小的较小值                      Linux没有默认对齐数               VS 默认对齐数是8
  3. 结构体的总大小必须是各个成员的对齐数中,最大那个对齐数的整数倍
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
     

以S1为例:

 以S2为例:

 练习一个:计算结构体S3的大小

struct S3
{
	double d;
	char c;
	int i;
};

图解解释:

 结构体中嵌套结构体怎么计算?

struct S3
{
	double d;
	char c;
	int i;
};

struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

图解解释:

7.为什么存在结构体内存对齐?

 大部分的参考资料都是如是说的:

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

总体来说

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

通过对比S1、S2我们发现结构体中同样的成员变量,排列顺序不一样,空间占用也不一样,既然同样都会浪费消耗空间,那怎么设计可以让其浪费空间比较少呢?

//12
struct S1
{
    char c1;
    int i;
    char c2;
};

//8
struct S2
{
    char c1;
    char c2;
    int i;
};

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

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

8.修改默认对齐数

之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。

#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认

#pragma pack(1)//设置默认对齐数为8
struct S2
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认

int main()
{
	//输出的结果是什么?
	printf("%d\n", sizeof(struct S1));//12
	printf("%d\n", sizeof(struct S2));//6

	return 0;
}

你可能会说,那我把默认对齐数都是1,那不就节省了空间了吗?————  还是那个问题,这样的确节省了空间,但是执行效率不高,我们采用以空间换取时间的思想。

结论:

结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。(         一般不会瞎更改,都是采用2的几次方形式        )

百度笔试题

写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明

考察: offsetof 宏的实现       

size_t offsetof(   structName,   memberName   );

offsetof是宏,因为其参数传递的是结构体的类型.....具体,以后学了宏之后再深究,今天先看看计算结构体某变量对于首地址的偏移量的计算方法。

#include<stdio.h>
#include<stddef.h>
struct S1
{
	char c1;
	int i;
	char c2;
};

int main()
{
	
	printf("%d\n", offsetof(struct S1,c1));//0
	printf("%d\n", offsetof(struct S1, i));//4
	printf("%d\n", offsetof(struct S1, c2));//8

	return 0;
}

9.结构体传参

值传递

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

void Print1(struct S tmp)
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", tmp.data[i]);
	}
	printf("\nnum=%d\n", tmp.num);
}

int main()
{
	
	struct S s = { {1,2,3,4,5,6,7,8,9,10},100 };
	Print1(s);

	return 0;
}

但是,值传递有明显缺点。就该题而言,我们是拷贝了一份值,打印的是拷贝的那块值,而这样拷贝,空间会占用很多,有没有一种节省空间的传参方式呢?————当然有,那就是,传递地址

#include<stdio.h>
void Print2(struct S* ps)
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->data[i]);
	}
	printf("\nnum=%d\n", ps->num);
}

int main()
{
	
	struct S s = { {1,2,3,4,5,6,7,8,9,10},100 };
	Print2(&s);

	return 0;
}

上面的 Print1 和 Print2 函数哪个好些?
答案是:首选print2函数。

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


结论:

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

二、位段

结构体讲完就得讲讲结构体实现 位段 的能力。
 

1.什么是位段?

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

  1. 位段的成员必须是 int、unsigned int 或signed int 。
  2. 位段的成员名后边有一个冒号和一个数字。

比如:如下程序A就是一个位段

//位段是可以节省空间的!
//位段 - 二进制位
// 
//性别
//男  女  保密   ...
//01  10  00    ...
struct A
{
	int _a : 2;//_a  2个bite位
	int _b : 5;//_b  5个bite位
	int _c : 10;//_c 10个bite位
	int _d : 30;//_d 30个bite位
};
//共47bite   - 6byte就够了?
//而实际上,是8byte

int main()
{
	printf("%d\n", sizeof(struct A));//8byte

	return 0;
}

2.位段的内存分配

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

比如:

 举例说明位段存储过程(      基于VS2019编译器,因为各个编译器可能位段是不一样的     )

 3.位段的跨平台问题

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

对于位段跨平台问题,主要有以下影响因素:

  1. int 位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
     

 解释:

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

struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

//在早期,16位平台下,int 是2byte - 16个bite ,
//那么_d :30超过最大范围16bite是不被允许的
// 
//而在,32位平台下,int _d:30就是可以的,
//因为32位平台下,int 是4个字节,32bite

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

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

例如刚刚所假设的

总结:

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

 那么,就目前大内存的电脑CPU而言,谁还会为了几个比特位而去考虑可以移植性差的位段呢??        别急,马上介绍位段的应用价值————

4.位段的应用

网络协议方面知识:

所以,位段存在是非常有必要的。在网络协议底层知识会有很多应用,后续学得多了,自然而然就会明白其重要性。

三、枚举

1.什么是枚举类型

枚举顾名思义就是一一列举。把可能的取值一一列举。比如我们现实生活中:

一周的星期一到星期日是有限的7天,可以一一列举
性别有:男、女、保密,也可以一一列举
月份有12个月,也可以一一列举
颜色也可以一一列举

2.枚举类型的定义

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

以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。 {}中的内容是枚举类型的可能取值,也叫 枚举常量
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。 例如:

 仅允许在enum枚举类型中,初始化枚举常量。在main()函数中不能更改枚举常量的值:

	//会报错
	RED = 8;

 在main()函数中,可以用枚举类型定义变量。

int main()
{
	enum Color c = GREEN;//建议写法

	//不建议写法:
	//enum Color c = 7;
	//将常数赋值给c,在.c文件可以执行,但是.cpp就不可以,不建议这样写

	return 0;
}

3.枚举类型的优点    为什么使用枚举类型?

用#define就可以同样达到枚举的效果,代码如下:请思考我们可以使用 #define 定义常量,为什么非要使用枚举?

#define RED 4
#define GREEN 7
#define BLUE 666
#include<stdio.h>
//enum Color
//{
//	RED = 4,
//	GREEN = 7,
//	BLUE = 666
//};

int main()
{
	printf("%d\n", RED);
	printf("%d\n", GREEN);
	printf("%d\n", BLUE);

	return 0;
}

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

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较,枚举有类型检查,更加严谨
  3. 防止了命名污染(封装)
  4. 便于调试
  5. 使用方便,一次可以定义多个常量
     

解释

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

因为用#define来直接定义未尝不可,只是把RED、GERRN、BLUE赋值成的值未免显得唐突些;而用enum就增强了代码可读性,只是将RED、GERRN、BLUE用一些值来代替。

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

在enum枚举类型定义的枚举常量,他们是有类型的,类型就是枚举类型;而#define定义的标识符没有类型,是无所谓的。所以枚举类型更加严谨。

3.防止了命名污染(封装)

在enum枚举类型中,枚举常量可能重名,但是他们处在枚举类型内部,对应的枚举类型不同;而#define中定义的变量名字位于全局,如果重复,就会产生bug。

4.便于调试

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

4.枚举的使用

#include<stdio.h>
enum Color
{
	RED = 1,
	GREEN = 2,
	BLUE = 4
};

int main()
{
	enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
	clr = 5; //err
    //不可更改

	return 0;
}

 应用:增加可读性的举例——>

可读性不高的代码:

#include<stdio.h>
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("请选择:>");
		scanf("%d\n", &input);
		switch (input)
		{
		case 1:
			break;
		case 2:
			break;
		case 3:
			break;
		case 4:
			break;
		case 0:
			break;
		}
	} while ();

	return 0;
}

 enum增加可读性的代码:

#include<stdio.h>
enum Option
{
	EXIT, //0
	ADD,  //1
	SUB,  //2
	MUL,  //3
	DIV   //4
};

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("请选择:>");
		scanf("%d\n", &input);
		switch (input)
		{
		case ADD:
			break;
		case SUB:
			break;
		case MUL:
			break;
		case DIV:
			break;
		case EXIT:
			break;
		}
	} while ();

	return 0;
}

四、联合(共用体)

1.联合类型的定义

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

比如:
结果是多少??      5?

//联合类型的声明
union Un
{
	char c;//1
	int i;//4
};

int main()
{
	//联合变量的定义
	union Un u;

	//计算两个变量的大小
	printf("%d\n", sizeof(u));

	return 0;
}

结果是4,因为是联合体(共用)。我们可以再试一下~(  见联合体的特点  ):

2.联合体的特点

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

//联合类型的声明
union Un
{
	char c;//1
	int i;//4
};

int main()
{
	//联合变量的定义
	union Un u;
	计算两个变量的大小
	//printf("%d\n", sizeof(u));

	printf("%p\n", &u);
	printf("%p\n", &(u.c));
	printf("%p\n", &(u.i));

	return 0;
}

结果:我们发现结果是一模一样的。这说明了联合体(共用体)是共用一块大空间的。

 就是图示这样:

面试题

判断当前计算机的大小端

 我们以前会这样写代码去判断大小端:

int main()
{
	int a = 1;
	//0x 00 00 00 01
	//判断计算机大小端
	//低 -----------------> 高
	//01 00 00 00 - 小端存储
	//00 00 00 01 - 大端存储

	char* p = (char*)&a;
	if (*p == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

学了联合体,我们就可以利用它的特点这样去判断计算机的大小端:

int main()
{
	int a = 1;
	//0x 00 00 00 01
	//判断计算机大小端
	//低 -----------------> 高
	//01 00 00 00 - 小端存储
	//00 00 00 01 - 大端存储

	//联合类型的声明
	union Un
	{
		char c;//1
		int i;//4
	}u;
	u.i = 1;
	if (u.c == 1)//因为是联合体,所以拿出第一个字节去判断即可
	{
		printf("小端\n");
	}
	else
		printf("大端\n");

	return 0;
}

当然,为了美观我们也可以将其封装成一个函数:

#include<stdio.h>
int check_sys()
{
	//联合类型的声明
	union Un
	{
		char c;//1
		int i;//4
	}u;
	u.i = 1;
	return u.c;
}
int main()
{
	int a = 1;
	//0x 00 00 00 01
	//判断计算机大小端
	//低 -----------------> 高
	//01 00 00 00 - 小端存储
	//00 00 00 01 - 大端存储

	if (check_sys() == 1)//因为是联合体,所以拿出第一个字节去判断即可
	{
		printf("小端\n");
	}
	else
		printf("大端\n");

	return 0;
}

3.联合体的应用场景

联合体应用于成员间   不同时出现(即:  用你了,就不用我   )。

就比如:学校教务系统~,老师、学生都可以登录。那么大概率情况下,老师就是老师,学生就是学生,不可能有既是老师,又是学生的情况。

这个时候,就可以应用联合体。

以后具体工程中,再慢慢领会即可~~~~~~~~~~~~~~~

4.联合体大小的计算

同时满足这个两个条件:

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

注释解释:

#include<stdio.h>
union Un1
{
	char c[5];//共5个字节,而char类型对齐数是1,所以最终对齐数还是          1
	int i;//共4个字节,VS编译器默认对齐数是8,取较小对齐数,所以最终对齐数是  4
};
//想取联合体中的最大成员大小作为联合体大小?
//err
//最大成员大小是5,不满足   最大成员大小(5)是最大对齐数(4)的整数倍
//所以,对齐到最大对齐数(4)的整数倍
//联合体大小是  8

union Un2
{
	short c[7];//共14个字节,而short类型对齐数是2,所以最终对齐数还是          2
	int i;//共4个字节,VS编译器默认对齐数是8,取较小对齐数,所以最终对齐数是    4
};
//想取联合体中的最大成员大小作为联合体大小?
//err
//最大成员大小是14,不满足   最大成员大小(14)是最大对齐数(4)的整数倍
//所以,对齐到最大对齐数(4)的整数倍
//联合体大小是  16

int main()
{
	printf("%d\n", sizeof(union Un1));//8
	printf("%d\n", sizeof(union Un2));//16

	return 0;
}

应用:通讯录

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

.阿Q.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值