17、std::move和移动语义详解
概述
std::move 是 C++ 标准库中的一个函数模板,用于将一个左值(左值引用)转化为右值引用,从而实现移动语义。移动语义是一种可以将资源(如内存)从一个对象转移到另一个对象的方式,而不是进行资源的复制。移动操作通常比复制操作更高效,对于大型的对象(如容器、字符串等)可以带来很大的性能优势。
左值与右值
在 C++ 中,左值是可以被取地址的表达式,而右值是临时的、不可取地址的表达式。 通常,左值是具有名称、有持久性的,而右值是临时性的、瞬时的。其具体区别如下:
- 左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体。
- 右值是在赋值号右边取出值赋给其他变量的值;右值可以在内存也可以在CPU寄存器。
- 一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。
- 左值:指表达式结束后依然存在的持久对象,可以取地址,具名变量或对象 。
- 右值:表达式结束后就不再存在的临时对象,不可以取地址,没有名字。
如:
int a = 3; // a是左值,3是右值 std::string str = "hello world"; // str是左值,"hello world"是右值
使用 std::move 可以告诉编译器将一个对象视为右值,从而触发移动语义的操作。那么到底什么事移动语义呢?
std::move实现移动语义的代码应用
我们知道对于C++的类来说,通常有构造函数、赋值构造函数、拷贝构造函数等适用于在不同的条件下创建新对象,从C++11开始,还有了移动构造函数,它不同于拷贝构造函数,通常不会进行资源的复制(除非在自定义的移动构造函数中非要进行资源的复制,相信大家不会这么做^_^),当进行参数传递等一些操作时,巧妙利用std::move实现移动语义,就可以减少之前不必要的拷贝构造,从而大幅提高程序效率,下面看代码有助于加深理解。
首先实现一个MyClass类,具体说明请看注释:
#include <stdio.h> #include <unistd.h> #include <iostream> #include <vector> class MyClass { public: MyClass(int value) :ptr_(new int(value)) { // 构造函数,存在开辟内存、复制资源的操作 std::cout << "Default constructor called: MyClass(int value)" << std::endl; } MyClass(const MyClass& other) // 拷贝构造函数,存在开辟内存、复制资源的操作 : ptr_(new int(*other.ptr_)) { std::cout << "Copy constructor called: MyClass(const MyClass& other)" << std::endl; } MyClass(MyClass&& other) noexcept // 移动构造函数,只是地址的复制,没有新开内存、资源复制 : ptr_(other.ptr_) { other.ptr_ = nullptr; std::cout << "Move constructor called: MyClass(MyClass&& other)" << std::endl; } MyClass& operator=(const MyClass& other) { // 赋值构造函数,也存在开辟内存、复制资源的操作 if (&other == this) { return *this; // 自我赋值,直接返回 } if (ptr_) { delete ptr_; // 释放原有内存 } // 逐个赋值 ptr_ = new int(*other.ptr_); return *this; } ~MyClass() { if (ptr_) { delete ptr_; } std::cout << "Destructor called." << std::endl; } int GetValue(void) { return *ptr_; } // 打印数据 void PrintData() const { std::cout << "Data: " << *ptr_ << std::endl; } private: int* ptr_; // 相当于Class内部管理的资源 };
下面我们看一下如何通过std::move触发移动构造函数:
int main (void)
{ MyClass obj1(10); // 调用默认构造函数 MyClass obj2 = std::move(obj1); // 调用移动构造函数 MyClass obj3(30); // 调用默认构造函数 MyClass obj4(std::move(obj3)); // 调用移动构造函数 return 0; }
运行后得到如下结果:
可以直观地看到利用obj1创建obj2时以及利用obj3创建obj4时,均调用了移动构造函数而不是赋值构造函数或拷贝构造函数。
接下来,我们以std::vector<MyClass>的相关操作为例,看一下std::move在工程中的具体应用:
int main() { std::vector<MyClass> vec; // 不使用移动语义 MyClass obj5(10); // 调用默认构造函数 vec.push_back(obj5); // 调用复制构造函数 // 使用移动语义 MyClass obj6(20); // 调用默认构造函数 vec.push_back(std::move(obj6)); // 调用拷贝+移动构造函数 for (auto &obj : vec) { obj.PrintData(); } return 0; }
编译后运行结果如下:
由上可知:首先,我们定义一个std::vector<MyClass>对象,并准备向其中push新元素。
传统做法是不使用移动语义的,这样会先调用默认构造函数创建新对象obj1,再通过拷贝构造函数将obj1的资源复制到vector新元素中,在拷贝构造函数中会涉及到开辟内存、资源复制等操作;
当使用了移动语义之后,我们首先通过默认构造函数创建了对象obj2,然后通过std::move直接将obj2转换为右值传递给vector,将obj2的所有权转移给vector中的新元素,从运行结果也可以看出由于std::vector本身的实现机制,在所有权转移过程中调用了两次移动构造函数,但是均不会涉及内存开辟、资源复制等操作,提高了代码效率。
std::move实现移动语义的优点
可以将对象从左值变为右值,避免拷贝构造,只是将对象状态或者所有权从一个对象转移到另一个对象,没有涉及内存的搬迁或者内存拷贝,从而极大地提高代码效率。
但需要注意,使用 std::move 后原对象(如上面的obj6)的状态是不确定的,不应再对其进行操作,否则程序运行时可能出现Segmentation fault (core dumped)报错!!!