前言
本篇博客位大家介绍C语言中一块儿重要的内容,那就是结构体,关于结构体的内容,大家需要深入掌握,在后面的学习中依然会用到,如果你对本文感兴趣,麻烦点进来的老铁一键三连。多多支持,下面我们进入正文部分。
1. 结构体类型的声明
1.1 什么是结构体
C语言已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类 型还是不够的,假设我想描述学生,描述⼀本书,这时单⼀的内置类型是不行的。
描述⼀个学生需要名字、年龄、学号、身高、体重等;
描述一本书需要作者、出版社、定价等。C语言为了解决这个问 题,增加了结构体这种自定义的数据类型,让程序员可以自己创造适合的类型。
结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如: 标量、数组、指针,甚至是其他结构体。
上面为大家介绍了结构体的概念,大家需要进行理解;结构体就是用于一些比较复杂的对象,这也给了程序员一定的自由发挥的空间;
1.2 结构体的声明方法
关于结构体的声明方法其实挺简单的,大家来看下面的代码;
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
这里大家可以看到,上面就是结构体的声明方法,这里要提醒大家一下,结尾的分号一定不能丢,否则程序将报错;
1.3 结构体的特殊声明
上面说了结构体的一般声明方法,然而还存在一类特殊的声明方法,大家请看下面的代码;
struct
{
int a;
char b;
double c;
}s = { 10,'a',3.14 };
int main()
{
printf("%d %c %lf", s.a, s.b, s.c);
return 0;
}
这里大家可以发现,我将结构体的名字省略掉了,这种声明方法声明的结构体叫做匿名结构体;这种声明方法声明的结构体在创建变量的时候,必须和声明同时进行,不能单独拿出来进行创建;
这里建议大家不要去使用匿名结构体类型,这种写法是容易出现问题的;
2. 结构体变量的创建和初始化
2.1 结构体变量的定义/创建
//代码1:变量的定义
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
大家可以看到,上面给出了两种创建方式,一种是在声明类型的同时进行定义;还有一种是单独进行定义;这两种方法都是正确的,大家根据自己的习惯进行选择。
2.2 结构体变量的初始化
//代码2:初始化。
struct Point p3 = {10, 20};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s1 = {"zhangsan", 20};//初始化
struct Stu s2 = {.age=20, .name="lisi"};//指定顺序初始化
//代码3
#include<stdio.h>
struct s
{
int a;
char b;
};
struct b
{
struct s a;
int* p;
char arr[10];
float sc;
};
int main()
{
struct b c = { {10,'w'},NULL,"hehe",85.5f };
}
这里大家仔细观察上面的代码,在代码2中,展示了两种初始化的方法,一种是常规的初始化方法,按照声明时的顺序进行初始化;第二种初始化方法是按照指定顺序进行初始化,这里大家需要注意其写法,用到了“.”操作符,这是结构体成员访问操作符,这个后面还会为大家介绍;
下面来说说代码3,前面说过,结构体的成员可以是不同类型的变量,这里当然就包括结构体了;那么大家来看代码3,这里大家可以看到,我声明了两个不同的结构体,并且用第一个结构体定义了一个变量将其放到第二个结构体中,当我对第二个变量进行初始化的时候,大家注意嵌套结构体初始化的方法,对于嵌套的结构体,我们需要使用{}进行初始化,这里希望大家可以根据上面的代码理解嵌套结构体初始化的方法。
3. 结构体成员访问操作符
3.1 结构体成员的直接访问
结构体成员的直接访问是通过点操作符(.)访问的;点操作符接受两个操作数;如下所示:
#include <stdio.h>
struct Point
{
int x;
int y;
}p = {1,2};
int main()
{
printf("x: %d y: %d\n", p.x, p.y);
return 0;
}
上面大家看到,使同方式:结构体变量.成员名,这就是对结构体成员直接访问的方法;
那么这里就有一个问题,如果是嵌套结构体呢?我们应该如何进行访问,大家来看下面的代码;
#include<stdio.h>
struct s
{
int a;
char b;
};
struct b
{
struct s a;
int* p;
char arr[10];
float sc;
};
int main()
{
struct b c = { {10,'w'},NULL,"hehe",85.5f };
printf("%c\n", c.a.b);
}
大家注意在访问嵌套结构体的时候,我们是按照顺序依次访问的,大家可以通过上面的代码进行理解,相信大家可以掌握。
3.2 结构体成员的间接访问
有时候我们得到的不是⼀个结构体变量,而是得到了⼀个指向结构体的指针。如下所示:
#include <stdio.h>
struct Point
{
int x;
int y;
};
int main()
{
struct Point p = {3, 4};
struct Point *ptr = &p;
ptr->x = 10;
ptr->y = 20;
printf("x = %d y = %d\n", ptr->x, ptr->y);
return 0;
}
这里大家注意,间接访问的方法:使用方式:结构体指针->成员名;
这里创建了一个结构体指针接受了我们创建了结构体变量p,我们将p的地址传给结构体指针,通过结构体指针来间接访问结构体的内容;这种访问方法大家也需要理解并且掌握。
4. 结构体的内存对齐
前面我们了解了结构体的基本内容,下面我们要讨论一个问题:计算结构体的大小;这就是我们接下来要说到的内容——结构体的内存对齐
4.1 对齐规则
首先为大家展示结构体的内存对齐规则,下面用一个例子来为大家解释;
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
struct S1 s;
printf("%zd\n", sizeof(s));
}
大家来看这段代码,这里我想要求出代码中的结构体的大小,那么就需要遵循上面说的对齐规则;
大家仔细来看上面的图,右边的数字就是偏移量。根据第一条规则,第一个成员对齐到结构体变量起始位置偏移量为0的地址处,如上图所示;
下面轮到第二个成员,这里大家要理解对齐数的概念,在VS里默认对齐数为8,而最终的对齐数是在默认对齐数和成员变量大小中取较小的那一个,比如这里的第二个成员,是int类型。大小为4字节,而VS默认值为8,所以结果就是4,也就是说第二个成员要对齐到4的整数倍的地址处,那么最近的就是从偏移量为4的地方开始存储,向后存4个字节;
第三个成员,和第二个同理,char类型的大小是1字节,VS默认值是8,所以对于第三个成员来说,对齐数就为1,那么第三个成员就需要对齐到1的整数倍的地址处,看一下图,就是偏移量为8的位置,在这里存储第三个成员,占1字节;
综上,可能有的同学就认为这个结构体的大小是9字节了,那么这里大家要注意第三条规则,结构体的总大小为最大对齐数的整数倍,那么在这个结构体中,第一个成员的对齐数为1,第二个成员的对齐数为4,第三个成员的对齐数为1,那么最大对齐数就为4了,所以整个结构体的大小就必须为4的整数倍,那么我们看图,占内存最小的就是12,那么这个结构体的大小就为12字节;
大家可以看到,上面是在VS2022环境下算出的结构体的大小,与我们上面的推理结果一致;希望大家可以通过这个例子理解对齐规则;
为了让大家更好地掌握对齐规则,下面有几道例题,大家一起来看一下;
//例题1
#include<stdio.h>
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
struct S2 s;
printf("%zd\n", sizeof(s));
}
大家来看一下这道题,和前面的分析方法一样,首先来对齐第一个成员,第一个成员是char类型,占1字节;
第二个成员,依然是char类型,VS默认值为8,char为1字节,所以其对齐数就是1,那么第二个成员将对齐到偏移量为1的地址处;
第三个成员,为int类型,大小4字节,VS默认值是8,所以其对齐数为4,那么第三个成员就需要对齐到4的整数倍的地址处,那么最近的就是偏移量为4的地址处,从那里往后占4字节;
综上,我们将三个成员都存进去了,下面我们要确定整个结构体的大小,很容易知道,最大对齐数为4,那么整个结构体的大小就必须为4的倍数,我们可以发现,前面我们刚好占用了8个字节,8又刚好是4的整数倍,所以整个结构体的大小就是8字节。
//例题2
#include<stdio.h>
struct S3
{
double d;
char c;
int i;
};
int main()
{
struct S3 s;
printf("%zd\n", sizeof(s));
}
这道题还是同样的分析方法;
首先来看第一个成员,double类型,我们知道它占8字节;
第二个成员,char类型,大小为1字节,默认值是8,那么其对齐数就是1,那么第二个成员就要对齐在1的整数倍的地址处,前面已经存过了第一个成员,那么第二个成员就可以放在偏移量为8的地址处;
第三个成员,int类型,大小4字节,默认值为8,其对齐数就为4,那么就应该对齐在偏移量为12的地址处,占4字节;
那么最终大家可以发现,我们一共占用了16个字节,我们知道最大对齐数为8,那么16是8的整数倍,所以这个结构体的大小就为16字节。
//例题3
#include<stdio.h>
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
struct S4 s;
printf("%zd\n", sizeof(s));
}
这道题大家要注意它和前两道是不一样的,它出现了嵌套结构体的情况,那么这个时候大家就要注意上面对齐规则中的第四条规则了;
第一个成员,char类型。大小1字节,直接存到起始位置;
第二个成员,这是一个结构体,那么第四条规则告诉我们:结构体成员对齐到自己的成员中最大对齐数的整数倍处,那么从例题2我们知道,S3结构体中最大对齐数是8,所以S3必须对齐到8的整数倍的地址处,在这里就需要对齐到偏移量为8的地址处,占16字节;
第三个成员,double类型,大小8字节,VS默认值为8,所以其对齐数为8,那么成员三就需要对齐到8的整数倍的地址处,大家自己画图会发现,就是偏移量为24的地址处,占8字节;
综上,我们一共占用了32个字节,那么整个结构体的大小是多少呢?再来看一下第四条规则,整个结构体的大小必须为所有最大对齐数的整数倍,那么我们知道,在S4结构体中,最大的对齐数为8,所以整个S4结构体的大小就要为8的整数倍,刚才说一共占用了32字节,32刚好是8的整数倍,那么S4结构体的大小就是32字节。
OK,上面我们讨论了几道关于结构体内存对齐的例题,希望大家通过这些例题能够更好地掌握内存对齐的规则;
不知道大家是否注意到,在内存对齐的过程中,其实造成了空间的浪费,那么我们为什么还要遵循内存对齐规则呢?下面我们继续来讨论为什么会有内存对齐?
4.2 为什么存在内存对齐
4.2.1 平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
4.2.2 性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用⼀个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法
那么,我们有没有一种方式,既满足对齐,又能节省空间呢?
大家来看下面的代码;
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
大家观察这段代码,并且取计算一下这两个结构体的大小;
这里大家发现,两个结构体虽然成员一样,但是最终大小却不一样;这里大家就要注意一点,在创建结构体的过程中,我们要让占有空间小的成员尽量集中在一起,这样我们就可以在满足对齐的情况下尽量节省空间。
4.3 修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数。
5. 结构体传参
#include<stdio.h>
struct S
{
int data[1000];
int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
大家看看上面的代码,一个是传结构体本身;还有一个是传结构体的地址;那么哪种方式更好呢?答案是print2;
为什么呢?
结论: 结构体传参的时候,要传结构体的地址。
6.结构体实现位段
6.1 位段的概念
位段的声明和结构是类似的,有两个不同:
1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型。
2. 位段的成员名后边有⼀个冒号和⼀个数字。
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
大家可以发现,位段是专门用来节省内存的;
这里还有一个问题,位段A的大小的多少呢?
大家可以自己去测试一下,相信很多同学的第一反应是6(因为有一共47个bit);
大家发现,这个结果并不是6,那这是什么原因呢?咱们继续看下面的内容;
6.2 位段的内存分配
1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
这里给大家举个例子,咱们来看下面的代码;
#include<stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
printf("%zd", sizeof(s));
return 0;
}
这里大家可以思考一下结果;
大家仔细看上面这张图,它描述了位段申请空间的过程;这里大家注意,每一个小格子代表一个bit;
这里一次开辟一个字节(8bit),在VS 中,在一个字节内部空间是从右向左进行使用的;那么剩余的空间是继续使用,还是浪费掉呢?在VS中,答案是浪费掉。最终我们可以发现,一共申请了3个字节的空间。
每个方块儿中存放的是对应数据的二进制,由于空间有限,所以数据并不能完整地存放到空间中,就会发生截断的现象;所以最终我们通过调试就可以看见位段s中的数据;
大家注意这里是十六进制的形式进行表示的;OK,到这里,关于位段内存分配的内容就为大家介绍完了;
6.3 位段的跨平台问题
1. int位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较大,无法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结: 跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
6.4 位段的应用
这里的内容大家作了解即可,在这里不需要深入研究,后面在学习网络的相关知识的时候,
6.5 位段使用的注意事项
位段的几个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位 置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。
所以不能对位段的成员使⽤&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在⼀个变量中,然后赋值给位段的成员。
int main()
{
struct A sa = {0};
scanf("%d", &sa._b);//这是错误的
//正确的⽰范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}
大家看一下上面的一段代码,我们是不能对位段的成员进行取地址操作的。
7. 总结
本篇文章为大家介绍了结构体的相关内容,结构体在C语言中也是一种重要的自定义类型,在后面的学习中大家会经常使用,所以本篇内容,建议大家认真阅读,注意其中的细节,掌握结构体的使用方法以及关于结构体的对齐规则;最后,希望本篇博客可以为大家带来帮助,谢谢!