【C语言】你真的了解结构体吗

  1. 引言✨

我们知道C语言中存在着整形(int、short...),字符型(char),浮点型(float、double)等等内置类型,但是有时候,这些内置类型并不能解决我们的需求,因为我们无法用这些单一的内置类型来描述一些复杂的对象,如一个学生,一本书等等。出于这个原因,C语言还给我们提供了一些自定义的数据类型使我们可以自己来构建类型,如结构体、枚举、联合体。其中最常使用的就是我们本期的主题:结构体

可能有很多人已经使用过结构体类型来解决一些实际问题了。但是对于结构体,还是有很多 细节值得我们去深挖的,下面就让我们来看看吧!
温馨提示: 可以通过目录进行快速定位哦 😍
  1. 结构体的声明💫

2.1 结构体的基础知识

在开启本期内容之前,我们先来回顾以下结构体的 基本概念:

结构体是C语言中一个非常重要的数据类型。该数据类型是由一组称为成员变量的数据组成,其中每个成员可以是不同类型的变量,甚至可以是另一个结构体变量。结构体通常用来表示类型不同但又相关的若干数据。

2.2 结构体的声明

结构体的声明格式如下:

struct tag
{
 member-list;
}variable-list;
  • struct是结构体关键字,我们要定义结构体类型时必须使用它

  • tag是结构体标签,它用来区分不同的结构体类型

  • 结构体关键词与标签共同组成了结构体的类型,与int,float这些是一个意思,我们可以使用struct tag+变量名来定义一个结构体变量。

  • member-list代表成员列表,它包含了结构体的成员变量。

  • variable-list表示变量列表,我们可以在声明结构体类型的同时创建结构体变量。当然我们也可以不写,仅声明一个结构体类型。

  • 结构体大括号后面的分号必不可少。

例如,我们可以这样使用结构体来描述一个学生:
//声明一个学生类型
struct Student
{
    char name[20];//姓名
    char sex[5];//性别
    char id[20];//学号
    int age;//年龄
    float score;//绩点
};

int main()
{
    struct Student s1;//定义一个学生结构体变量s1
}

当然,如果你嫌结构体的类型名太长,写起来麻烦,可以使用typedef对类型进行重命名,如下:

//声明一个学生类型,并用typedef类型重定义为Stu
typedef struct Student
{
    char name[20];//姓名
    char sex[5];//性别
    char id[20];//学号
    int age;//年龄
    float score;//绩点
}Stu;

int main()
{
    Stu s1;//相当于sturuct Student s1
}

2.3 特殊的声明

除以上的声明方式,我们也可以使用不完全的声明。例如:

//声明匿名结构体类型
struct
{
    int a;
    char b;
    float c;
}x;

struct
{
    int a;
    char b;
    float c;
}a[20], *p;
上面两个结构体的声明 省略了结构体标签tag,我们把这样的结构体类型称作 匿名结构体类型
但是,这样子的声明往往是 一次性的。由于我们省略了标签,我们就无法在其他地方使用这个类型来创建一个结构体变量。毕竟连名字都没有,怎么用来定义变量。
当然,如果 你只想用一次你创建的类型,或者 你不想要这个结构体类型被别人使用,你可以声明一个匿名结构体类型。

那么问题来了:

int main()
{
    //在上面匿名结构体声明的基础上,下面的代码合法吗?
    p = &x;
}

答案是编译器会报警告:

尽管两个匿名结构体的成员列表一模一样,但是编译器依然会将其 当作两个完全不同的类型,两个不透类型的指针相互赋值自然是 非法的。
  1. 结构体的自引用🌟

我们在创建链表时,往往用结构体来表示链表的结点。结构体的成员分为数据域与指针域:

数据域:用来存储当前结点的值
指针域:用来存储指向下一结点的地址
typedef int ListDataType;
struct ListNode
{
    ListDataType val;//数据域
    struct ListNode* next;//指针域
};
我们将上面这种 结构体中包含有指向自身结构体变量的指针的方式称作 结构体的自引用。其中val占4个字节,next是个指针,占4/8个字节, 结构体具有一个确定的大小

那既然我们这样声明结点目的是为了能够找到下一结点的位置,那我们可不可以这样设计结点:

typedef int ListDataType;
struct ListNode
{
    ListDataType val;//数据域
    struct ListNode next;//保存下一结点
};
答案是 不行的。假如可以这样设计, 那么sizeof( struct ListNode)的大小该是多少呢?我们是求不出来的,因为假设我们用这个类型创建了一个结构体变量n,那么n中包含着next,next也是结构体变量,又包含着一个next变量,next又包含着next...,这样下去就变成了 无限套娃。既然不知道大小,我们又要如何分配空间给结构体变量呢?

注意:

//这样写代码,可行否?
typedef struct
{
    int data;
    Node* next;
}Node;
显然是不行的, 凡是都要讲究个 先来后到。当我们在成员列表中定义Node*类型的变量时,此时编译器还不知道Node是什么鬼东西,自然会报错。我们可以这样修改代码:
//解决方案:
typedef struct Node
{
    int data;
    struct Node* next;
}Node;

Node* pn;//定义一个结构体指针pn

4. 结构体变量的定义和初始化🌊

有了结构体类型,那我们要如何定义变量呢?实则很简单

struct Point
{
    int x;
    int y;
}p1; //声明类型的同时定义变量p1

struct Stu    //类型声明
{
    char name[15];//名字
    int age; //年龄
};
int main()
{
    //定义结构体变量p2
    struct Point p2; 
    //初始化:定义变量的同时赋初值。
    struct Point p3 = { 3, 4 };
    //初始化
    struct Stu s = { "zhangsan", 20 };
}

结构体嵌套结构体的初始化方式如下:

struct Point
{
    int x;
    int y;
}p1; //声明类型的同时定义变量p1
struct Node
{
    int data;
    struct Point p;
    struct Node* next;
}n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化

int main()
{
    struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始化
}

5.结构体的内存对齐

🔉快醒醒,别睡了

终于到了本期的重点内容了,我们来看下面例题:

//练习
struct S1
{
    char c1;
    int i;
    char c2;
};
struct S2
{
    char c1;
    char c2;
    int i;
};
int main()
{
    printf("%d\n", sizeof(struct S1));
    printf("%d\n", sizeof(struct S2));
    return 0;
}

答案如下:

这里可能有人就纳闷了,欸,char类型占1个字节,int类型占4个字节,s1与s2的大小不应该都是1+1+4=6吗?怎么会是12和8呢?这就要谈到结构体在内存中的存储了,即 结构体的内存对齐

实际上S1在内存中的存储方式是这样子的:

我们看到c1存放完后,i并不是紧挨着c1进行存放,而是从偏移量为4的地方开始存储,中间空出三个字节的空间。这就是结构体的内存对齐,下面我们来了解其内存对齐的规则:
  • 结构体的第一个成员在与结构体变量偏移量为0的地址处

  • 其他成员变量要对齐到某个数字(我们称作对齐数)的整数倍的地址处

  • 对齐数=编译器默认的一个对齐数与该变量大小的较小值。vs的默认对齐数为8

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

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

我们可以模拟一下s1的内存对齐方式:

同样,S2的内存对齐方式如下:

如果你还是不确定,C语言给我们提供了offsetof宏计算结构体成员的偏移量,原型如下:

需要注意:使用时我们需要先包含stddef.h头文件:

#include<stddef.h>
#include<stdio.h>
struct S1
{
    char c1;
    int i;
    char c2;
};
struct S2
{
    char c1;
    char c2;
    int i;
};
int main()
{
    printf("结构体S1中c1的偏移量为%zd\n",offsetof(struct S1,c1 ));
    printf("结构体S1中i的偏移量为%zd\n", offsetof(struct S1, i));
    printf("结构体S1中c2的偏移量为%zd\n", offsetof(struct S1, c2));
    printf("结构体S2中c1的偏移量为%zd\n", offsetof(struct S2, c1));
    printf("结构体S2中c2的偏移量为%zd\n", offsetof(struct S2, c2));
    printf("结构体S2中i的偏移量为%zd\n", offsetof(struct S2, i));
    return 0;
}

结果如下,与我们上述的分析过程如出一辙:


我们再来看一个例子:
//结构体嵌套问题
struct S3
{
    double d;
    char c;
    int i;
};
struct S4
{
    char c1;
    struct S3 s3;
    double d;
};

int main()
{
    printf("%d\n", sizeof(struct S4));
    return 0;
}
怎么样,你做对了吗 👀
步骤如下:
  1. 根据内存对齐算出s3所占的空间大小为16

  1. 根据对齐规则的第5点得出s3的要对齐到8的整数倍,即对齐到偏移量为8处

  1. double d的对齐数为8,因此对齐到偏移量为24处

  1. 最终大小为最大偏移量8的整数倍,即为32。


