C++ Primer 动态数组

欢迎阅读我的 【C++Primer】专栏

专栏简介:本专栏主要面向C++初学者,解释C++的一些基本概念和基础语言特性,涉及C++标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级程序设计技术。希望对读者有帮助!

在这里插入图片描述
在这里插入图片描述

目录

  • 12.2动态数组
    • new和数组
    • 分配一个数组会得到一个元素类型的指针
    • 初始化动态分配对象的数组
    • 释放动态数组
    • 智能指针和动态数组
    • allocator类
    • allocator类
    • allocator分配未构造的内存
    • 拷贝和填充未初始化内存的算法

12.2动态数组

new和delete运算符一次分配/释放一个对象,但某些应用需要一次为很多对象分配内存的功能。例如,vector和string都是在连续内存中保存它们的元容器需要重新分配内存时,必须一次性为很多元素分配内存。

为了支持这种霁求,C++语言和标准库提供了一种一次分配一个对象数组的方法.C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组。标准库中包含一个名为allocator的类,允许我们将分配和初始化分离。使用allocator通常会提供更好的性能和更灵活的内存管理能力。

很多(可能是大多数)应用都没有直接访问动态数组的需求。当一个应用需要可变数量的对象时,我们在StrBlob中所采用的方法几乎总是更简单、更快速并东更安全的一一即,使用vector(或其他标准库容器),使用标准库容器的优势在新标准下更为显著。在支持新标准的标准库中,容器操作比之前的版本要快速得多。

Best“大多数应用应该使用标准库客器而不是动态分配的数组。使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能。

如前所述,使用容器的类可以使用默认版本的拷贝、赋值和析构操作。分配动态数组的类则必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存。

new和数组

为了让new分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目。在下例中,new分配要求数量的对象并(假定分配成功后)返回指向第一个对象的指针:

//调用get_size确定分配多少个int
int*pia=new int[get_size()];//pia指向第一个int

方括号中的大小必须是整型,但不必是常量。

也可以用一个表示数组类型的类型别名来分配一个数组,这样,new表达式中就不需要方括号了

typedef int arrT[42];//arrT表示42个int的数组类型
int*p = new arrT;//分配一个42个int的数组;p指向第一个int

在本例中,new分配一个int数组,并返回指向第一个int的指针。即使这段代码中没有方括号,编译器执行这个表达式时还是会用new[]。即,编译器执行如下形式:

int*p=new int[42];

分配一个数组会得到一个元素类型的指针

虽然我们通常称new[]分配的内存为“动态数组“,但这种叫法某种程度上有些误导。当用new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。即使我们使用类型别名定义了一个数组类型,new也不会分配一个数组类型的对象。在上例中,我们正在分配一个数组的事实甚至都是不可见的一一连[num]都没有。new返回的是一个元素类型的指针。

由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end。这些函数使用数组维度来返回指向首元素和尾后元素的指针。出于相同的原因,也不能用范围for语句来处理(所谓)动态数组中的元素。

初始化动态分配对象的数组

默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号。

int*pia=new int[10];//10个未初始化的int
int*pia2=new int[10]();//10个值初始化为0的int
string*psa=new string[10];//10个空string
string*psa2=new string[10]();//10个空string

在新标准中,我们还可以提供一个元素初始化器的花括号列表:

//10个int分别用列表中对应的初始化器初始化
int*pia3=newint[10]{0,1,2,3,4,5,6,7,9};
//10个string,前4个用给定的初始化器初始化,制余的进行值初始化
string*psa3=new string[10]{"a","an","the",string(3,'x')};

与内置数组对象的列表初始化一样,初始化器会用来初始化动态数组中开始部分的元素。如果初始化器数目小于元素数目,剩余元素将进行值初始化。如果初始化器数目大于元素数目,则new表达式失败,不会分配任何内存。在本例中,new会抛出一个类型为bad_array_new_length的异常。类似bad_alloc,此类型定义在头文件new中。

