《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,比较纯粹的构造题。有网络流做法,…...
设计模式和设计原则回顾
设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍
文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结: 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析: 实际业务去理解体会统一注…...
【Nginx】使用 Nginx+Lua 实现基于 IP 的访问频率限制
使用 NginxLua 实现基于 IP 的访问频率限制 在高并发场景下,限制某个 IP 的访问频率是非常重要的,可以有效防止恶意攻击或错误配置导致的服务宕机。以下是一个详细的实现方案,使用 Nginx 和 Lua 脚本结合 Redis 来实现基于 IP 的访问频率限制…...

代码规范和架构【立芯理论一】(2025.06.08)
1、代码规范的目标 代码简洁精炼、美观,可持续性好高效率高复用,可移植性好高内聚,低耦合没有冗余规范性,代码有规可循,可以看出自己当时的思考过程特殊排版,特殊语法,特殊指令,必须…...

C# 表达式和运算符(求值顺序)
求值顺序 表达式可以由许多嵌套的子表达式构成。子表达式的求值顺序可以使表达式的最终值发生 变化。 例如,已知表达式3*52,依照子表达式的求值顺序,有两种可能的结果,如图9-3所示。 如果乘法先执行,结果是17。如果5…...
作为测试我们应该关注redis哪些方面
1、功能测试 数据结构操作:验证字符串、列表、哈希、集合和有序的基本操作是否正确 持久化:测试aof和aof持久化机制,确保数据在开启后正确恢复。 事务:检查事务的原子性和回滚机制。 发布订阅:确保消息正确传递。 2、性…...
为什么要创建 Vue 实例
核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...

PHP 8.5 即将发布:管道操作符、强力调试
前不久,PHP宣布了即将在 2025 年 11 月 20 日 正式发布的 PHP 8.5!作为 PHP 语言的又一次重要迭代,PHP 8.5 承诺带来一系列旨在提升代码可读性、健壮性以及开发者效率的改进。而更令人兴奋的是,借助强大的本地开发环境 ServBay&am…...

Ubuntu Cursor升级成v1.0
0. 当前版本低 使用当前 Cursor v0.50时 GitHub Copilot Chat 打不开,快捷键也不好用,当看到 Cursor 升级后,还是蛮高兴的 1. 下载 Cursor 下载地址:https://www.cursor.com/cn/downloads 点击下载 Linux (x64) ,…...