本篇博客和大家分享一下自定义类型:结构体。这里我们先提一下类型,类型有内置类型(char、short、long、int等)和自定义类型,内置类型创造出来可以直接使用,C语言本身自带的一种类型,当自定义类型不能满足的时候,支持自定义一些类型(当然不是随便定义的):结构体、枚举、联合体。数组(一种相同类型的集合)其实也是一种自定义类型,前面博客已经写过了这里就不提了,接下来介绍自定义类型中的结构体:
1. 结构体类型的声明
1.1 结构体的概念
当我们描述一个人的年龄时会写 int age=10; 但当描述稍微复杂的类型时,直接使用内置类型时不行的,例如我们描述一个人的信息(包括姓名、年龄等)。这时候就需要一个自定义类型来描述,就有了结构体。结构体中可以包含各种类型的数据,用来描述一个复杂对象的各种属性。结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.2 结构体的声明
struct tag //名字可以自己定义
{
member-list;//成员列表,可以有多个成员
}variable-list;//变量列表
我们试着使用结构体描述一个学生:
struct student
{
char name[20];//名字
int age;//年龄
char sex[10];//性别
}
1.3 特殊的声明
在声明结构体的时候,可以不完全声明。例如:
struct
{
int a;
char b;
float c;
}x;//定义全局变量x并且只能在这里创建变量
strcut
{
int a;
char b;
float c;
}a[10],*p;
上面两个结构在声明的时候省略了结构体的标签(tag)。那么问题来了,在上面代码的基础上,下面的代码合法吗?
p=&x;
很明显是不合法的,原因是:编译器会把上⾯的两个声明当成完全不同的两个类型,所以是非法的。匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用⼀次。
1.4 结构的自引用
在结构中包含⼀个类型为该结构本⾝的成员是否可以呢?⽐如,定义⼀个链表的节点:
struct Node
{
int data;
struct Node next;
};
这段代码正确吗?如果正确那么sizeof(struct Node)是多少呢?仔细分析一下其实是不⾏的,因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的大小就会无穷的大,是不合理的。正确的自引用方式:
struct Node
{
int data;
struct Node *next;
};
在结构体自引用使用的过程中,夹杂了typedef对匿名结构体类型重命名,也容易引入问题,看看下面的代码,可行吗?
typedef struct
{
int data;
Node *next;
} Node;
答案是不行的,因为Node是对前⾯的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的。解决方案如下:定义结构体不要使用匿名结构体了。
typedef struct Node
{
int data;
struct Node *next;
}Node;
2. 结构体的创建和初始化
有了结构体类型,那如何定义变量,其实很简单,结构体变量的初始化使用{}。
struct point
{
int x;
int y;
}p1;//声明类型的同时创建变量
struct point p2;//定义结构体变量
struct point p3={1,2};//初始化
struct Node
{
int data;
struct point p;
struct Node *next;
}n={10,{1,2,},NULL};//嵌套初始化
有的就想搞特殊点,初始化可以不按顺序初始化吗?当然是可以滴,指示器初始化,这种方式允许不按照成员顺序初始化:
struct student
{
char name[10];
int age;
};
struct student a={"张三",10};//常规初始化
struct student b={.age=15,.name="李四"};//指示器初始化
那我们怎样来打印结构体中的数据呢?打印使用 自己定义的变量名称+.+对应的数据 请看下面这段代码:
#include<stdio.h>
struct student
{
char name[10];
int age;
}a={"张三",10};
int main()
{
printf("%s %d\n",a.name,b.age);
return 0;
}
3. 结构体内存对齐
我们已经了解了结构体的基本用法了,现在我们深入讨论一个问题:计算结构体的大小,这也是一个比较热门的话题:结构体内存对齐。
3.1 对齐规则
我们来了解一下结构体对齐规则:
1.结构体的第一个成员对齐到相对结构体变量起始位置偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的⼀个对齐数与该成员变量大小的较小值。(因为我自己使用的VS编 译器,这里我提一下,VS对齐数默认的值为8)3.结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大 的)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍 处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对⻬数)的整数倍。
这里拿一段代码举个例子:
struct S
{
char a;
int b;
char c;
}a={'s',10,'z'};
int main()
{
printf("%d\n",sizeof(a));
return 0;
}
这时大家是否有疑问呢?char类型占1个字节,int类型占4个字节,1+4+1算出来不是6吗?为什么是12?别急对照着下面这张图给大家讲解:
首先,我们假设为0的那个表格为起始地址,a的地址从0处开始, 那么每个数字所对应的表格就相当于它的偏移量,根据对齐规则第一条,我们可以知道s存放在偏移量为0的位置,又因为它是char类型只占1个字节,所以橘黄色的部分代表它的存储,接下来我们要放第二个成员了,根据对齐规则第二条可以算出对齐数为4,所以10要对齐到偏移量为4的倍数的地方(即为图中偏移量为4的位置),粉色部分代表它的存储,那有人就要问了那前面没涂色的地方怎么办呢?浪费了。同理蓝色部分代表z的存储,然后根据对齐规则的第三条,我们可以算出结构体总大小为12。
这里给大家补充一个函数:offsetof 他是用来计算结构体成员相对于起始位置的偏移量,它需要头文件<stddef.h>,我们看一下cplusplus官网对他的介绍:
那我们通过对下面的代码的调试来了解offsetof函数:
#include<stdio.h>
#include<stddef.h>
struct S
{
char a;
int b;
char c;
};
int main()
{
printf("%d\n",offsetof(struct S,a));
printf("%d\n",offsetof(struct S,b));
printf("%d\n",offsetof(struct S,c));
return 0;
}
3.2 为什么存在内存对齐
这里没有一个特别笃定的原因,大部分参考资料是这么写的:
1. 平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地都对齐成8的倍数,那么就可以用⼀个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
3.3 修改默认对齐数
#pramga 这个预处理指令,可以修改编译器的默认对齐数,具体操作如下:
#pramga pack(1)//将默认对齐数改为1
#pramga pack()//取消修改的默认对齐数,还原成最初的默认对齐数
本篇博客到这里就结束啦,大家有问题可以评论出来或者私信我哦。