主播刚刚考完计算机组成原理哈,然后浅浅荒废了几天的时间,leetcode也没刷,八股也没看,不过计算机组成原理终究是过了,姑且算对计算机有了更深入的一点点了解,过几天的信号与系统和电动力学才是折磨。

碎碎念结束。

————————————————————————————————————————————————————————————

c++11中引入了一个经常见到的函数,std::move,初次见面是在unique_ptr那一节中,后来在移动构造函数中也有见面,它的作用可以简单的理解为实现将左值转换为右值。

那么我们首先介绍一下什么是左值和右值。

左值和右值

左值,可以理解为有内存地址的值,与之相对,右值就是没有内存地址的值。

从硬件上说,左值由内存存储,右值由寄存器存储,所以右值就是马上就要消亡的值,生命周期一般只有所在的那一行代码

常见的右值形式包括:

1.int x = 5;里面的5等常量

2.调用函数的返回值

3.算术表达式或者逻辑表达式

而左值就是我们常说的变量及所有可以被左赋值的值

从而我们得到了左值引用和右值引用

1
2
3
int a=1;
int &x=a;//左值引用
int &&b=2;//右值引用

而左值引用和右值引用又可以用同一种方式表示,即万能引用

有两种形式

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
void fun(T &&arg)
{
//这里就是万能引用
}
auto &&ref=x;
auto &&ref1=5;//这是另一种万能引用的表现方式,这里ref被自动推导为左值引用,ref1被自动引用为右值引用
fun(x);
fun(5);
fun(ref);
fun(ref1);//四种情况中万能引用被推导为左值,右值,左值,右值引用

这里左值和右值的引用相遇,具体的结果有一个规则,即折叠引用,可以理解为两者都右值才右值,否则有一个左值,就被推导为左值

右值引用!=万能引用

右值引用是已经确定的,只能传入右值的,而万能引用由模板和auto实现,本质是不确定的

为什么要移动语义

前菜讲完,接下来是移动语义

在常见的类的赋值中,我们常常调用拷贝构造函数,而其底层是深拷贝,是需要在堆上申请空间new的,这样的空间开销是非常大的,特别是在占用空间特别大的类当中,除了这个还有两个原因

  1. 性能开销大: 深拷贝涉及到内存分配(可能很慢)和大量数据的复制。对于包含大量数据的对象(如 std::vector, std::string),这会非常耗时。
  2. 资源浪费: 在很多情况下,我们复制一个对象仅仅是为了临时使用,或者原对象在复制后很快就会被销毁。例如,函数返回一个大对象时,会创建一个临时对象,然后将这个临时对象拷贝到接收变量中。这个临时对象马上就没用了,但我们却花费了昂贵的代价去复制它的内容。
  3. 无法处理独占资源: 有些资源是不能被复制的,比如文件句柄、互斥锁(mutex)、智能指针 std::unique_ptr 等。这些资源通常是独占的。如果一个类管理着这样的独占资源,那么它的拷贝构造函数和拷贝赋值运算符就无法实现(或者被显式删除),导致这类对象无法进行正常的复制操作,限制了它们的使用场景(比如不能作为函数返回值,不能放入需要复制的标准容器)。

于是为了提高效率,处理独占资源,避免不必要的拷贝,我们采用了移动语义

举一个移动语义的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class S
{
public:
char[5] student;
S(const S& other)
{
size=new char[5];
for(int i=0;i<5;++i)
{
student[i]=other.student[i];
}
}//拷贝构造函数
S(S&& other)
{
student=other.student;
other.student=nullptr;
}//移动构造函数

~S()
{
...
}
};

这样的移动构造函数类似浅拷贝,参数要是右值,可以是真正的右值,也可以是move实现的左值转换成的右值

需要移动语义的典型场景:

  • 函数返回大对象:

    1
    2
    3
    4
    5
    6
    7
    std::vector<int> createLargeVector() {
    std::vector<int> vec(1000000);
    // 填充数据
    return vec; // C++11 后,这里通常会发生移动而不是拷贝
    }

    std::vector<int> myVec = createLargeVector(); // 接收返回值时,使用移动构造函数

    在没有移动语义之前,返回 vec会创建一个临时std::vector对象,并将vec的所有元素拷贝到这个临时对象中,然后将这个临时对象拷贝到myVec中(如果 RVO/NRVO 不起作用的话)。有了移动语义,vec的资源可以直接转移给临时对象,再从临时对象转移给myVec

    ,避免了两次昂贵的拷贝。

  • 将对象放入容器:

    1
    2
    3
    4
    5
    std::vector<std::string> names;
    std::string s = "very long string...";
    names.push_back(s); // 拷贝
    names.push_back(std::move(s)); // 移动,s 的内容被转移,s 变为空或有效但未指定状态
    names.push_back("another long string..."); // 临时右值,移动

    当std::vector需要扩容时,它会分配新的内存并将现有元素转移到新位置。有了移动语义,这个转移过程是移动操作,而不是拷贝操作,大大提高了性能。

  • 对象交换:

    1
    2
    3
    std::vector<int> v1 = {1, 2, 3};
    std::vector<int> v2 = {4, 5, 6, 7, 8};
    std::swap(v1, v2); // std::swap 的高效实现依赖于移动语义

    std::swap 的标准实现通常是:通过三次移动操作实现高效交换。

    1
    temp = std::move(a); a = std::move(b); b = std::move(temp);