自定义类型详解

Posted 正义的伙伴啊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义类型详解相关的知识,希望对你有一定的参考价值。

自定义类型种类:

结构体

结构体的定义

数组——一些类型相同的值的集合
结构体——一些值的集合(可以是不同的类型),这些值被称为成员变量。

结构体的声明

struct tag //结构体类型名
{
	number - list;  //成员列表
};
  • 成员列表由自己定义,且不用初始化
  • 结构体可以嵌套另一个结构体,如
struct c
{
	char a;
	int a;
}d;
struct p
{
	struct c d;
	char arr[20];
	int a;
}a,b;

结构体类型变量的定义

1.先声明结构体类型,再定义该类型变量:

形式:

struct 结构体类型名
{
成员表列
};
struct 结构体类型名 变量名表列;

结构体变量初始化如图

#include<stdio.h>
struct c
{
	char a;
	int a;
};
struct c a = { 'w',5 };    //1
int main()
{
	struct c b{'a',6 };     //2
	return 0;
}

注意:
1 定义的是全局变量,而2是局部变量

2.在声明的同时定义变量:

形式:

struct 结构体类型名
{
成员表列
}变量名表列;

结构体变量初始化

#include<stdio.h>
struct c
{
	char a;
	int a;
}b;
int main()
{
    b = { 'w' , 5 };
	b.a = 'w';
	return 0;
}

3.不指定类型名直接定义结构体类型变量:

形式:

struct
{
成员表列
}变量名表列;

实际上就是省略了结构体的标签(tag)

结构体变量初始化

#include<stdio.h>
struct
{
	char b;
	int a;
}n={'w',5};         
int main()
{
	n = { 'a',5 };
	printf("%d", n.a);
	return 0;
}

实际上这个结构体只能定义一次,原因是在接下的引用的过程中如果出现多个无名结构体,那调用的结构体是哪一个呢?

结构体的自引用

结构体包含一个类型是本身的结构体的类型

struct Node
{
	char b;
	struct Node next;
};

这种调用实际上是错误的,原因是调用了定义不完整的类型,实际上计算机也无法计算出Node的内存大小!

结构体包含一个指向自身类型的指针

struct Node
{
	char b;
	struct Node* next;
};

实际上与数据类型有关——链表
而我们知道链表的数据是由 数据域-指针域 组成
而这里的next是指向下一个数据的指针,指针的大小在32位操作平台上是固定的,所以结构体的内存大小也能计算出来。

结构体内存对齐

这个问题的实质是:计算结构体的大小

#include<stdio.h>
struct s1
{
	char c1;
	int i;
	char c2;
};
struct s2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	printf("s1的大小位:%d\\n",sizeof(s1));
	printf("s2的大小位:%d\\n", sizeof(s2));
	return 0;
}


实际上s1和s2只是成员定义的顺序不同,但是导致了结构体内存大小的不同。

结构体的对齐规则1——(不含嵌套结构体)

  • 结构体第一个成员放在结构体变量在内存中存储位置的偏移处开始。
  • 从第二个成员往后的所有成员,都放在自身对齐数的整数倍的地址处。

对齐数=编译器默认的一个对齐数 与 该成员大小 的较小值
vs中默认对齐数为8 (其他编译器有自己的规则,这里以vs编译器为例)

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

s1的内存分配:

结构体的对齐规则2——(含嵌套结构体)

实际上结构体也是一种数据类型,结构体嵌套结构体我们要搞清楚两点:

  1. 结构体的对齐数:结构体的对齐数为结构体内所有类型对齐数的最大值,所以要对齐到自己最大对齐数的整数倍处
  2. 结构体的大小:按照规则1计算
#include<stdio.h>
struct s1
{
	char c1;
	int i;
	char c2;
};
struct s3
{
	char a;
	struct s1 s;
	int b;
};
int main()
{
	printf("%d\\n", sizeof(struct s1));
	printf("%d\\n", sizeof(struct s3));
	return 0;
}

由前面可知:s1的大小为 12——— 最大对齐数为 4(int)

为什么要内存对齐?

1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总体来说: 结构体内存对齐是拿空间换取时间的做法
那再涉及结构体的时候我们可以人为的 将占用空间小的成员尽量集中在一起
例如:

struct s1
{
	char c1;
	int i;
	char c2;
};
struct s2
{
	char c1;
	char c2;
	int i;
};

这两种声明顺序不一样,造成s1和s2的内存不同,分别为:12-----8

修改默认参数

我们可以通过#pragma这个预处理指令


#include<stdio.h>
#pragma pack(2)//把默认对齐数改为2
struct S1
{
	char c1;// 对齐数 1
	int i;// 对齐数 2
	char c2;// 对齐数 1
};
#pragma pack()

#pragma pack(1)//把默认对齐数改为1
struct S2
{
	char c1;//对齐数 1  
	int i;//对齐数 1  
	char c2;//对齐数 1 
};
#pragma pack()

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

结构体传参


struct s
{
	int data[1000];
	int num;
};
struct s b = { {0},100 };
void print1(struct s b);
void print2(struct s* ps);
int main()
{
	print1(b);  //传结构体
	print2(&b); //传地址
	return 0;
}

