自定义类型:结构体、枚举与联合

时间:2021-06-10 16:09:12

结构体

1、结构体的声明

基础知识:结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同的变量。

结构体也是一种类型。

结构的声明:

struct tag   //struct 关键字     tag 名称(标签)(可以但不建议省略)
{
	member_list;     //成员不能为空(C语言中)
}variable_list;      //可以省略

举个例子:

struct Stu     //描述一个学生
{
	char name[20];
	int age;
	char sex[5];
	char id[20];
};  //注意:分号不能丢

特殊的声明:

在声明结构的时候,可以不完全的声明

比如:

struct
{
	int a; 
	char b;
	float c;
}x;
struct
{
	int a;
	char b;
	float c;
}a[20],*p;     //这两个都为匿名结构体类型,省略了结构体标签(tag)
但若在上面的基础上,
p=&x;

合法吗?

答案是非法的,,,因为编译器会把上面的两个声明当成是完全不同的两个类型。

2、结构的成员

结构的成员可以是标量、数组、指针、甚至是其他结构体。

结构体成员的访问:

结构体变量访问成员是通过点操作符(.)访问的。

如上述的结构体:

struct S s;
strcpy(s.name,"zhangsan");
s.age = 20;

但有时候我们得到的不是一个结构体变量,而是指向一个结构体的指针,那么则应该这样访问:

struct S
{
	char name[20];
	int age;
}s;

void print(struct S* ps)
{
	printf("name=%s  age=%d\n", (*ps).name, (*ps).age);
	printf("name=%s  age=%d\n", ps->name, ps->age);
}

3.结构的自引用

结构中包含一个类型为该结构本身的成员。

例如:

typedef struct Node
{
	int date;
	struct Node* next;
}Node;

结构的不完整声明:

若两个结构体互相包含,且要正常使用,则需要这样:

struct B;
struct A
{
	int _a;
	struct B* pb;
};
struct B
{
	int _b;
	struct A* pa;
};

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

定义的时候有两种方式:

①声明类型的同时定义变量;  ②直接定义结构体变量;

struct Point     //①
{
	int x;
	int y;
}p1;
struct Point p2;    //②

初始化:定义变量的同时赋初值

struct Point p3 = { x, y };

struct Stu    //类型声明
{
	char name[15];
	int age;
};
struct Stu s = { "zhangsan", 20 }; //初始化

结构体还可以嵌套初始化:

struct Node
{
	int date;
	struct Point p;
	struct Node* next;
}n1 = { 10, { 4, 5 }, NULL };

struct Node n2 = { 20, { 5, 6 }, NULL };

5.结构体内存对齐

首先我们要知道为什么要存在内存对齐?

①平台原因(移植原因):

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

②性能原因:

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

原因是要访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问只需要一次。

结构体的对齐规则(重要):

1.第一个成员在与结构体变量偏移量为0的地址处(所以结构体成员的第一个元素你需要内存对齐,默认对齐);

2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。     对齐数:编译器默认的一个对齐数与该成员大小得到较小值;                        VS中默认为8       Linux中默认为4

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

4.如果嵌套了结构体,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小为所有最大对齐数的整数倍。

当我们了解了结构体的内存对齐后,就要来计算结构体的大小了:

来看几个例子:

//练习1
	struct S1
	{
		char c1;   //偏移量为1		
		int i;     //大小为4,所以需要对齐,所以总大小为1+3+4		
		char c2;   //大小为1,不需要对齐   1+3+4+1
	};             //所以总大小为最大对齐数的倍数: 12	
		printf("%d\n", sizeof(struct S1));
//练习2
	struct S2
	{
		char c1;
		char c2;
		int i;
	};                //这次的结果是8      只是变换了一下成员的位置,结果就不一样,正好说明了结构体的对齐规则
	printf("%d\n", sizeof(struct S2));     
//练习3
	struct S3
	{
		double d;
		char c;
		int i;
	};               //按照规则,结果为16
	printf("%d\n", sizeof(struct S3));

下面来一个复杂的,结构体嵌套:

//练习4
	struct S4
	{
		char c1;         //1+3
		struct S3 s3;    //由上可知   16
		double d;        //8
	};                       //∴  1+3+16+8=28    但要满足总大小为最大对齐数的倍数  ∴结构体总大小为32
	printf("%d\n", sizeof(struct S4));

总的来说:结构体的内存对齐就是用空间来换取时间。

扩展:

#pragma pack()

这个可以用来修改编译器默认的对齐数;

括号中只能是1、2、4、8; 若括号中什么都不写,表示恢复默认。

结构体在传参的时候不发生降维;且尽量不要直接传结构体变量,而应该传结构体指针。

位段

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

①位段的成员必须是int,unsigned int,signed int。

②位段的成员名后边有一个冒号和数字。

如:

struct A
	{
		int _a : 2;
		int _b : 5;
		int _c : 10;
		int _d : 30;
	};     //因为它的单位是比特位,所以它的大小是8

位段的内存分配:

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

②位段的空间上是按照需要以4个字节(int)或者一个字节(char)的方式来开辟的;

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

位段的跨平台问题:

①int位段被当成是有符号数还是无符号数是不确定的;

②位段中最大位的数目不能确定;

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

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

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

枚举

枚举其实就是把可能的取值一一列举;

这里就不介绍枚举了,我们来看看它的优点吧:

①增加代码的可读性和可维护性;

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

③防止了命名污染(封装)

④便于调试;

⑤使用方便,一次可以定义多个常量。

联合(共用体)

联合其实也是一种特殊的自定义类型,它定义的变量也包含一系列的成员,

最大的特点就是这些成员共用同一块空间; 

那么一个联合变量的大小,就至少是最大成员的大小;

联合大小的计算:

①联合的大小至少是最大成员的大小;

②当最大成员大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍。

这里我们看一个联合和结构体的巧妙使用:

union ip_addr
	{
		unsigned long addr;
		struct
		{
			unsigned char c1;
			unsigned char c2;
			unsigned char c3;
			unsigned char c4;
		}ip;
	};
	union ip_addr my_ip;
	my_ip.addr = 176238749;
	printf("%d.%d.%d.%d\n", my_ip.ip.c4, my_ip.ip.c3, my_ip.ip.c2, my_ip.ip.c1);
可以将long类型的IP地址转化为点分十进制的表示形式。