当前位置: 首页 > news >正文

C++ 11 之右值引用和移动语义

文章目录

      • 左值引用与右值引用
        • 1、左值与右值
        • 2、纯右值、将亡值
        • 3、左值引用与右值引用
        • 4、右值引用和 std::move 使用场景
      • 引用限定符
      • 移动语义—std::move()
      • 完美转发
      • emplace_back 减少内存拷贝和移动
      • 总结

c++11中引用了右值引用和移动语义,可以避免无谓的复制,提高了程序性能。

左值引用与右值引用

1、左值与右值

  • 概念1:
    • 左值:可以放到等号左边的东西叫左值。
    • 右值:不可以放到等号左边的东西就叫右值。
  • 概念2
    • 左值:可以取地址并且有名字的东西就是左值。
    • 右值:不能取地址的没有名字的东西就是右值。
  • 概念3
    • 左值是指那些在表达式执行结束后依然存在的数据,也就是持久性的数据。
    • 右值是指那些在表达式执行结束后不再存在的数据,也就是临时性的数据。

有一种很简单的方法来区分左值和右值:对表达式取地址,如果编译器不报错就为左值,否则为右值。例如:int a = b + c;,a 是左值,有变量名,可以取地址,也可以放到等号左边,表达式 b+c 的返回值是右值,没有名字且不能取地址,&(b+c) 不能通过编译,而且也不能放到等号左边。

左值一般有

  • 变量名和函数名(注意:是函数名不是函数调用)
  • 返回左值引用的函数调用
  • 前置自增自减表达式++i、–i
  • 由赋值表达式或赋值运算符连接的表达式(a=b, a += b等)
  • 解引用表达式 *p
  • 字符串字面值 “abcd”

2、纯右值、将亡值

纯右值和将亡值都属于右值。

纯右值:运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda 表达式等都是纯右值。举例:

  • 除字符串字面值外的字面值
  • 返回非引用类型的函数调用
  • 后置自增自减表达式 i++、i–
  • 算术表达式 a+ba*ba&&ba==b
  • 取地址表达式等,&a

将亡值
将亡值是指 c++11 新增的和右值引用相关的表达式,通常指将要被移动的对象、T&& 函数的返回值、std::move函数的返回值、转换为 T&& 类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。举例:

class A {xxx;
};A a;
auto c = std::move(a);         // c是将亡值
auto d = static_cast<A&&>(a);  // d是将亡值

3、左值引用与右值引用

左值引用就是对左值进行引用的类型,右值引用就是对右值进行引用的类型,他们都是引用,都是对象的一个别名,并不拥有所绑定对象的堆存,所以都必须立即初始化。引用可以通过引用修改变量的值,传参时传引用可以避免拷贝。

type &name = exp;  // 左值引用
type &&name = exp; // 右值引用

左值引用
左值引用:能指向左值,不能指向右值的就是左值引用:

int a = 5;
int& b = a;  // b是左值引用
b = 4;int& c = 10;  // error,10无法取地址,无法进行引用
const int& d = 10;  // ok,因为是常引用,引用常量数字,这个常量数字会存储在内存中,可以取地址。

引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值,等号右边的值必须可以取地址,如果不能取地址,则会编译失败。

但是,const 左值引用(常量引用)是可以指向右值的:const 左值引用不会修改指向值,因此可以指向右值,这也是为什么要使用 const & 作为函数参数的原因之一。

右值引用
c++11 标准新引入了另一种引用方式,称为右值引用,用 “&&” 表示。如果使用右值引用,那表达式等号右边的值需要是右值(不能是左值),可以使用 std::move 函数强制把左值转换为右值。

int a = 4;
int&& b = a;             // error, a 是左值
int&& c = std::move(a);  // okint num = 10;
int && a = num;         //error, 右值引用不能初始化为左值
int && a = 10;          // ok

【注意】和声明左值引用一样,右值引用也必须立即进行初始化操作。

左值引用与右值引用本质

(1)右值引用指向左值

int a = 5; // a是个左值
int &ref_a_left = a; // 左值引用指向左值
int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向
cout << a; // 打印结果:5

