C++-右值引用和移动构造

目录

1. 两种引用方式:

 1.1 左值引用: 

 1.2右值引用

1.3如何判断左右值:

1.4左值引用与右值引用比较

2. 浅拷贝、深拷贝

3.1右值引用的意义:

函数参数传递

函数返还值传递

万能引用

引用折叠

完美转发 std::forward


🌼🌼前言:C++11出现的右值相关语法可谓是很多C++程序员难以理解的新特性,不少人知其然而不知其所以然,面试被问到时大概就只知道可以减少开销,但是为什么减少开销、减少了多少开销、什么时候用...这些问题也不一定知道,于是我写下了这篇夹带自己理解的博文,希望它对你有所帮助。

 

1. 两种引用方式:

😊传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名

引用 是 C++ 相对于 C语言 的升级点之一,引用 既能像指针那样获取资源的地址,直接对资源进行操纵,也不必担心多重 引用 问题,对于绝大多数场景来说,引用 比 指针 好用得多。

其实引用的底层还是指针

 int a=10;int*pa=&a;int& ra=a; 

 const修饰左值引用表示不可以修改引用对象的值

int a=10;
const int& ra=a;
ra++;//报错,不可以修改

 1.1 左值引用: 

什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,也可以出现在右边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值但是可以取它的地址左值引用就是给左值的引用,给左值取别名。

int main()
{
// 以下的p、b、c、*p都是左值,左值在左边的情况
int* p = new int(0);
int b = 1;
const int c = 2;// 以下几个是对上面左值的左值引用,左值在右边的情况
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}

 1.2右值引用

 什么是右值?什么是右值引用?

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。

  • 右值(Rvalue):

    • 表示临时对象,通常是计算表达式的结果或者不可被进一步赋值的值。
    • 右值没有存储在内存中的明确地址,不能作为赋值语句的左操作数
    • 例子:
      • 字面值(如 10
      • 表达式结果(如 x + y, fmin(x, y)
  • 右值引用(Rvalue Reference):

    • && 定义的一种引用,可以绑定到右值。
    • 允许程序开发者操作右值的生命周期(如延长临时对象的存活时间)。
    • 例子:int&& rr1 = 10;
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;//x+y的返回值是个临时变量
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}

 代码之中的 10;是一个字面值,没有明确的地址

需要注意的是右值是不能取地址(左值可以)的,但是给右值取别名后,会导致右值被存储到特定位置(把将要消亡的值找一块安全的地方,使其不被销毁,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

 😊简单来说就是右值被右值引用以后,编译器暂时不销毁它了,还给它一个安全的地址,保护它,右值引用+const也不可以修改了

1.3如何判断左右值:

快速判断 左值 / 右值 的方法之一就是 看看能不能取地址

// 判断左值 / 右值
int main()
{int a = 10;// 左值cout << &a << endl;// 右值cout << &10 << endl; // 【报错】cout << &int() << endl; // 【报错】return 0;
}

 以下是区分**左值(Lvalue)右值(Rvalue)**的表格总结:

特性左值(Lvalue)右值(Rvalue)
定义可以取地址的值,指向内存中的某个具体位置不能取地址的值,通常是临时值或计算结果
能否作为赋值目标可以作为赋值语句的左操作数,也可以做右操作数不能作为赋值语句的左操作数
是否占用内存地址有明确的内存地址通常没有固定的内存地址
生命周期生命周期与其作用域相关,通常更持久生命周期短暂,通常是表达式结果或临时对象
常见例子变量、数组元素、对象、引用字面值、表达式结果、函数返回的临时对象
引用类型只能被普通引用(&)绑定只能被右值引用(&&)绑定

1.4左值引用与右值引用比较

左值引用总结:

  • z1. 左值引用只能引用左值,不能引用右值。
  • 2. 但是const左值引用既可引用左值,也可引用右值
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名//int& ra2 = 10; // 编译失败,因为10是右值// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10; const左值引用,引用右值const int& ra4 = a;
return 0;
}

 右值引用总结:

  • 1. 右值引用只能右值,不能引用左值。
  • 2. 但是右值引用可以move以后的左值

std::move() 的介绍

  • 定义std::move() 是标准库中的一个函数模板,用于将左值显式转换为右值

  • 作用

    • 表示资源的所有权转移,允许将资源从一个对象移动到另一个对象,而不是拷贝
    • 常与右值引用结合使用,避免拷贝构造,提升效率。
  • 实现原理 std::move() 并不移动数据,而是将左值强制转换为右值,使其能够绑定到右值引用。

int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}