print1 和 print2 分别是传结构体 和 传地址,我们再写代码时优选print2 因为如果结构体占的内存
很大,print1函数传结构体时需要压栈,会有时间和空间上的系统开销,这样会导致性能下降。传地址可以解决这一问题。

位段

结构体还有一种功能就是实现位段

什么是位段

  1. 位段的成员必须是int、signed int和unsigned int
  2. 位段的成员名后面必须有一个冒号 和 一个数字
struct A
{
	int a : 2;
	int b : 5;
	int c : 10;
	int d : 30;
};

A就是一个位段

位段的内存分配

printf("%d",sizeof(struct A));
  1. 位段的成员可以是int、 unsigned int、signed int 或 char(属于整型家族)类型
  2. 位段的空间上按照需要以4个字节(int)或者1个字节(char)的方式来开辟
  3. 位段涉及很多不确定因素,位段时不跨平台的,注意可移植的程序应该避免使用位段
#include<stdio.h>
struct A
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
struct A a = { 0 };
int main()
{
	a.a = 10;
	a.b = 12;
	a.c = 3;
	a.d = 4;
	return 0;
}

我们由位段的声明就可以判断出需要三个字节,但是这三个字节里内存是怎么存放的,我们大胆猜测一下,比特位由高地址向低地址存放,读出来的八进制位应该是0x 62 03 04,在调试中打开内存监视,结果也验证了猜想。但是这vs2019编译器出来的结果,别的环境的结果就不得而知。

位段的跨平台问题

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

枚举类型

枚举顾名思义就是一一举例,把可能的值一一列举
例如
一周有:周一、周二、、、、、
颜色有:蓝、黄、绿、、、、、


enum DAY//日期
{
	MON,
	TUES,
	WED,
	THUR,
	FRI,
};
enum color//颜色
{
	BLUE,
	YELLOW,
	GREEN
};

以上定义的 enum DAY、enum color都是枚举类型。{}中的内容是枚举类型的可能取值,也叫枚举常量
枚举常量既然是常量肯定是有值的,第一个枚举常量默认是从0开始,依次递增一,当然也可以在定义的时候赋初值:


enum color
{
	BLUE=1,
	YELLOW=2,
	GREEN
};

枚举的注意事项:

枚举是一种数据类型,虽然枚举常量是常量,但是不能用常量给枚举类型赋值:

enum color b = BLUE;//正确,赋值号两边类型相同
	b = 5;//不能把整型赋给枚举类型

枚举的优点

和#define相比,我们为什么要用枚举?

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

#define是在预编译阶段将代码发生替换,所以在检查代码的时候,不宜与发现错误

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

#define定义的标识符是没有类型的

  1. 防止了命名污染
  2. 使用方便,一次可以定义多个常量

联合体(共用体)

联合类型的定义

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


//联合体类型的声明
union un
{
	char c;
	int i;
};
//联合变量的定义
union un n;
//计算联合体变量的大小
printf("%d", sizeof(un));

联合体的特点

  1. 联合体的成员共用同一块内存空间,这样一个联合变量的大小,至少是最大成员的大小
  2. 因为共用同一块空间,改动一个类型的变量,内存发生改变顾所有变量的值都要改变。

#include<stdio.h>
union un
{
	int i;
	char c;
};
union un n;
int main()
{
	printf("%p\\n", &(n.c));
	printf("%p\\n", &(n.i));

	n.i = 0x10223344;
	n.c = 0x20;
	printf("%x\\n", n.i);
}

从代码中可以看出union用的是同一块内存空间,地址都是相同的,所以下面改变n.c的之的时候n.i的值也随之发生改变。我们可以用联合体的特性来实现大小端的判断。

#include<stdio.h>
union un
{
	int i;
	char c;
};
union un n;
int main()
{
	n.i = 1;
	if (n.c == 0)
		printf("大端存储");
	else
		printf("小段存储");
}

联合体大小计算

  • 联合的大小至少是最大成员的大小
  • 当最大成员大小不是最大对齐数(类型的字节数)的整数倍的时候,就要对齐到最大对齐数的整数倍
union un
{
	int i;
	char c;
};

根据int类型大小为4,char类型大小为1,所以联合体大小至少为4,4又是最大对齐数4的倍数所以联合体的大小为4.

#include<stdio.h>
union i
{
	char b[5];
	int i;
};

int main()
{
	printf("%d", sizeof(union i));
}


这里b[5]的大小为5,i的大小为4顾联合体的最小大小为5,然而最大对齐数是int类型的4,最大成员5不是最大对齐数4的倍数,所以结果为8。

以上是关于自定义类型详解的主要内容,如果未能解决你的问题,请参考以下文章

vs code 自定义代码片段

在片段中创建自定义列表视图时出错所需活动,找到片段

VSCode创建自定义用户片段

在python 3.6中处理自定义编码时遇到类型错误

VSCode自定义代码片段——CSS选择器

将vscode打造成无敌的IDE添加自定义的snippet