C语言自定义数据类型详解(二)——结构体类型(下)

书接上回,前面我们已经给大家介绍了如何去声明和创建一个结构体,如何初始化结构体变量等这些关于结构体的基础知识。下面我们将继续给大家介绍和结构体有关的知识:

今天的主题是:结构体大小的计算并简单了解一下位段的相关知识。

目录

一、结构体的内存对齐

(1)发现并提出问题: 

(2)内存对齐规则:

(3)解决问题:

附一:offsetof宏的介绍:

(4)为什么会存在结构体的内存对齐:

附二:带来的启发:

附三:修改编译器的默认对齐数:

附四:关于前面问题的解答:

二、关于位段:

(1)搭建应用场景:

(2)位段的概念性理解:

(3)位段的内存分配:


学习指南:如果你是期末备考的大学生,简单学习博主的结构体类型(上)的内容就够了。如果你对自己有更高的要求,希望系统深入了解结构体知识可以继续跟上博主的步伐啦!

一、结构体的内存对齐

(1)发现并提出问题: 

如果要谈结构体大小的计算,就不得不提结构体的底层特性——内存对齐。什么是内存对齐呢?我们先来看一下,下面这两个例子:

#include<stdio.h>
struct S1
{char c1;char c2;int i;
};int main()
{printf("%zd\n", sizeof(struct S1));return 0;
}

 这个结构体大小是多少呢?是不是 1 + 1 + 4(sizeof(char) + sizeof(char) + sizeof(int))的结果呢?,我们不妨在自己的机器上运行一下。

下面这张截图是在博主2022 Visual Studio这款IDE上运行的结果:

这个结果挺莫名奇妙的,我一时也想不清楚这个结果为啥是这个。嗯……我们现在不妨再来做一个事情,我们继续声明和创建一个S2的结构体,然后呢,这个结构体有和S1一样的成员变量(c1,c2,i)。然后只是改一下它们之间的顺序,我们来看一下这个结构体变量的大小又是多少。如图所示:

#include<stdio.h>
struct S1
{char c1;char c2;int i;
};
struct S2
{char c1;int i;char c2;
};int main()
{printf("%zd\n", sizeof(struct S1));printf("%zd\n", sizeof(struct S2));return 0;
}

运行截图:

 这也太不可思议了,我仅仅是改了一下结构体各个成员变量的顺序,竟然就改变了整个结构体变量的大小。显然种种迹象均表明:结构体变量的大小并不是各个成员变量大小的简单相加

那这具体是怎回事呢,欸,我们接着往下探索!

(2)内存对齐规则:

前面我们通过实验发现,结构体变量的大小不是其各个成员变量的简单相加。其实其本质的原因就在于有内存对齐的底层特性存在。

那关于结构体的内存对齐有哪些规则呢?这里博主总结了关于内存对齐这一块,大致有以下四条规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处;
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处;
  3. 结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍;
  4. 如果嵌套了结构体,嵌套结构体要对齐到自己的最大对齐数的整数倍处,其他规则照旧。

上述规则中的一些概念,我们这里做简单的阐释。首先关于什么是对齐数

对齐数 = Min(编译器默认的一个对齐数,成员变量自身的大小)

一般来说:结构体的每一个成员变量都有自己的一个对齐数,不同大小的成员变量一般有不同的对齐数,其中我们把一个结构体它所有成员变量中最大的那个对齐数,称之为最大对齐数

特别地,如果成员变量是一个数组。那这里成员变量的大小,注意啦,不是指数组的大小,而是数组中单个元素的大小。我们下面以VS 2022为例子(编译器的默认对齐数是8),来说明这个问题:

struct S
{char c;       //8 < 1 —— 对齐数是1int  i;       //8 < 4 —— 对齐数是4char name[12];//8 < 12 ?No,No,No!    8 < 1 ——对齐数是1
};

另外关于编译器的默认对齐数,如果通过查阅相关编译器的技术文档,你能够查到该编译器的默认对齐数那当然再好不过了。但是如果说你实在查不到,那你就忽略这个编译器的默认对齐数(或者说假设它是无穷大)。如果说此时发现假设和实际结果不相一致,我们通过对结果进行分析,也可以推导出编译器的默认对齐数的大小。

那话说回来,我们还是希望大家记一些常见的关于编译器的默认对齐数:像VS系列都是8,然后gcc系列是无穷大,无穷大的另一种理解就是:对齐数 = 成员变量自身的大小。

