C++的左值引用与右值引用详解

你能学到

  • 左值与右值
  • 左值引用与右值引用 基本用法与作用
  • 拷贝构造函数 与 移动构造函数
  • 移动语义 与 std::move
  • 完美转发:std::forward

前言

本文代码片段中变量命名规则如下:

  • 小写字母:一般类型的变量(非指针、非引用)
  • p:pointer,指针类型的变量(p1、p2…)
  • r:reference,引用类型的变量
    • lr_*:比如 lr_a,a 的左值引用
    • rr*:右值引用(rr1、rr2…)
  • 上述均是非 const 变量,如果名称以 c 开头,说明该变量为 const
  • fun:function,函数名(fun1、fun2…)
  • 大写字母:自定义类名

在说左值引用与右值引用之前,有必要先说说什么是左值,什么是右值:

c++ 中的左值与右值没有标准的定义,没必要死套公式、死扣细节,只需要理解即可。
如果你想要了解更多,可以通过关键字 c++ value categories 搜索相关资料。


1. 左值与右值

  • 左值:lvalue
  • 右值:rvalue

就它俩的英文缩写以及中文翻译,使得有一部分人将其解释为:

lvalue: left value
rvalue: right value

因此也诞生了对左值与右值的一种解释:

左值:能出现表达式左边
右值:不能出现在表达式左边

这种解释从某方面来说也可以是对的。下面我从另外一方面来解释什么是左值什么是右值:


1.1 左值

lvalueloactor value 的缩写

左值指的是 存储在内存中,有明确内存地址的数据。因此在语法层面:

左值包含两部分信息

  • 内存地址:记录对象在内存中的位置
  • 数据值:记录对象的值

所以它可以

  • 出现在表达式的左边
  • 出现在表达式的右边
  • 取地址(&)
int a = 0;	// a 为左值,值为 1int b = a;	// 正确:a 能出现在右边
a = 1;	// 正确:a 能出现在左边
&a;	 // 正确:a 能取地址

【总结】 左值 可以取地址的、有名字的、非临时的,它是用户创建的,能通过作用域的规则知道它的生存周期。


1.2 右值

lvaueread value 的缩写

右值指的是 可以提供数据值的数据。在语法层面:

右值仅包含数据值,因此它

  • 只能出现在表达式左边
  • 不可以取地址
int a = 0, b = 1;100;	// 100 是一个右值
100 = a;	// 错误:右值不能出现在左边
&100;	// 错误:右值不能取地址

【总结】 右值 不能取地址、没有名字、临时的,它的创建与销毁实际由编译器在幕后控制,对用户而言有用的信息仅仅是它的数据值。


【扩展】
在 c++ 中,右值实际分为两种:

  • 纯右值 (prvalue)
    • 返回类型不是引用的一般函数的返回值
    int fun() { int a = 1; return a; }
    fun() = 1;	// 错误:fic() 返回值为纯右值
    
    • 运算表达式的结果
    int a = 0, b = 1;
    a + b = 1;	// 错误:运算表达式 a + b 返回的是纯右值
    
    • 原始字面量
    • lambda 表达式
    • 取地址操作
    • … …
  • 将亡值 (xvalue,就字面意思:即将死亡的临时对象):
    • 返回类型为右值引用的函数的返回值
    • std::move 的返回值
    • … …

总的来说,最直接地区分左值与右值的方法为:是否可以取地址


2. 左值引用与右值引用

此部分讲解的主要内容在于:用左值或者右值给左值引用与右值引用进行赋值,不会讲解如果加了 const 修饰需要注意什么问题。如果你对后者感兴趣,看见作者的另外一篇文章:C++ const 关键字详解

无论是左值引用还是右值引用,在语法层面都是 给对象起别名,与被绑定的对象共用同一块内存。在这里先记住一个结论:

  • 左值引用视为左值
  • 右值引用
    • 有名称的 视为左值
    • 否则 视为右值

2.1 左值引用

顾名思义,是对左值的引用,给左值起别名。

语法:类型 & 名称 = 左值

遵循以下规则:

  • 非 const 的左值引用 只用 左值 来赋值。
  • const 的左值引用 可以 用 右值 来赋值。

看到这里你可能会疑惑:左值引用不是只针对左值吗,为什么还能用右值来赋值?别急,往后看。