虽然我们用空括号对数组中元素进行值初始化,但不能在括号中给出初始化器,这意着不能用auto分配数组。动态分配一个空数组是合法的可以用任意表达式来确定要分配的对象的数目:

size_t n = get_size();//get_size返回需要的元素的数目
int p = new int[n];//分配数组保存元素
for(int*q= p;q!= p+n;++q)
/*处理数组*/;

这产生了一个有意思的问题:如果get_size返回0,会发生什么?答案是代码仍能正常工作。虽然我们不能创建一个大小为0的静态数组对象,但当n等于0时,调用new[n]是合法的:

char arr[0];//锦误:不能定义长度为0的数组
char*cp=new char[0];//正确:但cp不能解引用

当我们用new分配一个大小为0的数组时,new返回一个合法的非宇指针。此指针保证与new返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样,我们可以像使用尾后迭代器一样使用这个指针。可以用此指针进行比较操作,就像上面循环代码中那样。可以向此指针加上(或从此指针减去0,也可以从此指针减去自身从而得到0。但此指针不能解引用一一毕竞它不指向任何元素。

在我们假想的循环中,若get_size返回0,则n也是0,new会分配0个对象。for循环中的条件会失败(p等于q+n,因为n为0)。因此,循环体不会被执行。

释放动态数组

为了释放动态数组,我们使用一种特殊形式的delete一在指针前加上一个空方括号对:

delete p;    // p必须指向一个动态分配的对象或为空
delete[] pa; // pa必须指向一个动态分配的数组或为空

第二条语句销毁pa指向的数组中的元素,并释放对应的内存。数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,依此类推。

当我们释放一个指向数组的指针时,空方括号对是必需的:它指示编译器此指针指向一个对象数组的第一个元素。如果我们在delete一个指向数组的指针时忽略了方括号(或者在delete一个指向单一对象的指针时使用了方括号),其行为是未定义的。

当我们使用一个类型别名来定义一个数组类型时,在new表达式中不使[]。即使是这样,在释放一个数组指针时也必须使用方括号:

typedef int arrT[42];//arrT是42个int的数组的类型别名
int*p = new arrT;//分配一个42个int的数组;p指向第一个元素
delete[] p;//方括号是必需的,因为我们当初分配的是一个数组

不管外表如何,p指向一个对象数组的首元素,而不是一个类型为arrT的单一对象。因此,在释放p时我们必须使用[]。

如果我们在delete一个数组指针时忘记了方括号,或者在delete一个单一对象的指针时使用了方括号,编译器很可能不会给出警告。我们的程序可能在执行过程中在没有任何警告的情况下行为异常。

智能指针和动态数组

标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对定方括号:

//up指向一个包含10个未初始化int的数组
unique_ptr<int[]> up(new int[10]);
up.release();//自动用delete[]销毁其指针

类型说明符中的方括号(<int[]>)指出up指向一个int数组而不是一个int。由于up指向一个数组,当up销毁它管理的指针时,会自动使用delete[]。

我们在表12.6中描述了这些操作。当一个unique_ptr指向一个数组时,我们不能使用点和箭头成员运算符。毕竟unique_ptr指向的是一个数组而不是单个对象,因此这些运算符是无意义的。另一方面,当一个unique_ptr指向一个数组时,我们可以使用下标运算符来访问数组中的元素:

for(size_t i= 0;i!=10;++i)
up[i]= i;//为每个元素赋予一个新值

表12.6:指向数组的unique_ptr

指向教组的unique_ptr不支持成员访问运算符(点和箭头运算符)。其他unique_ptr操作不变。
unique_ptr<T[]>u可以指向一个动态分配的数组,数组元素类型为T
unique_ptr<T[]>u§u指向内置指针p所指向的动态分配的数组。p必须能转换为类型 T*
u[]返回u拥有的数组中位置i处的对象 u必须指向一个数组

与unique_ptr不同,shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器:

//为了使用shared_ptr,必须提供一个删除器
shared_ptr<int>sp(new int[10],[](int*p){delete[]p;});
sp.reset();//使用我们提供的lambda释放数组,它使用delete[]

本例中我们传递给shared_ptr一个lambda作为删除器,它使用delete[]释放数组。

如果未提供删除器,这段代码将是未定义的.默认情况下,shared_ptr使用delete销毁它指向的对象。如果此对象是一个动态数组,对其使用adelete所产生的问题与释放一个动态数组指针时忘记[]产生的问题一样。

shared_ptr不直接支持动态数组管理这一特性会影响我们如何访问数组中的元素:

shared_ptr未定义下标运算符,并且不支持指针的算术运算

for(size_t i=0;i!=10;++i)
*(sp.set()+i)=i;//使用get获取一个内置指针

shared_ptr未定义下标运算符,而且智能指针类型不支持指针算术运算。因此,为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素。

allocator类

new有一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在了一人细起。类似的,delete将对象析构和内存释放组合在了一起。我们分配单个对象时,通常希望将内存分配和对象初始化组合在一起。因为在这种情况下,我们几乎肯定知道对象应有什么值。

当分配一大块内存时,我们通常计划在这块内存上按需构造对象。在此情况下,我们希望将内存分配和对象构造分离。这意味着我们可以分配大块内存,但只在真正需要时才真正执行对象创建操作(同时付出一定开销)。

一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费。例如:

string*const p=new string[n];//构造n个空string
string s;
string*q=p; //q指向第一个string
while(cin>>8&&q!=p+n)
    *q++=s;//赋予*q一个新值
const size_t size=q-p;//记住我们读取了多少个string
//使用数组
delete[] p;//P指向一个数组;记得用delete[]来释放

new表达式分配并初始化了n个string。但是,我们可能不需要n个string,少量string可能就足够了。这样,我们就可能创建了一些永远也用不到的对象。而且,对于那些确实要使用的对象,我们也在初始化之后立即赋予了它们新值。每个使用到的元素都被赋值了两次:第一次是在默认初始化时,随后是在赋值时。

更重要的是,那些没有默认构造函数的类就不能动态分配数组了。

allocator类

标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。表12.7概述了allocator支持的操作。在本节中,我们将介绍这些allocator操作。

类似vector,allocator是一模版。为了定义一个allocator对象,我们必须指明这个allocator可以分配的对象类型。当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:

allocator<string>alloc; //可以分配string的allocator对象
auto const p=alloc.allocate(n);//分配n个未初始化的strtng

这个allocate调用为n个string分配了内存。

表12.7:标准库allocator类及其算法

allocatora定义了一个名为a的allocator对象,它可以为类型为的对象分配内存
a.allocate(n)分配一段原始的、未构造的内存,保存n个类型为的对象
a.deallocate(p,n)释放从7*指针p中地址开始的内存,这块内存保存了n个类型为的对象;p必须是一个先前由allocate返回的指针,且n必须是p创建时所要求的大小。在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destroy
a.constuct(p,args)p必须是一个类型为04的指针,指向一块原始内存;arg被传递给类型为7的构造函数,用来在p指向的内存中构造一个对象
a.destroy§p为T*类型的指针,此算法对p指向的对象执行析构函数

allocator分配未构造的内存

allocator分配的内存是未构造的(unconstructed)。我们按需要在此内存中构造对象。在新标准库中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象。类似make_shared的参数,这些额外参数必须是与构造的对象的类型相匹配的合法的初始化器:

auto q=p;//q指向最后构造的元素之后的位置
alloc.construct(q++);//*q为空字符争
alloc.construct(q++,10,c1);//xq为cccccccccc
alloc.construct(q++,"hi");//*q为hi!

在早期版本的标准库中,construct只接受两个参数:指向创建对象位置的指针和一个元素类型的值。因此,我们只能将一个元素拷贝到未构造空间中,而不能用元素类型的任何其他构造函数来构造一个元素。

还未构造对象的情况下就使用原始内存是错误的:

cout<<*p<<endl;//正确:使用string的输出运算答
cout<<*q<<endl;//灾难:q指向未构造的内存!

为了使用allocate返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。

当我们用完对象后,必须对每个构造的元素调用destroy来销毁他们。函数destroy接受一个指针,对指向的对象执行析构函数:

while(q!=p)
alloc.destroy(--q);//释放我们真正构造的string

在循环开始处,q指向最后构造的元素之后的位置。我们在调用destroy之前对q进行了递减操作。因此,第一次调用destroy时,q指向最后一个构造的元素。最后一步循环中我们destroy了第一个构造的元素,随后q将与p相等,循环结束。

我们只能对真正构造了的元素进行destroy操作

一旦元素被销毁后,就可以重新使用这部分内存来保存其他string,也可以将其归还给系统。释放内存通过调用deallocate来完成:

alloc.deallocate(p,n);

我们传递给deallocate的指针不能为空,它必须指向由allocate分配的内存。而且,传递给deallocate的大小参数必须与调用allocated分配内存时提供的大小参数具有一样的值。

拷贝和填充未初始化内存的算法

标准库还为allocator类定义了两个伴随算法,可以在未初始化内存中创建对象。表12.8描述了这些函数,它们都定义在头文件memory中。

表12.8:allocator算法

这些函数在给定目的位置创建元素,而不是由系统分配内存给它们。
uninitiallized_copy(b,e,b2)从迭代器b和e指出的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中。b2指向的内存必须足够大,能容纳输入序列中元素的拷贝
uninitialized_copy_n(b,n,b2)从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中
uninitialized_fill(b,e,t))在迭代器b和e指定的原始内存范围中创建对象,对象的值均为t的拷贝
uninitialized_fi11_n(b,n,t)从迭代器b指向的内存地址开始创建n个对象。b必须指向足够大的未构造的原始内存,能够容纳给定数量的对象

