💕痛苦难道是白忍受的吗?💕
作者:Mylvzi
文章主要内容:自定义类型讲解
一.结构体
定义:
数组:多组相同类型元素的集合
结构体:多组不同类型元素的集合-->管理多组不同类型数据的集合体,结构体中的数据也叫做结构体成员。
例如:管理学生的基本信息,需要的数据有学生的年龄,性别,身高等等
结构体关键字:struct
结构体的声明:
struct Stu//创建了一个结构体类型-->struct Stu-->整体是一种数据类型
{
int age;
float height;
char name[20];
}s1,s2;//可直接在末尾添加你所需要的变量名
struct Stu s1, s2;//使用类型创建变量 类型+变量名 int a;
一种特殊的声明:匿名声明(忽略掉tag标签)
//特殊的声明-->匿名声明-->不告诉你具体名字
struct {
int a;
float b;
}x;//匿名定义了一个结构体变量x
//缺点:只能使用一次,无法对其修改
//优点:安全性高
//注意:未知tag,保证了其使用的唯一性
struct
{
int a;
char c;
float f;
}x;
struct
{
int a;
char c;
float f;
}* p;
int main()
{
p = &x;//err
//尽管成员列表相同,但都是匿名结构体变量,未知类型,会发生类型转换报错
return 0;
}
结构体的自引用:
//结构体的成员列表不能存在一个类型和该结构体一样的结构体
//套娃是非法的;无法计算具体的大小但可以有和原结构体类型相同的结构体指针变量,指向下一个结构体;(链表中常使用)
//通过结构体访问下一个结构体
struct Node
{
int data;
struct Node next;//err
//sizeof(struct Node)是多少?无法计算
};
//改进
struct Node
{
int data;
struct Node* next;//存放下一个结构体的地址
};
int main()
{
printf("%zd\n", sizeof(struct Node));
return 0;
}
//错误的命名方式
typedef struct
{
int data;
Node* next;
}Node;
//先typedef为Node后才能使用Node,不能直接在成员列表内使用
typedef struct Node
{
int data;
struct Node* next;
}Node;
尽量不要使用匿名的方式声明结构体,可能声明错误;
结构体定义和初始化:
注意:使用.操作符初始化结构体时,可以不按照顺序初始化;否则,一定要严格按照结构体成员顺序进行初始化
struct SN
{
char c;
int i;
}sn1 = { 'q', 100 }, sn2 = {.i=200, .c='w'};//全局变量
结构体的内存对齐(重要):
先来计算两个结构体的大小:
再来看成员相较于结构体初始地址的偏移量(利用到offsetof宏)
通过以上两个现象,我们知道,结构体成员在内存中存储时并不是连续存储的,且其大小也不能简单的通过成员大小加和的方式得到;实际上,结构体在内存中的存储以及其大小是有一定的规则,这个规则叫做结构体内存对齐
结构体内存对齐规则:
1.结构体第一个成员的起始地址总是位于结构体偏移量为0的地址处;
2.从第二个成员开始,剩下的成员在内存中存放时要对齐到其对齐数的整数倍处;
对齐数:默认对齐数和成员自身大小的较小值,vs的默认对齐数8,Linux中无默认对齐数,对齐数是成员大小本身
3.结构体内存大小:必须是最大对齐数的整数倍
4.嵌套结构体:如果一个结构体嵌套了一个结构体,嵌套的结构体在内存对齐时对齐到其最大对齐数的整数倍处,整个结构体的内存大小是最大对齐数的整数倍(含嵌套的结构体的对齐数)
利用内存对齐规则分析上述两个结构体的内存分布及内存大小:
为什么要进行内存对齐呢?
有两个原因:
1.平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。硬件不同,读取数据的方式不同,读取到的内容也就不同,通过内存对齐可以实现跨硬件读取数据;2.性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总而言之,结构体内存对齐是一种拿空间换时间的做法,尽管浪费掉了一些内存空间,但我们访问数据的速度大大提升;
但是,我们也可以做到结构体空间的最优化-->将内存空间小的数据集中在一起,比如s1,s2
修改默认对齐数:
#pragma预处理指令
#pragma pack(16)//修改默认对齐数为16
struct Stu
{
int i;
char c1;
char c2;
};
#pragma pack()//恢复默认对齐数
结构体传参:
传递结构体时,尽量传递结构体地址(使用结构体指针接收)
struct S
{
int data[1000];
int num;
};
void print1(struct S p)//传递结构体本身
{
printf("%d\n", p.num);//形参是实参的临时拷贝,传递结构体本身会重新开辟一块儿内存空间
}
void print2(struct S* p)//传递结构体地址 //如果不希望p所指向的内容被改变,添加const修饰
{ //const struct S* p
printf("%d\n", p->num);
}
//print2效率更高,减少了空间的开辟。提高效率;
int main()
{
struct S s1;
print1(s1);
print2(&s1);
return 0;
}
二.位段
位段的定义及内存分配
讲完结构体就需要讲一下结构体实现位段的能力
位段-->给成员分配具体大小内存空间的结构体
注意:
1.声明和结构体相同
2.成员必须是int,unsigned int,char类型
3.设计格式:成员类型 成员名:具体大小
4.位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
观察下列位段在内存中的分配:
位段的跨平台问题:
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机 器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。
位段的应用:
不难发现,位段是一种对空间极度优化的结构体,往往应用到空间利用率高的数据存储,或者大量开关信息的存储;
举例:信息的传递(ip数据包的传递)
三.枚举
定义:
枚举也是一种存储数据的自定义类型,顾名思义,如果取值能够被一一枚举,那么我们就可以使用枚举来存储相应的数据
枚举关键字:enum
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
//都有默认取值
RED,//0
GREEN,//1 GREEN = 5;也可以人为赋值
BLUE//2
};
//enum Color,enum Sex都是枚举类型
优点:
1. 增加代码的可读性和可维护性
更加规范,代码量少;便于维护
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
只能使用枚举类型的数据进行赋值,否则会报错
3. 便于调试
4. 使用方便,一次可以定义多个常量
enum Color//颜色
{
RED = 1,
GREEN = 2,
BLUE = 4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5;//ERR
枚举的应用(来源于chatgpt)
四.联合体(共用体)
定义:
联合体也是一种存储多种数据的自定义类型,其特点是所有的成员共用同一块内存(所以也叫共用体)
联合体关键字:union
//联合体
union Un
{
int a;
char b;
};
int main()
{
printf("%d\n", sizeof(Un));//4
return 0;
}
特点:
所有成员共用同一块儿空间
联合体大小计算:
1.内存大小至少是最大成员的内存大小(必须能够保存该数据)
2.且最终大小要是最大对齐数的整数倍
利用联合体检验当前计算机存储方式(大小端的检验)
//大小端的检验
//之前写法
//检查首地址元素的值
int check_sys(int* p)
{
int b = *(char*)p;//得到首地址
return b;
}
int main()
{
int a = 1;
int ret = check_sys(&a);
if (ret == 1)
printf("小端");
else
printf("大端");
return 0;
}
int check_sys()
{
union
{
int i;
char c;
}un = {un.i=1};
return un.c;
}
int main()
{
int ret = check_sys();
if (ret == 1)
printf("小端");
else
printf("大端");
return 0;
}
五.总结
这篇文章详细介绍了四种自定义类型,结构体(struct),位段(指定成员大小的结构体),枚举类型(enum),联合体(union);要掌握他们的声明,定义方式,在内存中的存储方式,以及内存大小的计算;尽管在目前学习中,自定义类型的应用场景较少,但在之后的学习中会大量使用