以下是左值引用右值引用的比较总结表格:

属性左值引用(Lvalue Reference)右值引用(Rvalue Reference)
语法使用 & 定义使用 && 定义
绑定的对象只能绑定到左值(具备内存地址的对象)只能绑定到右值(临时对象、字面值、表达式结果)
用途用于访问和修改现有对象用于操作临时对象,主要用于移动语义和性能优化
是否能延长生命周期不延长生命周期,绑定的是已有的左值对象可以延长右值(如临时对象)的生命周期
赋值操作可直接用于修改原对象可操作绑定的右值,但不能直接修改字面值或表达式
典型场景- 传递和修改已有的变量- 移动构造、移动赋值
- 参数传递时避免拷贝- 减少临时对象拷贝,提高性能
结合 std::move()无法绑定右值,std::move() 会将左值转换为右值可与 std::move() 配合,接收转换后的右值
示例
定义与绑定int& ref = x;int&& rref = 10;
用于参数传递void func(int& x);void func(int&& x);
代码示例

代码对比

左值引用:

#include <iostream>
void modify(int& ref) {  // 左值引用参数ref += 10;           // 修改传入的变量
}int main() {int x = 20;modify(x);           // 传入左值变量std::cout << x;      // 输出:30return 0;
}

右值引用:

#include <iostream>
void process(int&& rref) {  // 右值引用参数std::cout << "Right value: " << rref << std::endl;
}int main() {process(42);            // 传入右值return 0;
}

结合 std::move()

#include <iostream>
void process(int&& rref) {std::cout << "Right value: " << rref << std::endl;
}int main() {int x = 42;process(std::move(x));  // 将左值 x 转换为右值return 0;
}

总结