作为一个例子,假定有一个int的vector,希望将其内容拷贝到动态内存中。我们将分配一块比vector中元素所占用空间大一倍的动态内存,然后将原vector中的元素拷贝到前一半空间,对后一半宇间用一个给定值进行填充:

//分配比vi中元素所占用空间大一倍的动态内存
auto p=alloc.allocate(vi.size()*2);
//通过指贝vi中的元素来构造从p开始的元素
auto d=uninitialized_copy(vi.begin(),vi.end(),p);
//将剩余元素初始化为42
uninitialized_fi11_n(q,vi.size(),42);

类似拷贝算法,uninitialized_copy接受三个迭代器参数。前两个表示输入序列,第三个表示这些元素将要拷贝到的目的空间。传递给uninitialized_copy的目的位置选代器必须指向未构造的内存。与copy不同,uninitialized_copy在给定目的位置构造元素。

类似copy,uninitialized_copy返回(递增后的)目的位置迭代器。因此,一次uninitialized_copy调用会返回一个指针,指向最后一个构造的元素之后的位置。在本例中,我们将此指针保存在q中,然后将q传递给uninitialized_fill_n。此函数类似fill_n,接受一个指向目的位置的指针、一个计数和一个值。它会在目的位置指针指向的内存中创建给定数目个对象,用给定值对它们进行初始化。

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

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

