疯狂还债中。
本篇内容主要在于讲解结构体的知识。
1.1 结构体长啥样
我们都知道结构体是多种数据类型的集合,具体要包含什么样的数据类型需要根据具体问题而定,而结构体一般长这样。
struct tag
{
member-list;
}variable-list;
那么既然我们在解决实际问题的时候要使用到结构体,那么我们势必无法绕开以下问题。
结构体如何正确声明?
结构体如何使用?
当然,了解了这两个问题后只是对结构体有了一个比较初步认识,在本文我们还会讲解关于结构体内存对齐的内容,以便各位读者对于结构体有更深层次的了解。
下面我们来探究第一个问题,结构体如何正确声明。
1. 结构体的声明
现在,我想用结构体来定义一个学生,结构体里面要包含学生的姓名,年龄,性别和学号这四个内容,那么我们应该如何使用结构体呢?
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
我想,这样去声明这样一个结构体就没有问题了。但是事实上结构体的声明还有另外一种特殊的方法,即省略tag(Stu)的声明方法,这种特殊的命名方法我们称为不完全声明。
但是有一个小问题,在指针进阶内容中,如果两个不同的指针指向了相同内容的字符串,那么在VS2019编译器的环境下,这两个指针是相等的。
那么对于结构体,有没有类似的结论存在呢?
我们来看看下面两个代码。
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
p = &x; //这样写代码可以嘛???
我们定义了两个在内容上一模一样的结构体,同时结构体变量p和x也都进行了声明,那么p = &x这样一穿代码是否是正确的呢?
答案是否定的,编译器会报错。由于结构体缺少了tag标签,虽然这两个结构体是有着一模一样的内容,但是在编译器的眼中这就是两个不一样的结构体,对应类型的结构体指针才能接收对应类型的结构体,所以这么写代码的是不好的。在C语言中,我们还是要养成良好的代码风格,带上标签也不费什么事儿,你说对吧。
既然我们已经知道了结构体如何声明,那么使用结构体就成了我们下一个需要解决的问题。
2. 结构体如何使用
2.1 结构体变量赋值
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
struct Point p3 = {4, 5}; //初始化:定义变量的同时赋初值。
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
我们看完了结构体变量初始化和赋值之后,不免得想到了一个问题,那就是结构体里面可以套用他自己嘛?结构体里面是否能含有另外一个结构体呢?
关于第一个问题,答案是可以的,但是需要明白怎么写。
结构体当然可以套用他自己,这种操作我们称为结构体自引用,我们可以用结构体来模拟实现数据结构中的链表。链表中的结点由数据部分和地址部分组成,数据部分存放应有的数据,地址部分存放下一个节点的地址。明白了这一点,我们来看看这个代码是否正确。
struct Node
{
int data;
struct Node next;
};
我们稍微动点脑筋思考一下,这个代码一看就是错误的。如果这个代码正确,那么在求这个结构体所占内存空间大小的时候只会出现一种情况,那就是根本停不下来。因为结构体里是他自己,相当于函数递归没有限制条件从而导致不停的递归。
那么,怎么样进行正确的自引用呢?我们再思考链表里结点的构成,就知道了,正确写法应该是这么写。
struct Node
{
int data;
struct Node* next;
};
那么,关于结构体中是否能含有另一个结构体,又怎么进行初始化,咱们看看下面的代码就知道了。
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {4, 5};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//
到此两个大问题就解决了,现在我们还有一个问题需要解决,结构体中存在一种现象叫做内存对齐,这是怎么一回事呢。
3.结构体的内存对齐
这个代码的结果是什么呢?是单纯的8+1+4嘛?
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S1));
如果你打开vs并运行这一串代码,你会发现答案出乎你的想象,这个代码最终的答案是16,而不是13,那出现这种情况的原因是什么呢?
我们先来介绍一下结构体内存对齐的规则。
结构体的对齐规则:
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
我们画个图,就能理解这个对齐规则的意思了。
我们来分析一下,既然结构体的第一个元素是double型,它自己的大小是8个字节,vs默认对齐数也是8字节,所以偏移量就是8个字节,所以从0开始,一直存到地址为7的地方,就结束。
接下来是c ,c自身大小是1字节,vs对齐数是8字节,所以偏移量就是1字节,正好8是1的倍数,所以地址为8的地方就存放了c。
最后的i,由于地址为9的内存不是4的倍数(为什么是4的倍数参照上面的分析),所以现在要浪费内存,直到到地址为12的地方,12是4的倍数,所以开始存放,一直存到15,此时结构体大小是16字节,S3结构体的最大对齐数就是8,而16正好是8的整数倍,所以结构体S3的大小就是16字节。
至于结构体里嵌套结构体按照上面的分析来就好,一层一层求即可。
但是很奇怪,为什么会有这种情况出现呢?
为什么存在内存对齐?
大部分的参考资料都是如是说的:
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则会有硬件异常的问题。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
大家也不必要过多的纠结,结构体的内存对齐是客观存在的现象,大家只需了解,不需要过分纠结。
那么本篇文章到这里就结束了,如果本篇文章帮到了你不妨给作者点一个免费的赞,如果能点一个关注那就更好了。你们的支持是支撑作者更新的最大动力!
感谢阅读