【c++11】右值引用和移动语义
【c++11】右值引用和移动语义
1.右值引用和移动语义
1.1 左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名
1. 左值(lvalue)
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时ct修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名
特点:
- 通常表示某个变量或可以引用的对象。
- 可以出现在赋值操作的左边或右边。
- 左值拥有明确的内存地址,生命周期比表达式长。
示例:
代码语言:javascript代码运行次数:0运行复制int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
ct int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
ct int& rc = c;
int& pvalue = *p;
return 0;
}
是一个表达式,可以取地址
2. 右值(rvalue)
定义: 右值是不能被持久访问的临时值,通常是表达式的结果或常量值。右值没有具体的内存地址,或者它的地址无法被直接访问。右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址
特点:
- 通常表示临时对象,生命周期只存在于当前表达式中。
- 不能被赋值,也不能绑定到普通的左值引用。
- 可以绑定到右值引用。
示例:
代码语言:javascript代码运行次数:0运行复制int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
. 引用和表达式类型
(1)左值引用
左值引用只能绑定到左值。
代码语言:javascript代码运行次数:0运行复制int x = 10;
int& ref = x; // OK,x 是左值
(2)右值引用
右值引用是 C++11 引入的一种引用类型,只能绑定到右值。
代码语言:javascript代码运行次数:0运行复制int&& rref = 10; // OK,10 是右值
int&& rref2 = std::move(x); // std::move 将 x 转换为右值
表达式分类
C++ 将表达式进一步分为以下几类:
- 左值(lvalue):可以取地址的持久对象。
- 纯右值(prvalue,pure rvalue):右值,通常是临时对象或字面量。
- 亡值(xvalue,expiring value):正在移动操作中的资源,属于右值的一种。
- 将亡值(glvalue,generalized lvalue):左值和亡值的集合。
- 值(rvalue):纯右值和亡值的集合。
示例:
代码语言:javascript代码运行次数:0运行复制int x = 10;
int&& rref = std::move(x); // std::move(x) 是亡值
int y = x + 10; // x + 10 是纯右值
1.2 左值与右值的场景与规则
特性 | 左值(lvalue) | 右值(rvalue) |
---|---|---|
是否可取地址 | 可以取地址(&a 有意义) | 通常不能取地址 |
生命周期 | 通常比表达式更长 | 生命周期短,通常是临时的 |
赋值能力 | 可以出现在赋值号左边或右边 | 只能出现在赋值号的右边 |
引用绑定 | 可以绑定到左值引用(T&) | 不能绑定到左值引用,但能绑定到右值引用(T&&) |
(1)赋值规则
- 左值引用总结:
- 左值引用只能引用左值,不能引用右值。
- 但是ct左值引用既可引用左值,也可引用右值。:
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// ct左值引用既可引用左值,也可引用右值。
ct int& ra = 10;
ct int& ra4 = a;
return 0;
}
- 右值引用总结:
-
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r = std::move(a);
return 0;
}
(2)函数返回值
如果函数返回左值,则返回的值可以被修改:
代码语言:javascript代码运行次数:0运行复制int& getValue(int& a) {
return a; // 返回左值引用
}
int main() {
int x = 10;
getValue(x) = 20; // OK,修改 x
}
如果函数返回右值,则返回的值是临时的,无法直接修改:
代码语言:javascript代码运行次数:0运行复制int getValue() {
return 42; // 返回右值
}
int main() {
int x = getValue(); // OK
// getValue() = 20; // 错误:右值不能被修改
}
()右值引用的应用 右值引用常用于 移动语义 和 避免拷贝,比如:
- 转移临时对象的资源。
- 优化性能。
#include <iostream>
#include <string>
int main() {
std::string a = "Hello";
std::string b = std::move(a); // 将 a 的资源转移给 b
std::cout << b << std::endl; // 输出 Hello
std::cout << a << std::endl; // a 的内容不再定义
return 0;
}
总结
- 左值:指可以被持久访问的对象,具有具体的内存地址,可以绑定到左值引用。
- 右值:通常是临时的、不可持久访问的对象,只能绑定到右值引用。
2.右值引用意义
引用本身就是为了减少拷贝,提高效率(传参与返回值引用),右值设计的目的也是为了解决左值引用的短板(没有彻底解决返回值的问题)
代码语言:javascript代码运行次数:0运行复制string& func2();
如果返回值是func2中局部对象,不能用引用返回
下面是我们自定义的string
代码语言:javascript代码运行次数:0运行复制namespace myown
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(ct char* str = "")//常量字符串构造
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(ct string& s)
:_str(nullptr)
{
cout << "string(ct string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(ct string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
ct char* c_str() ct
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
代码语言:javascript代码运行次数:0运行复制namespace myown
{
myown::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
myown::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), ());
return str;
}
}
这个函数返回的是局部变量str,当函数结束时,局部变量 str 的生命周期结束,因此返回的是一个临时对象。如果试图用左值引用绑定这个返回值,就会产生编译错误
例如:
代码语言:javascript代码运行次数:0运行复制myown::string& result = to_string(42); // 错误:不能将临时对象绑定到左值引用
原因:
- 返回值
str
是一个临时对象,它的生命周期仅限于当前表达式。 - 左值引用需要绑定到一个具备长期生命周期的对象,而临时对象不满足这一条件。
右值引用(T&&
)专门设计用于绑定临时对象(右值)。例如:
myown::string&& result = to_string(42); // OK,可以绑定右值
右值引用可以安全地延长资源的生命周期,避免因局部对象销毁导致的问题。
2. 解决问题的方案
(1)返回值不使用引用 修改调用方式,避免绑定到左值引用。直接接收返回值,使用右值或拷贝构造:
代码语言:javascript代码运行次数:0运行复制myown::string result = to_string(42); // OK,将返回值拷贝到 result 中
(2)使用右值引用 如果需要明确处理返回值,可以使用右值引用绑定返回值:
代码语言:javascript代码运行次数:0运行复制myown::string&& result = to_string(42); // OK,右值引用延长临时对象生命周期
()返回 ct
左值引用
虽然不建议直接返回局部对象的引用,但如果需要返回一个类的成员或某些长期存在的对象,可以使用 ct
左值引用:
ct myown::string& to_string_ref(int value) {
static myown::string str; // 使用静态变量,避免生命周期问题
// 填充 str...
return str;
}
注意:
- 返回静态变量可能会引入线程安全问题,慎用。
- 如果多个调用共用同一个静态变量,可能会导致意料之外的错误。
我们这里屏蔽移动构造
to_string的返回值是一个右值,用这个右值构造ret2,如果没有移动构造,调用就会匹配调用拷贝构造,因为ct左值引用是可以引用右值的,这里就是一个深拷贝。
移动构造
1. 移动构造函数
定义
代码语言:javascript代码运行次数:0运行复制string(string&& s)
: _str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
目的
- 这个构造函数接受一个右值引用
string&& s
,表示将从另一个临时对象中转移资源到当前对象。 - 它通过调用
swap
方法将右值s
的资源交给当前对象,而避免了重新分配和拷贝内存,从而提升性能。
过程分析
- 参数类型:
string&& s
是一个右值引用,只能绑定到右值(例如临时对象或使用std::move
转换后的对象)。
- 初始化列表:
- 将
_str
设置为nullptr
,并将_size
和_capacity
设置为 0,确保当前对象处于安全的初始状态。
- 将
- 调用
swap(s)
:- 将
s
的内部数据(如字符串内容和容量等)与当前对象交换。 s
的资源被转移到当前对象,而s
被置于空的状态。- 这样,当前对象接管了
s
的资源,而不需要重新分配内存。
- 将
优点
- 高效性:移动构造函数通过转移资源避免了深拷贝,性能大幅提升。
- 避免重复资源分配:通过交换指针和元数据,可以直接使用已有资源。
- 安全性:交换后,
s
被置于有效但为空的状态,不会对系统造成影响。
触发条件
该函数在以下场景中被调用:
将一个右值(如临时对象)用于构造另一个对象:
代码语言:javascript代码运行次数:0运行复制string s1("hello");
string s2(std::move(s1)); // 调用移动构造函数
返回一个局部对象:
代码语言:javascript代码运行次数:0运行复制string createString() {
return string("temp");
}
string s = createString(); // 调用移动构造函数
移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己
移动赋值运算符
定义
代码语言:javascript代码运行次数:0运行复制string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
目的
- 这个赋值运算符接受一个右值引用
string&& s
,将右值s
的资源转移到当前对象。 - 它在赋值时避免了深拷贝,通过资源转移实现高效的赋值。
- 参数类型:
string&& s
是右值引用,只能绑定到右值。
- 调用
swap(s)
:- 将当前对象的资源与右值
s
的资源交换。 - 当前对象接管
s
的资源,而s
被置于空的状态。
- 将当前对象的资源与右值
- 返回当前对象:
- 返回
*this
以支持链式赋值,如s1 = s2 = std::move(s)
。
- 返回
触发条件
当一个右值用于赋值时,调用移动赋值运算符:
代码语言:javascript代码运行次数:0运行复制string s1("hello");
string s2;
s2 = std::move(s1); // 调用移动赋值运算符
- 移动语义通过资源转移避免了深拷贝,从而显著提升性能。
- 移动构造函数和移动赋值运算符是实现移动语义的核心部分。
- 移动构造函数:在构造时,通过交换资源将右值对象的资源转移到新对象中。
- 移动赋值运算符:在赋值时,通过交换资源将右值对象的资源转移到已有对象中。
性能优势
- 避免深拷贝:通过指针和元数据的交换,不需要重新分配和复制资源。
- 简化内存管理:交换后的右值被置于空的状态,可以被安全销毁。
示例代码 结合上述移动构造函数和移动赋值运算符的实际用法:
代码语言:javascript代码运行次数:0运行复制int main() {
string s1("hello");
string s2(std::move(s1)); // 调用移动构造函数
string s;
s = std::move(s2); // 调用移动赋值运算符
return 0;
}
输出:
代码语言:javascript代码运行次数:0运行复制string(string&& s) -- 移动语义
string& operator=(string&& s) -- 移动语义
.move
std::move
是 C++11 引入的一个标准库函数,主要用于转换左值为右值。它并不真正“移动”任何内容,而是提供了一种显式的方式,告诉编译器可以“偷走”资源,启用 移动语义,而不进行昂贵的深拷贝操作。它是实现 移动构造函数 和 移动赋值运算符 的关键工具。
1. std::move
的作用
将左值转换为右值:
std::move
实际上并不会执行任何内存移动或拷贝操作,它的作用仅仅是将一个左值转换为右值引用(T&&
),允许后续的移动操作。
示例:
代码语言:javascript代码运行次数:0运行复制int a = 42;
int&& b = std::move(a); // 将 a 转换为右值引用 b
启用移动语义:
通过 std::move
,你可以显式地告诉编译器某个对象可以安全地从一个地方转移到另一个地方,而不是拷贝数据。这对于性能优化非常重要,尤其是在涉及动态内存管理的类(如 std::vector
、std::string
)时,可以避免不必要的深拷贝。
移动语义通常是通过移动构造函数和移动赋值运算符实现的,这些函数会使用 std::move
来将资源从一个对象转移到另一个对象,而不进行复制。
2. std::move
的工作原理
std::move
将传入的对象转换成右值引用,使得该对象能够被“移动”。右值引用是 C++11 引入的一个新特性,它允许资源从一个对象转移到另一个对象,避免了拷贝的开销。
template <typename T>
T&& move(T&& arg) {
return static_cast<T&&>(arg); // 将传入的参数转换为右值引用
}
解释:
std::move
实际上使用static_cast
将传入的对象转换为右值引用,并不会真的做任何移动操作。- 它只是为后续的移动操作提供支持。
. std::move
的典型用法
(1)移动构造函数
通过 std::move
,可以将一个对象的资源转移到新对象,而不是进行拷贝。
#include <iostream>
#include <vector>
class MyClass {
public:
MyClass(std::vector<int> data) : data_(std::move(data)) {
std::cout << "Move ctructor called" << std::endl;
}
private:
std::vector<int> data_;
};
int main() {
std::vector<int> v = {1, 2, };
MyClass obj(std::move(v)); // 使用 std::move 来移动资源
// v 现在是空的
return 0;
}
在这个例子中,std::move(v)
将 v
转换为右值引用,并把 v
的内容移动到 obj
中,而不是拷贝。
(2)移动赋值运算符
移动赋值运算符允许通过 std::move
将一个对象的资源转移到另一个对象,而不是进行深拷贝。
#include <iostream>
#include <vector>
class MyClass {
public:
std::vector<int> data_;
// 移动赋值运算符
MyClass& operator=(MyClass&& other) {
std::cout << "Move assignment called" << std::endl;
if (this != &other) {
data_ = std::move(other.data_); // 使用 std::move 进行资源移动
}
return *this;
}
};
int main() {
MyClass obj1, obj2;
obj1.data_ = {1, 2, };
obj2 = std::move(obj1); // 使用移动赋值运算符
// obj1 现在的 data_ 是空的
return 0;
}
在上面的例子中,obj2 = std::move(obj1);
使用 std::move
将 obj1
的数据资源转移到 obj2
中,而不是复制。
std::move
的作用:将左值转换为右值引用,启用移动语义,以避免深拷贝的开销。- 适用场景:
std::move
主要用于在移动构造函数、移动赋值运算符以及容器类等地方提高效率。 - 注意事项:使用
std::move
后,原对象的状态变得不可预测,因此应避免在移动后访问该对象。
4.完美转发,模板中的&& 万能引用
代码语言:javascript代码运行次数:0运行复制void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(ct int& x) { cout << "ct 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(ct int&& x) { cout << "ct 右值引用" << endl; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10);// 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
ct int b = 8;
PerfectForward(b);// ct 左值
PerfectForward(std::move(b)); // ct 右值
return 0;
}
PerfectForward
函数模板展示了 完美转发 的用法,依靠 std::forward<T>(t)
保持传入参数的值类别(左值或右值)及其 ct
属性。
运行结果分析 运行结果如下:
代码语言:javascript代码运行次数:0运行复制右值引用
左值引用
右值引用
ct 左值引用
ct 右值引用
-
PerfectForward(10);
10
是一个右值。- 函数模板
PerfectForward
的模板参数T
被推导为int
(值类别为右值)。 std::forward<T>(t)
等价于std::move(t)
,将t
作为右值传递。- 调用
Fun(int&& x)
,输出 “右值引用”。
-
int a; PerfectForward(a);
a
是一个左值。- 模板参数
T
被推导为int&
(因为a
是左值)。 std::forward<T>(t)
等价于t
(保持左值属性)。- 调用
Fun(int& x)
,输出 “左值引用”。
-
PerfectForward(std::move(a));
std::move(a)
将a
转换为右值。- 模板参数
T
被推导为int
(因为std::move(a)
是右值)。 std::forward<T>(t)
等价于std::move(t)
,保持右值属性。- 调用
Fun(int&& x)
,输出 “右值引用”。
-
ct int b = 8; PerfectForward(b);
b
是一个ct
左值。- 模板参数
T
被推导为ct int&
(因为b
是ct
左值)。 std::forward<T>(t)
等价于t
(保持左值属性)。- 调用
Fun(ct int& x)
,输出 “ct 左值引用”。
-
PerfectForward(std::move(b));
std::move(b)
将b
转换为右值,但b
是ct
类型,因此std::move(b)
的值类别是ct int&&
。- 模板参数
T
被推导为ct int
。 std::forward<T>(t)
等价于std::move(t)
,保持右值属性。- 调用
Fun(ct int&& x)
,输出 “ct 右值引用”。
完美转发与万能引用
T&&
在模板中是一种特殊形式的 万能引用(也称为 转发引用),其行为取决于传入参数的值类别:
- 如果传入参数是 左值:
T
被推导为类型&
。T&&
展开为类型& &
,折叠规则将其简化为类型&
(左值引用)。std::forward<T>(t)
等价于t
,保持左值属性。
- 如果传入参数是 右值:
T
被推导为类型
。T&&
展开为类型&&
(右值引用)。std::forward<T>(t)
等价于std::move(t)
,保持右值属性。
- 万能引用的意义:
- 模板参数
T&&
是万能引用,可以接受左值、右值、ct
类型等不同类别的参数。 - 结合
std::forward<T>(t)
实现完美转发,保持参数的原始值类别及ct
性质。
- 模板参数
- 完美转发的关键:
std::forward<T>(t)
在左值情况下返回左值,在右值情况下返回右值。- 保证函数模板中的转发与调用者的意图一致。
- 函数调用的解析:
- 编译器根据参数的具体类型和值类别,推导模板参数并选择对应的函数重载。
总结:
- ct左值引用能给右值取别名
- 右值引用可以给move以后的左值取别名
- 右值引用的对象的状态变得不可预测,移动后原对象不应再访问
在这段代码中:
代码语言:javascript代码运行次数:0运行复制std::string&& s1 = std::string("12");
-
std::string&& s1
是一个 右值引用,用于绑定到一个 右值(std::string("12")
)。 - 使用
std::string&&
定义s1
,可以直接引用这个右值。 此时,s1
成为右值引用,绑定到该临时对象。虽然是右值引用,但s1
本身是一个左值,因为它有名字(可以通过名字访问它)。 - 在 C++ 中,右值引用
std::string&&
只限制它可以绑定右值,但它本质上是一个普通的变量,存储了右值的引用。 - 一旦
std::string&& s1
定义后,s1
变成了一个左值变量,可以像普通变量一样操作它的地址。
右值引用本身是左值,这样的意义是为了移动构造和移动赋值,转移资源的语法是自洽的
右值引用的属性如果是右值,那么移动构造和移动赋值,要转移资源的语法逻辑是矛盾的,右值是不能被改变的
代码语言:javascript代码运行次数:0运行复制string&& s1 = string("11111");
string& s2 =s1;
s1 是一个右值引用变量,绑定了临时对象 std::string("11111")
。
s2 是一个左值引用,引用了 s1 所绑定的对象。
此时,s1 和 s2 都指向同一个对象(std::string("11111"))
移动语义与赋值构造针对的是自定义类型的深拷贝的类的效率提升,因为深拷贝的类才有转移资源的说法,对于内置类型和浅拷贝自定义类型,没有移动系列函数
代码语言:javascript代码运行次数:0运行复制list<int> lt;
lt.push_back(10);
int x = 20;
lt.push_back(x);
代码语言:javascript代码运行次数:0运行复制void push_back(ct T& value); // 插入一个左值
void push_back(T&& value); // 插入一个右值(支持移动语义)
对于 int,移动构造和拷贝构造的效果相同,因为 int 是一个标量类型,没有动态资源
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2025-01-19,如有侵权请联系 cloudcommunity@tencent 删除对象函数c++stdstring#感谢您对电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格的认可,转载请说明来源于"电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格
推荐阅读
留言与评论(共有 12 条评论) |
本站网友 法令纹是什么 | 12分钟前 发表 |
并不会真的做任何移动操作 | |
本站网友 郑州招聘司机 | 12分钟前 发表 |
如果多个调用共用同一个静态变量 | |
本站网友 telemedicine | 24分钟前 发表 |
从而提升性能 | |
本站网友 扉之外 | 15分钟前 发表 |
generalized lvalue):左值和亡值的集合 | |
本站网友 石楠叶 | 21分钟前 发表 |
我们这里屏蔽移动构造 to_string的返回值是一个右值 | |
本站网友 新传宽频 | 12分钟前 发表 |
本站网友 a8报价 | 21分钟前 发表 |
std | |
本站网友 儿童预防接种证 | 15分钟前 发表 |
本站网友 诚如神之所说 | 17分钟前 发表 |
vector<int> v = {1 | |
本站网友 川贝母的功效与作用 | 11分钟前 发表 |
本站网友 北京枫丹丽舍 | 13分钟前 发表 |
不能引用左值 |