相关文章

第四十一:Axios 模型的 get ,post请求

Axios 的 get 请求方式 9.双向数据绑定 v-model - 邓瑞编程 Axios 的 post 请求方式&#xff1a;

神经网络:AI的网络神经

神经网络&#xff08;Neural Networks&#xff09;是深度学习的基础&#xff0c;是一种模仿生物神经系统结构和功能的计算模型。它由大量相互连接的节点&#xff08;称为神经元&#xff09;组成&#xff0c;能够通过学习数据中的模式来完成各种任务&#xff0c;如图像分类、语音…

20250304在Ubuntu20.04的GUI下格式化exFAT格式的TF卡为ext4格式

20250304在Ubuntu20.04的GUI下格式化exFAT格式的TF卡为ext4格式 2025/3/4 16:47 缘起&#xff1a;128GB的TF卡&#xff0c;只能格式化为NTFS/exFAT/ext4。 在飞凌的OK3588-C下&#xff0c;NTFS格式只读。 exFAT需要改内核来支持。 现在只剩下ext4了。 linux R4默认不支持exFAT…

FPGA之硬件设计笔记-持续更新中

目录 1、说在前面2、FPGA硬件设计总计说明3、 原理图详解 - ARITX - 7 系列3.1 顶层框图介绍3.2 FPGA 电源sheet介绍&#xff1a;3.2.1 bank 14 和 bank 15的供电3.2.2 bank 0的供电3.2.3 Bank34 35 的供电 3.3 核电压和RAM电压以及辅助电压 4 原理图详解-- Ultrascale ARTIX4.…

