C语言学习记录—自定义类型详解:结构体、枚举、联合

第一章:结构体

1.1 结构的基础知识

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

1.2 结构的声明

struct tag//tag是结构体标签
{
 member-list;//成员列表
}variable-list;//变量列表
示例:学生
struct Stu
{
 char name[20];//名字
 int age;//年龄
 char sex[5];//性别
 char id[20];//学号
}; //分号不能丢

struct Stu1
{
	char name[20];//结构体变量,也叫成员
	int age;
}s1, s2;//利用上面结构体类型所创建的结构体变量(此写法创建的是全局变量)

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

1.3 特殊的声明

匿名结构体类型
匿名结构体类型
只能使用一次
struct 
{
	char name[20];
	int age;
}s1;//这里是使用

匿名结构体特点
struct
{
	int a;
	char b;
	float c;
}x;

struct
{
	int a;
	char b;
	float c;
}a[20], * p;//(这个*p是结构体类型指针,是由这颗星和前面匿名结构体类型组合起来)

int mian()
{
	p = &x;//编译器会报警告。虽然他们的成员一模一样,但是两边的类型是不一样的
	return 0;
}

1.4 结构体的自引用

数据结构有:线性、顺序表、链表、二叉树。其中链表可以用结构体实现。假设12345都是节点,1节点包含2节点的地址就能找到2,以此类推。

示例
//代码1
//此写法错误,因为结构体包含了下一个节点,下个节点有数据且有下一个节点,所以无法确定大小
struct Node //节点
{
	int data;//第一个节点的数值
	struct Node next;//下一个节点
};
int main()
{
	sizeof(struct Node);
	return 0;
}


//代码2
//指针大小是固定的,所以可以存放下个节点的地址,最后一个节点的地址置为NULL
//把一个节点分为两个部分,一个存放数据是数据域,一个存放地址是指针域
struct Node
{
	int data;
	struct Node* next;//这个指针能够找到自己同类型的一个节点
	//自己找到一个与自己同类型的节点叫结构体自引用
};

关于匿名结构体的自引用
//此写法错误,不能既匿名,又自引用
//匿名结构体首先要存在,才能typedef重新起名字。也就是下方部分要先存在
//struct
//{
//	int data;
//	Node* next;
//};
//存在的意思就是成员Node必须要有,而Node又是对存在的结构体重命名产生的,这样就会导致分不清先后顺序的混乱
typedef struct
{
	int data;
	Node* next;
}Node;

//正确写法
typedef struct Node
{
	int data;
	struct Node* next;
}Node;


补充结构体指针重命名
//写法1 - 这种写法即定义了结构体,又重命名了struct Node*
typedef struct Node
{
	int data;
	struct Node* next;
}* linklist;//此写法是对struct Node*重命名为linklist

//写法2
struct Node
{
	int data;
	struct Node* next;
};
typedef struct Node* linklist;
//上面两种写法一样

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

结构体变量的定义
struct Point
{
	int x;
	int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2

//结构体变量的初始化
struct Point
{
	int x;
	int y;
}p1 = { 2,3 }, p2 = { .y = 200, .x = 100 }; //结构体变量的初始化
//结构体创建类似图纸,变量才是盖了房子。创建结构体没有开辟空间,创建变量才开辟了空间


struct Stu1
{
	char name[20];
	int age;
};

struct score
{
	int n;
	char ch;
};
struct Stu2
{
	char name[20];
	int age;
	struct score s;
};
int main()
{
	struct Point p2 = { 3,4 };
	struct Stu1 s1 = { "zhangsan",20 };
	struct Stu2 s2 = { "zhangsan", 20, {100,'q'} };
	printf("%s %d %d %c", s2.name, s2.age, s2.s.n, s2.s.ch);
	return 0;
}

1.6  结构体内存对齐

如何计算结构体的大小,首先得掌握结构体的对齐规则:

1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。