属性左值引用(&右值引用(&&
绑定目标左值右值
生命周期控制不延长可延长
性能无显著优化通过移动语义显著优化性

2. 浅拷贝、深拷贝

这里举个例子:

class Vector{int num;int* a;
public:void ShallowCopy(Vector& v);void DeepCopy(Vector& v);
};

浅拷贝:按位拷贝对象,创建的新对象有着原始对象属性值的一份精确拷贝两者值同一块内存)。

//浅拷贝
void Vector::ShallowCopy(Vector& v){this->num = v.num;this->a = v.a;
}

深拷贝:拷贝所有的属性(包括属性指向的动态分配的内存)。换句话说,当对象和它所引用的对象一起拷贝时即发生深拷贝。(不同内存)

//深拷贝
void Vector::DeepCopy(Vector& v){this->num = v.num;this->a = new int[num];for(int i=0;i<num;++i){a[i]=v.a[i]}
}

可以看到,深拷贝的开销往往比浅拷贝大(除非没有指向动态分配内存的属性),所以我们就倾向尽可能使用浅拷贝。

但是浅拷贝的有一个问题:当有指向动态分配内存的属性时,会造成多个对象共用这块动态分配内存,从而可能导致冲突。一个可行的办法是:每次做浅拷贝后,必须保证原始对象不再访问这块内存(即转移所有权给新对象),这样就保证这块内存永远只被一个对象使用。

那有什么对象在被拷贝后可以保证不再访问这块内存呢?相信大家心里都有答案:临时对象


3.1右值引用的意义:

前面对移动语义的认识我们都是基于C++98时左值、右值概念,而C++11对左值、右值类别被重新进行了定义,因此现在我们重新认识一下新的类别。

C++11使用下面两种独立的性质来区别类别:

  1. 拥有身份:指代某个非临时对象。
  2. 可被移动:可被右值引用类型匹配。

每个C++表达式只属于三种基本值类别中的一种:左值 (lvalue)、纯右值 (prvalue)、将亡值 (xvalue)

  • 拥有身份且不可被移动的表达式被称作 左值 (lvalue) 表达式,指持久存在的对象或类型为左值引用类型的返还值。
  • 拥有身份且可被移动的表达式被称作 将亡值 (xvalue) 表达式,一般是指类型为右值引用类型的返还值。
  • 不拥有身份且可被移动的表达式被称作 纯右值 (prvalue) 表达式,也就是指纯粹的临时值(即使指代的对象是持久存在的)。
  • 不拥有身份且不可被移动的表达式无法使用。

如此分类是因为移动语义的出现,需要对类别重新规范说明。例如不能简单定义说右值就是临时值(因为也可能是std::move过的对象,该代指对象并不一定是临时值)。

Vector& func1();
Vector&& func2();
Vector func3();int main(){Vector a;a;              //左值表达式func1();        //左值表达式,返还值是临时的,返还类型是左值引用,因此被认为不可移动。func2();        //将亡值表达式,返还值是临时的,返还类型是右值引用,因此指代的对象即使非临时也会被认为可移动。func3();        //纯右值表达式,返还值为临时值。std::move(a);  //将亡值表达式,std::move本质也是个函数,同上。Vector();       //纯右值表达式
}

而现在我们可以归纳为:

  • 左值(lvalue) 指持久存在(有变量名)的对象或返还值类型为左值引用的返还值,是不可移动的。
  • 右值(rvalue) 包含了 将亡值、纯右值,是可移动(可被右值引用类型匹配)的值。

实际上C++ std::move函数的实现原理就是的强转成右值引用类型并返还之,因此该返还值会被判断为将亡值,更宽泛的说是被判定为右值。

函数参数传递

void func1(Vector v) {return;}
void func2(Vector && v) {return;}int main() {Vector a;Vector &b = a;Vector c;Vector d;//请回答:不开优化的版本下,调用以下函数分别有多少Copy Consturct、Move Construct的开销?func1(a);func1(b);func1(std::move(c));func2(std::move(d));
}

实际上在不开优化的版本下,如果实参为右值,调用func1的开销只比func2多了一次移动构造函数和析构函数。

实参传递给形参,即形参会根据实参来构造。其结果是调用了移动构造函数;函数结束时则释放形参。

倘若说对象的移动构造函数开销较低(例如内部仅一个指针属性),那么使用无引用类型的形参函数是更优雅的选择,而且还能接受左值引用类型或无引用的实参(尽管这两种实参都会导致一次Copy Consturct)。可以说,这种情况下,只提供非引用类型的版本,也是可以接受的。

那我们在写一般函数形参的时候,若参数有支持移动构造(或移动赋值)的类型,是否有必要每个函数都提供关于&&形参的重载版本吗?

回答:从极致的优化角度来看是有必要的,应该提供右值引用类型的重载版本,更准确说应该同时提供左值引用(匹配左值)和右值引用(匹配右值)两种重载版本。

这里纠正了以前的说法。

函数返还值传递

Vector func1() {Vector a;return a;
}Vector func2() {Vector a;return std::move(a);
}Vector&& func3() {Vector a;return std::move(a);
}int main() {//请回答:不开优化的版本下,执行以下3行代码分别有多少Copy Consturct、Move Construct的开销?Vector test1 = func1();Vector test2 = func2();Vector test3 = func3();
}

同样的道理,执行这3行代码实际上都没有任何Copy Construct的开销(这其中也有NRV技术的功劳),都是只有一次Move Construct的开销。

此外一提,func3是危险的。因为局部变量释放后,函数返还值仍持有它的右值引用。

因此,这里也不建议函数返还右值引用类型,同前面传递参数类似的,移动构造开销不大的时候,直接返还非引用类型就足够了(在某些特殊场合有特别作用,准确来说一般用于表示返还成一个右值,如std::move的实现)。

结论:

1. 我们应该首先把编写右值引用类型相关的任务重点放在对象的构造、赋值函数上。从源头上出发,在编写其它代码时会自然而然享受到了移动构造、移动赋值的优化效果。

2. 形参:从优化的角度上看,若参数有支持移动构造(或移动赋值)的类型,应提供左值引用和右值引用的重载版本。移动开销很低时,只提供一个非引用类型的版本也是可以接受的。

3. 返还值:不要且没必要编写返还右值引用类型的函数,除非有特殊用途。

万能引用


接下来的内容都是属于模板的部分了:万能引用、引用折叠、完美转发。这部分更加难以理解,不编写模板代码的话可以绕道了。

万能引用(Universal Reference):

  • 发生类型推导(例如模板、auto)的时候,使用T&&类型表示为万能引用,否则表示右值引用。
  • 万能引用类型的形参既能匹配任意引用类型的左值、右值。

也就是说编写模板函数时,只提供万能引用形参一个版本就可以匹配左值、右值,不必编写多个重载版本。

template<class T>
void func(T&& t){return;
}int main() {Vector a,b;func(a);                //OKfunc(std::move(b));     //OK
}

此外需要注意的是,使用万能引用参数的函数是最贪婪的函数,容易让需要隐式转换的实参匹配到不希望的转发引用函数。例如下面代码:

template<class T>void f(T&& value);void f(int a);
//当调用f(long类型的参数)或者f(short类型的参数),则不会匹配int版本而是匹配到万能引用的版本

引用折叠


使用万能引用遇到的第一个问题是推导类型会出现不正确的引用类型:例如当模板参数T为Vector&或Vector&&,模板函数形参为T&&时,展开后变成Vector& &&或者Vector&& &&。

template<class T>
void func(T&& t){return;
}int main(){func(Vector()); //模板参数T被推导为Vector&&
}

但显然C++中是不允许对引用再进行引用的,于是为了让模板参数正确传递引用性质,C++定义了一套用于推导类型的引用折叠(Reference Collapse)规则:
所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。

引用折叠&&&
&&&
&&&&&

Example1:

func(Vector());

模板函数func的T被推导为Vector&&,形参object为T&&即展开后为Vector&& &&。由于折叠规则的存在,形参object最终被折叠推导为Vector&&类型。

Example2:

func(a);

模板函数func的T在这里被推导为Vector&,形参object为T&&即展开后为Vector& &&。由于折叠规则的存在,形参object最终被推导为Vector&类型。

完美转发 std::forward<T>


当我们使用了万能引用时,即使可以同时匹配左值、右值,但需要转发参数给其他函数时,会丢失引用性质(形参是个左值,从而无法判断到底匹配的是个左值还是右值)。

//当然我们也可以写成如下重载代码,但是这已经违背了使用万能引用的初衷(仅编写一个模板函数就可以匹配左值、右值)
template<class T>
void func(T& t){doSomething(t);
}template<class T>
void func(T&& t){doSomething(std::move(t));
}

完美转发(Perfect Forwarding):C++11提供了完美转发函数 std:forward<T> 。它可以在模板函数内给另一个函数传递参数时,将参数类型保持原本状态传入(如果形参推导出是右值引用则作为右值传入,如果是左值引用则作为左值传入)。

于是现在我们可以这样做了:

template<class T>
void func(T&& object){doSomething(std::forward<T>(object));
}

不借助std::forward<T>间接传入参数的话,无论object是左值引用类型,还是右值引用类型,都会被视为左值。

std::forward<T>()的实现主要就一句return static_cast<T&&>(形参),实际上也是利用了折叠规则。从而接受右值引用类型时,将右值引用类型的值返还(返还值为右值)。接受左值引用类型时,将左值引用类型的值返还(返还值为左值)。

而std::move<T>()的实现还需要先移除形参的所有引用性质得到无引用性质的类型(假设为T2),然后再return static_cast<T2&&>(形参),从而保证不会发生引用折叠,而是直接作为右值引用类型的值返还(返还值为右值)。

一句话: 右值引用的意义其实就是利用了将亡值,减少了深拷贝的次数😊

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

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

相关文章

新能源汽车充电插口类型识别-YOLO标记,可识别Type1,ccs2的充电标准

前言: CCS标准定义的Type-2 CCS汽车充电端口&#xff0c;右侧装有直流充电枪的插头。汽车的充电端口设计巧妙地将交流部分&#xff08;上半部分&#xff09;与直流部分&#xff08;下半部分的两个粗大的接口&#xff09;集于一体。在交流和直流充电过程中&#xff0c;电动汽车…

Pytest使用Jpype调用jar包报错:Windows fatal exception: access violation

问题描述 ​   之前我们有讲过如何使用Jpype调用jar包&#xff0c;在成功调用jar包后&#xff0c;接着在Pytest框架下编写自动测试用例。但是在Pytest下使用Jpype加载jar包&#xff0c;并调用其中的方法会以下提示信息&#xff1a; ​   虽然提示信息显示有Windows显示致命…

Netty基本原理

目录 前言 原生NIO VS Netty 原生NIO存在的问题 Netty的优点 线程模型 传统阻塞 I/O (Blocking I/O) 2. 非阻塞 I/O (Non-blocking I/O) 3. 多路复用 I/O (Multiplexed I/O) 4. Reactor 模式 常见的 Reactor 模式的变体&#xff1a; Netty线程模型 工作原理 前言 N…

MySQL系列之数据类型(Numeric)

导览 前言一、数值类型综述二、数值类型详解1. NUMERIC1.1 UNSIGNED或SIGNED1.2 数据类型划分 2. Integer类型取值和存储要求3. Fixed-Point类型取值和存储要求4. Floating-Point类型取值和存储要求 结语精彩回放 前言 MySQL系列最近三篇均关注了和我们日常工作或学习密切相关…

一学就废|Python基础碎片,格式化F-string

Python 3.6 中引入了 f-string语法&#xff0c;提供了一种简洁直观的方法来将表达式和变量直接嵌入到字符串中进行字符串格式化&#xff0c;f -string背后的想法是使字符串插值更简单。 要创建 f -string&#xff0c;在字符串前加上字母 “f”即可&#xff0c;与字符串本身的格…

在 Mac(ARM 架构)上安装 JDK 8 环境

文章目录 步骤 1&#xff1a;检查系统版本步骤 2&#xff1a;下载支持 ARM 的 JDK 8步骤 3&#xff1a;安装 JDK步骤 4&#xff1a;配置环境变量步骤 5&#xff1a;验证安装步骤 6&#xff1a;注意事项步骤7&#xff1a;查看Java的安装路径 在 Mac&#xff08;ARM 架构&#xf…

【AI绘画】Midjourney进阶:色调详解(上)

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: AI绘画 | Midjourney 文章目录 &#x1f4af;前言&#x1f4af;Midjourney中的色彩控制为什么要控制色彩&#xff1f;为什么要在Midjourney中控制色彩&#xff1f; &#x1f4af;色调白色调淡色调明色调 &#x1f4af…

【C++】LeetCode:LCR 023. 相交链表

题干 LCR 023. 相交链表 的头节点 headA 和 headB &#xff0c;请找出并返回两个单链表相交的起始节点。如果两个链表没有交点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交&#xff1a; 题目数据 保证 整个链式结构中不存在环。 注意&#xff0c;函数返回结果…

【Linux学习】【Ubuntu入门】2-5 shell脚本入门

1.shell脚本就是将连续执行的命令携程一个文件 2.第一个shell脚本写法 shell脚本是个纯文本文件&#xff0c;命令从上而下&#xff0c;一行一行开始执行&#xff0c;其扩展名为.sh&#xff0c;shell脚本第一行一定要为&#xff1a;#!/bin/bash&#xff0c;表示使用bash。echo…

【C++】list模拟实现(完结)

1.普通迭代器&#xff08;补充&#xff09; 1.1 后置和后置-- 我们迭代器里面实现了前置和前置--&#xff0c;还需要实现后置和后置--。 在list.h文件的list_iterator类里面实现。 //后置/-- Self& operator(int) {Self tem(*this);//保存原来的值_node _node->_nex…

基于Python的飞机大战复现

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

MR30分布式 IO 模块在冷却水泵系统中的卓越应用

在当今各类工业生产以及大型设施运行的场景中&#xff0c;冷却水泵系统起着至关重要的作用&#xff0c;它犹如保障整个运转体系顺畅运行的 “血液循环系统”&#xff0c;维持着设备适宜的温度环境&#xff0c;确保其稳定、高效地工作。而随着科技的不断发展&#xff0c;明达技术…

银河麒麟桌面系统——桌面鼠标变成x,窗口无关闭按钮的解决办法

银河麒麟桌面系统——桌面鼠标变成x&#xff0c;窗口无关闭按钮的解决办法 1、支持环境2、详细操作说明步骤1&#xff1a;用root账户登录电脑步骤2&#xff1a;导航到kylin-wm-chooser目录步骤3&#xff1a;编辑default.conf文件步骤4&#xff1a;重启电脑 3、结语 &#x1f49…

多线程常见问题集

一、多线程预防和避免线程死锁 如何预防死锁&#xff1f; 破坏死锁的产生的必要条件即可&#xff1a; 破坏请求与保持条件&#xff1a;一次性申请所有的资源。破坏不剥夺条件&#xff1a;占用部分资源的线程进一步申请其他资源时&#xff0c;如果申请不到&#xff0c;可以主动释…

Java ArrayList 与顺序表:在编程海洋中把握数据结构的关键之锚

我的个人主页 我的专栏&#xff1a;Java-数据结构&#xff0c;希望能帮助到大家&#xff01;&#xff01;&#xff01;点赞❤ 收藏❤ 前言&#xff1a;在 Java编程的广袤世界里&#xff0c;数据结构犹如精巧的建筑蓝图&#xff0c;决定着程序在数据处理与存储时的效率、灵活性以…

【第三方云音乐播放器SPlayer本地安装结合内网穿透打造个性化远程音乐库】

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” 文章目录 前言1. 安装Docker2. 创建并启动Splayer容器3. 本地访问测试4. 公网远程访问本地Splayer4.1 内网穿…

easyui combobox 只能选择第一个问题解决

easyui combobox 只能选择第一个问题解决 问题现象 在拆分开票的时候&#xff0c;弹出框上面有一个下拉框用于选择需要新增的明细行&#xff0c;但是每次只能选择到第一个 选择第二条数据的时候默认选择到第一个了 代码如下 /*新增发票编辑窗口*/function addTicketDialog…

从零开始:Linux 环境下的 C/C++ 编译教程

个人主页&#xff1a;chian-ocean 文章专栏 前言&#xff1a; GCC&#xff08;GNU Compiler Collection&#xff09;是一个功能强大的编译器集合&#xff0c;支持多种语言&#xff0c;包括 C 和 C。其中 gcc 用于 C 语言编译&#xff0c;g 专用于 C 编译。 Linux GCC or G的安…

transformer.js(三):底层架构及性能优化指南

Transformer.js 是一个轻量级、功能强大的 JavaScript 库&#xff0c;专注于在浏览器中运行 Transformer 模型&#xff0c;为前端开发者提供了高效实现自然语言处理&#xff08;NLP&#xff09;任务的能力。本文将详细解析 Transformer.js 的底层架构&#xff0c;并提供实用的性…

STM32 Keil5 attribute 关键字的用法

这篇文章记录一下STM32中attribute的用法。之前做项目的时候产品需要支持远程升级&#xff0c;要求版本只能向上迭代&#xff0c;不支持回退。当时想到的方案是把版本号放到bin文件的头部&#xff0c;设备端收到bin文件的首包部数据后判断是否满足升级要求&#xff0c;这里就可…