// 以下变量都是左值
int  a = 0;
int* p = &a;
int& r = a;// 以下都是给左值起别名
int&  r_a = a;	
int*& r_p = p;	// 对 int* 的引用,类型解析从右到左/*** 直观上 r_r 是 r 的别名,* 但由于 r 是 a 的别名,* 因此 r_r 也可以看作 a 的别名 */
int& r_r = r;	// 特殊
int& r1 = 100;	// 错误:非 const 左值引用不能用右值初始化
const int& cr = 100;	// 正确:const 左值引用能用右值初始化

对于

const int& cr = 100;

这可能就会让人感到疑惑:

为什么左值引用 (const) 能引用右值?

先来看一个例子:

int fun(int& a) { }

如果我们尝试如下调用这个函数

fun(0);

会报错
在这里插入图片描述

这是因为 1 是一个右值,非 const 的左值引用不能用右值初始化。在 c++11 以前不存在 右值引用的概念,因此为了解决这一问题,引入了规则:

const 的左值引用既可以用 左值 赋值,也可以 用 右值 赋值。

因此如果参数类型为左值引用,更建议使用 const 左值引用,也是出于这个考虑。
比如在老版本的 vector 模板的 push_back() 方法:

void push_back(const value_type& __x);vector<int> t;
int a = 1;// 以下均正确
t.push_back(a);
t.push_back(1);

2.2 右值引用

顾名思义,对右值的引用,给右值起别名。

语法:类型 && 名称 = 右值

遵循以下规则:

  • 右值引用 只能用 右值 初始化
int   a = 1, b = 1;
int*  p  = &a; 
int&  lr = a;
int&& rr = 100;
int&& fun() { return 0 }// 以下均是右值
100;
a + b;
&a;// 以下均正确
int&&   rr1 = 100;	
int&&   rr2 = a + b;
int* && rr3 = &a;	// 对 int* 指针的右值引用
int&&   rr4 = fun();	// fun() 的返回值为 int&&,无名称,视为右值// 以下均错误
int&& rr5 = a;	// a 是左值
int&& rr6 = p;	// p 是指针,为左值
int&& rr7 = lr;	// lr 为左值引用,视为左值
int&& rr8 = rr;	// rr 有名称右值引用,视为左值

// 以下均错误
int&& rr6 = lr;	// lr 为左值引用,为左值
int&& rr7 = rr;	// rr 是右值引用,为左值

也验证了之前所说的

  • 左值引用视为左值
  • 右值引用
    • 有名称的 视为左值
    • 否则 视为右值

既然 有名称的右值引用 是左值,它是用右值初始化的。也就是说

右值引用使得右值 “重获新生”,让此右值的生命周期 与 对应的右值引用的生命周期一样:只要该右值引用还活着,该右值也将一直存活下去

基本语法搞定之后,那么你会好奇这么一个问题:

既然左值引用已经解决了用右值给左值引用初始化的问题,那为什么还要引入 右值引用 呢?

这是为了性能考虑。


3. 移动语义 与 std::move

move,可译为移动,但译为 转移 更为合适


3.1 移动语义

移动语义:转移对象的资源控制权

这么直接说定义比较难以理解,下面举个例子:

这例子源于某网站博主的文章 (文章结尾有指出本文的所有参考文章)


问题一:如何将大象放入冰箱?

答案是众所周知的。首先你需要有一台特殊的冰箱,这台冰箱是为了装下大象而制造的。你打开冰箱门,将大象放入冰箱,然后关上冰箱门。

问题二:如何将大象从一台冰箱转移到另一台冰箱?

  • 普通解答:打开冰箱门,取出大象,关上冰箱门,打开另一台冰箱门,放进大象,关上冰箱门。

  • 2B解答:在第二个冰箱中启动量子复制系统,克隆一只完全相同的大象,然后启动高能激光将第一个冰箱内的大象气化消失。


这里的 问题二 就比较好的说明了什么是移动语义。分析一下这个例子:
假设现在我们已经有了一个实体:大象A,需要通过 A 创建另外一个大象B,那么我们两套方案:

  • 普通解答:将 A “移动” 到 B,即将 A 的资源 转移给 B
  • 2B解答:拷贝一份 A 的资源给 B,然后再将 A 的资源回收(即析构大象A)