  • VS中默认的值为8
  • Linux中没有默认对齐数,对齐数就是成员自身的大小

3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

#include <stdio.h>
#include <stddef.h>//为了使用offsetof宏
//示例1
struct S1
{
	char c1;//1
	int i;//4
	char c2;//1
};

//示例2
struct S2
{
	char c1;//1
	char c2;//1
	int i;//4
};

//示例3
struct S3
{
	double d;
	char c;
	int i;
};

//示例4-结构体嵌套问题
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

int main()
{
	printf("%d\n", sizeof(struct S1));//12
	printf("%d\n", sizeof(struct S2));//8
	printf("%d\n", sizeof(struct S3));//16
	printf("%d\n", sizeof(struct S4));//32
	
	//offsetof();//可以计算结构体成员相较于结构体起始位置的偏移量
	printf("%d\n", offsetof(struct S1, c1));//0
	printf("%d\n", offsetof(struct S1, i));//4
	printf("%d\n", offsetof(struct S1, c2));//8

	printf("%d\n", offsetof(struct S2, c1));//0
	printf("%d\n", offsetof(struct S2, c2));//1
	printf("%d\n", offsetof(struct S2, i));//4

	return 0;
}

为什么存在内存对齐?

1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

假设结构体有两个成员:char c 和 int i,且是32位机器
如果不对齐那么第一次读取了c和i的前三个字节,这时为了完整的读取i,还需要在读取一次。
如果对齐,第一次读取i和后面3个空的字节,第二次完整的读取了i
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
 

1.7 修改默认对齐数

#pragma pack(4)
struct S
{
	int i;
	double d;
};
#pragma pack()//括号内不写就是为默认大小

#pragma pack(1)
struct S1
{
	char c1;//1
	int i;//4
	char c2;//1
};
#pragma pack()//默认对齐数一般修改为2的某个次方

int main()
{
	printf("%d\n", sizeof(struct S));//默认对齐为8时,大小是16;对齐改为4时,大小是12
	printf("%d\n", sizeof(struct S1));//默认对齐为8时,大小是12;对齐改为1时,大小是6
	return 0;
}

//扩展:
//#pragma once
//头文件中使用,功能是:防止头文件被多次引用
百度笔试题:

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

#include <stddef.h>
#define OFFSETOF(type, m_name) ((size_t)&(((type*)0)->m_name))
//假设0地址处放一个结构体对象的地址(也可以理解为使用一个空指针类型强制转换为结构体类型指针),
//通过结构体指针找到成员再取出成员的地址,把地址强制转换成size_t得到偏移量
struct S
{
	char c1;
	int i;
	char c2;
};
int main()
{
	struct S s = { 0 };
	printf("%d\n", OFFSETOF(struct S, c1));//0
	printf("%d\n", OFFSETOF(struct S, i));//4
	printf("%d\n", OFFSETOF(struct S, c2));//8
	//printf("%d\n", offsetof(struct S, c1));//0
	//printf("%d\n", offsetof(struct S, i));//4
	//printf("%d\n", offsetof(struct S, c2));//8
	return 0;
}

1.8 结构体传参

struct S
{
	int data[1000];
	int num;
};

void print1(struct S ss)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", ss.data[i]);
	}
	printf("%d\n", ss.num);
}

void print2(const struct S* ps)//加上const就可以防止指针修改该结构体
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", ps->data[i]);
	}
	printf("%d\n", ps->num);
}

int main()
{
	struct S s = { {1,2,3}, 100 };
	print1(s);//传值调用 - 传值调用还要在拷贝一份临时数据,占用太多空间
	print2(&s);//传址调用
	//上面的 print1 和 print2 函数哪个好些?
	//答案是:首选print2函数。
	//原因:
	//	   函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
	//	   如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
	//结论:
	//	   结构体传参的时候,要传结构体的地址。
	return 0;
}

第二章:位段

2.1 什么是位段

位段是通过结构体来实现的一种以位(bit位)为单位的数据存储结构,它可以把数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作。
 

位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是整型家族。
2.位段的成员名后边有一个冒号和一个数字。
 

示例:
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

冒号后面的数字表示多少比特位
为什么整型只用2个比特位?
假设变量_a的取值范围只是0~3,那么只需要用00 01 10 11就可以表示,所以只需要2个比特位
所以位段是可以节省空间的
 

