自定义类型详解

目录

前言

本期内容介绍

一、结构体

1.1什么是结构体?

1.2结构体的声明

1.3结构的特殊声明

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

1.5结构体成员的访问

1.6结构体传参

1.7结构体的自引用

1.8结构体内存对齐

1.9为什么有内存对齐?

1.a修改默认对齐数

二、位段

2.1什么是位段?

2.2位段的内存分配

2.3位段的跨平台性

2.4位段的应用

三、枚举

3.1什么是枚举?

3.2枚举类型的定义

3.3枚举的优点

3.4枚举类型的使用

四、联合

 4.1什么是联合?

4.2联合类型的定义

4.3联合体的特点

4.4联合体计算大小


前言

我们以前介绍过数组,他是相同类型元素的集合。那不同类型的元素能不能也被集合在一起呢?其实是可以的!他就是结构体!本期小编将带你彻底搞明白结构体、枚举以及联合!

本期内容介绍

结构体

  结构体类型的声明

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

  结构体成员的访问

  结构体传参

  结构体类型的自引用

  结构体内存对齐

  结构体实现位段

枚举

  枚举类型的定义

  枚举的优点

  枚举的使用

联合

  联合类型的定义

  联合的特点

  联合的大小计算

一、结构体

1.1什么是结构体?

结构是一些值的集合,这些值被称为成员变量。结构体的成员变量可以是不同类型!也即是说结构体是不同类型元素的集合!

1.2结构体的声明

struct tag

{

        member-list;
}variable-list;

tag是自定义类型的标签名!struct是结构体的关键字!花括号里面是描述这个对象的属性,也就是成员属性!下面variable-list是变量列表,可以在这里直接创建结构体变量!要注意的是结构体后面要有一个封号;

ok ,举个栗子(描述一个学生):

我们知道要描述一个学生至少得知道他的姓名、年龄、和性别吧!我们先以这三个描述一个学生(还可以加很多)!

struct Student
{
	char name[20];
	char sex[5];
	int age;
};

这样就描述了一个学生的最基本的信息!注意C语言中没有字符串类型所以用字符数组来存储!还有就是在C语言中char占一个字节,而一个汉字是两个字节,所以性别还得使用一个字符数组存储!结构体的成员变量可以是一般的类型的的变量、数组、指针以及其他结构体!

OK我们来初始化一个实例出来看看:

struct Student
{
	char name[20];
	char sex[5];
	int age;
};

int main()
{
	struct Student s1 = { "huahua","男",20 };
	printf("%s\t%s\t%d\n", s1.name, s1.sex, s1.age);
	return 0;
}

看结果:

自定义类型详解_第1张图片

当然也可以在上面变量列表那里创建变量:

struct Student
{
	char name[20];
	char sex[5];
	int age;
}s2 = { "hehe","男",30 };

自定义类型详解_第2张图片

1.3结构的特殊声明

先来看一段代码:

struct
{
	int a;
	float f;
	char c;
};

我们发现这个结构体没有标签名!其实他叫:匿名结构体!既然匿名他就不能直接在主函数中创建变量!因为他连名字都没有啊!

我们看一下:

自定义类型详解_第3张图片

那他如何创建变量呢?其实我们可以在变量列表那里创建:

struct
{
	int a;
	float f;
	char c;
}s = {2,3.1f,'h'};

这样编译就不会报错了:

自定义类型详解_第4张图片

注意:匿名结构体只能用一次!这个和java 中的那个匿名代码块很相似!我以前了解过匿名代码块是开机跑进程的!我们猜测匿名结构也应该是类似的作用!

下面我们再来看一个栗子:

struct
{
	int a;
	char c;
}x;

struct
{
	int a;
	char c;
}*p;

int main()
{
	if (p == &x)
	{
		printf("true");
	}
	return 0;
}

会不会输出true?我们来看一下:

自定义类型详解_第5张图片

不但没有输出true还报了个警告!说是p与&x的类型不兼容!这是为什么呢?他们命名成员变量都一样啊!其实主要的原因是他们是匿名的,即使成员变量都一样! 由于没有标签名不知道是哪一个类型的!所以此时编辑器会认为上面的两个结构体的类型不同!所以他们的指针也不能匹配!

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

