🔥个人主页:艾莉丝努力练剑
🍓专栏传送门:《C语言》
🍉学习方向:C/C++方向
⭐️人生格言:为天地立心,为生民立命,为往圣继绝学,为万世开太平
前言:前面几篇文章介绍了c语言的一些知识,包括循环、数组、函数、VS实用调试技巧、函数递归、操作符、指针、字符函数和字符串函数、C语言内存函数、数据在内存中的存储等,在这篇文章中,我将开始介绍结构体的一些重要知识点!对结构体感兴趣的友友们可以在评论区一起交流学习!
一、结构体类型的声明结构体我们之前在操作符那一块介绍过,一起来回顾一下:
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
(一)结构的声明及结构体变量的创建和初始化1、结构的声明原型如下:
代码语言:javascript复制struct tag
{
member-list;
}variable-list; variable-list可以不写,但是分号不能丢:
代码语言:javascript复制struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//分号不能丢 2、结构体变量的创建和初始化代码语言:javascript复制#include
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu s = { "张三", 20, "男", "20230818001" };
printf("name: %s\n", s.name);
printf("age : %d\n", s.age);
printf("sex : %s\n", s.sex);
printf("id : %s\n", s.id);
//按照指定的顺序初始化
struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex =
"⼥" };
printf("name: %s\n", s2.name);
printf("age : %d\n", s2.age);
printf("sex : %s\n", s2.sex);
printf("id : %s\n", s2.id);
return 0;
}(二)结构的特殊声明在声明结构时,可以不完全声明:
下面这个就是匿名结构体类型:
代码语言:javascript复制struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], * p;上面的两个结构在声明的时候省略掉了结构体标签(tag)。
思考一下,在上面代码的基础上,下面的代码合法吗?
p = &x;
注意:
1、编译器会把上面的两个声明当成完全不同的两个类型,因此是非法的;
2、匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能用一次。
(三)结构的自引用在结构中包含一个类型为该结构本身的成员是否可以哩?
打个比方,我们定义一个链表的节点:
代码语言:javascript复制struct Node
{
int data;
struct Node next;
};这个代码对吗?仔细想想,是不行的,因为一个结构体再包含一个同类型的结构体变量,这样结构体变量的大小就会无穷大,这是不合理的。
正确的自引用方式:
代码语言:javascript复制struct Node
{
int data;
struct Node* next;
}; 在结构体自引用的使用过程中,夹杂了typedef对匿名结构体类型的重命名,也容易引入问题:
代码语言:javascript复制typedef struct
{
int datas;
Node* next;
}Node; 这个代码可行吗?答案是不行。因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量是不行的。
解决方案:定义结构不要再使用匿名结构体了
代码语言:javascript复制typedef struct Node
{
int data;
struct Node* next;
}Node;二、结构体内存对齐到这里,我们已经掌握了结构体的基本使用了,接下来我们将深入研究如何计算结构体的大小。这就是一个十分热门的考察点:结构体内存对齐。
(一)对齐原则首先得掌握结构体的对齐原则:
1、结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处;
2、其他成员变量要对齐到某个数字(对齐数)整数倍的地址处;
对齐数 = 编译器默认的一个对齐数与该成员自身的较小值;
VS中默认的值为8
Linux中gcc没有没人对齐数,对齐数就是成员自身的大小
3、结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍;
4、如果嵌套了结构体的情况,嵌套的结果体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
我们这里有四道例题,大家做一做:
代码语言:javascript复制//练习1
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
//练习2
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
//练习3
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
//练习4-结构体嵌套问题
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4)); 例题1:
例题2:
例题3:
例题4:
(二)内存对齐存在的原因为什么会存在内存对齐?大部分参考资料上都是说有平台原因(移植原因)和性能原因这两个原因,那我们这里就介绍一下这两个原因:
1、平台原因(移植原因) (1)不是所有的硬件平台都能访问任意地址上的任意数据的;
(2)某些硬件平台只能在某些地址处取某些特定类型的数据,否则会跳出硬件异常的报错。
2、性能原因 数据结构(尤其是栈)应该尽可能在自然边界上对齐,原因:为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问只需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数,如果我们能让所有的double类型数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值哩。否则,我们可能就需要执行两次内存访问,因为对象可能被分放在两个8字节内存块里头。
总体来说:结构体的内存对齐是拿空间换取时间的做法。
在设计结构体的时候,我们既要满足对齐,又要节省空间:
让占用空间小的成员尽量集中在一起
举个例子:
代码语言:javascript复制struct S1
{
char c1;
int i;
char c2;
};
struct s2
{
char c1;
char c2;
int i;
};s1和s2类型的成员一模一样,但是s1和s2所占空间的大小有一些区别。
(三)修改默认对齐数 #pragma这个预处理指令可以改变编译器的默认对齐数。
代码语言:javascript复制//修改默认对齐数
#include
#pragma pack(1)//设置默认对齐数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack(2)//取笑设置的对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S));
return 0;
} 结果是:
注意:在结构体对齐方式不适合的时候,我们可以自己更改默认对齐数。
三、结构体传参代码语言:javascript复制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;
}这里的print1和print2函数哪个好一些?
答案是: 首选print2函数。
原因如下图中所示:
即
1、函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销;
2、如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销会比较大,所以会导致性能的下降。
结论: 结构体传参的时候,要传结构体的地址。
四、位段 我们介绍完了结构体,接下来就得说说结构体实现位段的能力了。
介绍位段,还是老样子,我们先得知道什么是位段,才能接着介绍像位段的内容分配、位段的跨平台问题、位段的应用以及使用的注意事项这些知识点,不要着急,听我慢慢道来!
(一)什么是位段位段的声明和结构体类型类似,区别在两点:
1、位段的成员类型必须是int、unsigened int或者sigede int,在C99标准中,位段成员的类型也可以选择其他类型;
2、位段的成员后面有一个冒号和一个数字。
咱们举个例子,就比如说:
代码语言:javascript复制struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};这里的2、5、10、30表示这个成员要占用的比特位的数量,也就是说:
数字表示这个成员要占用的比特位的数量
这里A就是一个位段类型。要知道位段A所占内存大小是多少,我们这样打印一下:
代码语言:javascript复制printf("%d\n", sizeof(struct A));结果是8:
这里我们可以发现,当我们设计成位段类型的时候,所占空间明显变小了,反过来,当我们设计成普通的结构体的时候(打印出来是16),占的空间明显就变大了。
这里八个字节相当于每个成员只分配给它两个字节,成员们都不可能完整地占四个字节,像_a,它能表示的值我们就只能写出这四种:
00 01 10 11
位段式的的结构虽然能够节省空间,但它也有约束,就是说位段的每个成员所占空间比较小,它能表示的范围也比较小,那这位段式什么时候用呢? 就是在你能明确这个成员就占几个比特位而且不会超过这个比特位的时候,我们就可以用位段式的结构了。
那我们就明确了,当我们知道几个成员占几个比特位且不会超过这个比特位的时候我们就可以用位段式的结构设计了,好处很明显:就是节省空间。
(二)位段的内存分配1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型;
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的;
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
代码语言:javascript复制struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4; 我们思考一下,位段内存的空间是怎么开辟的?
我们这里有两个不确定的点:
1、一个字节(整型)的内存中,到底是从左向右使用的,还是从右向左使用的,这点不确定
我们只能先假设从右向左使用(后面经验证,在VS上正确)
2、剩余的空间不能满足下一个成员的时候,是否造成空间浪费,也不确定(这里我们假设浪费,进后来验证,在VS上正确)
就像这样:
验证一下:
结果就是我们推测的:62 03 04
以下是在VS2013环境测试得到的结果:
再画个图理解一下:
intl类型,先申请4个字节,这里像30的话空间不够, 会再向内存申请4个字节,还是从右向左,如图。这样加起来就是我们之前得到的所占8个字节的的结果啦。
(三)位段的跨平台问题介绍完位段的内存分配,接下来我们就来说说位段的跨平台问题,这也是位段的缺点,不能说是缺陷,对于位段我们要合理运用。
主要有以下几点:
1. int 位段被当成有符号数还是无符号数是不确定的;
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题;
3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义;
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结一下: 跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
原因:在于位段的标准尚未定义,不同的平台可能会有不同的实现。
(四)位段的应用 在网络协议中,从IP数据报的格式,我们可以看到其中很多的属性只需要几个比特位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较小一些,对网络的畅通是有帮助的。
像网上和别人交流,实现数据的交互 ,比如张三给李四发了个“hehe”,就是这个道理;再比如,我们网上购物,快递也不是就给你发个商品,有打包、有各种信息,像电话号码、家庭地址、从哪来到哪去之类的:
(五)位段使用的注意事项注意事项:
1、位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的比特位是没有地址的。2、因此我们不能对位段的成员使用&操作符,这样也就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。
代码语言:javascript复制struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A sa = { 0 };
scanf("%d", &sa._b);//这是错误的
//正确的示范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}结尾往期回顾:
结语:本篇文章就到此结束了,本文为友友们分享了结构体相关的一些重要知识点,如果友友们有补充的话欢迎在评论区留言,下一篇博客,我们将介绍另一种自定义类型:联合和枚举,敬请期待,感谢友友们的关注与支持!