即便你没有对底层有多了解,听了这个例子你也能得出:普通解答显然效率更高。

这两个解答就很好地对应了 c++ 的 2B解答——拷贝构造函数普通解答——移动构造函数

本文只讲解 拷贝/移动构造函数,不讲解 拷贝/移动赋值函数

不考虑其他的,移动构造函数 的效率比 拷贝构造函数 要高。
因为 拷贝构造函数 会拷贝一份 对象A 的资源,需要向操作系统申请资源(系统资源是十分昂贵的),再将资源赋给 对象B,这就降低了性能;而 移动构造函数 则是直接将 对象A 的资源转移给 对象B,不存在申请资源操作。

既然 移动构造函数 的效率更高,那么为什么还保留 拷贝构造函数?

需要注意,当调用了移动构造函数后,A 资源被转移了,那么 A 此时相当于是个 “废物” 了,如果你在之后仍然使用 A 对象,那么会导致未定义行为。
换句话说,移动构造函数 相当于进行了 废物利用:当明确 对象A 在后续一定不会被使用时,那么它的资源可以转给其他需要此类型资源的对象,不需要重新申请资源,也不用释放资源。

废物也不是完全无用,因为它可以 回收利用 😐

但是如果 对象A 你在后续仍然会使用,并且需要创建 对象B,那么就应该调用 拷贝构造函数。

从形式上看:

  • 拷贝构造函数:参数为 const 的左值引用
  • 移动构造函数:参数为 右值引用

上面说了一堆理论,下面用代码来实现:

A 类如下

class A
{
public:A(int v) :_val{ new int(v) }{ }// * 拷贝构造函数A(const A& a)   // 使用 const,保证了原对象不会改变:_val{ new int(*(a._val)) } 	// new: 向操作系统申请了资源{ cout << "调用了拷贝构造函数" << endl; }// * 移动构造函数A(A&& a)    // 采用引用的方式,因为需要转移资源:_val{ a._val } 	// 转移资源{ /*** 由于 a 的资源被转移了,* 因此将 a 的资源 _val 指针指向 nullptr,* 避免之后误用 a*/a._val = nullptr; 	cout << "调用了移动移动函数" << endl;    }~A() {if (_val != nullptr) {delete _val;}} private:int* _val;
};

现在我们来使用 A 类

int main()
{A a(10);A b(a);	A c(std::move(a));	// move: 将 a 转为右值引用return 0;
}

执行输入为:
在这里插入图片描述

A 类的构造函数由多个(重载函数),但是编译器会利用指定的规则取匹配最合适的函数。
比如上述代码中:

  • A b(a);
    a 的类型为 A,此时最合适的函数是 A(const A& a);
  • A c(std::move(a));
    std::move(a) 的返回值为 A&&,即便 A(const A& a); 也能匹配,但是最合适的是 A(A&& a);

看完上面代码,相信让你比较疑惑的一点是 std::move


3.2 std::move

源码如下:

template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); 
}

可能有的地方你看不懂,但是不要紧,重点在 static_cast 关键字,它用于强制类型转换。

如果你对它的源码剖析感兴趣,可以见文章: C++11的右值引用、移动语义(std::move)和完美转发(std::forward)详解

也就是说,std::move 的作用 仅仅将一个传入的参数类型强制转换为右值引用,(由于是返回值,没有名称,因此被视为右值) ,我们可以用此函数来 辅助 实现移动语义。

比如上面代码中的 A c(a);
当我们确定 a 对象不在使用时,同时需要创建 c 对象,那么可以 废物利用,调用 移动构造函数 将 a 的资源转移给 c。

【易错】

  • std::move 仅仅是做强制类型转换没有实现资源转移的功能。

    如果将 A 类的移动构造函数删除,那么执行 A c(std::move(a));,此时调用的是 拷贝构造函数。

  • 移动构造函数的 资源转移 功能依赖于它自己的内部实现,并不是说你调用了 移动构造函数 就实现了 资源转移

    如果将 A 类的移动构造函数改为:

    A(A&& a) { 	}
    

    此时执行 A c(a);,即便调用的是拷贝构造函数,但是你并没有在内部实现如何转移资源。

  • 资源转移 并不包含 析构对象
    在执行完 A c(std::move(a)); 后,除非调用析构函数,否则 a 对象仍然存在,只不过它的资源 (_val) 被转移给 c 对象了(由移动构造函数实现)。