想必有人会有疑问,内存对齐那么麻烦,为什么存在内存对齐?主要有以下两点原因:

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

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

总的来说:

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

那在设计结构体的时候,我们既要满足内存,又要节省空间,我们要如何做到:

让占用空间小的成员尽量集中在一起
//例如:
//c1与c2不相邻
struct S1
{
    char c1;
    int i;
    char c2;
};

//c1与c2相邻
struct S2
{
    char c1;
    char c2;
    int i;
};

虽然S1和S2类型的成员一模一样,但是S1占12个字节,S2占8个字节,这就是合理安排位置所带来的好处。

6.默认对齐数的修改🌷

在C语言中,我们也可以修改结构体的默认对齐数,只需用#pragma这个预处理指令即可。如下:

#include<stdio.h>
#pragma pack(1) //修改默认对齐数为1
struct  S1
{
    char c1;
    int i;
    char c2;
};

struct S2
{
    char c1;
    char c2;
    int i;
};
int main()
{
    printf("%d\n", sizeof(struct S1));
    printf("%d\n", sizeof(struct S2));
    return 0;
}
上面我们将默认对齐数设置成1,由于对齐数是默认对齐数和成员大小 较小者,因此默认对齐数为1相当于不对齐,S1与S2的结果相同都为6:

7. 结构体的传参

话不多说,我们直接上代码来说明:

#include<stdio.h>
struct S
{
    int data[1000];
    int size;
};
//传值
void print(struct S s)
{
    printf("%d", s.size);
}
//传址
void print(struct S* sp)
{
    printf("%d", sp->size);
}
int main()
{
    struct S s1;
    print1(s1);
    print2(&s1);
}

print1()和print2()哪个函数好呢?

答案是print2()函数。

为什么呢?

print1()和print2()分别对应着 传值调用传址调用 。我们知道无论是传值还是传址,函数在将要调用时实参都会 形成临时拷贝并压入栈中 压栈 的这个过程是需要 成本 的, 成本体现在时间和空间上
如果传递一个结构体对象的时候,结构体过大(例如我们上面的s1),参数压栈的的成本比较大,就会 导致性能的下降 。所以我们传递像结构体这种数据量较大的变量时,一般 传地址 ,地址占4个或者8个字节,极大程度上减少了所需的成本。
综上所述,我们进行结构体传参时要传结构体的地址。

8.位段🌸

8.1 位段的特征与声明

讲完结构体后我们就必须再来讲讲结构体实现位段的能力,位段满足以下两点特征:

1.位段的成员必须是int、unsigned int或者char这些 整型家族的成员
2.位段的成员名后有一个冒号和数字,数字表示成员占多少个 二进制位(bit位)
3.位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

例如下面的A就是一个位段类型:

struct A
{
    char a : 1;
    char b : 4;
    char c : 5;
    char d : 5;
};

其中a占1个二进制位,b占4个二进制位,c占5个二进制位,d占5个二进制位。那么位段A的大小是多少呢?这就要来谈谈位段的内存分配了。

8.2 位段的内存分配

事实上, C语言并没有明确规定位段的内存分配方式,也就是说:
1.我们并不知道位段中的成员在内存中是 从左向右分配二进制位还是 从右向左分配二进制位
2.我们不清楚当一个结构包含两个以上位段,第二个位段成员比较大,第一个位段剩余的二进制位无法容纳第二个位段,是 舍弃剩余的位还是将其 利用,这是不确定的。

正因如此,位段在不同的编译环境下所展现出来的效果很可能会有所不同。我们可以探究一下A当其从右向左分配并且不足时舍去剩余位时的内存分配情况,如下(VS2022环境下):

struct A
{
    char a : 1;
    char b : 4;
    char c : 5;
    char d : 5;
}s={0};
int main()
{
    s.a = 11;
    s.b = 12;
    s.c = 3;
    s.d = 4;
    printf("%d", sizeof(s));//计算s所占大小
    return 0;
}
我们发现按照我们的假设计算出来的结果与vs2022监视器中内存的分配结果一模一样,因此我们可以得知 在vs2022编译器下位段是从右向左分配且不足时舍弃剩余位。

8.3 位段的跨平台问题

由于以下问题的存在,位段的可移植性很差,即存在着跨平台问题:

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

8.4 位段的应用

位段在网络中的应用比较多,例如以下ip数据包格式:

当我们在网络上给某人发送一个消息时,这个消息就会封装成如上图所示的一个数据包用于在网络上精确地找到接收人。我们可以看出每一行都恰好的被设计成了int型宽度,每个部分我们使用位段来进行排列封装,使得空间最大利用。而如果我们使用结构体来进行封装每个部分,由于内存对齐的原因,势必会额外浪费空间造成数据包变得巨大,从而使网络状态变差。

总的来说,跟结构体相比,位段也可以达到一样的效果,其可以帮助我们 节省空间,但是也带来了 跨平台性的问题。

以上,就是本期的全部内容啦🌸

制作不易,能否点个赞再走呢🙏

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

k8s部署prometheus

k8s部署prometheus 版本说明&#xff1a; k8s&#xff1a;1.24.4 prometheus&#xff1a;release-0.12&#xff08;https://github.com/prometheus-operator/kube-prometheus.git&#xff09; 本次部署采用operator的方式将prometheus部署到k8s中&#xff0c;需对k8s和prom…

springboot+vue驾校管理系统 idea科目一四预约考试,练车

加大了对从事道路运输经营活动驾驶员的培训管理力度&#xff0c;但在实际的管理过程中&#xff0c;仍然存在以下问题&#xff1a;(1)管理部门内部人员在实际管理过程中存在人情管理&#xff0c;不进行培训、考试直接进行发证。(2)从业驾驶员培训机构不能严格执行管理部门的大纲…

SpringBoot解析指定Yaml配置文件

再来个文章目录 文章目录前言1、自定义配置文件2、配置对象类3、YamlPropertiesSourceFactory下面还有投票&#xff0c;帮忙投个票&#x1f44d; 前言 最近在看某个开源项目代码并准备参与其中&#xff0c;代码过了一遍后发现多个自定义的配置文件用来装载业务配置代替数据库…

使用 Python 从点云生成 3D 网格

从点云生成 3D 网格的最快方法 已经用 Python 编写了几个实现来从点云中获取网格。它们中的大多数的问题在于它们意味着设置许多难以调整的参数&#xff0c;尤其是在不是 3D 数据处理专家的情况下。在这个简短的指南中&#xff0c;我想展示从点云生成网格的最快和最简单的过程。…

继承和派生

&#x1f436;博主主页&#xff1a;ᰔᩚ. 一怀明月ꦿ ❤️‍&#x1f525;专栏系列&#xff1a;线性代数&#xff0c;C初学者入门训练&#xff0c;题解C&#xff0c;C的使用文章&#xff0c;「初学」C &#x1f525;座右铭&#xff1a;“不要等到什么都没有了&#xff0c;才下…

手撕数据结构—队列

队列队列的话只允许在一端插入&#xff0c;在另外一端删除。插入数据的那一段叫做队尾&#xff0c;出数据的那一段叫做队头&#xff08;从尾巴插入&#xff09;。因此的话队列是先进先出的。入的顺序与出的顺序的话是一样的。这个与栈是不一样的&#xff0c;因为栈的话就是说如…

问题【Java 基础】

基础1、成员变量与局部变量的区别2、静态变量有什么作用3、字符型常量和字符串常量的区别4、静态方法为什么不能调用非静态成员5、静态方法和实例方法有何不同6、重载和重写有什么区别7、什么是可变长参数8、Java 中的几种基本数据类型了解么9、基本类型和包装类型的区别10、包…

【数据结构】树和二叉树的概念及结构

目录 1.树概念及结构 1.1 树的概念 1.2 树的相关概念 1.3树的表示 1.4 树在实际中的应用 2.二叉树概念及结构 2.1 概念 2.2 特殊的二叉树 2.2.1 满二叉树 2.2.2 完全二叉树 1.树概念及结构 1.1 树的概念 树是一种非线性的数据结构&#xff0c;它是由n(n>0) 个有…

一款专门为自动化测试打造的集成开发工具【Aqua】,“能快速构建自动化测试项目”,就问你爽不爽吧,,,

你好&#xff0c;我是不二。 随着行业内卷越来越严重&#xff0c;自动化测试已成为测试工程师的必备技能&#xff0c;谈及自动化测试肯定少不了编程&#xff0c;说到编程肯定离不开集成开发工具&#xff0c;比如&#xff1a;IntelliJ IDEA可以帮助我们快速构建Maven项目、sprin…

前端已死?后端已亡?弯弯绕绕,几分真几分假

