移动语义是 C++11 引入的一项重要特性,它允许对象的资源(如堆上分配的内存)在不进行深度复制的情况下进行转移。通过移动语义,可以将对象的资源从一个对象转移到另一个对象,从而避免不必要的内存拷贝,提高程序性能和效率。
基本用法:
在上面的示例中,move(vec1)将 vec1 转换为右值引用,这允许vec2 的构造函数通过移动语义接管 vec1 的资源,而不是复制它们。这样,vec1 的资源被转移给 vec2,而 vec1 变为空。
#include <iostream>
#include <vector>
using namespace std;
int main()
{vector<int>vecl={1,2,3,4,5};vector<int>vec2=move(vecl);//将vecl的资源移动到 vec2cout<<"vecl size:"<< vecl.size()<< endl;//输出:vecl size:0cout<<"vec2 size:"<< vec2.size()<< endl;//输出:vec2 size:5return 0;
}
移动语义的作用:
优化性能:
减少不必要的复制:使用 move 可以减少在对象赋值或函数返回时发生的不必要的资源复制,特别是对于大型对象或容器,这可以显著提高性能。
对于对象赋值或者函数返回操作,编译器会根据参数的类型(左值还是右值)来选择调用拷贝构造函数还是移动构造函数。如果没有move,对于一个左值对象,通常会调用拷贝构造函数,这可能导致资源的复制。
以vector为例,它包含一个指向动态分配数组的指针、容量信息和当前大小信息等。如果在没有移动语义的情况下将一个vector对象赋值给另一个对象,会复制整个数组,这在数组很大时是非常耗时的。
当我们使用move将一个vector左值转换为右值引用后,在赋值或者返回操作时,编译器会调用移动构造函数。移动构造函数可以简单地将原对象的内部指针(指向动态分配的数组)、容量和大小等信息 “移动” 到新对象中,而不是复制数组中的所有元素。
优化临时对象的使用:在函数参数传递中使用 move 可以避免临时对象的复制,提高效率。
临时对象是指在表达式求值过程中临时创建的对象,比如函数返回值(如果不是返回引用)或者通过一些运算产生的临时结果。这些临时对象通常在完整的表达式执行完后就会被销毁。
当我们在函数参数传递中使用move,它可以将左值转换为右值引用,从而触发移动语义。对于一些拥有移动构造函数的对象,移动语义允许将一个对象的资源(如内部指针、计数器等)直接转移到另一个对象,而不是进行复制。
代码如下:
#include <iostream>
#include <utility>
using namespace std;
class A {
public:int* data;size_t size;A(size_t n) : size(n) {data = new int[n];for (size_t i = 0; i < n; ++i) {data[i] = i;}}// 移动构造函数A(A&& other) noexcept {data = other.data;size = other.size;other.data = nullptr;other.size = 0;}// 拷贝构造函数A(const A& other) : size(other.size) {data = new int[size];for (size_t i = 0; i < size; ++i) {data[i] = other.data[i];}}//析构函数~A() {delete[] data;}
};// 函数接收对象按值传递(无move),会触发拷贝构造函数
void p1(A res) {cout << "拷贝构造" << endl;
}// 函数接收右值引用,使用move传递参数可触发移动构造函数
int p2(A&& res) {cout << "移动拷贝构造" << endl;int sum = 0;for (size_t i = 0; i < res.size; ++i){sum += res.data[i];}return sum;
}int main()
{A a(5);// 不使用move,调用p1会触发拷贝构造函数进行复制p1(a);// 使用move,调用p2会触发移动构造函数进行资源转移int cnt=p2(move(a));cout << "计算结果: " << cnt << std::endl;//计算结果为10return 0;
}
实现高效的数据结构:在实现数据结构如动态数组、链表等时,使用 move 可以在元素插入、删除或移动时减少资源复制,提高数据结构的性能。
下面是动态数组的实现,代码如下:
#include <iostream>
#include <utility>
#include <vector>
using namespace std;// 简单的动态数组类实现
class A {
public:vector<int> d;// 在指定位置插入元素,使用move减少复制void insert(int v, int n) {d.push_back(0); // 先在末尾添加一个占位元素,以便后面移动元素腾出空间// 从最后一个元素开始,逐个将元素向后移动一位,直到指定位置for (int i = d.size() - 1; i > n; --i) {d[i] = move(d[i - 1]);}// 将新元素插入指定位置d[n] = v;}// 删除指定位置的元素,使用move避免不必要的复制void remove(int n) {// 将指定位置之后的元素逐个向前移动一位,覆盖要删除的元素for (int i = n; i < d.size() - 1; ++i) {d[i] = move(d[i + 1]);}// 移除末尾的元素d.pop_back();}
};int main() {A arr;arr.d = { 1, 2, 3, 4, 5 };// 插入元素示例arr.insert(10, 2);// 输出数组元素,验证插入操作for (int num : arr.d) {cout << num << " ";}cout << endl;// 删除元素arr.remove(3);// 再次输出数组元素,验证删除操作for (int num : arr.d) {cout << num << " ";}cout << endl;return 0;
}
注意事项:
使用 move 后,原对象通常处于未定义的状态,不应再使用该对象,在使用 move 时需要谨慎,确保不会导致资源泄露或无效引用。
右值引用
右值应用的定义:
在 C++ 中,右值引用是一种引用类型,用于绑定到右值。右值是指那些不具有持久存储位置或者是即将被销毁的值。例如,字面常量(如5、3.14等)、临时对象(函数返回的临时值等)都是右值。右值引用的语法形式是类型&&,其中&&表示右值引用。
例如,int&& rref = 5;,这里rref就是一个右值引用,它绑定到了字面常量5这个右值。
实现方式:
移动构造函数
移动构造函数是实现移动语义的关键。它的参数是一个右值引用,用于接收一个即将被销毁的对象的资源。
在这个移动构造函数B(B&& other)中,other是一个右值引用。当一个临时的B对象(右值)被用来构造另一个B对象时,就会调用这个移动构造函数。在函数内部,将other对象的data指针赋值给新对象的data指针,然后将other对象的data指针设置为nullptr,这样就实现了资源(字符串内容)从一个对象到另一个对象的 “移动”,而不是复制。
class B{
public:char* data;B() : data(nullptr) {}B(const char* str) {if (str) {data = new char[strlen(str) + 1];strcpy(data, str);} else {data = nullptr;}}// 移动构造函数MyString(B&& other) noexcept {data = other.data;other.data = nullptr;}~MyString() {delete[] data;}
};
移动赋值运算符
除了移动构造函数,移动赋值运算符operator=(类型&&)也用于实现移动语义。它用于将一个右值引用的对象赋值给另一个对象。
在下面这个移动赋值运算符中,首先检查是否是自我赋值。然后释放当前对象的资源(delete[] data),接着将other对象的资源(data指针)赋值给当前对象,最后将other对象的data指针设置为nullptr,完成资源的移动赋值。这样,当使用右值引用进行赋值操作时,就可以避免不必要的资源复制,实现移动语义。
B& operator=(B&& other) noexcept {if (this!= &other) {delete[] data;data = other.data;other.data = nullptr;}return *this;
}