2.2 位段的内存分配

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;
};
int main()
{
	struct S s = { 0 };
	printf("%d\n", sizeof(struct S));//3 - 意味着b变量使用完空间后剩下的1个比特位被浪费了,d同理
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;

	//将上述值赋值给结构体成员变量时,内存是如何存储的?
	//首先可以确定,struct S占用3个字节,并且将每个成员初始化为0,所以在内存的情况如下
	//00000000 00000000 00000000
	//char a需要3个比特位,先开辟一个字节。10的二进制序列是1010,但只有3个比特位空间,所以只存010。
	//且假设从右向左存储,即从二进制序列的低位到高位
	//此时内存空间为
	//00000 010 00000000 00000000
	//char b需要4个比特位,第一个字节空间还够。12的二进制序列是1100,继续从低位向高位(从右向左)存储
	//01100 010 00000000 00000000
	//char c需要5个比特位,没有足够空间,浪费掉一个比特位后,开辟第二个字节。3的二进制序列是0011
	//01100010 000 00011 00000000
	//此时第二个字节还剩3个比特位,而char d需要4个字节,所以浪费剩下3个比特位,开辟第三个字节。4的二进制序列是0100
	//01100010 00000011 0000 0100
	//62 03 04 - 上面二进制序列转换为16进制,且与调试看到的结果一致(vs2019环境)
	//注意!!!这里没有大小端字节序概念,因为大小端字节序是字节和字节的顺序,不是二进制序列的顺序,这里一次只开辟1个字节
	return 0;
}

2.3 位段的跨平台问题

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

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

第三章:枚举

枚举顾名思义就是一一列举。把可能的取值一一列举。
比如我们现实生活中:
一周的星期一到星期日是有限的7天,可以一一列举。
性别有:男、女、保密,也可以一一列举。
月份有12个月,也可以一一列举
这里就可以使用枚举了。

3.1 枚举类型的定义

enum Day//星期
{
	//默认值
	Mon, //0
	Tues,//1
	Wed, //2
	Thur,//3
	Fri, //4
	Sat, //5
	Sun  //6
};

int main()
{
	enum Day d = Fri;
	
    printf("%d\n", Mon);//0
	printf("%d\n", Tues);//1
	printf("%d\n", Wed);//2
	return 0;
}

//可以修改默认值
enum Day//星期
{
	//枚举常量
	Mon=1, 
	Tues,//2
	Wed, //3
	Thur,//4
	Fri, //5
	Sat, //6
	Sun  //7
};

3.2 枚举的优点

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

3.3 枚举的使用

enum Color//颜色
{
	RED = 1,
	GREEN = 2,
	BLUE = 4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。

第四章:联合(共用体)

4.1 联合类型的定义

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

union Un
{
	int a;
	char c;
};

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

	return 0;
}

4.2 联合的特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。

union Un
{
	int a;
	char c;
};

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

	//三个地址相同
	printf("%p\n", &u);
	printf("%p\n", &(u.a));
	printf("%p\n", &(u.c));

	//改变c也会改变a
	u.a = 0x11223344;
	u.c = 0x00;
    printf("%x\n", un.i);//0x11223300
	return 0;
}

使用联合判断大小端
//指针方法
int check_sys()
{
	int a = 1;
	return *(char*)&a;
}

//联合方法
int check_sys()
{
	//union Un
	//{
	//	int i;
	//	char c;
	//}u;

	//因为上方联合体只使用一次,所以可以使用匿名联合体
	union
	{
		int i;
		char c;
	}u;

	u.i = 1;
	return u.c;
	//因为联合共用空间,所以int a的第一个字节也是char c
	//所以返回1是小端,返回0是大端
}

int main()
{
	//int a = 1;//00 00 00 01 - 1的十六进制
	//低 ---------> 高  地址
	//01 00 00 00 -- 小端
	//00 00 00 01 -- 大端

	int ret = check_sys();
	if (ret == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

4.3 联合大小的计算

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

union Un
{
	char arr[5];//5  这里是按照char来对齐,相当于5个char类型的变量。对齐数是1
	int i;//4  int的对齐数是4
	//联合体大小必须是最大对齐数的倍数
};

union Un2
{
	short arr[7];//大小14 对齐数2
	int i;//大小4 对齐数4
};

int main()
{
	//联合也存在对齐
	printf("%d\n", sizeof(union Un));//8
	printf("%d\n", sizeof(union Un2));//16
	return 0;
}

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值