(3)解决问题:

OK,当然啦,说了这么多,我们就可以分析和解决前面所提到的那个问题。我们这里假设是在VS 2022的环境下,也就是说编译器的默认对齐数是8。

#include<stdio.h>
struct S1
{char c1;char c2;int i;
};
struct S2
{char c1;int i;char c2;
};int main()
{printf("%zd\n", sizeof(struct S1));printf("%zd\n", sizeof(struct S2));return 0;
}

对于结构体struct S1而言,它的内存布局即为下图所示:

分析:

红色块是c1的内存空间,旁边的数字对应的是这个内存块相较于结构体起始地址的偏移量。

由于c1是第一个成员,因此偏移量为0。而且c1只需要1个字节的空间,综上红色块就是c1的内存空间。

绿色块是c2的内存空间,这是因为: 8(编译器默认对齐数)< sizeof(c2) == 1,因此对于c2来说它的对齐数就是1,也就是说c2内存的起始位置在偏移量为1的倍数的位置上,显然就要我们绿色块的位置上。

蓝色块是i的内存空间,这是因为:8(编译器默认对齐数)< sizeof(i) == 4,因此对于i来说它的对齐数就是4,偏移量为4的倍数的位置,恰好就是对应我们蓝色块的起始位置。然后往下延展4个字节,这是因为sizeof(i) == 4。

最后,显然最大对齐数就是4,此时c1 + c2 + i的内存大小是8,正好就是4的倍数,因此结构体的内存大小是8。

对于结构体struct S2而言,它的内存布局即为下图所示:

分析:

c1,i,c2的分析略,这里我们简单说一下结构体的大小分析。对于这个结构体我们不难推导出最大对齐数——4,然后此时c1 + i + c2的内存大小是9,9不是最大对齐数4的倍数,于是向下延展至12个字节,此时正好是4的倍数,由此结构体变量的大小是12。

如果你现在觉得自己强得可怕,不妨试着继续挑战一下,下面这两个结构体大小的计算:

struct S3
{char c1;int i;char name[12];
};
struct S4
{int i;struct S3 s;char c1;
};

对于结构体struct S3而言,大家只需要注意博主给大家提醒的如果成员变量是一个数组。那对齐数那里所说的成员变量的大小,不是指数组的大小,而是数组中单个元素的大小。因此最后结构体的最大对齐数也不是12,而是4。结构体整体的大小就是20。如图所示:

其次对于结构体struct S4而言,它里面嵌套了其他的结构体struct S3 s,我们只要注意s的对齐数是这个结构体的最大对齐数。就可以了。如果所有的一些没有问题,最后struct S4的大小就是28。如图所示:

附一:offsetof宏的介绍:

offsetof是一个宏,有了offsetof我们的用户,只需要给出一个结构体,然后再给出这个结构体的一个成员变量,你就可以获得:这个成员变量距离结构体起始位置的偏移量。然后如果你想使用这个宏,请#include<stddef.h>

我们这里以前面的struct S4结构体为例子,来看一下这个宏具体是如何使用的:

#include<stdio.h>
#include<stddef.h>
struct S3
{char c1;int i;char name[12];
};
struct S4
{int i;struct S3 s;char c1;
};int main()
{printf("struct S4 的i 的相较于起始位置的偏移量:%zd\n", offsetof(struct S4, i));printf("struct S4 的s 的相较于起始位置的偏移量:%zd\n", offsetof(struct S4, s));printf("struct S4 的c1的相较于起始位置的偏移量:%zd\n", offsetof(struct S4, c1));return 0;
}

运行截图:

而这些和我们所分析的结果是一致的。

有小伙伴可能对这个宏的底层实现比较感兴趣,我们这里也把这个offsetof宏的一种实现方式给大家放在下面了:

#define offsetof(type, member) ((size_t)&((type*)0)->member)

它的基本策略就是把地址为0的位置当作是结构体的起始地址,然后结构体的成员member再取地址就是这个结构体成员相较于起始位置的偏移量了。

(4)为什么会存在结构体的内存对齐:

C语言的结构体,为什么会有内存对齐的特性。有一种说法是说如果结构体的内存是对齐的话,会带来更好的内存访问效率。

OK,为了更好的说明这个问题,我们先来假定一些场景:

假如我们的用户创建了结构体S,并定义了一个该结构体的变量s,如图所示:

struct S
{char c;int i;
}s;

然后我们假定我们拥有一台32位的机器,注意这里的32位是指这台机器的机器字长是32位,机器字长32位也就意味着这台机器的CPU一次性要从内存拿32bit的数据,然后能一次性对32bit的数据进行处理(现代CPU对数据的各种处理都是基于机器字长为单位的)

 然后如果说我不是内存对齐的,我这次想要处理变量s中的一个成员i,那这个过程可以用下图进行阐述:

这个图片告诉我们:如果说我读的数据是i,我们的CPU会从结构体变量s的起始地址处开始读数据(如果可以CPU可以选择不这么做,显然这里我们不得不这么做),一次读4Byte数据(4Byte == 32bit,图中绿框所标识的范围),显然我第一次只能拿到i的前3个字节的内容,我需要再读一遍才能完整地读取到i。也就是说: 如果内存是不对齐的,对i变量的读取CPU要读两次。而且对于变量i来说,还有一些没必要的东西被读进来了。

紧接着,我们继续探讨,如果说,我们的内存的是对齐的。那这个时候的过程则为下图所示:

显然,这时我们发现,如果说CPU要对i进行处理,它完全可以做到只需要读一次,就可以将变量i完整地读进来。

从另一个角度,当你的数据是对齐的情况下,我们内存控制器和CPU的设计呢,会更加简单。因为显然的一点,如果内存对齐的情况下,我们的CPU不需要去处理那种跨边界的数据请求;非对齐的情况下进行访问,出现跨边界的数据请求时可能需要多次内存操作才能获取到所需要的数据。这增加了我们硬件设计的复杂性并带来了潜在的延迟。

因此我们这里可也以认为是说:这种结构体存储成员的策略,是一种利用空间效率来换取时间效率的做法。

这是博主能给大家介绍的一种为什么说会存在有内存对齐的一个原因。当然呢,也有其他地方有说法是说:这种内存对齐的策略会有利于硬件平台的移植……嗯,我们不希望在这里把这个问题搞得更复杂了,因此这些其他的原因就留给感兴趣的读者朋友去探索一下吧,不再进行深入的探讨。

附二:带来的启发:

在设计结构体时,如果我既希望满足对齐,又希望所浪费的空间尽可能的少。我们应该做到:让占用空间小的成员尽量集中在一起。比如说下面两个结构体的设计,显然struct S1的设计优于struct S2的设计:

 struct S1{char c1;char c2;int i;};
struct S2{char c1;int i;char c2;};
附三:修改编译器的默认对齐数:

C语言提供了#pragma pack(对齐数)这种格式的预处理指令,来对编译器的默认对齐数进行修改。理论上,对齐数都是大于0的正整数,特别地,如果指令格式里面的对齐数不写或写成0(其他的数据类型或负数往往会被认为非法),那就表示使用默认对齐数。

我们来看一个例子:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//先将编译器的默认对齐数设置为1:
#pragma pack(1)
struct S1
{char c;int i;
};
//再将编译器的默认对对齐数设置回来:
#pragma pack()
struct S2
{char c;int i;
};int main()
{printf("sizeof(struct S1) == %zd\n", sizeof(struct S1));printf("sizeof(struct S2) == %zd", sizeof(struct S2));return 0;
}

运行截图:

我们会发现,如果编译器的默认对齐数是1,带来的感觉就是没有进行内存对齐。当然无论怎么说,这应该和我们所预期的结果(推理的结果和实际结果一致)是一致的。

附四:关于前面问题的解答:

我们在C语言自定义数据类型详解(一)——结构体类型(上)-CSDN博客里面留了一个问题:为什么说结构体的传参,一般情况下是传址优于传值。现在我们对于一个结构体它的大小是如何计算的应该都已经是小case了,回过头来思考这个问题:如果我结构体传址的话,就如果是指针的话,那该参数对于函数栈的消耗要么是4Byte,要么是8Byte(x86是4,x64是8)。而一个结构体变量的大小往往会超过8个字节(你想一个char,一个int,然后还有内存对齐,这个大小已经就有8Byte了,更不要说更复杂的结构体)。这相应地,所带来的压栈消耗也会更大。

因此我们说结构体传参,传址往往优于传值。

二、关于位段:

(1)搭建应用场景:

A公司最近购入了四台机器。小明作为该公司的程序员被派遣了一个任务:要求建立一个数据结构用于维护四台机器的各自状态,这其中就包括了机器各自的id(0,1,2,3),机器的运行时间time,标识机器是否正常运行的状态指针state……这些可能就是作为维护人员,我们需要去关心的一些参数:

于是小明据此声明了下面这种结构,用来维护工厂里的这四台机器:

struct Machine
{int id;     //唯一标识一台机器的idint time;   //机器的运行时间int state; //机器目前的状态:1表示正常运行,0表示不正常
}

但是小明后来在使用过程中也注意到了一个问题,在这个结构体中,有一些成员的取值是非常有限的,比如说一台机器的id只有0,1,2,3四种可能的结果,一个机器的状态也就正常和故障两种。

而对于这样的成员变量,我们用int来存,嗯……可以是可以,但是我实际上能用到的也就2个二进制位而已(00:0,01:1,10:2,11:3)。也就是我当前的这种结构,id中有30bit都是无效的。

有小伙伴说用char,当然啦,这也行,但是有没有其他的方法呢?OK,这里跟着博主的步伐来认识一下我们接下来的主角——位段

(2)位段的概念性理解:

在某些应用场景中,为了尽可能地节省空间,我们的C语言结构体一般都有能够实现位段的能力。

位段的声明和结构体非常类似,但是有两个本质上的区别:

  1. 位段的成员只能是int,unsigned int signed int。C99之后支持了更多的数据类型,但是基本上就是int,char这样的数据类型。
  2. 在位段的成员名的后面,往往有一个冒号一个数字用来表示该成员变量的大小。以bit为单位。

也就是说,位段的成员虽然冠以int自称,但是它的实际大小往往低于int类型的4Byte。我们可以来看一个例子:

//我们这里在位段的成员名前面都加了下划线_,仅仅是为了便于区分它和一般的结构体成员变量,即这仅仅是个命名习惯罢了:
struct S
{int _a : 2;  //_a是位段成员,占2  bit位int _b : 5;  //_b是位段成员,占5  bit位int _c : 10; //_c是位段成员,占10 bit位int d;
};

(3)位段的内存分配:

对于位段成员的内存分配,C语言也没有提供具体的标准,但是一个大致的方向是:当你定义了一个位段成员时,它会先给你1Byte或4Byte空间(这一般取决于你前面的类型写的是int还是char),然后你不够了,编译器会继续再给你1Byte或4Byte空间。

什么意思呢?就拿我们前面定义的结构体来说,我们先把它拿过来:

#include<stdio.h>
//我们这里在位段的成员名前面都加了下划线_,仅仅是为了便于区分它和一般的结构体成员变量,即这仅仅是个命名习惯罢了:
struct S
{int _a : 2;  //_a是位段成员,占2  bit位int _b : 5;  //_b是位段成员,占5  bit位int _c : 10; //_c是位段成员,占10 bit位int d;
};int main()
{printf("sizeof(struct S) == %zd",sizeof(struct S));return 0;
}

首先编译器看你需要一个int类型的成员变量,于是它会先给你开辟4Byte空间。显然4Byte = 32bit,这对于_a,_b,_c显然是够用了的。因此_a,_b,_c会公用这4byte的空间。加上之后的int类型的b变量,这个结构体最终的大小大概就是8Byte左右。

显然这比你直接用4个int所需要的内存少多了。如图所示:

这就是一种简单地分析位段大小的一种方式。这里面其实还有很多的细节,如图所示:

具体到内部,_a要2bit,这2bit开辟,是从左往右,还是从右往左是C语言标准没有规定的。

另外,如果说是这样的:

#include<stdio.h>
//我们这里在位段的成员名前面都加了下划线_,仅仅是为了便于区分它和一般的结构体成员变量,即这仅仅是个命名习惯罢了:
struct S
{int _a : 15;  //_a是位段成员,占15 bit位int _b : 12;  //_b是位段成员,占12 bit位int _c : 10;  //_c是位段成员,占10 bit位
};

这里我们当然知道说这个结构体是8byte,然后_a和_b用了前4Byte的27bit位。问题是来给_c分配空间的时候:我们知道前面4Byte用了,它还有5bit没有。这5bit会不会给到_c,然后再从后面的4Byte拿出5bit,凑成最终的5bit呢。