4. 完美转发:std::forward

4.1 引入

在 模板 以及 自动推导类型 中,并不是说你指定了 && 它的类型就是 右值引用。

  • 在模板中:T&&
  • 自动推导类型:auto&&

中的 && 被称为 未定的引用类型,因为它既可能是左值引用,也可能是右值引用。

const auto && 或者 const T && 就是右值引用

【注】 std::forward 主要时搭配模板中的 T&&(也成为万能引用) 使用,因此下面的【扩展】部分如果你觉得繁琐,可以直接跳过。

对于未定的引用类型,推导满足:

  • 当 模板参数 或者 auto 推导的变量 为 右值时,未定的引用类型的结果为 右值引用
  • 其他都是 左值引用

有的也将这种特性称为 引用折叠


【扩展】

  • 自动推导类型
int   a = 1;
int&  lr = a;
int&& rr = 100;
int&& fun() { return 0; }const auto&& rr1 = rr;	// 正确:rr1为右值引用
const auto&& rr2 = lr;	// 错误:rr2 是右值引用,不能用左值初始化// 以下变量类型均为右值引用
auto&& rr3 = 100;
auto&& rr4 = fun();// 以下变量类型均为左值引用	
auto&& lr1 = lr;	
auto&& lr2 = rr;  // 因为 rr 被视为左值
  • 模板函数的 const T&& 与 T&&
template <typename T>
void fun1(const T&& t) { }	// t 就是 const 右值引用
fun1(100);    // 正确int&& rr = 100;	
fun1(rr);	// 错误:rr 视为左值,不能给 右值引用初始化
-------------------------------------------------------template <typename T>
void fun2(T&& t> { }
fun2(100);	// t: 右值引用int  a = 0;
int& r = a;
// 以下 t: 左值引用
fun2(a);
fun2(r);

4.2 std::forward

std::forward 主要功能是:实现既能保留右值属性,也能保留左值属性

这句话可能也很抽象,下面来分析一个例子:

#include <iostream>
using namespace std;void print(int&) { cout << "int&" << endl; }
void print(int&&) { cout << "int&&" << endl; }template <typename T>
void fun(T&& t)
{print(t);
}int main()
{int   a  = 0;int&  lr = a;int&& rr = 0;fun(a);fun(0);fun(lr);fun(rr);return 0;
}

程序如上,在模板函数 fun 中调用了 print 函数,并且 传递了参数 t
下面分析程序的执行结果:

  • fun(a)
    a 为左值,所以 T&& 被推导为 int&,那么 print 应输出 int&

  • fun(0)
    0 为右值,所以 T&& 被推导为 int&&,此时你以为 print 应输出 int&&,然而结果并不是。前面我们提到过 有名称的右值引用 被视为 左值,因此虽然 t 为int&&,但是它被视为 左值,所以 print 仍然输出 int&&

说到这里就不在分析之后两个函数调用结果了,留给读者自行分析。

最后程序运行结果为:

在这里插入图片描述

分析完之后,我们发现了关键的问题所在:有名称的右值引用 被视为 左值
如果我们想保留参数 t 的右值属性,也就是说如果传递给 fun 函数的参数 t 是右值,那么 fun内部调用其他函数时,所传递的参数 t 也要保留右值属性,即调用 print(int&&) 函数。
那么怎么实现呢?
有一个方案是 std::move,假设我们将 fun 函数修改为

template <typename T>
void fun(T&& t)
{print(std::move(t));
}

显然可以保留 t的右值属性,但是这出现了一个问题:如果 t 是左值引用,那么 t 也被转为了右值。这不是我们想要的,我们更期望 它既能保留右值属性,也能保留左值属性,这就是之前提到的 std::forward 函数

template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ 	return static_cast<_Tp&&>(__t); 
}template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{static_assert(!std::is_lvalue_reference<_Tp>::value,"std::forward must not be used to convert an rvalue to an lvalue");return static_cast<_Tp&&>(__t);
}

对于调用函数 std::forward<T>(t)

  • 如果 t 是左值引用类型,那么此函数将返回 T 类型的 左值
  • 否则返回 T 类型的 右值

因此,正确的做法是将 fun 函数改写为

template <typename T>
void fun(T&& t)
{	print(std::forward<T>(t));
}

