C语言——结构体讲解

目录

一、结构体类型的声明

二、结构体变量的定义和初始化 

三、结构体的重命名

四、结构体的自引用 

 五、结构体内存对齐

六、结构体传参

七、结构体实现位段

7.1  什么是位段

7.2  位段的声明和使用

7.3  位段的空间大小计算

7.4  位段的内存分配

7.5  位段的跨平台问题

7.6  位段的应用

7.7  位段使用的注意事项 

C语言已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类 型还是不够的,假设我想描述学生,描述⼀本书,这时单⼀的内置类型是不行的。描述⼀个学生需要名字、年龄、学号、身高、体重等;描述⼀本书需要作者、出版社、定价等。C语言为了解决这个问题,增加了结构体这种自定义的数据类型,让程序员可以自己创造适合的类型。

 结构体,他就将不同类型的数据存放在一起,作为一个整体进行处理。

一、结构体类型的声明

struct tag  //结构体标签tag
{member - list; //括号里面就是写成员变量的地方,可以写多个变量并且可以是不同类型的
}variable - list; //这个地方可写可不写,写了的话就是创建了一个全局的该结构体变量struct tag
{member - list;
}* variable - list;//在这个地方加一个*号就是该结构体指针类型*variable - list就是一个全局的结构体指针变量int main()
{struct stu s;//在这个地方创建的结构体变量是局部的结构体变量return 0;
}

例如描述⼀个学生: 

struct Stu
{char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号
}; //分号不能丢

特殊声明: 

在声明结构体的时候,可以不完全的声明。

//匿名结构体类型
struct  //匿名结构体类型就是省略了结构体标签
{int a;char b[5];char c;
}st1,st2; //但是只能在这个位置创建该结构体变量且是全局的,也可以创建多个变量struct
{int a;char b[5];char c;
}*t1,st2;//这个地方加一个*,t1就是该结构体指针类型,但是st2并不是指针。也是属于匿名结构体(指针)类型int main()
{t1 = &st1;//这个地方是错误的,编译器会将这两种声明当成完全不同的两个类型,所以是非法的return 0;
}

二、结构体变量的定义和初始化 

struct Point
{int x;int y;
}p1; //声明类型的同时定义变量p1(全局变量)
struct Point p2; //定义结构体变量p2(全局变量)//初始化:定义变量的同时赋初值。
struct Point p3 = { x, y };  //(全局变量)
struct Stu        //类型声明
{char name[15];//名字int age;      //年龄
};
struct Stu s = { "zhangsan", 20 };//初始化(全局变量)
struct Node
{int data;struct Point p;struct Node* next;
}n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化(全局变量),含别的类型的结构体时初始化就在中间加一个大括号初始化嵌套的别的结构体
struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始化(全局变量)int main()
{struct Node n3 = {30, {6,7}, NULL};//结构体嵌套初始化(局部变量)
}

三、结构体的重命名

结构体的重命名就是给一个结构体类型重新起一个名字

使用typedef关键字

比如,不想要struct前缀就可以重命名:

typedef struct Node//在struct前面加一个typedef
{int data;
}Node;//在这里写下新的名字int main()
{Node d1;//这里就定义了一个Node类型的结构体struct Node d2;//但是加上struct也是可以的~return 0;
}

四、结构体的自引用 

在结构中包含一个类型为该结构本身的成员就是结构体的自引用

struct Node
{int data;struct Node* next;//带一个自身结构体的指针类型就是结构体的自引用
};

我们看一下错误的自引用

//错误的自引用:
//ex1
struct Node
{int data;struct Node next;//结构体的自引用只能引用自身结构体指针!!!
};
//ex2
typedef struct Node
{int data;Node* next;//结构体的自引用不能使用重命名的名字,因为还没有重命名成功。
}Node;

 五、结构体内存对齐

内存对齐是一个存储数据的规则,可以依此来计算结构体的大小

我们先来猜一下两个结构体的大小是多少:

struct node1
{char a;int i;char b;
};struct node2
{int i;char a;char b;
};
int main()
{printf("%d\n%d\n", sizeof(struct node1), sizeof(struct node2));return 0;
}

 

虽然两个结构体的成员是一样的,但是结构体的大小确实不一样的,这就要考虑内存对齐了。

每个成员都要在该成员对齐数的整数倍的偏移量处开始存放!

结构体内存对齐的规则:

1. 第一个成员在与结构体变量偏移量为0的地址处。

2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

     对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值

3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的      整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

接下来还是参照上面的两个结构体来说明: 

从0偏移量开始,

a为char类型占一个字节,默认对齐数位8,较小值为1,所以对齐数为1,从0偏移量的地方开始存放;

i为int类型,占四个字节,默认对齐数为8,较小值为4,所以对齐数为4,要放在偏移量为4的倍数的地方,从而就跳过了三个字节的空间;

b与a相同,如上图所示,直接放到8偏移量的位置。

存放完所有数据后,共占用的9个字节,该结构体最大的成员变量对齐数为4,而9并不是4的倍数,所以要多浪费3个字节,最终该结构体的大小就为12.

与上一个结构体成员一样,只不过成员的顺序不同,结果也不同;

这里也就不赘述了,规则都是一样的。

为什么存在内存对齐?

1. 平台原因(移植原因):

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常。

2. 性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总的来说,结构体的内存对齐是拿空间来换时间的做法。

小tips:

在声明结构体时,将相同类型的数据放在一起,可以节省空间!

修改默认对齐数:

使用预指令#pragma来改变默认对齐数;

#include <stdio.h>
#pragma pack(2)//设置默认对齐数为8
struct S1
{char c1;int i;char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{char c1;int i;char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{//输出的结果是什么?printf("%d\n", sizeof(struct S1));//结果为8printf("%d\n", sizeof(struct S2));//结果为6return 0;
}

#pragma pack(n); //设置默认对齐数为n

#pragma pack(); //恢复默认对齐数

六、结构体传参

结构体传参可以传结构体,也可以传结构体地址;

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;
}

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

所以一般结构体传参的时候要传结构体地址。性价比高。

七、结构体实现位段

结构体讲完就得讲讲结构体实现位段的能力。

7.1  什么是位段

       位段(或称“位域”,Bit field)为一种数据结构,可以把数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作。这种数据结构的好处:

  • 可以使数据单元节省储存空间,当程序需要成千上万个数据单元时,这种方法就显得尤为重要。
  • 位段可以很方便的访问一个整数值的部分内容从而可以简化程序源代码。

  而位域这种数据结构的缺点在于,其内存分配与内存对齐的实现方式依赖于具体的机器和系统,在不同的平台可能有不同的结果,这导致了位段在本质上是不可移植的

位段的声明和结构是类似的,有两个不同:

1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型。

2. 位段的成员名后边有⼀个冒号和⼀个数字。

7.2  位段的声明和使用

虽然位段可以决定用多少位来储存数据,但是切不可认为位段就是可以自定义一个数据类型。位段是依赖结构体来实现的,我们可以认为位段是可以将一个盒子里面格子自定义大小。

位段的声明:

struct A
{int _a : 2;int _b : 5;int _c : 10;int _d : 30;
};

需要注意的是:

  • 这里面的数字代表的不是字节,是比特(bit)。
  • 位段成员的类型只能是整型家族的,例如:int, unsigned int, signed int, char。

位段的使用:

int main()
{struct A a;a._a = 2;a._b = 3;a._c = 5;a._d = 10;return 0;
}

相当于实例化后的a里面的不同大小的内存里放入了数据。 

7.3  位段的空间大小计算

因为不同平台上的规则都是不太一样的,计算出来的结果也会有些许差异,以下使用vs2022的x64环境下运行的
例如:

#include <stdio.h>
struct A
{int _a : 2;//二进制位int _b : 5;int _c : 10;int _d : 30;
};int main()
{printf("%d", sizeof(struct A));return 0;
}

上面代码的输出结果是8。