其实我们在上面就已经使用过初始化了!我们下面再来看看:

struct Student
{
	char name[20];//姓名
	int age;//年龄
	int id;//学号
}s1 ,s2;//全局变量

int main()
{
	struct Student s3, s4;//局部变量
	return 0;
}

在声明的时候给一些值就叫初始化!

struct Student
{
	char name[20];//姓名
	int age;//年龄
	int id;//学号
}s1 = {"李华",20,22111001}, s2 = { "小明",21,22111002 };//全局变量

int main()
{
	struct Student s3 = { "张三",19,22111003 }, s4 = { "李四",18,22111004 };//局部变量
	return 0;
}

1.5结构体成员的访问

上面已经创建了结构体变量,该如何访问这些结构体变量的成员呢?我们就介绍一下结构体成员的访问:. 和  -> 我们在操作符详解里介绍过这两个操作符!我们当时只是说了他是用在结构体中的,但如何使用没有过多介绍下面我们开介绍一下:

. 操作符是结构体变量直接来访问结构体成员变量的!而->操作符是用结构体指针来访问结构体成员变量的!

OK!我们举个栗子:

typedef struct Student
{
	char name[20];
	int age;
}Stu;

void print1(Stu s)
{
	printf("%s %d\n", s.name, s.age);
}

void print2(Stu* ps)
{
	printf("%s %d\n", ps->name, ps->age);
}

int main()
{
	Stu s1 = { "张三",20 };
	Stu s2 = { "李四",40 };
	print1(s1);
	print2(&s2);
	return 0;
}

这个栗子就很好的说明了.和 ->的各自作用!这里还有一个点就是tpyedef 这是个关键字!它的作用就是重命名,我把上面那个结构体类型重命名为Stu为了方便简洁!

1.6结构体传参

我们通过上面的栗子也看到了,结构体传参的时候既传结构体类型,也可以传结构体指针!那哪一种会更好呢?我们来分析一下:

struct S
{
	int arr[100];
	char c;
};

void print1(struct S s)
{
	printf("%c\n", s.c);
}

void print2(struct S* ps)
{
	printf("%c\n", ps->c);
}

int main()
{
	struct S s = { {1,2,3},'h' };
	print1(s);
	print2(&s);
	return 0;
}

看结果:

自定义类型详解_第6张图片

效果一摸一样!那他两在传参是谁好一点呢? 其实地址更好一点!原因如下:

函数传参的时候,参数需要压栈,会有时间和空间上的系统开销!如果你把整个结构体对象传过去那他的空间开销很大,从而导致下效率下降!所以在传参的时候尽量传指针!关于这里如果还有不明白的,请点击函数栈帧的创建和销毁了解详情!

1.7结构体的自引用

结构体的自引用顾名思义就是结构体引用自己!我们看下面代码:

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

是否正确呢?我们分析一下,假设他是正确的!那他的大小是多少?我分画图分析一下:

自定义类型详解_第7张图片

这样的写法我们好像永远无法计算出结构体的大小!像套娃一样一直套下去了!那我们该如何做呢?其实名字是自引用,只要让他能找到他自己就行了!我们这里的处理方式是:用结构体指针来记录!到这里相信学过数据结构的朋友一下子就看出来了,这其实就是链表!我们来改一下:

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

这样就ok了,我来画个图解释一下这种结构:

自定义类型详解_第8张图片

这里我们后面还会详解的!这里了解一下!

1.8结构体内存对齐

 我们上面已经大概介绍了结构体,那结构体的大小是多少呢?我们先来看一个例子:

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

我们来看结果之前来分析一下:

自定义类型详解_第9张图片我们初步分析,应该是6个字节!看结果:

自定义类型详解_第10张图片

这好像和我们分析的不一样哎!它两的成员变量就换了一个位置怎么差别这么大呢?怎么回事呢?我们可以使用offsetof这个宏来看一看每个成员距开始位置的大小:

#include
int main()
{
	printf("%d %d %d\n", offsetof(struct s1, c1), offsetof(struct s1, i), offsetof(struct s1, c2));
	printf("%d %d %d\n", offsetof(struct s2, c1), offsetof(struct s2, c2), offsetof(struct s2, i));
	return 0;
}

