C++左值右值以及std::move的使用
1. c++左值和右值的概念
在赋值语句中,左值通常指的是等号左边的值,右值通常指的是等号右边的值。但是在 C++ 中,左值和右值的概念不仅仅局限于赋值语句,还包括其他表达式和语句。
左值是指具有持久性、有名称、可寻址的对象或函数,它可以出现在赋值语句的左边或右边,也可以出现在其他表达式中。左值通常可以取地址,可以被引用或修改。
右值是指临时对象、字面常量、表达式的结果等,它通常具有短暂的生命周期和无名称,只能出现在表达式的右边。右值通常不能取地址,也不能被引用或修改。
例如,下面的代码中,变量 a
和 b
是左值,表达式 a + b
是一个右值:
int a = 10;
int b = 20;
int c = a + b; // a + b 是一个右值
总之,左值和右值的概念不仅仅局限于赋值语句,它们是 C++ 中重要的概念,用于区分不同类型的表达式和语句。左值通常具有持久性和可修改性,右值通常具有短暂性和不可修改性。
2. 左右值引用绑定的解释
左值引用和右值引用的绑定方式指的是引用类型绑定到表达式的类型。
左值引用只能绑定到左值表达式,左值是指具有持久性、有名称、可寻址的对象或函数。左值引用通常用于传递对象的别名,并且可以修改被引用对象的值。
例如,下面的代码中,变量 a
是一个左值,ref_a
是一个左值引用,它绑定到了变量 a
上:
int a = 10;
int& ref_a = a; // 左值引用绑定到左值 a
右值引用只能绑定到右值表达式,右值是指临时对象、字面常量、表达式的结果等,通常具有短暂的生命周期和无名称。右值引用通常用于实现移动语义或转移对象的所有权。
例如,下面的代码中,表达式 10
是一个右值,rref_a
是一个右值引用,它绑定到了表达式 10
上:
int&& rref_a = 10; // 右值引用绑定到右值 10
绑定方式是引用类型和表达式类型之间的匹配关系,它决定了引用类型可以绑定到哪些表达式上。左值引用只能绑定到左值,右值引用只能绑定到右值,这是 C++ 中的一个基本规则。
3. 左值引用和右值引用的常见使用场景
左值引用和右值引用都是 C++ 中重要的引用类型,它们有着不同的应用场景和使用方法。下面举例说明左值引用和右值引用的常见使用场景:
- 左值引用的常见使用场景:
- 函数参数传递:左值引用通常用于函数参数传递,可以避免对象的复制和移动,提高程序的效率。例如:
void func(int& a); // 左值引用作为函数参数
int x = 10;
func(x); // 传递左值 x
- 返回值类型:左值引用可以作为函数的返回值类型,可以返回函数内部的局部变量或对象的别名。例如:
int& func() {
int x = 10;
return x; // 返回局部变量 x 的引用(错误)
}
- 对象的别名:左值引用可以用于创建对象的别名,可以通过左值引用修改被引用对象的值。例如:
int x = 10;
int& ref_x = x; // 创建 x 的别名
ref_x = 20; // 修改 x 的值为 20
- 右值引用的常见使用场景:
- 移动语义:右值引用通常用于实现移动语义,可以将对象的资源所有权从一个对象转移到另一个对象,提高程序的效率。例如:
class MyString {
public:
MyString() { data_ = nullptr; }
MyString(const char* str) {
size_t len = strlen(str);
data_ = new char[len + 1];
strcpy(data_, str);
}
MyString(MyString&& other) {
data_ = other.data_;
other.data_ = nullptr;
}
~MyString() { delete[] data_; }
private:
char* data_;
};
MyString func() {
MyString str("hello");
return std::move(str); // 返回右值引用
}
- 转移对象的所有权:右值引用可以用于转移对象的所有权,可以将临时对象或函数返回值转移给其他对象。例如:
class MyVector {
public:
MyVector() { data_ = nullptr; }
MyVector(MyVector&& other) {
data_ = other.data_;
other.data_ = nullptr;
}
~MyVector() { delete[] data_; }
private:
int* data_;
};
MyVector createVector() {
MyVector vec;
// 初始化 vec
return std::move(vec); // 返回右值引用
}
- 移动赋值运算符:
在下面的移动赋值运算符中,我们首先检查是否为自赋值,然后使用 std::move
将 other.data_
的资源所有权转移到 data_
,并将 other
的成员变量设置为默认值。
class MyVector {
public:
// 移动赋值运算符
MyVector& operator=(MyVector&& other) {
if (this != &other) {
delete[] data_;
data_ = std::move(other.data_);
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
// ...
private:
int* data_;
int size_;
};
应该注意,std::move()
只是将左值强制转换为右值引用,并不会真正地移动对象的资源所有权。只有在std::move()
函数传入的参数对象实现了移动构造函数和移动赋值运算符时,才能实现移动对象的资源所有权的效果。
4. std::move语法说明
std::move 的原型如下:
template <typename T>
typename std::remove_reference<T>::type&& move(T&& arg) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(arg);
}
其中,typename std::remove_reference<T>::type
表示去除 T
的引用类型,&&
表示返回右值引用。因此,std::move
的返回值类型为 T&&
,即右值引用。参数 arg 是一个万能引用(universal reference),可以接受任何类型的参数,既可以是左值,也可以是右值。函数返回一个右值引用,表示将 arg 强制转换为右值引用,这样就可以使用右值引用的特性,例如移动语义和转移对象的所有权。
使用 std::move 的示例代码如下:
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);
在上面的代码中,std::move(v1)
将 v1 转换为右值引用,然后将其作为参数传递给 v2 的构造函数。由于右值引用表示可以移动的对象,因此 v2 的构造函数将会使用移动语义,将 v1 的资源所有权转移到 v2,而不是进行不必要的拷贝和内存分配。
**注意点:**std::move对传入的参数没有限制,即使传入的参数是左值引用,std::move
也会将其强制转换为右值引用。对传参有限制的是移植构造函数和移动赋值运算符,它们必须使用右值引用作为参数,通常使用std::move的返回值作为它们的传参。
举个例子,假设有一个类 MyClass
,它有一个移动构造函数和一个移动赋值运算符:
class MyClass {
public:
MyClass() { /* 构造函数 */ }
MyClass(MyClass&& other) { /* 移动构造函数 */ }
MyClass& operator=(MyClass&& other) { /* 移动赋值运算符 */ }
};
现在有一个 MyClass
类型的左值对象 obj
,我们想将其转移给另一个对象 new_obj
。如果直接使用赋值运算符,会调用拷贝赋值运算符,导致对象的复制:
MyClass obj;
MyClass new_obj = obj; // 调用拷贝构造函数,复制 obj 的值
如果使用 std::move
,可以将 obj
转换为右值引用,以便调用移动构造函数或移动赋值运算符:
MyClass obj;
MyClass new_obj = std::move(obj); // 调用移动构造函数或移动赋值运算符,转移 obj 的值