0
点赞
收藏
分享

微信扫一扫

征服C语言之自定义类型:结构体,枚举,联合

月白色的大狒 2022-02-08 阅读 103

目录

本章重点

1.结构体

1.1 结构的基础知识

 1.2 结构的声明

1.3 特殊的声明

 1.4 结构的自引用

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

1.6 结构体内存对齐

1.7 修改默认对齐数

1.8 结构体传参

2. 位段

2.1 什么是位段

 2.2 位段的内存分配

2.3 位段的跨平台问题

2.4 位段的应用  

3.枚举

3.1 枚举类型的定义 

3.2 枚举的优点

 3.3 枚举的使用

4. 联合(共用体) 

 4.2 联合的特点

4.3 联合大小的计算

5. 练习

通讯录-静态版本


本章重点

结构体

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

枚举

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

联合

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

1.结构体

1.1 结构的基础知识

 1.2 结构的声明

例如描述一个学生:

1.3 特殊的声明

比如:

上面的两个结构在声明的时候省略掉了结构体标签( tag )。

那么问题来了?

警告:

编译器会把上面的两个声明当成完全不同的两个类型。

所以是非法的。

 1.4 结构的自引用

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

  • 结构体是不能自己嵌套自己的

正确的自引用方式:

注意

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
typedef struct
{
	int data;
	Node* next;
}Node;

int main()
{
	Node n;

	return 0;
}

  •  上面这样的写法是有问题的,编译器还是会报错,

正确书写

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
typedef struct Node
{
	int data;
	struct Node* next;
}Node;

int main()
{
	Node n;

	return 0;
}

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

 有了结构体类型,那如何定义变量,其实很简单。

 顺便一提结构体的访问:

  • 直接访问用  .  操作符
  • 间接访问用指针再使用   .    ->   操作符

1.6 结构体内存对齐

我们已经掌握了结构体的基本使用了。

现在我们深入讨论一个问题:计算结构体的大小。

这也是一个特别热门的考点: 结构体内存对齐

考点: 结构体内存对齐

如何计算

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

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

2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值

VS中默认的值为8

3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整

体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

代码一

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

int main()
{
	printf("%d\n", sizeof(struct S1));
	return 0;
}

代码二

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct S2
{
	char c1;
	char c2;
	int i;
};

int main()
{
	printf("%d\n", sizeof(struct S2));
	return 0;
}

代码三

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct S3
{
	double d;
	char c;
	int i;
};

int main()
{
	printf("%d\n", sizeof(struct S3));
	return 0;
}

代码四

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
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;
}

为什么存在内存对齐 ?

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

1. 平台原因 ( 移植原因 )

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特

定类型的数据,否则抛出硬件异常。

2.性能原因

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

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

比如说:

  •  存在内存对齐,一次就能拿到i的所以字节
  • 而如果没有内存对齐,拿i的话,就需要访问两次

总体来说:

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

例如

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
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;
}

 

 S1S2类型的成员一模一样,但是S1S2所占空间的大小有了一些区别。

1.7 修改默认对齐数

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

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#pragma pack(2)
struct S
{
	char c1;
	int i;//
	char c2;//
};

int main()
{
	printf("%d\n", sizeof(struct S));
	return 0;
}

结论:

百度笔试题:

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

考察: offsetof 宏的实现

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stddef.h>
#pragma pack(2)
struct S
{
	char c1;
	int i;
	char c2;
};

int main()
{
	printf("%d\n", offsetof(struct S, c1));
	printf("%d\n", offsetof(struct S, i));
	printf("%d\n", offsetof(struct S, c2));

	return 0;
}

  •  这里打印的就是刚才struct S的成员c1,i,c2的偏移量

注:这里还没学习宏,可以放在宏讲解完后再实现。

这里再多说一嘴:Linux中的gcc是没有对齐数的

1.8 结构体传参

直接上代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct S 
{
	int data[1000];
	int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
void print1(struct S s) 
{
	printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps) 
{
	printf("%d\n", ps->num);
}
int main()
{
	print1(s);//传结构体
	print2(&s); //传地址
	return 0;
}

  •  虽然结果是一样的,但首选print2函数。
  • 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
  • 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
  • 以后在细说,现在解释有点麻烦

结论:

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

2. 位段

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

2.1 什么是位段

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

  • 其实,char 也行 

比如:

A 就是一个位段类型,那位段A 的大小是多少?

 这里我就不打印了,感兴趣的话可以试试:

 2.2 位段的内存分配

1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型

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

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

看代码 

#define _CRT_SECURE_NO_WARNINGS 1
#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;
	s.b = 12; 
	s.c = 3; 
	s.d = 4;
	return 0;
}

 

 

  •  大小端是字节序才考虑的这里不考虑
  • 上面说了:位段涉及很多不确定因素,位段是不跨平台的
  • 这里我是在vs2019中测试的,仅供参考

