《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,比较纯粹的构造题。有网络流做法,…...
Spark Streaming DStream转换
DStream上的操作与RDD的类似,分为Transformations(转换)和Output Operations(输出)两种,此外转换操作中还有一些比较特殊的算子,如:updateStateByKey()、transform()以及各种Window相…...
水果商城,可运行
文章目录项目介绍一、技术栈二、本项目分为前后台,有管理员与用户两种角色;1、管理员角色包含以下功能:2、用户角色包含以下功能:三、用户功能页面展示四、管理员功能页面展示五、部分代码展示六、获取整套项目源码项目介绍 一、…...
LiveGBS国标GB/T28181国标视频流媒体平台-功能报警订阅配置报警预案告警截图及录像
LiveGBS国标GB/T28181国标视频流媒体平台-功能报警订阅配置报警预案告警截图及录像1、报警信息1.1、报警查询1.2、配置开启报警订阅1.2.1、国标设备编辑1.2.2、选择开启报警订阅1.3、配置摄像头报警1.3.1、配置摄像头报警通道ID1.3.2、配置摄像头开启侦测1.3.3、尝试触发摄像头…...
软件测试---测试分类
一 : 按测试对象划分 1.1 可靠性测试 可靠性(Availability)即可用性,是指系统正常运行的能力或者程度,一般用正常向用户提供软件服务的时间占总时间的百分比表示。 1.2 容错性测试 行李箱 , 四个轮子 , 坏了一个 , 说明这个容错…...
剑指 Offer II 015. 字符串中的所有变位词
题目链接 剑指 Offer II 015. 字符串中的所有变位词 mid 题目描述 给定两个字符串 s和 p,找到 s中所有 p的 变位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。 变位词 指字母相同,但排列不同的字符串。 示例 1: 输…...
【SpringCloud】SpringCloud详细教程之微服务比较
目录前言一.什么是微服务?为什么要使用微服务二.微服务对比三.企业开发场景前言 我会通过实际代码来给展示每个组件的用法 一.什么是微服务?为什么要使用微服务 分布式,把一个项目拆分成多个模块,每一个模块相当于一个服务。 微…...
二.项目使用vue-router,引入ant-design-vue的UI框架,引入less
根据前文《使用Vue脚手架工具搭建vue项目》搭建好脚手架后使用 1.vue-router 2.引入UI框架ant design vue 3.引入less 1.vue-router vue-router分为两种模式(默认为hash模式): hash history hash: 特征: 1.hash会在浏览器路径里带#号&#…...
网络安全怎么学?20年白帽子老江湖告诉你
很多人都知道龙叔是个老程序员,但却不知道其实我也是个H客,20年前我就开始痴迷于H客技术,可以说是网络安全方面的老江湖了。 到现在,我还依然会去研究这一块,偶尔会和一些网安的朋友交流技术,比如说红盟的…...
药房管理系统;药库管理系统
第一,主要功能: 本系统集日常销售、药品进销存、会员积分、GSP管理等药店所需的所有功能于一体,实现店铺管理的全部自动化。第二、新功能: 增加了“按功能查询药品”的功能,使软件用户可以根据客户的症状推荐合适…...
深眸科技|机器视觉提升制造性能,焕发传统企业智造新活力!
随着机器视觉技术的成熟与发展,其在工业制造中得到越来越广泛的应用。机器视觉在工业制造领域的应用朝着智能识别、智能检测、智能测量以及智能互联的完整智能体系方向发展。此外,快速变化的市场需求,不断涌入行业的竞争对手,让传…...
wordpress怎么显示摘要/谷歌 google
前言 对Java开发的知识点进行深入的学习,并打算做成系列,先从基础常用的重点**(面试点)**知识开始,后续会陆续扩展,加油! 涉及到底层的可能代码块会多点,但要学习底层不看代码看啥?代码说明一切…...
个人怎么申请注册商标/seo资料
var utils{}; /*** 获取时区方法* returns {number} 8代表东8 -8西8*/utils.getLocalTime function () {var date new Date();return date.getTimezoneOffset() / -60;};/*** 获取当前时间方法* returns {string}对应格式的当前时间*/utils.getCurrentTime function (st…...
程序员给别人做的网站违法了/网站免费高清素材软件
叨逼叨两句 放纵身体和感情,只能获得短期的快乐,长期这样,那种空虚感会把你逐渐吞没唯有自律才能降低不确定性,减缓焦虑也唯有自律才能保持足够的精力,用于抵消学习过程中的挫败感,获得技能。技能带来竞争壁…...
温州做网站多少钱/页面设计漂亮的网站
假设有一个 int 类型的数,值为5,那么,我们知道它在计算机中表示为:00000000 00000000 00000000 000001015转换成二制是101,不过int类型的数占用4字节(32位),所以前面填了一堆0。现在想知道,-5在…...
做购物网站平台/深圳知名网络优化公司
存在即是合理的。 表格的现在还是较为常用的一种标签,但不是用来布局,常见处理、显示表格式数据。创建表格在HTML网页中,要想创建表格,就需要使用表格相关的标签。创建表格的基本语法格式如下:单元格内的文字......在上…...
深圳高端家具公司/上海专业seo排名优化
C初始化之超级大坑起因类中定义成员变量的初始化问题解决方法采用如下初始化方法栈区定义类的加括号与不加括号问题起因 平时很少用leetcode写题(一般都是用ACWing)今天看到个题用leetcode写了哈,结果遇到了两个语法大坑 类中定义成员变量的…...