文章目录
1.结构体的声明与初始化1.1 普通声明1.2 创建及初始化1.3 特殊声明1.4 结构体的自引用 2.结构体内存对齐2.1 对齐规则2.2 结构体对齐存在的重要性2.2.1 平台原因2.2.2 性能原因 2.3 修改默认对齐数 3.结构体传参4.位段的实现4.1 什么是位段4.2 位段的内存分配4.3 位段的跨平台问题4.4位段的使用 希望读者们多多三连支持小编会继续更新你们的鼓励就是我前进的动力!
前面在学习操作符的时候,已经对结构体有了初步了解,结构体不仅在C语言中经常使用,也为C++学习类和对象打下基础,本篇 vlog 将对结构体进行详细的解析
传送门:关于我、重生到500年前凭借C语言改变世界科技vlog.10——进制转化&&操作符进阶
1.结构体的声明与初始化
数组用于存放同类型的数据,而结构体是用于存放不同类型变量的函数的集合
其语法形式为:
struct tag{ member-list;}variable-list;
1.1 普通声明
比如我们想要描述一个学生
struct Stu{ char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号}; //分号不能丢
声明只需要写出返回类型,参数,函数名
返回类型是 struct ,参数是学生的信息,函数名是 Stu
1.2 创建及初始化
struct 是一种自定义类型,那么就规定需要为其创建对象,那么如何理解对象呢?
假设类是一个大房子,struct 就是房子的类型,Stu就是房子的名字,房子里入住的人就是对象,对象能够在同一种类中,但是他们的个人信息可以不同
初始化的方式有两种:
1)
struct Stu{ char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号}s1,s2;
在 variable-list 处写出对象名
2)
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;}
也可以在结构体外创建对象,根据之前讲过的操作符,可以按结构体的默认顺序来初始化,也可以用结构体成员访问操作符直接或间接访问,这里不涉及指针,所以不用 ->
1.3 特殊声明
声明结构体可以不完全声明
//匿名结构体类型struct{ int a; char b; float c;}x;struct{ int a; char b; float c;}a[20], *p;
这两个结构体省略了函数名,编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的,匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次
1.4 结构体的自引用
struct Node{ int data; struct Node next;};
这个代码是错误的
要确定一个 struct Node 实例的大小,就需要先确定它内部成员 next 的大小(因为结构体成员是依次排列在内存中的),但 next 本身又是 struct Node 类型,要确定它的大小又得先确定它内部 next 的大小…… 这样就陷入了无限循环的困境,编译器无法准确计算出 struct Node 结构体到底应该占用多少内存空间,所以这种写法在内存布局上就是不合理且无法实现的
修改后:
struct Node{ int data; struct Node* next;};
通过让 next 指针指向另一个 Node 结构体,就可以实现数据元素之间的链式存储关系
那如果夹杂了 typedef 对匿名结构体类型重命名呢?
typedef struct{ int data; Node* next;}Node;
答案是错误的,他在对 struct 重命名完成之前就在内部使用了重命名后的名字
2.结构体内存对齐
那么结构体的大小该如何计算呢?
这也是近几年面试题竞赛题常考的点
2.1 对齐规则
结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为 0 的地址处其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处• 对齐数 = 编译器默认的⼀个对齐数与该成员变量大小的较小值
• VS 中默认的值为 8(可修改)
• Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的整数倍如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
是不是看的感觉有些迷糊?举个例子你就明白了
代码1
struct S1{ char c1; int i; char c2;};printf("%d\n", sizeof(struct S1));
本题的答案是 12
刚开始 c1 对其到地址为 0 处,然后比较 i 和 8 发现 i 小,所以这里对齐数为 4 ,所以在 4 的尽可能最小倍数处对齐,即地址为 4 处,同理 c2 与 8比较发现 c2 小,所以这里对齐数为 1,所以在 1 的尽可能最小倍数处对齐,则总体大小为最大对齐数的尽可能最小倍数,即最大对齐数为 4,则总体大小为 12
代码2
struct S4{ char c1; struct S1 s1; double d;};printf("%d\n", sizeof(struct S4));
答案是 24
这里说一下 struct S1 的最大对齐数为 4 ,大小为 12 ,其他都与代码 1 的处理方式同理
2.2 结构体对齐存在的重要性
结构体的内存对齐是拿空间来换取时间的做法
2.2.1 平台原因
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
2.2.2 性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐,原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问,假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数,如果我们能保证将所有的 double 类型的数据的地址都对齐成8的倍数,那么就可以用⼀个内存操作来读或者写值了,否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中
比如刚才的代码1
如果没有内存对齐,假设一个机器是 4 位读取,那么 int i 就需要分两次才能得到这个数据,或许你觉得这没什么,但是在大型的项目的数据里,这种方式是十分低效率的,按照对齐规排序能够保证读取能够读取一个完整数据
2.3 修改默认对齐数
结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数
#include <stdio.h>#pragma pack(1)//设置默认对⻬数为1struct S{ char c1; int i; char c2;};#pragma pack()//取消设置的对⻬数,还原为默认int main(){ //输出的结果是什么? printf("%d\n", sizeof(struct S)); return 0;}
#pragma 这个预处理指令,可以改变编译器的默认对齐数
3.结构体传参
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;}
看过我前面介绍传值调用和传址调用的uu们应该知道,传地址的效果明显好很多
原因:
• 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销
• 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导 致性能的下降
4.位段的实现
4.1 什么是位段
位段的声明和结构与结构体类似
不同的是:
位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型位段的成员名后边有一个冒号和一个数字举个例子:
struct A{ int _a:2; int _b:5; int _c:10; int _d:30;};
这里的 A 就是一种位段,后面的数字表示其所占的位数,单位是比特
可是一个 int 类型所占的比特位不应该是32位吗?
4.2 位段的内存分配
位段的内存分配准确来说和结构体内存分配不同的地方在于位段尽可能压缩了内存的占用,但其局限性在于限制了比特位,只能输出特定范围的数据
位段的成员可以是 int unsigned int signed int 或者是 char 等类型位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段举个例子:
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;
这里设置 a b c d 的内存占用比特位为 3 4 5 4(注意不要将其误认为初始化),为这几个数赋值 10 12 3 4
VS2013测试数据:
由分析可得,冒号后的位数表示占用比特位,放进去的数由于位数的限制,会导致其只能输出限制位所能表达的二进制数
4.3 位段的跨平台问题
int 位段被当成有符号数还是无符号数是不确定的位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的所以跟结构体相比,位段能够起到很好的节省空间的效果,但是其平台移植性差,其他的机器不一定能够适应其结构
4.4位段的使用
首先我们要知道内存中每个字节分配一个地址,一个字节内部的 bit 位是没有地址的,而位段的地址不在某个字节处,所以我们不能在 scanf 函数中使用 & 进行取地址赋值
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;}
正确的方法是先对某一个变量进行初始化赋值,然后再将该变量赋值给位段,进行二进制位数的处理,从而达到位段的效果