看结果:

自定义类型详解_第11张图片

 两个成员变量就换了一个位置,其实位置居然差别这么大,为什么呢?这里我们就得介绍一下结构体的内存对齐了!

结构体内存对齐规则:

1.结构的第一个成员变量永远放在相较于结构体起始位置偏移量为0处!

2.从第二个开始,后面的每个元素都要对齐到某个对齐数的整数倍处!对齐数:结构体成员自身大小和默认对齐数的较小值!

在VS上默认对齐数是8,而在gcc是没有默认对齐数的(对齐数就是成员的自身大小)!

3.结构体的总大小,必须是最大对齐数的整数倍。最大对齐数:所有成员的对齐数中的最大值!

4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数的整数倍(包含嵌套结构体的对齐数)!

什么意思呢?下面我来一一解释:

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

自定义类型详解_第12张图片

看结果:

自定义类型详解_第13张图片 OK,我们多来看几个:

struct s1
{
	int arr[3];
	char c;
	float f;
};

 自定义类型详解_第14张图片

看结果:

自定义类型详解_第15张图片

再来看一个:

struct s1
{
	char c;
	int arr[2];
};

struct s2
{
	char c1;
	struct s1;
	int a;
};

自定义类型详解_第16张图片

看结果:

自定义类型详解_第17张图片现在我们再来分析一下上面的那个题:

自定义类型详解_第18张图片

自定义类型详解_第19张图片OK,介绍到这里你可还在疑惑为什么有内存对齐?下面我们就来介绍一下为社么有内存对齐:

1.9为什么有内存对齐?

关于内存对齐主要有两个原因:

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

不是所有硬件平台都能访问任意地址上的任意数据的,某些硬件只能在某些地址处取某些特定类型的数据,否则会抛出硬件异常!

2.性能原因(效率原因)

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

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

(这种思路就是典型的空间换时间)!

什么意思呢?我来画个图解释一下:

自定义类型详解_第20张图片

自定义类型详解_第21张图片 

我们思考能不能设计出一种又省空间又满足内存对齐的结构体呢?其实自习思考一下还是能的:

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

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

这是一开始我们举的例子,两个结构体虽然成员变量就差了一个顺序但结构体的大小差的很多!显然我们发现第二个结构体更节省内存空间!观察发现它是将占空间小的char集中放在了一起!其实这就是节省空间的办法:让占内存小的成员尽量集中在一起!

1.a修改默认对齐数

我们上面已经了解了默认对齐数!VS是8,gcc上没有对齐数(默认对齐数就是成员变量自身大小)。默认对齐数能不能被修改呢?答案是 :可以的!如何修改呢?我们下面一起来看看:

这里要介绍一个预处理指令:#pragma 他可以帮我们修改我们的默认对齐数!

OK上个栗子演示一下:

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

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

我们一开始默认对齐数是8,他两的结果我们刚刚看完是:12和8!

自定义类型详解_第22张图片

下面我们把第二个默认对齐书数为1:(改为 1应该就连着放了就应该是6):

#pragma pack(8)
struct s1
{
	char c1;
	int i;
	char c2;
};
#pragma pack(1)
struct s2
{
	char c1;
	char c2;
	int i;
};

 看结果:

自定义类型详解_第23张图片

再来把两个的默认对齐数全部改成 1:

自定义类型详解_第24张图片

这与我们一开始分析的一样的,我们一开始分析是默认为1的,而实际上他是有默认对齐数的!

注意:有了#pragma这个预处理指令,我们可以在结构体内存对齐不合适的时候可以自己按需要修改!但不要太随意的修改,要按需求适当的修改!!! 

二、位段

2.1什么是位段?

位段又称位域。是一种以结构体来实现的一种以位(bit)为单位的数据存储结构!它可以把数据存储的紧凑!其实看到这里就明白了,位段时为了节约内存空间而设计的!位段的声明和结构体是很类似的,有两个不同点:

(1)位段的成员必须是 int 、 unsigned int char(后面加的)。