  • 声明类型是int类型的,所以一开始先开辟4个字节的内存,也就是32bit。
  • _a用掉了2bit,还剩下30bit。
  • _b用掉了5bit,还剩下25bit。
  • _c用掉了10bit,还剩下15bit。
  • _d需要30bit的空间,但是预先开辟的空间只剩下15bit,所以我们还需要再开辟一个int大小的空间,之前剩下的15bit的空间选择不使用,_d的30bit全放在第二个空间内。
  • 结果为8

注意:

  大家有没有发现,我们在声明位段的时候,如果定义的是int,那么冒号后面跟上的数字不能超过32,如果定义的是char,那么冒号后面跟上的数字不能超过8。如果超过以后,就会报错。

       其实根据内存对齐原则,如果超出以后,处理器就需要访问两次才能完整的得到数据。所以在定义的时候,应该避免超出应有的内存大小。

7.4  位段的内存分配
  • 位段分配的内存中的比特位是从左向右使用的,还是从右向左使用的呢?
  • 如何证明内存分配剩余的比特位不够使用时,是继续使用还是浪费掉呢?

接下来我们分析:
用例代码:

#include <stdio.h>
struct A
{char _a : 3;char _b : 4;char _c : 5;char _d : 4;
};
int main()
{struct A a = {0};a._a = 10;a._b = 12;a._c = 3;a._d = 4;return 0;
}

我们假设:位段分配的内存中的比特位是从右向左使用的,分配剩余的比特位不够使用时,浪费掉剩余内存。
则:

  • 我们先定义位段,如下图: 

  • 执行程序:a._a = 10; 10的二进制为1010,放入_a中,由于_a只有3bit,需要截断,所以舍弃最高位1,放入010:

  • 执行程序:a._b = 12;,12的二进制为1100,刚好可以放入,如下图: 

 

  • 执行程序:a._c = 3;,3的二进制为11,由于_c有5bit,高位添0,放入00011,如下图: 

  • 执行程序:a._d = 4;,4的二进制为100,放入0100,如下图: 

  • 程序就基本执行完了,那么内存中是什么样的呢?根据上面分析,我们一开始给结构体初始化为0,我们可以得到: 

由于机器是小端存储,所以内存上应该是:62 03 04.
经过调试,可以看到: 

以上也证明了, 在VS2022上,位段分配的内存中的比特位是从右向左使用的,分配剩余的比特位不够使用时,浪费掉剩余内存,重新开辟新的空间。 

7.5  位段的跨平台问题
  1. int 位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)。
  3. 位段中的成员在内存中从左向右分配还是从右向左分配的标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳打一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

总结:跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

7.6  位段的应用

位段由于跨平台的问题,真正的用途的其中一个是计网的IP数据报:

7.7  位段使用的注意事项 

位段的几个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/423487.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

力扣 | 560. 和为 K 的子数组

