目录
前言
“自定义类型”又是一个新鲜的名词,之前我们常见的char,int,double,float这都是C语言库中原本就有的数据类型,有这些数据类型是远远不够的,这些类型无法处理我们在编程中某些特定情况下所遇到的问题。因此我们便引入了自定义类型,自定义类型又分为,结构体,枚举和联合体,这期主要为大家讲述的是自定义数据类型中的结构体。
结构体的引入
大家思考这样一个问题,为什么我们要引入结构体呢?创建结构体为了解决什么问题呢?
我们要求两个整数的加法,我们会引入两个整型变量a和b,此时的两个变量的类型是整型,创建两个整型变量这是很自然的事情,但是我现在如果要让你创建一个学生变量,咣,脑子里懵,学生变量怎么创建呢?为什么会懵,这是因为我们以前创建的变量都是不抽象的,一个整数就创建一个int变量,一个字符就创建一个char变量,一个小数就创建一个float或者double变量,但是一个学生,就体现了抽象性,学生究竟是什么类型呢?
为了解决这种现象的出现,我们引入了结构体类型。
结构体的声明
我们要创建一个结构体来表示学生的相关信息,代码如下:
struct student {
char name[10];
float height;
int age;
};
上述代码中,struct student表示结构体类型,花括号中的各个变量是结构体中的成员变量,后续结构体类型创建的变量可以实现对这些成员变量的访问。
当然还有另一种写法:
typedef struct student {
char name[10];
float height;
int age;
}stu;
这种写法也是可行的,就相当于是将结构体类型struct student简化成了stu,这样方便以后创建结构体变量。
结构体的自引用
何为结构体的自引用,结构体的自引用就是结构体的自引用,哈哈,开个玩笑,结构体的自引用就是在结构体内部声明一个结构体指针,这个指针指向了下一个结构体变量,一次往后套娃,这就是结构体的自引用。代码如下:
struct student {
char name[10];
float height;
int age;
struct student* ptr1;
};
结构体变量的定义和初始化
结构体变量的定义和之前我们创建一般变量的方式是类似的。初始化也是类似的,不过要带上花括号。
定义
struct student {
char name[10];
float height;
int age;
}stu1,stu2;
struct student stu3;
int main() {
struct student stu4;
return 0;
}
上述代码我们总共定义了四个结构体变量,stu1,stu2,stu3是全局变量,而stu4是局部变量。
初始化
struct imformation {
char home[20];
char phone_num[11];
};
struct student {
char name[10];
float height;
int age;
struct imformation inf;
};
int main() {
struct student stu4 = { "yjd",165.5,18,{"gouxiongling","10087"}};
printf("name = %s , height = %f ,age=%d ,home= %s,phone_num = %s ",stu4.name,stu4.height,stu4.age,stu4.inf.home,stu4.inf.phone_num);
return 0;
}
上述代码实现了结构体变量的初始化。
结构体传参
我们要创建一个打印函数实现,结构体变量的成员变量的打印:
之前我们学习过值传递和地址传递的方式,当然结构体我们推荐使用按地址传递的方式,因为我们知道一个结构体变量的大小是取决于结构体中成员变量的大小(大致取决于,因为还要考虑到内存对齐,内存对齐等下我们会去讲的)的,值传递就相当于形参就是实参的一份临时拷贝,相当于开辟了与实参大小相等的空间。所以如果实参的大小太大,那么形参创建时消耗的空间也是很大的,这样就会导致内存空间的浪费,所以不建议使用值传递的方式进行传参。地址传递就是传过去了实参的地址,是通过指针变量直接访问实参来实现函数相关的功能,这就不会产生额外的空间,不会导致内存资源的浪费,不懂得小伙伴可以去前面查看前几期的内筒,这些内容之前我们是讨论过的。
代码如下:
struct imformation {
char home[20];
char phone_num[11];
};
struct student {
char name[10];
float height;
int age;
struct imformation inf;
};
void print(struct student* ptr)
{
printf("name = %s , height = %f ,age=%d ,home= %s,phone_num = %s ", ptr->name, ptr->height, ptr->age, ptr->inf.home, ptr->inf.phone_num);
}
int main() {
struct student stu4 = { "yjd",165.5,18,{"gouxiongling","10087"} };
print(&stu4);
}
接下里就到了本期的重头戏-----------------内存对齐。
内存对齐
何为内存对齐?
简单来说,就是一种耗费了空间而节省了时间的一种存储方式。
为什么要采用内存对齐?
这个答案等我们学完内存对齐之后再来揭晓。
一般结构体的内存对齐
内存对齐其实就是从结构体的大小来引入的,先来看几段代码:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
请问:上述两个结构体的内存大小是多少?
So easy,char是1个字节,int是4个字节,所以上面两个结构体的大小都是6个字节。
对吗?当然不对,真要这么简单,就没有必要引入内存对齐这个概念了。
先给出内存对齐的规则:
1.结构体的第一个成员变量是存放在结构体的起始位置,偏移量为0的位置。
2.结构体从第二个成员变量开始,成员变量存放在距离起始位置,偏移量为对齐数的整数倍处。
对齐数=成员变量自身的大小与编译器默认对齐数的较小值(linux默认对齐数是4,vs下默认对齐数是8)
结构体的对齐数就是所有成员变量中的最大对齐数。
举个例子:如果一个成员变量是int类型,那么它的大小就是4个字节,而vs下的默认对齐数是8,取较小值,所以这个成员变量的对齐数就是4,应该放在距离结构体初始位置偏移量为4的整数倍处。
3.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
4. 结构体的总大小应该是结构体中所有成员变量的对齐数中最大的对齐数的整数倍。
就如上面的两个结构体,分别有三个成员变量,int,char,char,这三个成员变量的对齐数中最大的对齐数就是int的对齐数4,所以整个结构体的大小应该是4的整数倍。
说完了这些规则,我们再来探讨刚开始的两个结构体真正的内存大小。
解析:
1.第一个成员变量一定放在结构体的起始位置,0偏移量处。所以s1和s2的第一个字节都是char成员变量。
2.结构体s1的第二个成员变量是int,对齐数是4,所以应该放在偏移量为4的位置上,此时已经浪费了3个字节的空间。因为int本身大小需要占4个字节大小的内存空间,绿色区域为其所占的空间,s1的第三个成员变量是char,对齐数是1,所以可以紧跟着第二个成员变量int放在偏移量为8处,蓝色区域为其所占的空间。到这里结构体已经占了9个字节的内存空间,你以为到这里就结束了,nonono,按照规则,我们还应该判断结构体的总空间是不是所有成员变量的对齐数中的最大对齐数的整数倍,0-8总共9个字节的内存空间,显然不是int的对齐数4(结构体中的最大对齐数)的整数倍,12是,所以结构体s1总共占12字节的空间。
结构体s2的第二个成员是char,其对齐数为1,所以可以紧跟着第一个成员变量char,放在偏移量为1的位置上,绿色为char所占的空间,第三个成员是int,对齐数是4,所以应该放在偏移量为4处,占4个字节的内存空间,蓝色为其占有的内存空间,0-7总共8个字节的空间是最大对齐数(int ,对齐数是4)的整数倍,所以结构体s2占8个字节的内存空间。
嵌套结构体的内存对齐
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
struct S1 s;
int i;
};
解析: 直说重点,嵌套结构体的内存对齐,大致上与一般结构体的对齐方式是类似的,但是需要注意的是,嵌套结构体中的成员变量结构体的对齐数是该成员变量结构体中的最大对齐数,就比如上述代码中的s2结构体中的成员变结构体s1,它的对齐数就是其成员变量对齐数中的最大值(int对齐数为4),即s1的对齐数就是4,大小仍然是12个字节,其它的计算步骤与上面两个结构体类似,最终计算结构体s2的大小为20个字节。
这便是内存对齐的具体实现,想必大家都已经看懂了。
修改默认对齐数
#pragma pack(4); //设置默认对齐数为4
#pragma pack(); //取消设置的默认对齐数,初始化为原来的默认值
具体的代码不给予展示,具体的分析过程如上即可。
解答
现在回归到刚开始的问题,为什么要引入内存对齐呢?
计算机在读取数据的时候,我们默认一次读取四个字节的数据,就比如上图的结构体(没有对齐),橙色部分表示一个char类型的数据,占一个字节,淡蓝色表示一个int类型的数据,占四个字节,char类型的数据一次可以读取完,但是由于没有对齐,所以计算机在第一次读取时只读取了int类型数据的前三个字节,所以最后一个字节的数据就需要第二次来读取,这样就浪费了时间,所以我们采取了内存对齐的方式这样,就能保证一个数据只需要一次读取就能读完。这便是引入内存对齐的原因。
这个原因可以是计算机性能导致的,还有一个原因:
平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
总结
以上便是本期自定义类型中的结构体类型的所有相关知识点,大家应该重点关注结构体传参的方式和内存对齐,内存对齐重点关注嵌套结构体的内存对齐。
好了,本期内容到此结束^_^