结构体
1、 结构体的声明方法
struct struct_name {
data_type member1;
data_type member2;
.
.
};
这是其中一种声明方式~
2、定义一个结构体变量
struct struct_name variable;
3、访问成员变量
. 运算
一个结构体变量访问其成员时,使用的是 . 运算
下面的代码就用到了 . 运算,可以运行观察一下结果:
#include "stdio.h"
#include "stdint.h"
struct rectangle {
uint32_t length;
uint32_t width;
};
int main()
{
//定义变量的同时并初始化成员的值
struct rectangle rec = { 20,30 };
//使用 . 运算打印其成员的值
printf("length = %d\nwidth = %d\n", rec.length, rec.width);
//修改成员的值
rec.length = 30;
rec.width = 15;
printf("修改之后的值:\n");
printf("length = %d\nwidth = %d\n", rec.length, rec.width);
return 0;
}
-> 运算
这个 -> 运算主要是结构体指针变量访问其成员时用到的,这是一个 减号 - 与 大于号> 组成的
将上面的那个例子修改为:
#include "stdio.h"
#include "stdint.h"
#include "malloc.h"
struct rectangle {
uint32_t length;
uint32_t width;
};
int main()
{
//定义一个结构体指针,并为其开辟空间
struct rectangle* rec_ptr = (struct rectangle*)malloc(sizeof(struct rectangle));
//初始化成员的值
rec_ptr->length = 30;
rec_ptr->width = 15;
//定义一个结构体变量
struct rectangle rec;
//初始化结构体变量rec成员的值
rec.length = 40;
rec.width = 20;
//打印成员的值
printf("结构体变量 rec:\n");
printf("length = %d\nwidth = %d\n", rec.length, rec.width);
printf("结构体指针 rec_ptr:\n");
printf("length = %d\nwidth = %d\n", rec_ptr->length, rec_ptr->width);
return 0;
}
上面的代码中用到了 malloc 这个函数,这个函数的作用是动态内存申请
计算机中分为栈区与堆区两部分,在C语言中基本数据类型声明的变量、函数等等都是存储在栈上,而malloc函数申请的内存是在堆区上的
4、结构体对齐方式
结构体的一般对齐方式是按照结构体中成员类型字节数最大的来对齐
看下面的结构体:
struct A {
char c;
int n;
};
由于最大的类型为 int 所以该结构体以 4 字节对齐
使用关键字 _Alignof 来输出一下对齐字节数
printf(“%zd\n”, _Alignof(struct A));
运行结果与预期的一样:4
我们再看一下这个结构体:
struct B {
char c;
char x;
int n;
float f;
double lf;
};
还是按照刚才的思路分析,先找到最大的类型的成员,我们可以得到最大类型的为 double 8 字节,所以推测出结构体 B 为 8 字节对齐
使用关键字 _Alignof 来验证一下对齐字节数是否为 8 字节对齐
printf(“%zd\n”, _Alignof(struct B));
运行结果与预期的一致:8
综上,所以说这个结论: 结构体的一般对齐方式是按照结构体中成员类型字节数最大的来对齐 是正确的
想一下对齐字节数的大小会影响什么呢?
5、结构体大小
如何计算一个结构体所占的字节数呢?
例如下面这个结构体:
struct C {
char c;
char x;
int n;
};
我想应该有人会说是6字节,计算方法就是把结构体中所有成员的类型字节之和
其实再仔细想想,然后再结合一下结构体对齐方式,此时你就会感觉算错了
一个结构体所占用的字节大小与结构体的对齐方式相关
首先我们需要先确定结构体 C 是以几字节对齐的,由上面的结论可以明显知道是以 4 字节对齐
此时我们知道了以 4 字节对齐,但有什么用呢? 别急,我们一步一步来
结构体是以 4 字节对齐,那成员呢? 结构体中的成员是按照成员的类型来进行对齐,若是不足结构体对齐的整数倍则编译器会自动填充(这部分不太会表达),所以成员 c、x是按照 1 字节对齐,二者共占用 2 个字节,此时不足 4 的倍数则由编译器进行填充,所以成员 c、x 占用了 4 个字节;成员 n 本身是 int 类型,是4的整数倍则不需要编译器进行填充,综上结构体 C 的所占字节数为:8
下图为成员的分布情况:
若是我们更换一下成员的定义顺序会不会改变结构体的大小呢? 答案是会的
还以结构体 C 为例,我们更改一下成员定义次序:
struct C {
char c;
int n;
char x;
};
此时成员的分布情况:
由图可以清楚的看到结构体大小变成了 12
由此可见,合理的规划成员的定义次序可以减少内存的使用
以上两个例子可以使用关键字: sizeof 打印看一下
6、结构体及成员的地址情况
在数组中我们知道,数组名就是数组的起始地址,而在结构体中,结构体的地址与结构体中第一个成员的地址一样
运行一下以下代码即可辨别真伪:
#include "stdio.h"
struct D {
char c;
int n;
char x;
};
int main()
{
struct D tmp;
printf("struct adder: %p\n", &tmp);
printf("member_1 adder: %p\n", &tmp.c);
return 0;
}
运行结果如下:
struct adder: 0x7ffd493eb520
member_1 adder: 0x7ffd493eb520
那第二个成员地址呢?第三个呢?第四个呢? …
其实想得到也不难,但是又牵扯到字节对齐问题了,第二个成员地址是第一个成员地址加上对齐字节数,我们可以验证一下:
struct D tmp;
printf("struct adder: %p\n", &tmp);
printf("member_1 adder: %p\n", &tmp.c);
printf("member_2 adder: %p\n", &tmp.n);
printf("member_3 adder: %p\n", &tmp.x);
输出结果如下:
struct adder: 0x7ffeb1a6f63c
member_1 adder: 0x7ffeb1a6f63c
member_2 adder: 0x7ffeb1a6f640
member_3 adder: 0x7ffeb1a6f644
此时如果我们更换一下成员定义次序,又会出现什么情况呢?
将代码更改为:
struct D {
char c;
char x;
int n;
};
再运行一下这段代码:
struct D tmp;
printf("struct adder: %p\n", &tmp);
printf("member_1 adder: %p\n", &tmp.c);
printf("member_2 adder: %p\n", &tmp.x);
printf("member_3 adder: %p\n", &tmp.n);
此时输出结果如下:
struct adder: 0x7ffd95a5c6a0
member_1 adder: 0x7ffd95a5c6a0
member_2 adder: 0x7ffd95a5c6a1
member_3 adder: 0x7ffd95a5c6a4
结果又发生了变化,联想一下前面的成员分布情况可以明白为啥得出这个结果
因此,结构体开辟的内存也是连续的
不行了,语言组织能力太弱了,就这样吧,结构体相关的就到此结束,下面再讨论一下共用体
共用体
在C语言中,共用体也是一种构造类型
共用体,共用体,它的特点就体现在 共用 二字上,所谓的共用就是一块地址,共用体内的所有成员共同使用,而这个地
址的大小则有成员中最大数据类型的那个决定,举一个我们都知道的例子:
我们可以把共用体想象成一个QQ群,群主与管理员相当于共用体成员,这个群能加入的人员数目上限相当于共用体的内存
在这个例子中,群主就是"老大",决定着“人员数目”的上限,群主的“身份”改变,就意味着“人员数目”上限的改变
群主与管理员都可以对人员数目进行"管理",但是决定“人员数目”上限的权利还是掌握在群主手里
共用体的声明、访问成员的值、改变成员的值等等都与结构体类似,把关键 struct 更换成 union 即可,就不一一解释了,重点是看一下二者的不同
1、内存大小
我们先声明好一个共用体:
union Data {
char c;
short s;
int x;
};
首先按照共用体的定义先分析一下上面共用体 Data 的一些参数,根据定义可知共用体的内存大小由:成员中最大数据类型的那个决定,所以推断出共用体Data的内存大小就是 4
printf("%d\n", sizeof(union Data));
通过printf运行的结果可得,我们的推断成立
2、改变成员的数据
还用上面那个共用体,定义一个共用体变量,然后进行初始化:
int main()
{
union Data tmp;
tmp.c = 'A';
tmp.s = 72;
tmp.x = 99;
printf("%c %d %d\n", tmp.c, tmp.s, tmp.x);
return 0;
}
先别看下面的分析,自己先分析一下运行结果~
我们运行之后发现结果是: c 99 99
是不是很惊讶,先别惊讶,我们一步一步分析
这个对应定义中的: 共用体中的所以成员共同使用同一块内存,第一步先往这块地址写入字符’A’,然后又写入 77,最后又写入99,最终这块地址上的值就是99了,正好对应输出结果
通过上面的结果可以总结为一句话:在修改共用体内的某个成员的值时,其他的成员的值也会跟着改变
是不是感觉共用体没多大用处,刚开始我也是这样认为的,当我接触到嵌入式开发时就发现了它的骚操作,会在下一章节来说明它的一个用处
位域
有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可。例如在单片机开发中,LED有点亮与熄灭两种状态,用0和1进行表示即可,此时可以使用位域这一数据结构。
在定义结构体时,我们可以指定某个成员所占用的二进制位数,使用方法如下:
struct domain {
unsigned int word;
unsigned char status: 1;
unsigned int half: 16;
};
: 后面的数字代表成员所占用的二进制位数,对于上面的代码来说,成员word没有被限制位数,所以word是32位的大小,成员status、half被 : 后面的数字限制了,依次为1bit、16bit
被限制后的成员存储的数据范围会发生变化,成员word没有被限制,能存储的数范围为:0~2^32,成员status被 : 限制为1bit,因此只能存储0或者1,同理成员half能存储的范围为:0~2^16。若是在赋值时,超出范围会报这种错误:
struct domain test;
test.status = 8;
main.c: In function ‘main’:
main.c:13:23: warning: unsigned conversion from ‘int’ to ‘unsigned char:1’ changes value from ‘8’ to ‘0’ [-Woverflow]
13 | test.status = 8;
|
接下来我们来看一下此时的结构体domain所占用的内存与内存分布情况
由上面的介绍我们可以直接得出结构体domain是以4字节对齐,内存分布情况是这样的:
所占内存大型为8字节。
可以使用sizeof计算一下:
printf(“size = %ld\n”, sizeof(struct domain));
输出结果:size = 8
接着我们改变一下成员的声明顺序,然后再看一下内存分布与内存大小,我们改变成下面这样:
struct domain {
unsigned char status : 1;
unsigned int word;
unsigned int half : 16;
};
此时的内存分布情况是这样的:
printf(“size = %ld\n”, sizeof(struct domain));
输出结果:size = 12
由此可见,合理的规划成员的定义次序可以减少内存的使用
以上就是对结构体、共用体、位域的相关介绍,有问题的地方请在评论区指出