(2)位段的成员名后边有一个冒号和一个数字!

OK,举个栗子:

struct A
{
	int a : 2;
	int b : 5;
	int c : 10;
	int d : 30;
};

这就是结构体实现的一个位段!这里你肯定会有疑问,结构体有大小,位段是用结构体实现的他也应该有大小!那他的大小是多少呢?我们一起来看看:

struct A
{
	int a : 2;
	int b : 5;
	int c : 10;
	int d : 30;
};

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

自定义类型详解_第25张图片

8个字节哎!为什么是8个字节呢?我们分析会不会和后面的数字有关呢?答案是肯定的!

位段的位实际上是二进制位!也就是后面的那些数字,他们表示在内存中所占二进制位的个数!

我们分析上面的二进制位加起来也才47个比特位也不够64位(8字节),我们猜测会不会有浪费呢?我们一起来看看位段是如何分配内存的!

2.2位段的内存分配

1、一般情况下,位段的成员是同一类型,不会夹杂不同的类型。

(原因是位锻本身是很不稳定的东西,如果成员的类型不同的话,就会使得变得复杂和充满      不确定!)

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

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

什么意思呢?我还是来画图解释一下:

自定义类型详解_第26张图片

那当前32位平台的VS是如何实现位段的内存分配的?我们举个例子来研究一下:

struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

 这个位的大小是多少?我们来分析一下:

自定义类型详解_第27张图片

是不是2个字节就够了呢?看结果:

 自定义类型详解_第28张图片

怎么是3个字节呢?是不是也存在浪费空间的的情况呢?我们再来换个思路分析:当某个字节放不下该成员的位时,重新开空间:

自定义类型详解_第29张图片 到底是不是这样呢?我们来赋值验证一下:

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

我们通过当前的分析把每个位都放进去,然后看内存即可:

自定义类型详解_第30张图片

果然是620304!~这也就证明了在当前VS上位段的内存分配时上述的那样!!!但由于标准并未规定位段的实现所以每个编译器可能不一样!这个只能说明VS是这样!!

2.3位段的跨平台性

我们前面就说过位段时不跨平台的,如果要程序要注重可移植性可跨平台行的话应该避免使用位段!位段为什么不夸平台呢?我们下面一起来看看:

1、int 位段被当成是有符号还是无符号的,这个是不确定的!(这关乎这最高一位是符号位还是数值位).

2、位段中最大的位数是不确定的!(例如早期16位机器int位2个字节(16bit),你如果把在这种平台上的位段放到32位平台上,他就会出错,根本有时候没有那么多位)

3、位段中的成员在内存中是如何分配的(从左到右,还是从右到左)这是标准未定义的!

4、当一个结构体包含有位段 ,第二个位段成员比较大,无法在第一位段剩余空间存储下的时候,是应该舍弃还是继续利用,标准未定义!!!

总结:和结构相比位段可以达到同样的效果,并且位段更加节省空间,但位段的跨平台存在问题!

2.4位段的应用

上面说的位段有很多缺点但为什么还要介绍它呢?他有什么具体应用呢?其实他的作用很大,平时和你对象聊天是就会有位段的运用(IP数据包):这是网络部分的知识,这里画个图了解一下:

自定义类型详解_第31张图片

三、枚举

3.1什么是枚举?

枚举故名思意就是一一列举,把可能的取值一一列举出来!例如:一周7天是不是可以列举出来,一个人的性别无非三种情况:男、女、保密!是不是也能一一列举出来!

3.2枚举类型的定义

 enum tag

{   

        member 1,

        member 2,

        member 3,

        member 4     

}variable_list;

枚举类型的关键字: enum 和struct 一样都是关键字!而且注意的是枚举除最后一个成员外,其他成员的最后都是逗号,不是封号,最后一个成员的后面没有符号!和结合体一样的是花括号后面有一个封号!!!{}中的member也叫做枚举常量他这里就是所有的取值可能,默认第一个是从0开始,然后依次逐增1,当然也可以在声明的时候赋值!

OK,了解了枚举类型的定义我们来试一下:

enum Clolor
{
	RED,
	GREEN,
	BLUE
};

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

看结果:

自定义类型详解_第32张图片

enum Clolor
{
	RED = 1,
	GREEN = 5,
	BLUE
};

看结果:

自定义类型详解_第33张图片

3.3枚举的优点

这里肯定会想,为社么有枚举?不是#define 定义常量不也可以解决吗?你可能会想会不会有类型检查?的确是,那加上const 不也可以吗!其实不然,枚举存在不光是这个原因!枚举由他独特的优势:

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

2、和#define 定义的常量相比,枚举有类型检查,更加严谨!

3、便于调试(#define是直接替换,而枚举会有类型检查。并且#define是看到的代码和调试的代码不一样的,这样会导致调试效率大大降低或调试不出来)

4、使用方便,一次性可以定义多个常量(#define 一次只能定义一个)

3.4枚举类型的使用

这里在使用的时候,顺便验证一下枚举类型的检查类型:

enum Color
{
	RED = 1,
	GREEN,
	BULE
};

int main()
{
	enum Color cc = 8;
	return 0;
}

在cpp的环境下验证的:

自定义类型详解_第34张图片

正常使用:

enum Color
{
	RED = 1,
	GREEN,
	BULE
};

int main()
{
	enum Color cc = GREEN;
	return 0;
}

 这样就不会报错了!!!当然你也可以像结构体一样在变量列表那里创建!自定义类型详解_第35张图片

四、联合

 4.1什么是联合?

联合体又称共用体,也是一种特殊的自定义类型。这中类型定义的变量也包含了很多成员,这些成员的总特征是共用一块内存空间,所以联合体也叫共用体!

OK,举个例子:

union Un
{
	int i;
	char c;
};

这就是一个联合体,他也是有关键字的:union

4.2联合类型的定义

枚举的定义和上面的两种都差不多:也是有关键字和标签名以及成员的!看看他的语法:

union tag

{

        member_list;

}variable_list;

这里要注意的是成员列表的后面是封号!他和上两个一样花括号后面有一个封号;

ok ,举个例子:


union Un
{
	int i;
	char c;
};

int main()
{
	union Un u = { 0 };
	u.i = 1;
	u.c = 0;
	return 0;
}

这就是联合定义和初始化!

4.3联合体的特点

前面也介绍过了!联合的成员是共用一块内存空间的!一个联合体的大小至少为成员中最大的那一个!(共用一块空间得至少放的下最大的哪一个吧)

OK,举个例子验证一下:

union Un
{
	int i;
	char c;
};

int main()
{
	union Un u = { 0 };
	u.i = 0x11223344;
	u.c = 0x55;
	return 0;
}

我们调试看一下内存:

自定义类型详解_第36张图片

介绍到这里我们就可以说一下在数据在内存中的存储那一期中留下的一个问题了:当时验证当前平台是大小端的时候提了一下用联合解决的方法,下面我们就来实现以下:首先回顾一下我们当时的解决方式:

int check_sys()
{
	int i = 1;
	return *(char*)&i;
}

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

看结果:

自定义类型详解_第37张图片

下面我们来用联合体实现以一下:

int check_sys()
{
	union
	{
		int i;
		char c;
	}u;
	u.i = 1;

	return u.c;
}

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

 看结果:

自定义类型详解_第38张图片

这里我设计成了匿名联合体,有朋友会好奇,不是返回时是u.c增么返回值是int 呢? 其实这里返回的是u.c那块空间存储的ASCII值!!!

4.4联合体计算大小

联合体也是有大小的,他的计算规则如下:

1、联合的大小至少是最大成员的大小。

2、当最大的成员大小不是最大对齐数的整数倍的时候,要对齐到最大对齐数的整数倍处!

OK,举个例子:

union u
{
	char c[5];
	int i;
};

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

这个联合体的大小是多少?我们分析一下:

自定义类型详解_第39张图片

看结果:

自定义类型详解_第40张图片

 再来看一个:

 

union u
{
	short s[7];
	int i;
};

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

自定义类型详解_第41张图片

OK,看结果:

自定义类型详解_第42张图片 

OK,本期分享就到这里!好兄弟,我们下期再见! 

你可能感兴趣的:(C语言从入门到进阶,c语言)