自定义类型——结构体,枚举,联合(详,真的太详了)

一.结构体

1.1什么是结构体,结构体如何声明和定义变量?

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
struct 变量名
{
     不同类型的成员变量;
};   一定要记得在花括号的后面加上分号

声明结构体: 

首先声明结构体用到的关键字是struct,当我们要描述一个对象时,需要多个不同类型的变量,比如说声明一个学生的结构体变量,需要多种不同类型的成员变量,定义一个名字需要的是字符串,定义年龄身高之类的需要的是单精度浮点数,定义学生的学号需要的是数组等,这些都算是关于学生的成员变量,这些就是我们声明的结构体。

#include<stdio.h>

struct student    //定义结构体时是在主函数外部定义的
{
	char name[10];//学生姓名;
	int id[15];   //学生学号
	int age;      //年龄
	float hight;  //身高
};


 
int main()
{
	return 0;
}

定义结构体变量: 

已上代码结构体定义完成后,上面的是struct student 相当于是类型,我们在主函数创建变量时就可以直接使用这个类型。给结构体对象赋初始值时,我们用的方式是(对象名 +  . )比如说我们定义A变量来表示A同学,定义B变量来表示B同学等,同时创建的对象的方式有两种。


第一种:

在主函数定义变量并初始化

#include<stdio.h>

struct student
{
	char name[10];
	int id[8];
	int age;
	float hight;
};


 
int main()
{
	int i = 0;
	struct student A = { "LiHua",{2,0,2,3,0,7,1,3},18,175.5 };
	printf("名字是%s 年龄是%d 身高是%.2f ", A.name, A.age, A.hight);
	printf("学号是");
	for (i = 0; i < 8; i++)
	{
		printf("%d", A.id[i]);//因为学生学号是整型数组,所以在打印时要用for循环遍历输出
	}

	return 0;
}

运行代码如下:

第二种:

在声明结构体时同时定义变量

#include<stdio.h>

struct student
{
	char name[10];
	int id[8];
	int age;
	float hight;
}B = { "XiaoMing",{2,0,2,3,0,7,1,3},20,180.7};


 
int main()
{
	int i = 0;
	printf("名字是%s 年龄是%d 身高是%.2f ", B.name, B.age, B.hight);
	printf("学号是");
	for (i = 0; i < 8; i++)
	{
		printf("%d", B.id[i]);//同理,因为学号是整型数组,打印整型数组要用for循环
	}
	
	return 0;
}

运行代码如下:

1.2结构体的特殊声明(匿名结构体类型)

在声明结构的时候,可以不完全的声明,相当于就是没有标签名,如下代码:

匿名结构体有两处限制的地方。


第一,因为没有标签名,即使两个匿名结构体的成员变量是相同的,编译器也会认为这是两种不同的类型。所以一般情况下,匿名结构体我们只能使用一次。

如下面代码:


#include<stdio.h>

struct
{
	char name[10];
	int id[8];
	int age;
	float hight;
}A;                  //声明匿名结构体
struct
{
	char name[10];
	int id[8];
	int age;
	float hight;
}*B;                 //声明匿名结构体指针
  
int  main()
{
	B = &A;          //成员变量相同,试图讲A的地址赋给指针B
	return 0;
}

能正常运行,但是发现有警告,即使两个结构体的成员都是相同的,编译器依然会把上面的两个声明当成完全不同的两个类型,所以是非法的。

第二,因为匿名结构体没有标签名,所以无法在主函数定义变量;

#include<stdio.h>

struct
{
	char name[10];
	int id[8];
	int age;
	float hight;
};


int  main()
{
	struct  A = { "LHY",{2,0,2,3,0,7,1,3},19,180.6 };
         //注意是没有标签名的
	return 0;
}

 运行报错,原因总的来说就是匿名结构体没有标签名,所以无法在主函数定义变量

1.3结构体自引用