import java.util.HashMap; import java.util.Map;public class _560_subarray_sum_equals_k {/* leetcode 560 号算法题&#xff1a;和为K的子数组给定一个整数数组和一个整数 k&#xff0c;你需要找到该数组中和为 k 的连续的子数组的个数。输入:nums [1,1,1], k 2输出: 2输…

s3fs挂载minio集群到本地目录

转载说明&#xff1a;如果您喜欢这篇文章并打算转载它&#xff0c;请私信作者取得授权。感谢您喜爱本文&#xff0c;请文明转载&#xff0c;谢谢。 1. 前言 MinIO 是一款高性能的对象存储&#xff0c;与 Amazon S3 云存储服务兼容&#xff0c;并且号称是世界上最快的对象存储服…

Docker Ipvlan l3s模式说明

看到Docker Ipvlan中有三种模式L2、L3、L3S模式&#xff0c;查阅了L3S&#xff0c;记录如下&#xff1a; 起因 Docker链接: IPvlan network driver 概念 注释说明&#xff08;摘选自: ipvlan-l3s模式&#xff09; L3S mode与L3 mode 的区别在于启用了iptables (conn-track…

JSON简单了解

文章目录 1、JSON介绍2、ES6模版字符串3、JS对象转化为JSON字符串3.1、手动JS对象转化为JSON字符串3.2、自动JS对象转化为JSON字符串 4、JS对象和java互相转换 1、JSON介绍 JSON 概念&#xff1a;JavaScript Object Notation。JavaScript 对象表示法&#xff0c;简单理解JSON是…

浅谈安科瑞直流电表在新加坡光伏系统中的应用

摘要&#xff1a;本文介绍了安科瑞直流电表在新加坡光伏系统中的应用。主要用于光伏系统中的电流电压电能的计量&#xff0c;配合分流器对发电量进行计量。 Abstract: This article introduces the application of Acrel DC meters in PV system in Indonesia.The device is …

Linux命令_vim的详细用法

简介 vim是一款针对Linux和其他类Unix操作系统的文本编辑器。它是Vi编辑器的升级版本&#xff0c;具有丰富的功能和强大的扩展性。vim有三种基本模式&#xff1a;命令模式、插入模式和可视模式。 命令模式&#xff1a;用户可以使用各种命令移动光标和进行编辑操作&#xff0c;如…

数据库用户映射报错用户、组或角色在当前数据库中已存在 错误15023

报错场景原因&#xff1a; 数据库恢复前用户已存在&#xff0c;恢复后不显示&#xff0c;现重新新建和之前一样的用户名时&#xff0c;对新建的用户名进行数据库权限分配就会出现以上报错错误15023问题。 解决方案&#xff1a; 示例如下> Use [数据库名] go sp_change_us…

移动开发行业——鸿蒙OS NEXT开出繁花

1月18日&#xff0c;华为宣布HarmonyOS NEXT开发者预览版开放申请&#xff0c;根据官方注解&#xff0c;这个版本的鸿蒙系统有个更通俗易懂的名字——“星河版”&#xff0c;也被称为“纯血”鸿蒙。 根据官方解释&#xff0c;之所以取名星河版&#xff0c;寓意鸿蒙OS NEXT就像…

Linux 快速构造大数据文件

文章目录如下 1. 如何生成数据文件 2. 使用 yes 命令构造数据 2.1. 基本用法 2.2. 构造数据文件 3. 使用 awk 命令构造数据 3.1. 基本用法 3.2. awk 循环输出 3.3. awk 指定分隔符 3.4. awk 随机数 3.5. awk 随机字符 3.6. awk 构造数据 4. 总结 1. 如何生成数据文…

开源项目CuteSqlite开发笔记(八):Windows 64位/32位使用GetWindowLongPtr钩子函数

需求描述 在开发CuteSqlite的时候&#xff0c; 有一个功能需要实现&#xff0c;鼠标移到WTL::CStatic上后&#xff0c;发送消息通知CToolTipCtrl弹出。 遇到问题 WTL::CStatic控件没有相应 WM_MOUSEMOVE 消息&#xff0c;需要返回一个HTCLIENT消息来让窗口处理函数执行 WM_MO…

Databend 开源周报第 129 期

Databend 是一款现代云数仓。专为弹性和高效设计&#xff0c;为您的大规模分析需求保驾护航。自由且开源。即刻体验云服务&#xff1a;https://app.databend.cn 。 Whats On In Databend 探索 Databend 本周新进展&#xff0c;遇到更贴近你心意的 Databend 。 支持标准流 标…

智慧工厂视频监控平台EasyCVR公网收流后内网设备无法播放是什么原因?

安防视频监控平台EasyCVR采用了开放式的网络结构&#xff0c;支持高清视频的接入和传输、分发&#xff0c;平台提供实时远程视频监控、视频录像、录像回放与存储、告警、语音对讲、云台控制、平台级联、磁盘阵列存储、视频集中存储、云存储等丰富的视频能力&#xff0c;此外&am…