前言:
在之前的学习中,我们已经对结构体有了初步了解,而在今天我们将更加深入了解结构体。让我们进入正文。
正文:结构体:
结构概述:
C语言允许用户自己指定这样一种数据结构,它由不同类型的数据组合成一个整体,以便引用,这些组合在一个整体中的数据时互相联系的,这样的数据结构叫做结构体。结构是一些值的集合,这些值被称为成员变量。结构体的每个成员可以是不同类型的变量。
2.结构体的声明:
这部分内容在前面初阶结构体就讲过了,这边就不再过多赘述了,再次仅以描述“学生”为例掩饰其声明与定义过程:
#include<stdio.h>
//结构体声明:
struct Stu
{
char name[20];
int age;
char sex;
float score;
}s1,s2;//定义结构体变量s1,s2;
//此处定义的变量是全局变量;
//也可以这样定义:
struct Stu s3,s4;
//此处定义的变量同样也是全局变量;
int main()
{
struct Stu s5;
//此处定义的变量s5是局部变量
return 0;
}
3.特殊说明:
今天我们要讲解其它声明方式,结构体的不完全声明,即匿名结构体类型:
#incldue<stdio.h>
struct //这里没有声明结构体标签,即为匿名结构体类型
{
char name[20];
int age;
char sex[5];
float score;
}student={"zhangsan",20,"man"90.4"};
//匿名结构体类型必须在声明的同时进行定义;
我们把这种在声明时省略掉结构体标签的结构体称之为匿名结构体类型,在使用这种方式进行生命是,由于没有声明结构体标签,导致一旦结构体结束声明,将无法再次进行定义,所以对于该类型的结构体就必须在生命结构体的同时进行定义(可以不用初始化);
之所以在这里强调这个知识点是因为,在进行完全声明时,例如我们上面的“学生”的示例中s1~s5这五个结构体变量因为声明了结构体标签,所以会被视为同一种类型进行处理和调用。我们来看看下面这个例子:
struct
{
char name[20];
char sex[5];
int age;
}x;
struct
{
char name[20];
char sex[5];
int age;
}*p;
这段代码中,虽然两个结构体类型内的成员完全一样的,但因为两者使用了匿名结构体的声明方式,编译器会把上面的两个声明当成完全不同的两个类型,于是在下面的代码中将被视为非法:
p=&x;
//一种类型的指针指向另一种不同类型,会被视为非法;
4.结构体的自引用:
顾名思义,结构体的自引用就是:结构体在自己的声明中引用了自己的一种声明方式。那么我们来看看下面这段代码,判断一下这样的引用方式是否正确:
struct test
{
int data;
struct test n;
};
int main()
{
struct test;
return 0;
}
我们说这种引用方式时非法的。因为当我们这样进行引用后,在我们定义的结构体变量时,会进行自引用,但在自引用中有嵌套了对自身的引用,如此循环反复,编译器并不知道何时停止自引用。
正确的自引用形式:
struct test
{
int data;
struct test*Next;
}
int main()
{
struct test n;
return 0;
}
当我们在进行结构体变量的定义时同样进行了自引用,但是这一次我们使用了一个指针,指向了下一个结构体变量的空间,而在这次指向之后,指针指向的空间被固定,不再指向其他空间,如此就实现了真正的结构体自引用。
我们还可以结构关键字typedef进行使用:
typedef struct test
{
int data;
struct test* next;
}test;
int main()
{
test n;
return 0;
}
我们结合关键字typedef来将我们声明的结构体变量进行重命名,方便我们结构体变量的定义和初始化。但要注意的是:在结构体声明内部进行自引用时,仍需写成完全形式,这是因为,编译器只有在在结构体声明结束之后才会对我们声明的结构体类型进行重命名。
5.结构体的定义与初始化
这部分内容在之前已讲解过了,这边就不再做过多的赘述,直接上例子:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct student
{
char name[20];
int age;
char sex[5];
float score;
}s1 = { "Zhang",21,"Man",98.4 };
//初始化结构体变量s1,此处的结构体变量是全局的
struct student s2 = { "Wang",20,"Woman",99.5 };
//初始化结构体变量s2,此处初始化的结构体变量等同于声明时初始化,也是全局的
int main()
{
struct student s3 = { "Jiao",21,"Man",67.2 };
//初始化结构体变量s3,此处的结构体变量是局部的
printf("%s %d %s %.1lf\n", s1.name, s1.age, s1.sex, s1.score);
printf("%s %d %s %.1lf\n", s2.name, s2.age, s2.sex, s2.score);
printf("%s %d %s %.1lf\n", s3.name, s3.age, s3.sex, s3.score);
return 0;
}
6.结构体内存对齐
经过上面的学习,我们就已经基本掌握了结构体的使用了。接下来我们将要深入研究结构体大小的计算过程,即结构体内存对齐。
我们先来看看下面这段计算结构体变量大小的代码:
#include<stdio.h>
struct test1
{
char a;
int b;
char c;
}test1;
struct test2
{
char d;
char e;
int f;
}test2;
int main()
{
printf("The size of test1 is %d\n", sizeof(test1));
printf("The size of test2 is %d\n", sizeof(test2));
return 0;
}
按我们之前所学知识来解答的话:这两个结构体类型中的成员均一样,都是两个占据1字节的char类型变量和一个占据4字节的int类型变量,所以这两个结构体变量的大小应该是相等的。
但是实际上我们上代码运行起来出现的是这样的结果:
![](https://img-blog.csdnimg.cn/img_convert/5e38946767525e8d85a5b1f1366d9d42.png)
我们看到,实际的计算结果与我们的猜想大相径庭,那么到底是哪里出现了问题呢?这就是我们在这里需要研究的内容:结构体内存对齐。
要想弄清楚究竟是如何进行结构体变量大小计算的,我们首先得掌握结构体的对齐规则:
1. 第一个成员在与结构体变量偏移量为0的地址处。(偏移量:该成员的存放地址与结构体空间起始地址之间的距离)
2. 其他成员变量要对齐到对齐数的整数倍的地址处。
3. 对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
4. 对齐数在VS中的默认值为8
5. 结构体总大小为最大对齐数的整数倍
6. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数的整数倍。
知道这些之后,我们回过头来分析一下上面代码两个结构体变量是如何计算的:
在结构体变量 test1 中,第一个成员为占据一个字节的 char 类型变量 a,我们按照规则将其放置在偏移量为0,即结构体空间的起始位置:
![](https://img-blog.csdnimg.cn/img_convert/636940f36a23e7c2281d1219f46e6cd5.png)
接着,第二个成员为占据4个字节的 int 类型变量 b,按照规则我们首先要计算它的对齐数,我们将变量 b 的大小4与对齐数默认值8进行比较,得出较小值为4,即对齐数为4,于是我们将它放在对齐数的整数倍处,即最近位置第四字节处:
![](https://img-blog.csdnimg.cn/img_convert/7fd9bdc0a2c8327dbab9d1378da02926.png)
再接下来是第三个结构体成员占据1个字节的 char 类型变量 c,同样按照规则我们计算出它的对齐数为1,并将它放在对齐数的整数倍处,即最近位置第八字节处:
![](https://img-blog.csdnimg.cn/img_convert/662c82a48c9d0d9eaebe5269d3e9129d.png)
最后,根据规则,结构体的总大小为最大对齐数的整数倍,而这三个变量中,对齐数最大的是 int 类型变量的对齐数4,则总大小应当为4的倍数。而既为4的倍数,又要能够容纳所有的结构体成员,最小的结构体大小应当为12个字节,即为结构体变量 test1 的大小。
同理各位小伙伴们下去以后可以自己尝试推算结构体变量 test2 的大小并进行验证。
但是我们发现,这样的方式造成了很大程度上的空间浪费,以 test1 为例,12个字节的大小中有六个字节的空间申请了但却没有被使用。那么为什么还要采用这样的办法呢?主要有以下两个原因:
①. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
②. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说: 结构体的内存对齐是拿空间来换取时间的做法。
7.结构体传参:
结构体传参与函数传参类似,没有什么疑难点,直接上例子:
#include<stdio.h>
struct TEST
{
int data[1000];
int num;
};
struct TEST test = { {1,2,3,4}, 1000 };
//结构体传参
void Print1(struct TEST test)
{
printf("%d\n", test.num);
}
//结构体地址传参
void Print2(struct TEST* p)
{
printf("%d\n", p->num);
}
int main()
{
Print1(test); //传结构体
Print2(&test); //传地址
return 0;
}
而在上面这段代码中,我们一般认为 Print2 函数更为优秀。原因是当函数传参的时候,参数是需要压栈的,在这个过程中就会产生时间和空间上的系统开销。如果传递一个结构体对象时结构体过大,那么将会导致参数压栈的的系统开销较大,最终将会导致程序性能的下降。
总结:
我们对结构体的了解就到此结束了,相信各位优秀的小伙伴一定可以熟练的掌握结构体的相关原理和使用。希望对大家有所帮助,同时本文仍有许多不足之处,欢迎各位小伙伴们随时批评指正!