在结构体里面的成员变量存放的是自己的结构体类型变量 


当我们直接在结构体中放入自己结构体类型的成员变量,可以吗?

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

int main()
{
	int sz = sizeof(struct Node);
	printf("%d", sz);
	return 0;
}

当我们尝试运行代码时,发现报错了。对代码进行调整 ,放入的是自己的结构体类型指针

#include<stdio.h>

struct Node
{
	int data;
	struct Node* next;
};
int main()
{
	int sz = sizeof(struct Node);
	printf("%d", sz);
	return 0;
}

运行代码如下:

 所以结构体的自引用是需要放入自己结构体类型指针的,这是为什么呢?

这就涉及到数据结构了, 数据结构描述的是数据在内存中的组织结构,相关的有线性数据结构和树形数据结构,这里要讲线性数据结构。


有顺序的,那自然有乱序的,当数据在内存中的位置是互相是乱序时,我们得想办法让数据之间发生关联,相当于是知道一个,就可以知道全部。

存储一个数据的结构相当于一个是一个节点,,比如我们有存储数据的五个节点,节点不仅存放数据,还存放下一个节点的地址(既然是地址,当然是用指针存放),这样就可以通过一个节点,可以访问到下一个节点了。这样的结构,我们称为链表。而在结构体中存放数据的,成为数据域,存放指针的,成为指针域。

 1.3结构体嵌套

在结构体中成员嵌套放入另外一个结构体。

比如我在声明“老师”的结构体里面,放入”学生“的结构体变量;注意在引用Student的结构体成员时是要用两次 ” . "



struct Student
{
	char name[20];
	int age;
};
struct Teacher
{
	char name[20];
	int age;
	struct Student stu;
};

int main()
{
	struct Teacher C = { "zhangsan",29,{"Lihua",18} };
	printf("老师名字是%s年龄是%d\n学生名字是%s年龄是%d", C.name,C.age ,C.stu.name, C.stu.age);
	return 0;
	
}

运行代码如下:

1.4结构体传参

结构体是一种类型,在函数调用,传参可以是直接传入结构体,也可以传入结构体的地址,但是又区别:

传入结构体时,函数内的任何操作都是对拷贝结构体的修改,不会影响到原本的结构体变化。

传入结构体地址时,该在函数用结构体指针接收,指针指向的是结构体起始地址,函数里的操作会改变结构体里面的内容,如果想要不被改变,可以加 const 修饰符。

如下面的代码:

#include <stdio.h>
struct S
{
	int data[1000];
	int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
void print1(const 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;
}

 一般情况,函数调用时是传结构体好呢,还是传结构体地址好呢?

答案:传结构体地址好。

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

 *1.5结构体内存对齐

我们已经掌握了结构体的基本使用了。 现在我们深入讨论一个问题:计算结构体的大小。
请看下面的代码,并尝试计算结构体的大小。
//代码1;
struct S1
{
	char c1;
	int i;
	char c2;
};

//代码2;
struct S2
{
	char c1;
	char c2;
	int i;
};

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

代码1和代码2都是结构体,里面的成员内容也是一样的,那它们的大小是否相同呢?

运行代码如下:

 结果发现代码1和代码2的结构体大小是不一样的,为什么呢?因为结构体有对齐规则(四点)

结构体的对齐规则:(同时以第一个代码为例)
1. 第一个成员在与结构体变量偏移量为0的地址处。
通俗点讲就是结构体的第一个成员占据的是内存的起始位置(从0开始)
比如:
struct S1
{
	char c1;
	int i;
	char c2;
};

char c1 是结构体的第一个成员,字符类型,大小是一个字节。


2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。(VS默认对齐数是8,gcc没有默认对齐数)(我用的是VS)
第一个成员 char c1位置已经放好了,如上图。
第二个成员 int i ,字节大小是4个字节,比默认对齐数小,又需要整数倍,所以位置对应是4的(1*4,整数倍是1);

第三个成员是 char c2,字节是一个大小,比默认对齐数小,位置对应整数倍,其实就是下一个位置。


3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
根据上面的成员占据的内存大小,应该是9个字节才对(0~8),但是结构体大小是最大对齐数的整数倍,上面的代码最大的对齐数是 4 (因为是 int ),所以应该是12个字节。


4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
S3的最大对齐数是8,S4的最大对齐数也是8,在对其过程中是32是8的整数倍
所以结构体大小是32个字节。
 


#include<stdio.h>

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

struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
int main()
{

    printf("S3的结构体大小为%d\n", sizeof(struct S3));
	printf("S4嵌套S3的结构体大小为%d\n", sizeof(struct S4));
	return 0;
}

 

上面的结构体内存对齐,有时候会浪费空间,那 为什么存在内存对齐 ?
大部分的参考资料都是如是说的:
1.平台原因 ( 移植原因 )
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常。
2. 性能原因
数据结构 ( 尤其是栈 ) 应该尽可能地在自然边界上对齐。 原因在于为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
举一个简单的例子:
示例代码:
struct S1
{
 char c1;
 int i;
 char c2;
};
struct S2
{
 char c1;
 char c2;
 int i;
};

运行代码: 

 1.6修改默认对齐数

使用  #pragma 这个预处理指令,可以改变我们的默认对齐数。

#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
	//输出的结果是什么?
	printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));
	return 0;
}

运行代码:

结论: 结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。

二.位段

2.1什么是位段

位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 intunsigned int signed int
2.位段的成员名后边有一个冒号和一个数字。
如下代码, A 就是一个位段类型。
#include <stdio.h>
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	printf("%d\n", sizeof(struct A));
	return 0;

}
那位段 A 的大小是多少?
这是怎么得来的呢?这就要看 位段的内存分配了

2.1位段的内存分配

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

struct S
{
 char a:3;
 char b:4;
 char c:5;
 char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;

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

位段上的空间根据四个字节(int)或一个字节(char)开辟空间,一个字节就是8个比特位,位段上的数字,单位就是比特位,,存储顺序我按照的是小端存储比如 char a:3 的意思就是在一个字节中的8个比特位占据3个比特

 占3个比特位后剩余5个比特位,如果下一个成员占据的空间不超过5比特位,就会在同一块空间中放入,否则就自己重新开辟一块空间。

在给结构体赋初始值时,初始值十进制转化成二进制,再从低位对应的位段开辟的数位,比如char a:3,就取后面3位,再放入内存中。如下图。

观察数据在内存中的改变:如下图

