移动构造函数与移动赋值函数

前言

拷贝构造函数创建了源对象的副本,但有些时候我们并不需要进行拷贝,而只需将源对象的资源移动到目标对象,因此C++11提供了一种新的构造方法——移动构造函数。其可以减少不必要的复制,带来性能上的提升。移动赋值函数则与移动构造函数类似,其允许将源对象的资源转移到目标对象中,并更新目标对象的状态。

移动构造函数

定义

移动的含义为:将源对象资源的控制权全部交给目标对象。
移动构造函数定义形式:

1
class_name(class_name && )右值引用传参

&&符号表示右值引用,下面介绍左值引用和右值引用的相关概念。

左值引用和右值引用

左值和右值

首先介绍左值和右值的概念。左值可以取地址,位于等号左边;而右值无法取地址,位于等号右边。可以通过如下两个示例理解:

1
int a = 5;
  • a可以通过&取地址,所以a是左值
  • 5无法通过&取地址,所以5是右值
1
2
3
4
5
6
7
8
9
struct A {
A(int a = 0) {
a_ = a;
}

int a_;
};

A a = A();
  • a可以通过&取地址,所以a是左值
  • A()是个临时值,无法通过&取地址,所以A()是个右值。

引用

引用的本质是别名,可以通过引用修改变量的值,在传参时避免拷贝。

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

1
2
3
int a = 5;
int &ref_a = a; // 左值引用指向左值,编译通过
int &ref_a = 5; // 左值引用指向了右值,会编译失败

左值引用是变量的别名,而右值没有地址,无法被修改,所以左值引用无法指向右值。但const左值引用可以指向右值:

1
const int &ref_a = 5; // 编译通过

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

右值引用的标志是&&,可以指向右值,不能指向左值。

1
2
3
4
5
6
int &&ref_a_right = 5; // 编译通过

int a = 5;
int && ref_a_left = a; //编译不通过,右值引用不可以指向左值

ref_a_right = 6; // 右值引用的用途,可以修改右值

右值引用有办法指向左值吗?

可以通过std::move实现右值引用指向左值:

1
2
3
4
5
int a = 5; // a是一个左值
int &ref_a_left = a;
int &&ref_a_right = std::move(a); // 通过std::move将左值转换为右值,可以被右值引用指向

cout << a;

std::move并不会移动什么,只是将左值强制转化为右值,让右值引用可以指向左值。而变量a依然存在,因此并不会有性能提升。

移动构造函数详解

下面我们用一个示例来详细解释移动构造函数做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass {
public:
int* data;

// 移动构造函数
MyClass(MyClass&& other) noexcept {
data = other.data; // 接管资源
other.data = nullptr; // 置空原对象资源,防止析构时释放
}

~MyClass() {
delete data;
}
};
  • other是一个右值引用对象
  • data = other.data让目标对象获取other的资源,不需要new新内存
  • other.data = nullptr,将源对象指向资源的指针置空,防止原对象other销毁时释放资源
    从上述示例中,我们可以得出移动构造函数可以实现不用复制资源,直接转移资源的所有权。这样一来避免了高代价的深拷贝,二来做到了零开销的资源转移。

为什么不用普通的引用而是使用右值引用?
如果使用普通引用&,程序无法区分传进来的对象是否还要继续使用。而右值引用是专门绑定右值对象的,可以放心的进行转移资源。

移动赋值函数

还是用一个例子来理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Item{
public:
int* x;

Item()=default;
Item(int val){ x = new int(val);};
Item(const Item& item){
x = new int(*item.x);
printf("copy\n");
};
Item(Item&& item){
x = item.x;
item.x = NULL;
printf("move\n");
};

Item& operator=(const Item& item){
if(this != &item){
this->x = new int(*item.x);
}
printf("copy=\n");
return *this;
}

// 移动赋值函数
Item& operator=(Item&& item){
if(this != &item){
this->x = item.x;
item.x = NULL;
}
printf("move=\n");
return *this;
}

~Item(){
delete x;
};
};

首先,观察移动赋值函数做了什么:

  • 判断当前对象和传入的右值对象是否是同一个,如果不是同一个:
    • 将当前对象的资源指针指向传入对象的资源,并将传入对象的资源指针置空
  • 返回当前对象
    可以看出移动赋值函数和移动构造函数的目的一致,都是将传入对象的资源转移到目标对象。但移动赋值函数会多做一步判断,如果传入对象不是当前对象才进行移动。

参考

C++移动构造函数
《C++中的移动构造函数与移动赋值运算符:高效编程的利器》
一文读懂C++右值引用和std::move


移动构造函数与移动赋值函数
https://delta0406.github.io/2025/05/28/技术/语言/CPP/移动构造函数与移动赋值函数/
作者
执妄
发布于
2025年5月28日
许可协议