还是说这前面4Byte遗留下的5bit不要了,_c的10bit全部在后面的4Byte里面。这件事情也是我们C语言标准所没有规定的事情。

换句话说,位段内部在给成员分配空间时,它的不确定性因素实在是太多了。这带来的直接问题便是:位段的跨平台性非常差!!!因为没有标准,每个编译器的厂家都是自己的那一套。因此我们实际中用位段其实用得非常非常少,这是希望大家注意到的一点。

博主在这里介绍也仅仅只是希望大家以后在纯C里面看到种定义结构体成员的方式能够看得懂。

至此,关于结构体所有相关的周边知识就介绍完啦,我们下期再见!🙂

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

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

相关文章

Niagara学习笔记

橙色 发射器 , 绿色 粒子, 红色 渲染器 Emitter State 发射器状态 Life Cycle Mode&#xff08;生命周期模式&#xff09; 选择Self就是发射器自身管理生命周期 Loop Behavior 决定粒子发射次数 一次&#xff08;Once&#xff09;&#xff1a;发射器只播放一次多次&#…

14-6-3C++STL的list

&#xff08;一&#xff09;list的插入 1.list.insert(pos,elem);//在pos位置插入一个elem元素的拷贝&#xff0c;返回新数据的位置 #include <iostream> #include <list> using namespace std; int main() { list<int> lst; lst.push_back(10); l…

可爱狗狗的404动画页面HTML源码

源码介绍 可爱狗狗的404动画页面HTML源码&#xff0c;源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改&#xff0c;双击html文件可以本地运行效果 效果预览 源码获取 可爱狗狗的404动画页面HTML源码

C++17 std::variant 详解:概念、用法和实现细节

文章目录 简介基本概念定义和使用std::variant与传统联合体union的区别 多类型值存储示例初始化修改判断variant中对应类型是否有值获取std::variant中的值获取当前使用的type在variant声明中的索引 访问std::variant中的值使用std::get使用std::get_if 错误处理和访问未初始化…

单路由及双路由端口映射指南

远程登录总会遇到登陆不上的情况&#xff0c;可能是访问的大门没有打开哦&#xff0c;下面我们来看看具体是怎么回事&#xff1f; 当软件远程访问时&#xff0c;主机需要两个条件&#xff0c;一是有一个唯一的公网IP地址&#xff08;运营商提供&#xff09;&#xff0c;二是开…

【Elasticsearch】RestClient操作文档

RestClient操作文档 新增文档实体类API语法 查询文档DSL查询 删除文档修改文档批量导入文档小结 新增文档 将数据库中的信息导入elasticsearch中 以商品数据为例 实体类 定义一个索引库结构对应的实体。 Data ApiModel(description "索引库实体") public class …

【项目】基于Qt开发的音乐播放软件

目录 项目介绍 项目概述 界面开发 界面分析 创建工程 主界面布局设计 窗口主框架设计 界面美化 主窗口设定 添加图片资源 head处理 播放控制区处理 自定义控件 BtForm 推荐页面 自定义CommonPage 自定义ListItemBox 自定义MusicSlider 自定义VolumeTool 音…

2025-01-28 - 通用人工智能技术 - RAG - 本地安装 DeepSeek-R1对话系统 - 流雨声

摘要 2025年1月28号 周二&#xff08;除夕&#xff09; 杭州 惠风和畅 小记: 昨天在图书馆整理访谈案例&#xff0c;除了吃饭基本没有停下来直到晚上9点才勉强搞完。每份访谈在3-5万字&#xff0c;总结梳理后每个访谈的字数也在 5000 字左右&#xff0c;接下来如何组织又是一…

电路研究9.2.3——合宙Air780EP中FTP——FTPGET 命令使用方法研究

怎么说呢&#xff0c;之前也是看的&#xff0c;但是也很迷茫&#xff0c;感觉上虽然是对的&#xff0c;但是无法联系到应用里面&#xff0c;今天研究一下FTP 命令使用方法吧。 15.29 使用方法举例 这里发现下面那些看的不懂呢&#xff0c;于是就返回FTP的应用了。 9.5.4 FTP 应…

[A-29]ARMv8/v9-GIC-中断子系统的安全架构设计(Security/FIQ/IRQ)