2.3 位段的跨平台问题

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

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

器会出问题。

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

4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是

舍弃剩余的位还是利用,这是不确定的。

总结:

2.4 位段的应用  

 这里我说一个简单一点的例子,

两个比特位有四种组合:00,01,10,11,

  • 性别:男,女,保密,用两个比特位足够表示,char a : 2就行了,不用int a 
  • 有时候用位段,比定义变量,定义结构体更加节省空间

3.枚举

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

把可能的取值一一列举。

比如我们现实生活中:

这里就可以使用枚举了。

3.1 枚举类型的定义 

以上定义的 enum Day enum Sex enum Color 都是枚举类型。

{} 中的内容是枚举类型的可能取值,也叫 枚举常量

这些可能取值都是有值的,默认从 0 开始,一次递增 1 ,当然在定义的时候也可以赋初值。

例如

3.2 枚举的优点

为什么使用枚举?

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

枚举的优点:

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

 这里顺便说一下,define定义的宏和字符在预处理就替换了

 3.3 枚举的使用

#define _CRT_SECURE_NO_WARNINGS 1
#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");
}

enum Option
{
	EXIT,//0
	ADD,//1
	SUB,//2
	MUL,//3
	DIV,//4
};

int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case ADD://case 1:
			break;
		case SUB://case 2:
			break;
		case MUL://case 3:
			break;
		case DIV://case 4:
			break;
		case EXIT://case 5:
			break;
		default:
			break;
		}
	} while (input);
	return 0;
}
  • 使用了枚举提高了代码的可读性,
  • 使用枚举比define直接定义字符要简洁,

4. 联合(共用体) 

4.1 联合类型的定义

联合也是一种特殊的自定义类型

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

比如:

 4.2 联合的特点

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
union Un
{
	char c;//1
	int i;//4
};

int main()
{
	union Un u = {10};
	u.i = 1000;
	u.c = 100;

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

	printf("%d\n", sizeof(u));//

	return 0;
}

 

 面试题:

代码一 

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int check_sys()
{
	int a = 1;
	if ((*(char*)&a) == 1)
	{
		return 1;//小端
	}
	else
	{
		return 0;//大端
	}
}

int main()
{
	int ret = check_sys();
	if (ret == 1)
		printf("小端\n");
	else
		printf("大端\n");

	return 0;
}
  • 在前几章的时候,我也有说过判断计算机的大小端,我当时使用(char *)void *,然后判断第一个字节是不是1,

代码二

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int check_sys()
{
	union U
	{
		char c;
		int i;
	}u;
	u.i = 1;
	return u.c;
	//返回1 就是小端
	//返回0 就是大端
}

int main()
{
	int ret = check_sys();
	if (ret == 1)
		printf("小端\n");
	else
		printf("大端\n");

	return 0;
}

 

  •  上面是用了联合体的方法实现的,主要是因为联合体的地址是共用的,char c和int i的地址是一样的

4.3 联合大小的计算

  • 联合的大小至少是最大成员的大小。
  • 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

比如:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
union Un
{
	char a[5];//1    5
	int i;//4
	char c;//1
};

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

	return 0;
}

  •  最大成员的大小是5,最大对齐数是4,所以结果是8

比如:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
union Un
{
	short s[5];//2 10
	int a;//4
};

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

	return 0;
}

  •  最大成员的大小是10,最大对齐数是4,所以结果是12

5. 练习

通讯录-静态版本

  1. 通讯录中能够存放1000个人的信息每个人的信息:名字+年龄+性别+电话+地址
  2. 增加人的信息
  3. 删除指定人的信息
  4. 修改指定人的信息
  5. 查找指定人的信息
  6. 排序通讯录的信息

通讯录-静态版本

  •  这里报了一个警告,后面我会用动态内存开辟解决

---------------------------------------------------------------------------------------------------------------------------------

本章完 

举报

相关推荐

0 条评论