此时程序的执行结果为:
在这里插入图片描述

需要注意最后一个输出对应:fun(rr);,rr 为 有名称的右值引用,被视为左值

符合了我们的要求。


本文参考

  1. <<C++ Primer 第五版>>
  2. C++11的右值引用、移动语义(std::move)和完美转发(std::forward)详解
  3. 详解 C++ 左值、右值、左值引用以及右值引用
  4. c++ 左值引用与右值引用
  5. 右值引用(大象装冰箱的例子)

本文如有错误,欢迎指正。

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

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

相关文章

Java跨平台的原理是什么?JDK,JRE,JVM三者的作用和区别?xxx.java和xxx.class有什么区别?看这一篇就够了

目录 1. Java跨平台相关问题 1.1 什么是跨平台(平台无关性)&#xff1f; 1.2 跨平台(平台无关性)的好处&#xff1f; 1.3 编译原理基础&#xff08;Java程序编译过程&#xff09; 1.4Java跨平台的是实现原理&#xff1f; 1.4.1 JVM(Java虚拟机) 1.4.2 Class文件 1.4.3 …

Object和?

Class<?> 和 Class<Object> 是不同的。 Class<?> 是一个通配符类型&#xff0c;表示未知的具体类型&#xff0c;它可以匹配任意类型。例如&#xff0c;Class<?> 可以表示 String.class、Integer.class 或者任何其他类的 Class 对象。 Class<Ob…

仅两家!云原生向量数据库 PieCloudVector 全项通过信通院「可信数据库」评测

7月16日&#xff0c;2024 可信数据库发展大会在北京隆重举行。大会以“自主、创新、引领”为主题&#xff0c;近百位数据库领域的专家、学者齐聚一堂&#xff0c;带来高质量的数据库技术洞察与实战经验。 本次可信数据库发展大会中&#xff0c;中国信通院正式公布 2024 年上半年…

全国产服务器主板:搭载飞腾FT2000+/64处理器的高性能加固服务器

近期很多朋友咨询全国产化的服务器主板。搭载的是飞腾FT-2000/64的全国产化服务器主板。他的主要特点是&#xff1a;①丰富的PCIe、千兆以太网、SATA接口&#xff0c;可用作数据处理、存储、通信服务器&#xff1b;②​​​​​​​板载独立显示芯片&#xff0c;对外HDMI/VGA/L…

c# .net core中间件,生命周期

某些模块和处理程序具有存储在 Web.config 中的配置选项。但是在 ASP.NET Core 中&#xff0c;使用新配置模型取代了 Web.config。 HTTP 模块和处理程序如何工作 官网地址&#xff1a; 将 HTTP 处理程序和模块迁移到 ASP.NET Core 中间件 | Microsoft Learn 处理程序是&#xf…

经典神经网络(14)T5模型原理详解及其微调(文本摘要)

经典神经网络(14)T5模型原理详解及其微调(文本摘要) 2018 年&#xff0c;谷歌发布基于双向 Transformer 的大规模预训练语言模型 BERT&#xff0c;而后一系列基于 BERT 的研究工作如春笋般涌现&#xff0c;预训练模型也成为了业内解决 NLP 问题的标配。 2019年&#xff0c;谷歌…

node解析Excel中的考试题并实现在线做题功能

1、背景 最近公司安排业务技能考试&#xff0c;下发excel文件的题库&#xff0c;在excel里查看并不是很方便&#xff0c;就想着像学习驾考题目一样&#xff0c;一边看一边做&#xff0c;做完之后可以查看正确答案。 2、开始分析需求 题目格式如下图 需求比较简单&#xff0c;…

阿里布达插画:成都亚恒丰创教育科技有限公司

阿里布达插画&#xff1a;梦幻与现实交织的绮丽画卷 在浩瀚的艺术长河中&#xff0c;总有一些作品以其独特的魅力&#xff0c;跨越时空的界限&#xff0c;触动着每一个观者的心灵。阿里布达插画&#xff0c;便是这样一股不可忽视的艺术清流&#xff0c;它以细腻的情感描绘、奇…

c++ Program to print pyramid pattern (打印金字塔图案的程序)

编写程序打印由星星组成的金字塔图案 例子 &#xff1a; 输入&#xff1a;n 6输出&#xff1a; * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 我们强烈建…