ver0.1 前言 打开这篇文章的时候,我们已经为每一个中断信号规划一条路径,在外设和PE-Core之间建立了消息通道,外设有紧急的情况下可以给SOC中的大哥打报告了。下面就把接力棒就交到了CPU手里了,但是PE-Core要交给那个Exception Level以及Security下运行的软件处理呢?本文…

PWM频率测量方法

测量PWM&#xff08;脉宽调制&#xff09;信号的频率是嵌入式系统中的常见需求&#xff0c;尤其是在电机控制、LED调光、传感器信号处理等场景中。 在这里介绍两种测量PWM频率的方法&#xff1a;测频法与测周法。 1、测频&#xff08;率&#xff09;法 原理&#xff1a;在闸门…

c++多态

1.多态的概念 通俗来说&#xff0c;就是多种形态&#xff0c;具体点就是去完成某个行为&#xff0c;当不同的对象去完成时会产生出不同 的状态。 2.多态的定义及实现 2.1多态的构成条件 多态是在不同继承关系的类对象&#xff0c;去调用同一函数&#xff0c;产生了不同的行为…

MySQL分表自动化创建的实现方案(存储过程、事件调度器)

《MySQL 新年度自动分表创建项目方案》 一、项目目的 在数据库应用场景中&#xff0c;随着数据量的不断增长&#xff0c;单表存储数据可能会面临性能瓶颈&#xff0c;例如查询、插入、更新等操作的效率会逐渐降低。分表是一种有效的优化策略&#xff0c;它将数据分散存储在多…

Deepseek的RL算法GRPO解读

在本文中&#xff0c;我们将深入探讨Deepseek采用的策略优化方法GRPO&#xff0c;并顺带介绍一些强化学习&#xff08;Reinforcement Learning, RL&#xff09;的基础知识&#xff0c;包括PPO等关键概念。 策略函数&#xff08;policy&#xff09; 在强化学习中&#xff0c; a…

cent6.6安装rabbitmq

cent6.6安装rabbitmq 如果对运维课程感兴趣&#xff0c;可以在b站上、A站或csdn上搜索我的账号&#xff1a; 运维实战课程&#xff0c;可以关注我&#xff0c;学习更多免费的运维实战技术视频 1.在服务器192.168.231.128上安装rabbitmq 1)安装编译工具 [rootlocalhost ~]# …

ubuntu 更新24LTS中断导致“系统出错且无法恢复,请联系系统管理员”

22LTS to 24LTS 更新过程中手jian把更新程序controlC导致的。 解决 目前企图完成更新来恢复&#xff0c;重启后有软件包冲突&#xff0c;sudo apt upgrade报冲突。无法进行。 将原来source.list重新 sudo dpkg --configure -a sudo apt install -f 这些都不管用。还是显示gno…

Databend x 沉浸式翻译 | 基于 Databend Cloud 构建高效低成本的业务数据分析体系

「沉浸式翻译」是一个非常流行的双语对照网页翻译扩展工具&#xff0c;用户可以用它来即时翻译外文网页、PDF 文档、ePub 电子书、字幕等。它不仅可以实现原文加译文实时双语对照显示&#xff0c;还支持 Google、OpenAI、DeepL、微软、Gemini、Claude 等数十家翻译平台服务的自…

LabVIEW纤维集合体微电流测试仪

LabVIEW开发纤维集合体微电流测试仪。该设备精确测量纤维材料在特定电压下的电流变化&#xff0c;以分析纤维的结构、老化及回潮率等属性&#xff0c;对于纤维材料的科学研究及质量控制具有重要意义。 ​ 项目背景 在纤维材料的研究与应用中&#xff0c;电学性能是评估其性能…

哈工大:LLM高质量嵌入模型KaLM-Embedding

&#x1f4d6;标题&#xff1a;KaLM-Embedding: Superior Training Data Brings A Stronger Embedding Mode &#x1f310;来源&#xff1a;arXiv, 2501.01028 &#x1f31f;摘要 &#x1f538;随着检索增强生成在大型语言模型中的盛行&#xff0c;嵌入模型变得越来越重要。尽…

Baklib打造高效内容管理平台提升协作与创作体验

内容概要 随着信息化时代的迅猛发展&#xff0c;高效的内容管理已成为各类团队日常工作中不可或缺的一部分。Baklib的内容管理平台正是应运而生&#xff0c;旨在为团队提供一个集协作、创作与管理于一体的解决方案。该平台通过其直观的界面和一系列创新功能&#xff0c;帮助用…