前段时间&#xff0c;我在掘金分享了一篇GPT-4 性能文章&#xff0c;也许是过于强大带来的威胁性&#xff0c;引来评论区的排队哀嚎&#xff08;如下图&#xff09;&#xff0c;所以“前端已死&#xff0c;后端已亡”这个概念真的成立吗&#xff1f;本文着重探讨前端。 前端和后…

警惕,3月20日WOS目录更新,50本SCI/SSCI被剔除,这个出版社多达18本

2023年3月SCI、SSCI期刊目录更新 2023年3月20日&#xff0c;Web of Science核心期刊目录再次更新&#xff01;此次2023年3月SCIE & SSCI期刊目录更新&#xff0c;与上次更新&#xff08;2023年2月&#xff09;相比&#xff0c;共有50本期刊被剔除出SCIE & SSCI期刊目录…

[ 网络 ] 应用层协议 —— HTTP协议

目录 1.HTTP协议 1.1URL urlencode和urldecode 2. HTTP协议格式 HTTP请求 HTTP响应 3.告知服务器意图的HTTP方法 GET&#xff1a;获取资源 POST&#xff1a;传输实体主体 GET和POST的区别 使用Cookie的状态管理 4.返回结果的HTTP状态码 状态码告知从服务器端返回的…

三月份跳槽了,历经字节测开岗4轮面试,不出意外,被刷了...

大多数情况下&#xff0c;测试员的个人技能成长速度&#xff0c;远远大于公司规模或业务的成长速度。所以&#xff0c;跳槽成为了这个行业里最常见的一个词汇。 前几天&#xff0c;我看到有朋友留言说&#xff0c;他在面试字节的测试开发工程师的时候&#xff0c;灵魂拷问三小…

【Shell】脚本

Shell脚本脚本格式第一个Shell脚本&#xff1a;hello.sh脚本常用执行方式1. bash或sh脚本的相对路径或绝对路径2. 输入脚本的绝对路径或相对路径3. 在脚本的路径前加上.或者source脚本格式 脚本以#!/bin/bash开头&#xff08;指定解析器&#xff09; #! 是一个约定的标记&…

让 new bing 使用 GPT-4 编写一个令人满意的程序全过程赏析

让 new bing 使用 GPT-4 编写一个令人满意的程序全过程赏析 标签&#xff1a;new bing、GPT-4 文章目录让 new bing 使用 GPT-4 编写一个令人满意的程序全过程赏析前言1 让 bing 编写一个画螺旋线的程序1.1 我的要求&#xff08;1&#xff09;1.2 bing 的回答全文&#xff08;…

p81 红蓝对抗-AWD 监控不死马垃圾包资源库

数据来源 注意&#xff1a;一下写的东西是在p80 红蓝对抗-AWD 模式&准备&攻防&监控&批量这篇文章的基础上进行的 演示案例&#xff1a; 防守-流量监控-实时获取访问数据包流量 攻击-权限维持-不死脚本后门生成及查杀 其他-恶意操作-搅屎棍发包回首掏共权限…

WPF 认识WPF

什么是WPF?WPF是Windows Presentation Foundation(Windows展示基础)简称&#xff0c;顾名思义是专门编写表示层的技术。WPF绚丽界面如下&#xff1a;GUI发展及WPF历史&#xff1f;Windows系统平台上从事图形用户界面GUI(Graphic User Interface)已经经历了多次换代&#xff0c…

web前端开发和后端开发哪个难度大?

前言 因为涉及到的具体的应用的领域不同&#xff0c;所以说不能简单地说哪一个难&#xff0c;对于前端而言你会感觉到入门会非常的简单&#xff0c;这也是会给许多人一种错觉&#xff0c;前端很简单&#xff0c;但是只能说是在入门理解上是有利于新手的&#xff0c;前端在主要…

二叉树系统刷题1

文章目录**BM26** **求二叉树的层序遍历****BM27** **按之字形顺序打印二叉树****BM28** **二叉树的最大深度****BM29** **二叉树中和为某一值的路径(一)****BM30** **二叉搜索树与双向链表****BM31** **对称的二叉树****BM32** **合并二叉树****BM34** **判断是不是二叉搜索树…

【数据结构】KMP算法细节详解

KMP算法细节详解前言一、字符串匹配问题1.BF算法2.KMP算法二、next数组三、手写nex思想四、机算next思想五、next数组细节理解六、nextVal数组七、KMP算法代码实现八、nextVal数组代码实现完结前言 KMP算法是为了字符串匹配问题而被研究出来的&#xff0c;字符串匹配问题就是查…