【弹性计算】弹性裸金属服务器和神龙虚拟化(一):功能特点

《弹性裸金属服务器》系列&#xff0c;共包含以下文章&#xff1a; 弹性裸金属服务器和神龙虚拟化&#xff08;一&#xff09;&#xff1a;功能特点弹性裸金属服务器和神龙虚拟化&#xff08;二&#xff09;&#xff1a;适用场景弹性裸金属服务器和神龙虚拟化&#xff08;三&a…

【Azure 架构师学习笔记】- Azure Databricks (15) --Delta Lake 和Data Lake

本文属于【Azure 架构师学习笔记】系列。 本文属于【Azure Databricks】系列。 接上文 【Azure 架构师学习笔记】- Azure Databricks (14) – 搭建Medallion Architecture part 2 前言 ADB 除了UC 这个概念之外&#xff0c;前面【Azure 架构师学习笔记】- Azure Databricks (1…

字节跳动发布 Trae AI IDE!支持 DeepSeek R1 V3,AI 编程新时代来了!

3 月 3 日&#xff0c;字节跳动重磅发布国内首款 AI 原生集成开发环境&#xff08;AI IDE&#xff09;——Trae 国内版&#xff01; Trae 不只是一个传统的 IDE&#xff0c;它深度融合 AI&#xff0c;搭载 doubao-1.5-pro 大模型&#xff0c;同时支持DeepSeek R1 & V3&…

大模型——CogView4:生成中英双语高清图片的开源文生图模型综合介绍

CogView4:生成中英双语高清图片的开源文生图模型综合介绍 CogView4 是由清华大学 KEG 实验室(THUDM)开发的一款开源文生图模型,专注于将文本描述转化为高质量图像。它支持中英双语提示词输入,尤其擅长理解中文提示并生成带有汉字的图像,非常适合广告设计、短视频创作等场…

AI大模型爆火背后,C++ 如何助力 AI 开发大显身手?

目录 ​编辑 一、本篇背景&#xff1a; 二、C 语言的起源与发展历程&#xff1a; 2.1 起源背景&#xff1a; 2.2 发展阶段&#xff1a; 三、C 的基础特性及优势&#xff1a; 3.1 高效性能&#xff1a; 3.2 底层控制能力&#xff1a; 3.3 面向对象编程&#xff1a; 3.…

深度学习R8周:RNN实现阿尔兹海默症(pytorch)

&#x1f368; 本文为&#x1f517;365天深度学习训练营中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 数据集包含2149名患者的广泛健康信息&#xff0c;每名患者的ID范围从4751到6900不等。该数据集包括人口统计详细信息、生活方式因素、病史、临床测量、认知和功…

【笔记ing】python

