《C++ Primer Plus》第18章:探讨 C++ 新标准(2)
移动语义和右值引用
现在介绍本书前面未讨论的主题。C++11 支持移动语义,这就提出了一些问题:为何需要移动语义?什么是移动语义?C++11 如何支持它?下面首先讨论第一个问题。
为何需要移动语义
先来看 C++11 之前的复制过程。假设有如下代码:
vector<string> vstr;
// build up a vector of 20,000 strings, each of 1000 characters
...
vector<string> vstr_copy1(vstr); // make vstr_copy1 a copy of vstr
vector 和 string 类都使用动态内存分配,因此它们必须定义使用某种 new 版本的复制构造函数。为初始化对象 vstr_copy1,复制构造函数 vector<string> 将使用 new 给 20000 个 string 对象分配内存,而每个string对象又将调用 string 的复制构造函数,该构造函数使用 new 为 1000 个字符分配内存。接下来,全部 20000000 个字符都将从 vstr 控制的内存中复制到 vstr_copy1 控制的内存中。这里的工作量很大,但只要妥当就行。
但这确实妥当吗?有时候答案是否定的。例如,假设有一个函数,它返回一个 vector<string> 对象:
vector<string> allcaps(const vector<string> & vs) {vector<string> temp;// code that stores an all-uppercase version of vs in tmepreturn temp;
}
接下来,假设以下面这种方式使用它:
vector<string> vstr;
// build up a vector of 20,000 strings, each of 1000 characters
vector<string> vstr_copy1(vstr); // #1
vector<string> vstr_copy2(allcaps(vstr)); // #2
从表面上看,语句 #1 和 #2 类似,它们都使用一个现有的对象初始化一个 vector<string> 对象。如果深入探索这些代码,将发现 allcaps() 创建了对象 temp,该对象管理着 20000000 个字符;vector 和 string 的复制构造函数创建这 20000000 个字符的副本,然后程序删除 allcaps() 返回的临时对象(迟钝的编译器甚至可能将 temp 复制给一个临时返回对象,删除 temp,再删除临时返回对象)。这里的要点是,临时对象被复制后被删了。如果编译器将对数据的所有权直接从temp转让给 vstr_copy2,不是更好吗?也就是说,不将 20000000 个字符复制到新地方,再删除原来的字符,而将字符留在原来的地方,并将 vstr_copy2 与之相关联。这类似于在计算机中移动文件的情形:实际文件还留在原来的地方,而只修改记录。这种方法被称为移动语义(move semantics)。有点和字面意思看起来相悖的是,移动语义实际上避免了移动原始数据,而只是修改了记录。
要实现移动语义,需要采取某种方式,让编译器知道什么时候需要复制,什么时候不需要。这就是右值引用发挥作用的地方。可定义两个构造函数。其中一个是常规复制构造函数,它使用 const 左值引用作为参数,这个引用关联到左值实参,如 语句#1 中的 vstr;另一个是移动构造函数,它使用右值引用作为参数,该引用关联到右值实参,如语句 #2 中的 allcaps(vstr) 的返回值。复制构造函数可执行深复制,而移动构造函数只调整记录。在将所有权转移给新对象的过程中,移动构造函数可能修改其实参,这意味着右值引用参数不应是 const。
一个移动示例
下面通过一个示例演示移动语义和右值引用的工作原理。下面的程序定义并使用了 Useless 类。这个类动态分配内存,并包含常规复制构造函数和移动构造函数,其中移动构造函数使用了移动语义和右值引用。为演示流程,构造函数和析构函数都比较啰嗦,同时 Useless 类还使用了一个静态变量来跟踪对象数量。另外,省略了一些重要的方法,如赋值运算符。
// useless.cpp -- an otherwise useless class with move semantics
#include<iostream>
using namespace std;// interface
class Useless {
private:int n; // number of elementschar * pc; // pointer to datastatic int ct; // number of objectsvoid ShowObject() const;
public:Useless();explicit Useless(int k);Useless(int k, char ch);Useless(const Useless & f); // regular copy constructorUseless(Useless && f); // move constructor~Useless();Useless operator+(const Useless & f) const;// need operator=() in copy and move versionsvoid ShowData() const;
};// implematation
int Useless::ct = 0;Useless::Useless() {++ct;n = 0;pc = nullptr;cout << "default constructor called; number of objects: " << ct << endl;ShowObject();
}Useless::Useless(int k) : n(k) {++ct;cout << "int constructor called; number of objects: " << ct << endl;pc = new char[n];ShowObject();
}Useless::Useless(int k, char ch) : n(k) {++ct;cout << "int, char constructor called; number of objects: " << ct << endl;pc = new char[n];for(int i = 0; i < n; i++){pc[i] = ch;}ShowObject();
}Useless::Useless(const Useless & f): n(f.n) {++ct;cout << "copy constructor called; number of objects: " << ct << endl;pc = new char[n];for (int i = 0; i < n; i++){pc[i] = f.pc[i];}ShowObject();
}Useless::Useless(Useless && f) : n(f.n) {++ct;cout << "move constructor called; number of objects: " << ct << endl;pc = f.pc; // steal addressf.pc = nullptr; // give old object nothing in returnf.n = 0;ShowObject();
}Useless::~Useless() {cout << "destructor called; objects left: " << --ct << endl;cout << "deleted object:\n";ShowObject();delete [] pc;
}Useless Useless::operator+(const Useless &f) const {cout << "Entering operator+()\n";Useless temp = Useless(n+f.n);for (int i = 0; i < n; i++) {temp.pc[i] = pc[i];}for (int i = n; i < temp.n; i++){temp.pc[i] = f.pc[i-n];}cout << "temp object:\n";cout << "Leaving operator+()\n";return temp;
}void Useless::ShowObject() const {cout << "Number of element: " << n;cout << " Data address: " << (void *) pc << endl;
}void Useless::ShowData() const {if (n == 0 ) {cout << "(object empty)";}else {for (int i = 0; i< n; i++){cout << pc[i];}}cout << endl;
}// application
int main() {{Useless one(10, 'x');Useless two = one; // calss copy constructorUseless three(20, 'o');Useless four (one + three); // calls operator+(), move contructorcout << "object one: ";one.ShowData();cout << "object two: ";two.ShowData();cout << "object three: ";three.ShowData();cout << "object four: ";four.ShowData();}
}
其中最重要的是复制构造函数和移动构造函数的定义。首先来看复制构造函数(删除了输出语句):
Useless::Useless(const Useless & f) : n(f.n) {++ct;pc = new char[n];for (int i = 0; i < n; i++ ) {pc[i] = f.pc[i];}
}
它执行深复制,是下面的语句将使用的构造函数:
Useless two = one; // calls copy constructor
引用 f 将指向左值对象 one。
接下来看移动构造函数,这里也删除了输出语句:
Useless::Useless(Useless && f) : n(f.n) {++ct;pc = f.pc; // steal addressf.pc = nullptr; // give old object nothing in returnsf.n = 0;
}
它让 pc 指向现有的数据,以获取这些数据的所有权。此时,pc 和 f.pc 指向相同的数据,调用析构函数时这将带来麻烦,因为程序不能对同一个地址调用 delete[] 两次。为避免这种问题,该构造函数随后将原来的指针设置为空指针,因为对空指针执行 delete[] 没有问题。这种夺取所有权的方式常被称为窃取(pilfering)。上述代码还将原始对象的元素设置为零,这并非必不可少的,但让这个示例的输出更一致。注意,由于修改了 f 对象,这要求不能在参数声明中使用 const。
在下面的语句中,将使用这个构造函数:
Useless four (one + three); // calls move constructor
表达式 one+three 调用 Useless::operator+(),而右值引用 f 将关联到该方法返回的临时对象。
下面是在 MicroSoft Visual C++ 2010 中编译时,该程序的输出:
int, char constructor called; number of objects: 1
Number of element: 10 Data address: 0xabe2c0
copy constructor called; number of objects: 2
Number of element: 10 Data address: 0xabe2e0
int, char constructor called; number of objects: 3
Number of element: 20 Data address: 0xabe300
Entering operator+()
int constructor called; number of objects: 4
Number of element: 30 Data address: 0xabe320
temp object:
Leaving operator+()
move constructor called; number of objects: 5
Number of elements: 30 Data address: 0xabe320
destructor called; objects left: 4
deleted object:
Number of elements: 0 Data address: 00000000
object one: xxxxxxxxxx
object two: xxxxxxxxxx
object three: oooooooooooooooooooo
object four: xxxxxxxxxxoooooooooooooooooooo
destructor called; objects left: 3
deleted object:
Number of element: 30 Data address: 0xabe320
destructor called; objects left: 2
deleted object:
Number of element: 20 Data address: 0xabe300
destructor called; objects left: 1
deleted object:
Number of element: 10 Data address: 0xabe2e0
destructor called; objects left: 0
deleted object:
Number of element: 10 Data address: 0xabe2c0
注意到对象 two 是对象 one 的副本:他们显示的数据输出相同,但显示的数据地址不同。另一方面,在方法 Useless::operator+() 中创建的对象的数据地址与对象 four 存储的数据地址相同,其中对象 four 是由移动复制构造函数创建的。另外,注意到创建对象 four 后,为临时对象调用了析构函数。之所以知道这是临时对象,是因为其元素和数据地址都是0.
如果使用编译器 g++4.5.0 和 标记 -std=c++11 编译该程序,输出将不同,这很有趣:
int, char constructor called; number of objects: 1
Number of element: 10 Data address: 0xabe2c0
copy constructor called; number of objects: 2
Number of element: 10 Data address: 0xabe2e0
int, char constructor called; number of objects: 3
Number of element: 20 Data address: 0xabe300
Entering operator+()
int constructor called; number of objects: 4
Number of element: 30 Data address: 0xabe320
temp object:
Leaving operator+()
object one: xxxxxxxxxx
object two: xxxxxxxxxx
object three: oooooooooooooooooooo
object four: xxxxxxxxxxoooooooooooooooooooo
destructor called; objects left: 3
deleted object:
Number of element: 30 Data address: 0xabe320
destructor called; objects left: 2
deleted object:
Number of element: 20 Data address: 0xabe300
destructor called; objects left: 1
deleted object:
Number of element: 10 Data address: 0xabe2e0
destructor called; objects left: 0
deleted object:
Number of element: 10 Data address: 0xabe2c0
注意到没有调用移动构造函数,且只创建了 4 个对象。创建对象 four 时,该编译器没有调用任何构造函数;相反,它推断出对象 four 是 operator+() 所做的工作的受益人,因此将 operator()+创建的对象转到 four 的名下。一般而言,编译器完全可以进行优化,只要结果与未优化时相同。即使您省略该程序中的移动构造函数,并使用 g++ 进行编译,结果也将相同。
移动构造函数解析
虽然使用右值引用可支持移动语义,但这并不会神奇地发生。要让移动语义发生,需要两个步骤。首先,右值引用让编译器知道何时可使用移动语义:
Useless two = one; // matches Useless::Useless(const Useless &)
Useless four ( one + three); // matches Useless::Useless(Useless &&)
对象 one 是左值,与左值引用匹配,而表达式 one+three 是右值,与右值引用匹配。因此右值引用让编译器使用移动构造函数来初始化对象 four。实现移动语义的第二步是,编写移动构造函数,使其提供所需的行为。
总之,通过提供一个使用左值引用的构造函数和一个使用右值引用的构造函数,将初始化分成了两组。使用左值对象初始化对象时,将使用复制构造函数,而使用右值对象初始化对象时,将使用移动构造函数。程序员可根据需要赋予这些构造函数不同的行为。
这就带来了一个问题:在引入右值引用前,情况是什么样的呢?如果没有移动构造函数,且编译器未能通过优化消除对复制构造函数的需求,结果将如何呢?在 C++98 中,下面的语句将调用复制构造函数:
Useless four (one + three);
但左值引用不能指向右值。结果将如何呢?第 8 章介绍过,如果实参为右值,const 引用形参将指向一个临时变量:
int twice(const int & rx) {return 2 * rx;
}
...
int main() {int m = 6;// below, rx refers to mint n = twice(m);// below, rx refers to a temporary variable initialized to 21int k = twice(21);
}
就 Useless 而言,形参 f 将被初始化一个临时对象,而该临时对象被初始化为 operator+() 返回的值。下面是使用老式编译器进行编译时,之前的程序(删除了移动构造函数)的部分输出:
Entering operator+()
int constructor called; number of objects: 4
Number of element: 30 Data address: 0x1785320
temp object:
Leaving operator+()
copy constructor called; number of objects: 5
Number of element: 30 Data address: 0x1785340
destructor called; objects left: 4
deleted object:
Number of element: 30 Data address: 0x1785320
copy constructor called; number of objects: 5
Number of element: 30 Data address: 0x1785320
destructor called; objects left: 4
deleted object:
Number of element: 30 Data address: 0x1785340
首先,在方法 Useless::operator+()内,调用构造函数创建了 temp,并在 0x1785320 给它分配了存储 30 个元素的空间。然后,调用复制构造函数创建了一个临时复制信息(其地址为 0x1785340),f 指向该副本。接下来,删除了地址为 0x1785320 的对象 temp。然后,新建了对象 four,它使用了 0x1785320 处刚释放的内存。接下来,删除了 0x1785340 处的临时参数对象。这表明,总共创建了三个对象,但其中的两个被删除。这些就是移动语义旨在消除的额外工作。
正如 g++ 示例表明的,机智的编译器可能自动消除额外的复制工作,但通过使用右值引用,程序员可以指出何时该使用移动语义。
赋值
适用于构造函数的移动语义考虑也适用于赋值运算符。例如,下面演示了如何给 Useless 类编写复制赋值运算符和移动赋值运算符:
Useless & Useless::operator=(const Useless & f) { // copy assignmentif (this == &if) return *this;delete [] pc;n = f.n;pc = new char[n];for ( int i = 0; i < n; i++ ) {pc[i] = f.pc[i];}return *this;
}Useless & Useless::operator=(Useless && f) { // move assignmentif (this == &f) {return *this;}delete [] pc;n = f.n;pc = f.pc;f.n = 0;f.pc= nullptr;return *this;
}
上述赋值运算符采用了第12章介绍的常规模式,而移动赋值运算符删除目标对象中的原始数据,并将源对象的所有权转让给目标。不能让多个指标指向相同的数据,这很重要,因此上述代码将源对象中的指针设置为空指针。
与移动构造函数一样,移动赋值运算符的参数也不能是 const 引用,因为这个方法修改了源对象。
强制移动
移动构造函数和移动赋值运算符使用右值。如果要让它们使用左值,该如何办呢?例如,程序可能分析一个包含候选对象的数组,选择其中一个对象供以后使用,并丢弃数组。如果可以使用移动构造函数或移动赋值运算符来保留选定的对象,那该多好啊。然而,假设您试图像下面这样做:
Useless choices[10];
Useless best;
int pick;
... // select one object, set pick to index
best = choices[pick];
由于 choices[pick] 是左值,因此上述赋值语句将使用复制赋值运算符,而不是移动赋值运算符。但如果能让 choices[pick] 看起来像右值,变将使用移动赋值运算符。为此,可使用运算符 static_cast<> 将对象的类型强制转换为 Useless &&,但 C++ 提供了一种更简单的方式——使用头文件 utility 中声明的函数 std::move()。下面的程序演示了这种技术,它在 Useless 类中添加了啰嗦的赋值运算符,并让以前啰嗦的构造函数和析构函数保持沉默。
// stdmove.cpp -- using std::move()
#include<iostream>
#include<utility>// interface
class Useless {
private:int n; // number of elementschar * pc; // pointer to datastatic int ct; // number of objectsvoid ShowObject() const;
public:Useless();explicit Useless(int k);Useless(int k, char ch);Useless(const Useless & f); // regular copy constructorUseless(Useless && f); // move constructor~Useless();Useless operator+(const Useless & f) const;Useless & operator=(const Useless & f); // copy assignmentUseless & operator=(Useless && f); // move assignmentvoid ShowData() const;
};// implementation
int Useless::ct = 0;Useless::Useless() {++ct;n = 0;pc = nullptr;
}Useless::Useless(int k) : n(k) {++ct;pc = new char[n];
}Useless::Useless(int k, char ch) : n(k) {++ct;pc = new char[n];for (int i = 0; i < n; i++ ){pc[i] = ch;}
}Useless::Useless(const Useless & f): n(f.n) {++ct;pc = new char[n];for(int i = 0; i < n; i++ ){pc[i] = f.pc[i];}
}Useless::Useless(Useless && f) : n(f.n) {++ct;pc = f.pc; // steal addressf.pc = nullptr; // give old object nothing in returnf.n = 0;
}Useless::~Useless() {delete [] pc;
}Useless & Useless::operator=(const Useless & f) { // copy assignmentstd::cout << "copy assignment operator called:\n";if (this == &f){return *this;}delete[] pc;n = f.n;pc = new char[f.n];for (int i = 0; i < n; i++){pc[i] = f.pc[i];}return *this;
}Useless & Useless::operator=(Useless && f) { // move assignmentstd::cout << "move assignment operator called:\n";if (this == &f) {return *this;}delete [] pc;n = f.n;pc = f.pc;f.n = 0;f.pc = nullptr;return *this;
}Useless Useless::operator+(const Useless &f) const {Useless temp = Useless(n + f.n);for (int i = 0; i< n; i++){temp.pc[i] = pc[i];}for (int i = n; i < temp.n; i++){temp.pc[i] = f.pc[i-n];}return temp;
}void Useless::ShowObject() const {std::cout << "Number of elements: " << n;std::cout << " Data address: " << (void *) pc << std::endl;
}void Useless::ShowData() const {if (n == 0){std::cout << "(object empty)";}else{for (int i = 0; i < n; i++){std::cout << pc[i];}}std::cout << std::endl;
}// application
int main(){using std::cout;{Useless one(10, 'x');Useless two = one + one; // calls move contructorcout << "object one: ";one.ShowData();cout << "object two: ";two.ShowData();Useless three, four;cout << "three = one\n";three = one;cout << "now object three = ";three.ShowData();cout << "and object one = ";one.ShowData();cout << "four = one + two\n";four = one + two; // automatic move assignmentcout << "now object four = ";four.ShowData();cout << "four = move(one)\n";four = std::move(one); // forced move assignmentcout << "now object four = ";four.ShowData();cout << "and object one = ";one.ShowData();}return 0;
}
该程序的输出如下:
object one: xxxxxxxxxx
object two: xxxxxxxxxxxxxxxxxxxx
three = one
copy assignment operator called:
now object three = xxxxxxxxxx
and object one = xxxxxxxxxx
four = one + two
move assignment operator called:
now object four = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
four = move(one)
move assignment operator called:
now object four = xxxxxxxxxx
and object one = (object empty)
正如您看到的,将 one 赋给 three 调用了复制赋值运算符,但将 move(one) 赋给 four 调用的是移动赋值运算符。
需要知道的是,函数 std::move() 并非一定会导致移动操作。例如,假设 Chunk 是一个包含私有数据的类,而您编写了如下代码:
Chunk one;
...
Chunk two;
two = std::move(one); // move semantics?
表达式 std::move(one) 是右值,因此上述赋值语句将调用 Chunk 的移动赋值运算符——如果定义了这样的运算符。但如果 Chunk 没有定义移动赋值运算符,编译器将使用复制赋值运算符。如果也没有定义复制赋值运算符,将根本不允许上述赋值。
对大多数程序员来说,右值引用带来的主要好处并非是让他们能够编写使用右值引用的代码,而是能够使用利用右值引用实现移动语义的库代码。例如,STL 类现在都有复制构造函数、移动构造函数、复制赋值运算符和移动赋值运算符。
相关文章:
《C++ Primer Plus》第18章:探讨 C++ 新标准(2)
移动语义和右值引用 现在介绍本书前面未讨论的主题。C11 支持移动语义,这就提出了一些问题:为何需要移动语义?什么是移动语义?C11 如何支持它?下面首先讨论第一个问题。 为何需要移动语义 先来看 C11 之前的复制过程…...

QML定时器
QML使用Timer使用定时器 Timer 计时器可用于触发操作一次,或以给定的间隔重复触发。 常用属性: interval 设置触发器之间的间隔(以毫秒为单位)。 默认间隔为 1000 毫秒。 repeat 设置重复,为真,则以指定的…...

第三章 opengl之纹理
OpenGL纹理纹理环绕方式纹理过滤多级渐远纹理加载和创建纹理stb_image.h生成纹理纹理的应用纹理单元纹理 用stb_image.h库,原先用SOIL库也可以实现。 可以为每个顶点添加颜色来增加图形的细节。但是想得到一个真实的图形,需要足够多的顶点,…...
【Flink】FlinkSQL中执行计划以及如何用代码看执行计划
FilnkSQL怎么查询优化 Apache Flink 使用并扩展了 Apache Calcite 来执行复杂的查询优化。 这包括一系列基于规则和成本的优化,例如: • 基于 Apache Calcite 的子查询解相关 • 投影剪裁 • 分区剪裁 • 过滤器下推 • 子计划消除重复数据以避免重复计算 • 特殊子查询重写,…...

从业者必读,一篇文章轻松掌握DevOps核心概念和最佳技能实践!
文章目录前言一. DevOps的定义及由来二. DevOps的价值三. devops工具有哪些3.1 devops工程师的硬实力3.2 devops工程师的软实力总结前言 大家好,又见面了,我是沐风晓月,本文是对DevOps的总结,一篇文章告诉你什么是DevOps. 对很多…...

2023爱分析·一体化HR SaaS市场厂商评估报告:北森
目录 1.研究范围定义 2. 一体化HR SaaS市场分析 3.厂商评估:北森 4.入选证书 1.研究范围定义 研究范围 伴随数字化转型走向深入,企业人力资源数字化也进入快速发展阶段,人力资源的价值也得到了重新审视和定义。政策层面,《…...
JAVA练习67-二叉树的中序遍历
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 目录 前言 一、题目-二叉树的中序遍历 1.题目描述 2.思路与代码 2.1 思路 2.2 代码 总结 前言 提示:这里可以添加本文要记录的大概内容: 3月3日练习…...

【JeecgBoot-Vue3】第1节 源码下载和环境安装与启动
目录 一. 资料 1. 源码下载 2. 官网启动文档 二、 前端开发环境安装 2.1 开发工具 2.2 前后端代码下载 2.3 前端启动 Step 1:安装nodejs npm Step 2:配置国内镜像(这里选阿里) Step 3:安装yarn Step 4&…...

WebAPI
WebAPI知识详解day11.Web API 基本认知作用和分类什么是DOM?DOM树的概念DOM对象2.获取DOM对象通过css选择器获取dom对象通过其他方法获取dom3.设置/修改DOM元素内容方法1. document.write() 方法方法2. 对象.innerText 属性方法3. 对象.innerHTML4.设置/修改DOM元素…...
Shell命令——date的用法
date命令可以用来显示或设定系统的日期与时间。 一、显示系统的日期与时间 (1)如果date命令后面不加任何参数,则会按照固定的格式显示时间信息: 星期几 月份 日 时:分:秒 时区 年xjhubuntu:~/iot/tmp$ date Fri Mar 3 16:56:4…...

XSS跨站脚本
XSS跨站脚本XSS简介XSS验证XSS危害XSS简介 XSS被称为跨站脚本攻击(Cross-site scripting),由于和CSS(Cascading Style Sheets)重名,所以改为XSS。XSS主要基于javascript语言完成恶意的攻击行为,因为javascript可以非常灵活的操作html、css和…...

【强烈建议收藏:MySQL面试必问系列之慢SQL优化专题】
一.知识回顾 学习本篇文章之前呢,我们可以先看一下【强烈建议收藏:MySQL面试必问系列之SQL语句执行专题】,看完这篇文章再来学习本篇文章可谓是如虎添翼。好的,那我们也不讲太多的废话,直接开始。 二.如何做慢SQL查询优化呢&…...
windows,liunx,java实现apk解压,去签名、重新签名,重新打包apk
背景:由于项目需要,需要将apk包加入服务端返回的静态资源文件到apk中,形成离线apk包供下载安装。经过调查研究,决定使用apktool实现。关于apktool的资料可以参考 https://blog.csdn.net/quantum7/article/details/124060620 htt…...

【Linux】进程信号
🌠 作者:阿亮joy. 🎆专栏:《学会Linux》 🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根 目录👉信号入门&…...

SpringBoot 集成Junit单元测试
学习文章: https://www.cnblogs.com/ysocean/p/6889906.html 开发工具: IDEA 2022.1.4 目录 目录 1. 概述 2. 实现步骤 2.1 maven导入依赖 2.2 随意代码演示(不推荐) 2.3 规范代码演示(推荐) 3. Junit相关其他注解 4. 注意事项 5. 结语 1. 概述 接触到Junit,…...

Android开发之简单控件
文章目录一 文本显示1.1 文本设置的两种方式1.2 常见字号单位类型2.2 设置文本的颜色三 视图基础3.1 设置视图的宽高3.2 设置视图的间距3.3 设置视图的对齐方式四常用布局4.1 线性布局LinearLayout4.2 相对布局RelativeLayout4.3 网格布局GridLayout4.4 滚动视图ScrollView五 按…...

树状数组讲解
树状数组 文章目录树状数组引入例题AcWing241.楼兰图腾思路代码AcWing 242. 一个简单的整数问题思路代码AcWing 244. 谜一样的牛思路代码总结引入 树状数组主要维护的是这样一个数据结构: tr[x]表示以x为终点的长度为lowbit(x)的前缀和、最大值、最小值、最大公约数…...

每个Android开发都应需知的性能指标~
无论你是发布一个新的 Android 应用,还是希望提高现有应用的性能,你都可以使用 Android 应用性能指标来帮助你。 在这篇文章中,我将解释什么是 Android 应用性能指标,并列出8个需要考虑跟踪的维度和建议的基线。 什么是 Android…...

MSYS2安装
最近在学习windows上编译FFmpeg,需要用到msys2,在此记录一下安装和配置过程。 点击如下链接,下载安装包: Index of /msys2/distrib/x86_64/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror 我下载的是:ms…...
3/3考试总结
时间安排 7:30–7:50 看题,怎么感觉三道构造,T3 貌似有网络流背景。 7:50–8:30 T1,有一些简单的性质,缩减两端点后枚举一下翻转的区间就可以了。然后花了一点时间写 spj 调试。 8:30–10:20 T2,比较纯粹的构造题。有网络流做法,…...

SpringBoot-17-MyBatis动态SQL标签之常用标签
文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…...

Prompt Tuning、P-Tuning、Prefix Tuning的区别
一、Prompt Tuning、P-Tuning、Prefix Tuning的区别 1. Prompt Tuning(提示调优) 核心思想:固定预训练模型参数,仅学习额外的连续提示向量(通常是嵌入层的一部分)。实现方式:在输入文本前添加可训练的连续向量(软提示),模型只更新这些提示参数。优势:参数量少(仅提…...

Nuxt.js 中的路由配置详解
Nuxt.js 通过其内置的路由系统简化了应用的路由配置,使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...
拉力测试cuda pytorch 把 4070显卡拉满
import torch import timedef stress_test_gpu(matrix_size16384, duration300):"""对GPU进行压力测试,通过持续的矩阵乘法来最大化GPU利用率参数:matrix_size: 矩阵维度大小,增大可提高计算复杂度duration: 测试持续时间(秒&…...
【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)
1.获取 authorizationCode: 2.利用 authorizationCode 获取 accessToken:文档中心 3.获取手机:文档中心 4.获取昵称头像:文档中心 首先创建 request 若要获取手机号,scope必填 phone,permissions 必填 …...

视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)
前言: 最近在做行为检测相关的模型,用的是时空图卷积网络(STGCN),但原有kinetic-400数据集数据质量较低,需要进行细粒度的标注,同时粗略搜了下已有开源工具基本都集中于图像分割这块,…...

保姆级教程:在无网络无显卡的Windows电脑的vscode本地部署deepseek
文章目录 1 前言2 部署流程2.1 准备工作2.2 Ollama2.2.1 使用有网络的电脑下载Ollama2.2.2 安装Ollama(有网络的电脑)2.2.3 安装Ollama(无网络的电脑)2.2.4 安装验证2.2.5 修改大模型安装位置2.2.6 下载Deepseek模型 2.3 将deepse…...

代码规范和架构【立芯理论一】(2025.06.08)
1、代码规范的目标 代码简洁精炼、美观,可持续性好高效率高复用,可移植性好高内聚,低耦合没有冗余规范性,代码有规可循,可以看出自己当时的思考过程特殊排版,特殊语法,特殊指令,必须…...
怎么让Comfyui导出的图像不包含工作流信息,
为了数据安全,让Comfyui导出的图像不包含工作流信息,导出的图像就不会拖到comfyui中加载出来工作流。 ComfyUI的目录下node.py 直接移除 pnginfo(推荐) 在 save_images 方法中,删除或注释掉所有与 metadata …...

MacOS下Homebrew国内镜像加速指南(2025最新国内镜像加速)
macos brew国内镜像加速方法 brew install 加速formula.jws.json下载慢加速 🍺 最新版brew安装慢到怀疑人生?别怕,教你轻松起飞! 最近Homebrew更新至最新版,每次执行 brew 命令时都会自动从官方地址 https://formulae.…...