一、具名和可被移动
具名(identity)
指的是一个表达式可以唯一地确定它所指代的对象或函数。一个变量名就是一个具名的表达式,因为它代表着内存中的一个特定位置。
具名性通常与左值(lvalue)相关联,左值是具有标识的表达式,可以出现在赋值语句的左侧。
可被移动(movable)
指的是一个表达式可以安全地将其资源转移到另一个实体,而自身变成无效状态。
可被移动性通常与右值引用(rvalue reference)和移动构造函数、移动赋值运算符等相关联。
例如,一个临时对象(右值)通常是可被移动的,因为它的生命周期很短,可以安全地将其资源转移到其他对象。
二、左值、纯右值、将亡值
在C++98中,表达式分为左值和右值,其将非左值表达式统称为右值。引用能绑定到左值但const的引用能绑定到右值。
在C++11中,表达式分为左值和右值,其中右值又可以细分为纯右值、将亡值。
左值(lvalue, left value)
具名且不可移动。
可以取地址的、有名字的就是左值。
纯右值(pvalue, pure ravlue)
不具名且可移动。
纯右值的概念等同于C++98中右值的概念,指的是临时变量和不跟对象关联的字面量值。
临时变量值:非引用返回的函数返回值、表达式等,例如函数int func()的返回值,表达式a+b。
不跟对象关联的字面量值:例如true、2、"Hello World"等。
将亡值(xvalue, expiring value)
具名且可移动。
顾名思义即将消亡的值,是C++11新增的跟右值引用相关的表达式,通常是将要被移动的对象(移为他用),比如:
返回右值引用T&&的函数返回值;
std::move的返回值;
转换为T&&的类型转换函数的返回值。
将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。(通过右值引用来续命)。
xvalue 只能通过两种方式来获得,这两种方式都涉及到将一个左值赋给(转化为)一个右值引用:
强制类型转化为右值引用:如
static_cast<T&&>(t),得到一个 xvalue。std::move(t)本质上也是static_cast<T&&>(t)。返回类型为右值引用的函数调用:如
T&& fun() { return t; },调用fun()时, 返回一个 xvalue。
三、左值引用和右值引用
左值引用(Lvalue Reference)
左值引用就是对左值的引用,给左值取别名。左值引用使用单个&符号来声明,例如:
int x = 10;
int& ref = x; // ref是x的左值引用左值引用只能引用左值,不能直接引用右值。但常量左值引用可以绑定到右值,例如:
const int &b =2; # 常量左值引用绑定到右值左值引用的实际意义:
减少拷贝,节省内存,提高效率。但是不能引用局部变量。
右值引用(Rvalue Reference)
右值引用就是对右值的引用,给右值取别名。主要作用是把延长对象的生命周期,一般是延长到作用域的scope之外。它使用双&&符号来声明,例如:
int&& rref = 5; // rref是一个右值引用c++11引入右值引用的作用是:右值引用配合c++11新引入的移动语义,可以高效地将资源(如动态分配的内存)从一个对象转移到另一个对象,而无需进行深复制,避免额外的资源拷贝。
右值引用只能引用右值,不能直接引用左值。要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值,例如:
int a;
int &&r1 = a; # 编译失败
int &&r2 = std::move(a); # 编译通过四、移动语义
在C++11中,"move"语义是指通过将资源的所有权从一个对象转移到另一个对象,以减少不必要的复制和内存分配。它通过使用特殊的移动构造函数和移动赋值运算符来实现。
通俗地说,当我们需要将一个对象的值赋给另一个对象时,通常会发生复制操作,其中包括拷贝构造函数和拷贝赋值运算符。这涉及到将源对象的数据复制到新对象中,这可能需要分配新的内存并进行数据复制。
然而,有时我们并不需要创建新对象并进行数据复制,而只是希望将源对象的资源转移到新对象中,同时将源对象置于有效但未定义的状态。这是"move"语义发挥作用的地方。
为了实现"move"语义,C++11引入了右值引用(rvalue references)和std::move()函数。右值引用允许我们标识一个临时对象或可以被移动的对象,而std::move()函数用于将一个对象转换为右值引用。
注:使用 std::move 不会强制执行移动操作,它只是提供了移动的提示,实际移动操作的实现 取决于对象的类型以及它是否定义了相应的移动构造函数和移动赋值运算符 。
#include <iostream>
#include <vector>
class MyObject {
public:
MyObject() {
std::cout << "Default Constructor" << std::endl;
}
MyObject(const MyObject& other) {
std::cout << "Copy Constructor" << std::endl;
}
MyObject(MyObject&& other) noexcept {
std::cout << "Move Constructor" << std::endl;
}
MyObject& operator=(const MyObject& other) {
std::cout << "Copy Assignment Operator" << std::endl;
return *this;
}
MyObject& operator=(MyObject&& other) noexcept {
std::cout << "Move Assignment Operator" << std::endl;
return *this;
}
};
int main() {
MyObject obj1;
MyObject obj2(std::move(obj1)); // 移动构造函数
MyObject obj3;
obj3 = std::move(obj2); // 移动赋值运算符
std::vector<MyObject> vec;
vec.push_back(std::move(obj3)); // 向容器中移动对象
return 0;
}拷贝构造函数和移动构造函数都是构造函数的重载函数,所不同的是:
拷贝构造函数的参数是 const左值引用,接收左值或右值;
移动构造函数的参数是右值引用,接收右值或被 move 的左值。
注:当传来的参数是右值时,虽然拷贝构造函数可以接收,但是编译器会认为移动构造函数更加匹配,就会调用移动构造函数。
总的来说,如果这两个函数都有在类内定义的话,在构造对象时:
若是左值做参数,那么就会调用拷贝构造函数,做一次拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次拷贝构造就会做一次深拷贝)。
若是右值做参数,那么就会调用移动构造,而调用移动构造就会减少拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次移动构造就会少做一次深拷贝)。
评论区