1 Python基础概念及环境搭建 1.1 python简介及发展史 之父Guido van Rossum。ABC语言的替代品。Python提供了高效的数据结构&#xff0c;还能简单有效地面向对象编程。Python语法和动态类型&#xff0c;以及解释性语言的本质&#xff0c;使之成为多数平台上写脚本和快速开发应…

IDEA 接入 Deepseek

在本篇文章中&#xff0c;我们将详细介绍如何在 JetBrains IDEA 中使用 Continue 插件接入 DeepSeek&#xff0c;让你的 AI 编程助手更智能&#xff0c;提高开发效率。 一、前置准备 在开始之前&#xff0c;请确保你已经具备以下条件&#xff1a; 安装了 JetBrains IDEA&…

【leetcode hot 100 189】轮转数组

错误解法一&#xff1a;申请一个数组&#xff0c;第i个数放在新数组的ik或ik-nums.length上 class Solution {public void rotate(int[] nums, int k) {int[] resultsnew int[nums.length];for(int i0; i<nums.length; i){if(ik<nums.length){results[ik] nums[i];}els…

Express MVC

1. 安装依赖 npm init -y npm install express npm install --save-dev typescript ts-node ejs types/node types/express tsc --init 2. 项目目录结构如下&#xff0c;没有的手动创建 /my-app/src/modelsuser.ts/viewsindex.ejsuserList.ejs/controllersuserController.ts…

AI数据分析:deepseek生成SQL

在当今数据驱动的时代&#xff0c;数据分析已成为企业和个人决策的重要工具。随着人工智能技术的快速发展&#xff0c;AI 驱动的数据分析工具正在改变我们处理和分析数据的方式。本文将着重介绍如何使用 DeepSeek 进行自动补全SQL 查询语句。 我们都知道&#xff0c;SQL 查询语…

从小米汽车召回看智驾“命门”:智能化时代 — 时间就是安全

2025年1月&#xff0c;小米因车辆“授时同步异常”召回3万余辆小米SU7&#xff0c;成为其造车历程中的首个重大安全事件。 从小米SU7召回事件剖析&#xff0c;授时同步何以成为智能驾驶的命门&#xff1f; 2024年11月&#xff0c;多名车主反馈SU7标准版的智能泊车辅助功能出现…

【论文阅读】Universal Adversarial Attacks for Visual Odometry Systems

一、背景 广义敌对攻击是一种针对深度学习的攻击方法&#xff0c;通过对整个输入进行微调&#xff0c;从而影响模型的运行结果。现有的针对回环检测的攻击以及物理贴图等方法&#xff0c;大多数都是针对一般的SLAM算法进行设计&#xff0c;而对于基于深度学习的视觉里程计来说…

A-LOAM工程笔记(一):工程编译及运行(ubuntu20.04 + ros_noetic)

1.编译前准备 需要提前安装Ceres solver和opencv和PCL&#xff0c;如果你安装的是完整版ROS那么PCL已经自动安装好了。安装好后将工程克隆到工作目录然后编译&#xff1a; cd ~/catkin_ws/src/ git clone https://github.com/HKUST-Aerial-Robotics/A-LOAM.git aloam_velodyn…

数据库基础五(数据库环境变量配置详细教程)

1、在小皮的设置界面检测3306端口&#xff0c;保障3306端口可用&#xff1b; 2、在小皮的首面界面&#xff0c;启动MySQL&#xff1b; 3、进行环境变量设置&#xff0c;找到MySQL的路径&#xff0c;进行复制&#xff1b; 4、在Windows的搜索栏内&#xff0c;输入“环境变量”&a…

【对话推荐系统综述】A Survey on Conversational Recommender Systems

文章信息&#xff1a; 发表于&#xff1a;ACM Computing Surveys 2021 原文链接&#xff1a;https://arxiv.org/abs/2004.00646 Abstract 推荐系统是一类软件应用程序&#xff0c;旨在帮助用户在信息过载的情况下找到感兴趣的项目。当前的研究通常假设一种一次性交互范式&am…