完美的用户体验:如何设计一个直观和有效的网站导航?

APP的顶部导航栏对我们来说很熟悉。导航栏是UI设计中不可或缺的一部分&#xff0c;几乎每个页面都使用导航栏。虽然导航栏看起来很简单&#xff0c;不需要太多精力&#xff0c;但是设计一个与产品需求和客户目标高度匹配的导航栏并不是那么容易的。导航栏的设计标准有很多细节需…

Java语言程序设计基础篇_编程练习题**14.29(游戏:豆机)

第十四章第二十九题 **14.29 (游戏&#xff1a;豆机) 请写一个程序&#xff0c;显示编程练习题 7.21 中介绍的豆机&#xff0c;如图 14-52c 所示 代码展示 package chapter_14;import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.layou…

“萝卜快跑”来了,AGV无人仓距离爆发还有多远?

AGV 今年7月&#xff0c;百度公司在武汉运营的无人出租车平台“萝卜快跑”&#xff0c;成为关注热点。 据悉&#xff0c;短短一个月时间内&#xff0c;“萝卜快跑”订单量突破300万。2024年百度计划投入1000辆无人车724小时全无人运营&#xff0c;覆盖武汉全城&#xff0c;大有…

leetcode94. 二叉树的中序遍历,递归法+迭代法。附带前序遍历方法

leetcode94. 二叉树的中序遍历 给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,3,2] 示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;[] 示例 3&#xff1a; …

MAVSKD-Java开源库mavsdk_server库macOS平台编译

1.下载源码 2.使用IDEA打开,进行mavsdk_server目录,使用gradle进行编译 3.开始编译时会自动下载依赖 4.下载完成后,会自动编译 5.编译成功 6.成功生成AAR文件

计算机毕业设计-基于Springboot的养老院管理系统-源码程序文档

项目源码&#xff0c;请关注❥点赞收藏并私信博主&#xff0c;谢谢~ 本系统开发采用技术为JSP、Bootstrap、Ajax、SSM、Java、Tomcat、Maven 此文章为本人亲自指导加编写&#xff0c;禁止任何人抄袭以及各类盈利性传播&#xff0c; 相关的代码部署论文ppt代码讲解答辩指导文件…

OWASP 移动应用 2024 十大安全风险

1. OWASP 移动应用 2024 十大安全风险 开放全球应用程序安全项目 &#xff08;OWASP&#xff09; 是一个非营利性基金会&#xff0c;致力于提高软件的安全性。自 2014、2016 年两次发布了移动应用的十大风险后&#xff0c;今年再次发布2024版。这对移动应用软件的检查工具有着…

Java二十三种设计模式-抽象工厂模式(3/23)

抽象工厂模式&#xff1a;复杂系统的灵活构建者 引言 在软件开发中&#xff0c;抽象工厂模式是一种提供接口以创建相关或依赖对象族的创建型设计模式。这种模式允许客户端使用一个共同的接口来创建不同的产品族&#xff0c;而无需指定具体类。 基础知识&#xff0c;java设计模…

【.NET全栈】ASP.NET开发Web应用——站点导航技术

文章目录 前言一、站点地图1、定义站点地图文件2、使用SiteMapPath控件3、SiteMap类4、URL地址映射 二、TreeView控件1、使用TreeView控件2、以编程的方式添加节点3、使用TreeView控件导航4、绑定到XML文件5、按需加载节点6、带复选框的TreeView控件 三、Menu控件1、使用Menu控…

初学者对 WebGL 与 WebGPU 的看法(A Beginner’s Perspective of WebGL vs WebGPU)

初学者对 WebGL 与 WebGPU 的看法&#xff08;A Beginner’s Perspective of WebGL vs WebGPU&#xff09; WebGL 和 WebGPU 之间的主要区别&#xff1a;WebGL 是什么以及它适合哪些人使用&#xff1f;WebGPU 是什么&#xff1f;它适合谁使用&#xff1f;WebGL 和 WebGPU 的代码…

<数据集>UA-DETRAC车辆识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;20500张 标注数量(xml文件个数)&#xff1a;20500 标注数量(txt文件个数)&#xff1a;20500 标注类别数&#xff1a;4 标注类别名称&#xff1a;[car, van, others, bus] 序号类别名称图片数框数1car201871259342…