 2.2位段的跨平台问题 

1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。( 16 位机器最大 16 32 位机器最大 32 ,写成 27 ,在 16
器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。
总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

2.3 位段的应用

1.可以使数据单元节省储存空间,当程序需要成千上万个数据单元时,这种方法就显得尤为重要。

2.位段可以很方便的访问一个整数值的部分内容从而可以简化程序源代码。

常用于数据包的传送 

三.枚举

 3.1什么是枚举

枚举顾名思义就是列举,把可能的取值一 一列举。枚举的关键字是 enum
比如以下代码:

列举一周的星期:
enum Day//星期
{
 Mon,
 Tues,
 Wed,
 Thur,
 Fri,
 Sat,
 Sun
};

列举颜色:

enum Color//颜色
{
 RED,
 GREEN,
 BLUE
};

以上定义的 enum Day   enum Color 都是枚举类型。 { }中的内容是枚举类型的可能取值,也叫 枚举常量
这些可能取值都是有值的,默认从 0 开始,一次递增 1
#include <stdio.h>
enum Day
{
		Mon,
		Tues,
		Wed,
		Thur,
		Fri,
		Sat,
		Sun
};
int main()
{
	printf("%d\n",Mon);
	printf("%d\n", Tues);
	printf("%d\n", Wed);
	printf("%d\n", Thur);
	printf("%d\n", Fri);
	printf("%d\n", Sat);
	printf("%d\n", Sun);



	return 0;

}

运行代码如下:


当然,我们也可以赋初始值,赋初始值后,后面的值也会跟着递增1。

#include <stdio.h>
enum Day
{
		Mon = 1,
		Tues,
		Wed,
		Thur,
		Fri,
		Sat,
		Sun
};
int main()
{
	printf("%d\n",Mon);
	printf("%d\n", Tues);
	printf("%d\n", Wed);
	printf("%d\n", Thur);
	printf("%d\n", Fri);
	printf("%d\n", Sat);
	printf("%d\n", Sun);



	return 0;

}


#include <stdio.h>
enum Color//颜色
{
	RED = 1,
	GREEN,
	BLUE = 4,
	Black= 256
};
int main()
{
	printf("%d\n", RED);
	printf("%d\n", GREEN);
	printf("%d\n",BLUE);
	printf("%d\n", Black);


	
	return 0;

}

运行代码如下: 


3.2枚举的使用 

只能拿枚举常量给枚举变量赋值

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

int main()
{
	enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
	printf("%d\n", clr);
	clr = 15;               
	printf("%d\n", clr);
	return 0;
}


运行代码如下:


3.3枚举的优点

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

四. 联合(共用体)

4.1什么是联合体 

联合也是一种特殊的自定义类型.
这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间(所以联合也叫共用体)。
比如以下代码:
#include<stdio.h>

union Un
{
	char c;
	int i;
};
int main()
{
	printf("%d\n", sizeof(union Un));//打印联合体大小
	union Un un = { 0 };
	un.i = 0x11223344;
	un.c = 0x55;

	printf("%p\n", &un);            //打印联合体的地址
	printf("%p\n", &(un.i));        //打印成员 i 的地址
	printf("%p\n", &(un.c));        //打印成员 c 的地址

	return 0;
}

 联合体的空间是根据最大成员的大小来开辟的,然后所有的成员共用这一块空间,比如在上面的代码中,int i 的大小最大,开辟4个字节空间,然后char c成员共用这块空间。

 运行代码:


因为共用同一块空间,所以“动一发而动全身”,改变任意成员的内容,都会改变其他成员内容

#include<stdio.h>

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

	return 0;
}

我们可以根据上面代码执行顺序查看内存 的变化

union Un un = { 0 };

1.给结构体初始值赋为0;


 2.给结构体成员 i 赋十六进制的值


3.给结构体成员c也赋值一个十六进制数,改动了成员 i 的内容;

 

 4.2联合体的计算

1.联合的大小至少是最大成员的大小。
举个简单例子:
union Un
{
	char c;
	int i;
    double x;
};
则联合的大小是x,8个字节的空间;

2.当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
举个简单例子:
#include<stdio.h>

union Un2
{
	char c[7];
	int i;
};
//下面输出的结果是什么?

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

	return 0;
}

联合的大小要对齐到最大对齐数的整数倍,上面代码,联合开辟的空间应该是7个字节,要对齐最大数是4(int i)的整数倍。所以联合空间应是8个字节。

 4.3用联合判断当前计算机的大小端存储

我们取一个整型,初始值为1,根据机器的大小端存储的位置不同,取出起始地址,如果是1,就是小端存储,如果是0,就是大端存储。

 举个简单例子:

union Un1
{
	int i;      //设置一个整型
};


int main()
{
	union Un1 un = {0};
	un.i = 1; 赋初始值为1

	printf("%d\n", (*(char *)&un.i)); 强制转换char*,并取出第一个字节的内容。

	return 0;
}

好了,今天的内容就到这里,制作不易,如果对您有帮助,就给个赞赞支持一下吧!如果有错误的地方,敬请在评论区批评指出,谢谢。

猜你喜欢

转载自blog.csdn.net/LHY537200/article/details/131703324