前面讲过可以使用 std::move 函数强制把左值转换为右值,实现右值引用指向左值。std::move 是一个非常有迷惑性的函数:

  • 不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量,比如在上边的代码里,看上去是左值 a 通过 std::move 移动到了右值 ref_a_right 中,那是不是a里边就没有值了?并不是,打印出a的值仍然是5。
  • 事实上 std::move 移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换: static_cast<T&&>(lvalue)。 所以,单纯的 std::move(xxx) 不会有性能提升

同样的,右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过 std::move

int &&ref_a = 5;
ref_a = 6;// 等同于以下代码:
int temp = 5;
int &&ref_a = std::move(temp);
ref_a = 6;

(2)左值引用、右值引用本身是左值还是右值?

被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。仔细看下边代码:

// 形参是个右值引用
void change(int &&right_value) { right_value = 8; }
int main() {int a = 5;                         // a是个左值int &ref_a_left = a;               // ref_a_left是个左值引用int &&ref_a_right = std::move(a);  // ref_a_right是个右值引用change(a);                         // 编译不过,a是左值,change参数要求右值change(ref_a_left);                // 编译不过,左值引用ref_a_left本身也是个左值change(ref_a_right);             // 编译不过,右值引用ref_a_right本身也是个左值change(std::move(a));            // 编译通过change(std::move(ref_a_right));  // 编译通过change(std::move(ref_a_left));   // 编译通过change(5);                       // 当然可以直接接右值,编译通过cout << &a << ' ';cout << &ref_a_left << ' ';cout << &ref_a_right;// 打印这三个左值的地址,都是一样的
}

看完后你可能有个问题,std::move 会返回一个右值引用 int && ,它是左值还是右值呢? 从表达式 int &&ref = std::move(a) 来看,右值引用 ref 指向的必须是右值,所以move返回的 int && 是个右值。所以右值引用既可能是左值,又可能是右值吗? 确实如此:右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值。

或者说:作为函数返回值的 && 是右值,直接声明出来的 && 是左值。 这同样也符合前面章节对左值,右值的判定方式:其实引用和普通变量是一样的, int &&ref = std::move(a) 和 int a = 5 没有什么区别,等号左边就是左值,右边就是右值。

(3)无论是左值引用还是右值引用都是引用

int temp = 5;
int &ref_t = temp;
int &&ref_a = std::move(temp);
ref_a = 6;
cout << &temp << "," << &ref_t << "," << &ref_a<<endl;
cout << "temp:" <<temp <<endl;
// 输出结果
// 0x61fe84  0x61fe84  0x61fe84 
// temp:6

最后,从上述分析中我们得到如下结论:

  • 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
  • 右值引用可以直接指向右值,也可以通过 std::move 指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
  • 作为函数形参时,右值引用更灵活。虽然 const 左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
void f(const int& n) {n += 1;  // 编译失败,const左值引用不能修改指向变量
}void f2(int&& n) {n += 1;  // ok
}int main() {f(5);f2(5);
}

4、右值引用和 std::move 使用场景

std::move 只是类型转换工具,不会对性能有好处;右值引用在作为函数形参时更具灵活性。他们有什么实际应用场景吗?

1、右值引用优化性能,避免深拷贝
(1)浅拷贝重复释放:对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的重复删除,比如下面的代码:

class A {
public:A(int size) : size_(size) { data_ = new int[size]; }A() {}A(const A& a) {size_ = a.size_;data_ = a.data_;cout << "copy " << endl;}~A() { delete[] data_; }int* data_;int size_;
};int main() {A a(10);A b = a;cout << "b " << b.data_ << endl;cout << "a " << a.data_ << endl;return 0;
}

上面代码中,两个输出的是相同的地址,a 和 b 的 data_ 指针指向了同一块内存,这就是浅拷贝,只是数据的简单赋值,那再析构时 data_ 内存会被释放两次,导致程序出问题,这里正常会出现 double free 导致程序崩溃的。

(2)深拷贝构造函数
在上面的代码中,默认构造函数是浅拷贝,在析构的时候会导致重复删除指针。正确的做法是提供深拷贝的拷贝构造函数,比如下面的代码:

#include <iostream>
using namespace std;
class A {
public:A() : m_ptr(new int(0)) { cout << "constructor A" << endl; }A(const A& a) : m_ptr(new int(*a.m_ptr)) {cout << "copy constructor A" << endl;}~A() {cout << "destructor A, m_ptr:" << m_ptr << endl;delete m_ptr;m_ptr = nullptr;}
private:int* m_ptr;
};// 为了避免返回值优化,此函数故意这样写
A Get(bool flag) {A a;A b;cout << "ready return" << endl;if (flag)return a;elsereturn b;
}int main() {{A a = Get(false);  // 正确运行}cout << "main finish" << endl;return 0;
}

深拷贝就是在拷贝对象时,如果被拷贝对象内部还有指针引用指向其它资源,自己需要重新开辟一块新内存存储资源,而不是简单的赋值。虽然深拷贝可以解决浅拷贝的问题,但是存在效率问题。

(3)移动构造函数
深拷贝构造函数可以保证拷贝构造时的安全性,但有时这种拷贝构造存在效率问题,比如上面代码中的拷贝构造就是不必要的。上面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象 b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。

有没有办法避免临时对象的拷贝构造呢?答案是肯定的。看下面的代码:

#include <iostream>
using namespace std;
class A {
public:A() : m_ptr(new int(0)) { cout << "constructor A" << endl; }A(const A& a) : m_ptr(new int(*a.m_ptr)) {cout << "copy constructor A" << endl;}// 移动构造函数,可以浅拷贝A(A&& a) : m_ptr(a.m_ptr) {a.m_ptr = nullptr;  // 为防止a析构时delete data,提前置空其m_ptrcout << "move constructor A" << endl;}~A() {cout << "destructor A, m_ptr:" << m_ptr << endl;if (m_ptr) delete m_ptr;}
private:int* m_ptr;
};// 为了避免返回值优化,此函数故意这样写
A Get(bool flag) {A a;A b;cout << "ready return" << endl;if (flag)return a;  elsereturn b;
}int main() {{A a = Get(false);  // 返回右值,调用移动构造函数}cout << "main finish" << endl;return 0;
}

上面的代码中实现了移动构造( Move Construct)。从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。

在实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。
这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。

移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义( move 语义),右值引用的一个重要目的是用来支持移动语义的(移动语义的分析详细见下文)。

引用限定符

将左值的类对象称为左值对象,将右值的类对象称为右值对象。默认情况下,对于类中用 public 修饰的成员函数,既可以被左值对象调用,也可以被右值对象调用,举个例子:

#include <iostream>
using namespace std;class demo {
public:demo(int num) : num(num) {}int get_num() { return this->num; }private:int num;
};int main() {demo a(10);cout << a.get_num() << endl;cout << move(a).get_num() << endl;return 0;
}

可以看到,demo 类中的 get_num() 成员函数既可以被 a 左值对象调用,也可以被 move(a) 生成的右值 demo 对象调用,运行程序会输出两个 10。

某些场景中,我们可能需要限制调用成员函数的对象的类型(左值还是右值),为此 c++11 新添加了引用限定符。所谓引用限定符,就是在成员函数的后面添加 “&” 或者 “&&”,从而限制调用者的类型(左值还是右值)。【注意】引用限定符不适用于静态成员函数和友元函数。

// 代码修改
class demo {
public:demo(int num) : num(num) {}int get_num() & { return this->num; }  // 添加了 "&",限定调用该函数的对象必须是左值对象private:int num;
};int main() {demo a(10);cout << a.get_num() << endl;  // 正确// cout << move(a).get_num() << endl;  // 错误return 0;
}
// 代码修改
class demo {
public:demo(int num) : num(num) {}int get_num() && { return this->num; }  // 添加了 "&&",限定调用该函数的对象必须是右值对象private:int num;
};int main() {demo a(10);//cout << a.get_num() << endl; // 错误cout << move(a).get_num() << endl; // 正确return 0;
}

const 和引用限定符
const 也可以用于修饰类的成员函数,习惯称为常成员函数。
const 和引用限定符修饰类的成员函数时,都位于函数的末尾。C++11 标准规定,当引用限定符和 const 修饰同一个类的成员函数时,const 必须位于引用限定符前面。如下:

#include <iostream>
using namespace std;
class demo {
public:demo(int num, int num2) : num(num), num2(num2) {}//左值和右值对象都可以调用int get_num() const& { return this->num; }//仅供右值对象调用int get_num2() const&& { return this->num2; }private:int num;int num2;
};

【注意】当 const && 修饰类的成员函数时,调用它的对象只能是右值对象;当 const & 修饰类的成员函数时,调用它的对象既可以是左值对象,也可以是右值对象。无论是 const && 还是 const & 限定的成员函数,内部都不允许对当前对象做修改操作。

移动语义—std::move()

所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象:之前的拷贝是对于别人的资源,自己重新分配一块内存存储复制过来的资源,而对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用,通过 c++11 新增的移动语义可以省去很多拷贝负担,怎么利用移动语义呢,是通过移动构造函数

移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 c++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。

class A {
public:A(int size) : size_(size) { data_ = new int[size]; }A() {}A(const A& a) {size_ = a.size_;data_ = new int[size_];cout << "copy " << endl;}A(A&& a) {                       // 移动构造函数this->data_ = a.data_;a.data_ = nullptr;cout << "move " << endl;}~A() {if (data_ != nullptr) {delete[] data_;}}int* data_;int size_;
};int main() {A a(10);A b = a;A c = std::move(a);  // 返回右值,调用移动构造函数return 0;
}

如果不使用 std::move(),会有很大的拷贝代价,使用移动语义可以避免很多无用的拷贝,提供程序性能,c++ 所有的 STL 都实现了移动语义,方便我们使用。

【注意1】移动语义仅针对于那些实现了移动构造函数的类的对象,对于那种基本类型 int、float 等没有任何优化作用,还是会拷贝,因为它们实现没有对应的移动构造函数。
【注意2】在实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。

完美转发

首先,解释一下什么是完美转发,它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。例如:

template <typename T>
void function(T t) {otherdef(t);
}

如上所示,function() 函数模板中调用了 otherdef() 函数。在此基础上,完美转发指的是:如果 function() 函数接收到的参数 t 为左值,那么该函数传递给 otherdef() 的参数 t 也是左值;反之如果 function() 函数接收到的参数 t 为右值,那么传递给 otherdef() 函数的参数 t 也必须为右值。

显然, function() 函数模板并没有实现完美转发。一方面,参数 t 为非引用类型,这意味着在调用 function() 函数时,实参将值传递给形参的过程就需要额外进行一次拷贝操作;另一方面,无论调用 function() 函数模板时传递给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值,也就是说,传递给 otherdef() 函数的参数 t 永远都是左值。总之,无论从那个角度看, function() 函数的定义都不“完美”。

接下来,那如何实现完美转发呢,答案是使用 std::forward()

  • 首先在定义模板函数时,采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值;
  • 其次,还需要使用 c++11 标准库提供的 std::forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数
    由此即可轻松实现函数模板中参数的完美转发,如下所示:
void PrintV(int& t) { cout << "lvalue" << endl; 
}
void PrintV(int&& t) { cout << "rvalue" << endl;
}template <typename T>
void Test(T&& t) {               // 1、采用右值引用的语法格式定义参数类型PrintV(t);PrintV(std::forward<T>(t));PrintV(std::move(t));
}int main() {Test(1);                       // lvalue rvalue rvalueint a = 1;Test(a);                       // lvalue lvalue rvalue// 2、使用 std::forword() 模板函数修饰被调用函数Test(std::forward<int>(a));    // lvalue rvalue rvalueTest(std::forward<int&>(a));   // lvalue lvalue rvalueTest(std::forward<int&&>(a));  // lvalue rvalue rvaluereturn 0;
}
  • Test(1):1是右值,模板中 T &&t 这种为万能引用,右值 1 传到 Test 函数中变成了右值引用,但是调用 PrintV() 时候,t 变成了左值,因为它变成了一个拥有名字的变量,所以打印 lvalue,而 PrintV(std::forward<T>(t)) 时候,会进行完美转发,按照原来的类型转发,所以打印 rvalue,PrintV(std::move(t)) 毫无疑问会打印 rvalue。
  • Test(a):a 是左值,模板中 T && 这种为万能引用,左值 a 传到 Test 函数中变成了左值引用,所以有代码中打印。
  • Test(std::forward<T>(a)):转发为左值还是右值,依赖于 T,T 是左值那就转发为左值,T 是右值那就转发为右值。
#include <iostream>
using namespace std;//重载被调用函数,查看完美转发的效果
void otherdef(int & t) {cout << "lvalue\n";
}void otherdef(const int & t) {cout << "rvalue\n";
}//实现完美转发的函数模板
template <typename T>
void function(T&& t) {otherdef(forward<T>(t));
}int main()
{function(5);  // rvalueint  x = 1;function(x);  // lvaluereturn 0;
}
// 打印结果
// rvalue  
// lvalue

emplace_back 减少内存拷贝和移动

对于STL容器,c++11 后引入了 emplace_back 接口。emplace_back 是就地构造,不用构造后再次复制到容器中,因此效率更高。考虑这样的语句:

vector<string> testVec;
testVec.push_back(string(16, 'a'));

上述语句足够简单易懂,将一个 string 对象添加到 testVec 中。底层实现:

  • 首先,string(16, ‘a’) 会创建一个 strin g类型的临时对象,这涉及到一次string 构造过程。
  • 其次,vector 内会创建一个新的 string 对象,这是第二次构造。
  • 最后在 push_back 结束时,最开始的临时对象会被析构。加在一起,这两行代码会涉及到两次 string 构造和一次析构。

c++11 可以用 emplace_back 代替 push_back,emplace_back 可以直接在vector中构建一个对象,而非创建一个临时对象,再放进vector,再销毁。emplace_back可以省略一次构建和一次析构,从而达到优化的目的。

emplace_back 内部没有使用拷贝构造函数,也没有使用移动构造函数,而是直接调用构造函数,因此更加高效。

总结

c++11 在性能上做了很大的改进,最大程度减少了内存移动和复制,通过右值引用、 forward、emplace 和一些无序容器我们可以大幅度改进程序性能。

  • 右值引用仅仅是通过改变资源的所有者(剪切方式,而不是拷贝方式)来避免内存的拷贝,能大幅度提高性能。
  • forward 能根据参数的实际类型转发给正确的函数(参数用 &&的方式)。
  • emplace 系列函数通过直接构造对象的方式避免了内存的拷贝和移动。

相关文章:

C++ 11 之右值引用和移动语义

文章目录左值引用与右值引用1、左值与右值2、纯右值、将亡值3、左值引用与右值引用4、右值引用和 std::move 使用场景引用限定符移动语义—std::move()完美转发emplace_back 减少内存拷贝和移动总结c11中引用了右值引用和移动语义&#xff0c;可以避免无谓的复制&#xff0c;提…...

【第一章:Spring概述、特点、IOC容器、IOC操作bean管理(基于xml方式)】

第一章&#xff1a;Spring概述、特点、IOC容器、IOC操作bean管理&#xff08;基于xml方式&#xff09; 1.Spring是什么&#xff1f; ①Spring是一款主流的java EE 轻量级开源框架。 ②广义的Spring&#xff1a;Spring技术栈&#xff0c;Spring不再是一个单纯的应用框架&#x…...

CSS变量

前端的开发工作中&#xff0c;CSS 是不可或缺的部分&#xff1b;实际工作中&#xff0c;我们通过JavaScript 来进行数据和交互工作&#xff0c;CSS 为用户呈现可视化的界面。有时&#xff0c;CSS 来进行部分交互效果是不是会比 JavaScript 更高效、更省事呢&#xff1f; 一、变…...

.net7窗口编程c#2022实战(1)-zip压缩精灵(1)

目录 创建ZIP精灵项目拖控件OpenFileDialog 类压缩与解压缩编写我们自己的代码其它参考内容创建ZIP精灵项目 VS2022中新建项目。 为窗体取一个标题名称 拖控件 左边工具栏里选择控件 拖三个按钮控件和一个listbox控件...

云计算|OpenStack|使用VMware安装华为云的R006版CNA和VRM

前言&#xff1a; FusionCompute架构 (CNA、VRM) CNA(ComputingNode Agent):计算节点代理VNA虚拟节点代理&#xff0c;部署在CNA上&#xff0c;实施计算、存储、网络的虚拟化的配置管理。VRM(Virtual Resource Manager):虚拟资源管理器 VNA可以省略不安装 本次实验使用的是V…...

中央一号文件首提“即时零售”,县域掀起消费业态新风潮

经过几年的探索&#xff0c;即时零售已经逐步走向成熟&#xff0c;并开始向三四线城市以及乡镇城市渗透。 过去一年&#xff0c;京东、美团、阿里争先布局即时零售市场&#xff0c;完善即时配送网络、培养用户消费习惯&#xff0c;即时零售订单迎来了骤增。2022年下半年&#…...

python多线程编程

Python多线程编程中常用方法&#xff1a; 1、join()方法&#xff1a;如果一个线程或者在函数执行的过程中调用另一个线程&#xff0c;并且希望待其完成操作后才能执行&#xff0c;那么在调用线程的时就可以使用被调线程的join方法join([timeout]) timeout&#xff1a;可选参数…...

小熊电器:精品与创意,走上“顶流之路”的两把“宝剑”

回顾2022年&#xff0c;小家电市场降温趋势明显&#xff0c;业绩表现整体低迷&#xff0c;如主打高端路线的北鼎&#xff0c;去年8亿元的营收出现个位数下滑&#xff0c;归母净利润同比下降超56%&#xff1b;苏泊尔营收也出现微降&#xff0c;归母净利润预计同比增长不到10%。而…...

如何描述元素与元素间的逻辑关系?

逻辑结构反映的是数据元素之间的关系&#xff0c;它们与数据元素在计算机中的存储位置无关&#xff0c;是数据结构在用户面前所呈现的形式。根据不同的逻辑结构来分&#xff0c;数据结构可分为集合、线性结构、树形结构和图形结构4种形式&#xff0c;接下来分别进行简要介绍。 …...

【3】linux命令每日分享——mv改名或移动

大家好&#xff0c;这里是sdust-vrlab&#xff0c;Linux是一种免费使用和自由传播的类UNIX操作系统&#xff0c;Linux的基本思想有两点&#xff1a;一切都是文件&#xff1b;每个文件都有确定的用途&#xff1b;linux涉及到IT行业的方方面面&#xff0c;在我们日常的学习中&…...

【2023最火教程】Python性能测试框架Locust实战教程(建议收藏)

01、认识Locust Locust是一个比较容易上手的分布式用户负载测试工具。它旨在对网站&#xff08;或其他系统&#xff09;进行负载测试&#xff0c;并确定系统可以处理多少个并发用户&#xff0c;Locust 在英文中是 蝗虫 的意思&#xff1a;作者的想法是在测试期间&#xff0c;放…...

深入浅出C++ ——手撕AVL树

文章目录前言一、AVL 树介绍二、AVL树节点的定义三、AVL树的插入四、AVL树的旋转五、AVL树的验证六、AVL树的删除七、AVL树的性能八、AVL树的实现前言 在前面的文章中介绍了map / multimap / set / multiset 容器&#xff0c;这几个容器的底层都是按照二叉搜索树来实现的。但是…...

将多个springboot项目的pom.xml文件整合

将多个springboot项目的pom.xml文件整合 0.0、前因 ​ 刚入公司敲代码时、发现一个项目中会包含多个子项目、每个子项目会代表一个功能模块、这属实是把我这个菜鸟惊叹到了。而这种分而治之的方式也引申出一个问题&#xff1a;各子项目的依赖如何统一管理&#xff1f; ​ 我…...

【Unity实战100例】Unity串口通讯的消息接收解析和发送指令

目录 一.串口通信介绍 1.串口通信 2.名词介绍 1.上位机: 2.下位机: 3.串行端口...

资源消耗降低 90%,速度提升 50%,解读 Apache Doris Compaction 最新优化与实现

背景LSM-Tree&#xff08; Log Structured-Merge Tree&#xff09;是数据库中最为常见的存储结构之一&#xff0c;其核心思想在于充分发挥磁盘连续读写的性能优势、以短时间的内存与 IO 的开销换取最大的写入性能&#xff0c;数据以 Append-only 的方式写入 Memtable、达到阈值…...

【Mysql】 锁

【Mysql】 锁 文章目录【Mysql】 锁1. 锁1.1 概述1.2 全局锁1.2.1 介绍1.2.2 语法1.2.2.1 加全局锁1.2.2.2 数据备份1.2.2.3 释放锁1.2.3 特点1.3 表级锁1.3.1 介绍1.3.2 表锁1.3.3 元数据锁1.3.4 意向锁1.4 行级锁1.4.1 介绍1.4.2 行锁1.4.3 间隙锁&临键锁1. 锁 1.1 概述…...

Android 流量统计

Android 流量统计最近项目上有一个应用流量统计的功能需要实现&#xff0c;在此总结一下 流量统计架构 在Android9.0之前&#xff0c;流量监控是基于xt_qtaguid模块的&#xff0c;通过读取/proc/net/xt_qtaguid/stats文件内容进行解析获取对应流量数据。 Android9.0之后&…...

如何保证数据的安全?对称和非对称加密,身份认证,摘要算法,数字证书等傻傻分不清?波哥图解带你彻底掌握

支付安全 1.基础概念 明文&#xff1a;加密前的消息叫“明文”&#xff08;plain text&#xff09; 密文&#xff1a;加密后的文本叫“密文”&#xff08;cipher text&#xff09; 密钥&#xff1a;只有掌握特殊“钥匙”的人&#xff0c;才能对加密的文本进行解密&#xff0c;…...

计算机网络概述

目录前言计算机网络的形成<font colorblue>计算机定义与分类计算机网络的定义计算机网络的分类1.按网络的覆盖范围分类2.按网络采用的传输技术分类按网络的拓扑分类计算机网络的组成计算机网络体系结构层次结构体系ISO/OSI 参考模型Tcp/ip体系结构这就是计算机网络的基础…...

小学生学Arduino---------点阵(二)动态图片以及文字

今天进阶了利用人眼视觉暂留原理制作动态的图片变换。 1、熟练掌握图片显示器的使用 2、创作多种动态图片、文字的显示 3、明确动态图片、文字显示过程 4、掌握图片显示器中清空指令的使用 5、搭建动态图片、文字的显示电路 6、编写动态图片、文字的程序 复习&#xff1a; 绘…...

uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖

在前面的练习中&#xff0c;每个页面需要使用ref&#xff0c;onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入&#xff0c;需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...

UE5 学习系列(三)创建和移动物体

这篇博客是该系列的第三篇&#xff0c;是在之前两篇博客的基础上展开&#xff0c;主要介绍如何在操作界面中创建和拖动物体&#xff0c;这篇博客跟随的视频链接如下&#xff1a; B 站视频&#xff1a;s03-创建和移动物体 如果你不打算开之前的博客并且对UE5 比较熟的话按照以…...

pam_env.so模块配置解析

在PAM&#xff08;Pluggable Authentication Modules&#xff09;配置中&#xff0c; /etc/pam.d/su 文件相关配置含义如下&#xff1a; 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块&#xff0c;负责验证用户身份&am…...

django filter 统计数量 按属性去重

在Django中&#xff0c;如果你想要根据某个属性对查询集进行去重并统计数量&#xff0c;你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求&#xff1a; 方法1&#xff1a;使用annotate()和Count 假设你有一个模型Item&#xff0c;并且你想…...

聊一聊接口测试的意义有哪些?

目录 一、隔离性 & 早期测试 二、保障系统集成质量 三、验证业务逻辑的核心层 四、提升测试效率与覆盖度 五、系统稳定性的守护者 六、驱动团队协作与契约管理 七、性能与扩展性的前置评估 八、持续交付的核心支撑 接口测试的意义可以从四个维度展开&#xff0c;首…...

蓝桥杯3498 01串的熵

问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798&#xff0c; 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...

rnn判断string中第一次出现a的下标

# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...

回溯算法学习

一、电话号码的字母组合 import java.util.ArrayList; import java.util.List;import javax.management.loading.PrivateClassLoader;public class letterCombinations {private static final String[] KEYPAD {"", //0"", //1"abc", //2"…...

安宝特案例丨Vuzix AR智能眼镜集成专业软件,助力卢森堡医院药房转型,赢得辉瑞创新奖

在Vuzix M400 AR智能眼镜的助力下&#xff0c;卢森堡罗伯特舒曼医院&#xff08;the Robert Schuman Hospitals, HRS&#xff09;凭借在无菌制剂生产流程中引入增强现实技术&#xff08;AR&#xff09;创新项目&#xff0c;荣获了2024年6月7日由卢森堡医院药剂师协会&#xff0…...

音视频——I2S 协议详解

I2S 协议详解 I2S (Inter-IC Sound) 协议是一种串行总线协议&#xff0c;专门用于在数字音频设备之间传输数字音频数据。它由飞利浦&#xff08;Philips&#xff09;公司开发&#xff0c;以其简单、高效和广泛的兼容性而闻名。 1. 信号线 I2S 协议通常使用三根或四根信号线&a…...