文章目录
-
- C++11新特性详解(二):右值引用与移动语义
- 一、左值与右值:概念的重新审视
-
- [1.1 传统的理解](#1.1 传统的理解)
- [1.2 现代C++的定义](#1.2 现代C++的定义)
- [1.3 代码验证](#1.3 代码验证)
- 二、引用的扩展:左值引用与右值引用
-
- [2.1 C++98的引用回顾](#2.1 C++98的引用回顾)
- [2.2 左值引用的特性](#2.2 左值引用的特性)
- [2.3 右值引用的诞生](#2.3 右值引用的诞生)
- [2.4 重要的属性规则](#2.4 重要的属性规则)
- 三、左值和右值的函数匹配
-
- [3.1 函数重载的优先级](#3.1 函数重载的优先级)
- [3.2 实际输出分析](#3.2 实际输出分析)
- 四、移动语义的实现与应用
-
- [4.1 为什么需要移动语义](#4.1 为什么需要移动语义)
- [4.2 实现移动构造函数](#4.2 实现移动构造函数)
- [4.3 实现移动赋值运算符](#4.3 实现移动赋值运算符)
- [4.4 测试移动语义](#4.4 测试移动语义)
- 五、移动语义解决传值返回问题
-
- [5.1 问题场景](#5.1 问题场景)
- [5.2 移动语义的解决方案](#5.2 移动语义的解决方案)
- [5.3 编译器优化的层次](#5.3 编译器优化的层次)
- 六、移动语义在容器中的应用
-
- [6.1 STL容器的接口升级](#6.1 STL容器的接口升级)
- [6.2 在list中实现移动语义](#6.2 在list中实现移动语义)
- 七、引用折叠与完美转发
-
- [7.1 引用的引用问题](#7.1 引用的引用问题)
- [7.2 引用折叠规则](#7.2 引用折叠规则)
- [7.3 万能引用](#7.3 万能引用)
- [7.4 完美转发](#7.4 完美转发)
- 八、值类别的细化分类
-
- [8.1 C++11的值类别体系](#8.1 C++11的值类别体系)
- [8.2 实际应用中的理解](#8.2 实际应用中的理解)
- 九、总结与最佳实践
-
- [9.1 核心概念回顾](#9.1 核心概念回顾)
- [9.2 实践建议](#9.2 实践建议)
- [9.3 性能对比](#9.3 性能对比)
- 十、练习与思考
-
- [10.1 练习题](#10.1 练习题)
- [10.2 思考题](#10.2 思考题)
- 十一、下一篇预告
C++11新特性详解(二):右值引用与移动语义
💬 欢迎讨论:右值引用和移动语义是C++11中最具革命性的特性,它从根本上改变了C++处理对象复制和资源管理的方式。如果你在学习过程中有任何疑问,欢迎在评论区留言交流!
👍 点赞、收藏与分享:这篇文章会比较深入,建议收藏后慢慢消化。如果觉得有帮助,请分享给更多的朋友!
🚀 系列导航:本文是C++11新特性系列的第二篇,重点讲解右值引用、移动语义、完美转发等核心概念。
一、左值与右值:概念的重新审视
1.1 传统的理解
在学习右值引用之前,我们必须先深入理解什么是左值,什么是右值。
传统定义
传统上,人们认为:
- 左值(lvalue):可以出现在赋值运算符左边的表达式
- 右值(rvalue):只能出现在赋值运算符右边的表达式
cpp
int x = 10; // x是左值,10是右值
x = 20; // OK,左值可以被赋值
// 10 = x; // 错误!右值不能被赋值
但这种理解是不完整的,因为:
cpp
const int y = 10;
// y = 20; // 错误!y不能被赋值,但y是左值
显然,仅凭"能否被赋值"来区分左值和右值是不够的。
1.2 现代C++的定义
在现代C++中,左值和右值的定义更加本质:
左值(Locator Value)
- 拥有持久性状态,存储在内存中
- 可以取地址
- 有明确的存储位置
- 在多条语句间保持存在
cpp
int a = 10;
int* p = &a; // OK,左值可以取地址
int* ptr = new int(5);
*ptr = 20; // OK,解引用指针得到左值
string s = "hello";
s[0] = 'H'; // OK,下标运算符返回左值
右值(Read Value)
- 临时性质,不存储在持久的内存位置
- 要么是字面值常量,要么是表达式求值过程中创建的临时对象
- 通常在当前语句结束后就销毁
cpp
10; // 字面值常量,右值
x + y; // 算术表达式结果是临时对象,右值
func(); // 函数返回的临时对象,右值
string("hello"); // 匿名临时对象,右值
// 以下都会编译错误,因为不能对右值取地址
// &10;
// &(x + y);
// &func();
// &string("hello");
核心区别
左值和右值的核心区别就是:能否取地址。
通常左值可取地址,右值不可取地址,但这不是严格标准。 例如位域(bit-field)是左值但不能取地址;std::move(x)
是将亡值(xvalue)但可以取地址。 更本质的区别是表达式是否具有对象身份(identity)。
1.3 代码验证
cpp
#include <iostream>
#include <string>
using namespace std;
int main()
{
// ========== 左值示例 ==========
int* p = new int(0);
int b = 1;
const int c = 2;
string s("hello");
// 这些都是左值,可以取地址
cout << &b << endl;
cout << &c << endl;
cout << &(*p) << endl;
cout << (void*)&s[0] << endl;
// ========== 右值示例 ==========
double x = 1.1, y = 2.2;
// 这些都是右值,不能取地址
10; // 字面值
x + y; // 表达式结果
fmin(x, y); // 函数返回值
string("world"); // 临时对象
// 以下代码都会编译错误
// cout << &10 << endl;
// cout << &(x + y) << endl;
// cout << &fmin(x, y) << endl;
// cout << &string("world") << endl;
return 0;
}
二、引用的扩展:左值引用与右值引用
2.1 C++98的引用回顾
在C++98中,引用就是给对象起别名:
cpp
int x = 10;
int& r = x; // r是x的别名
r = 20; // 修改r就是修改x
cout << x; // 输出20
C++11之后,为了区分,我们把C++98中的引用称为左值引用。
2.2 左值引用的特性
基本规则
cpp
int a = 10;
int& r1 = a; // OK,左值引用绑定左值
// int& r2 = 10; // 错误!左值引用不能直接绑定右值
const左值引用的特权
const左值引用是一个特例,它可以绑定右值:
cpp
const int& r1 = 10; // OK
const int& r2 = x + y; // OK
const string& r3 = string("hi"); // OK
为什么const左值引用可以绑定右值?
这是C++为了支持函数传参的便利性而设计的特性。编译器会创建一个临时对象来存储右值,然后让const引用绑定到这个临时对象上:
cpp
// 实际发生的事情:
// const int& r = 10;
// 等价于:
const int temp = 10; // 创建临时对象
const int& r = temp; // 绑定到临时对象
2.3 右值引用的诞生
C++11引入了右值引用 ,使用&&表示:
cpp
int&& rr1 = 10; // OK,右值引用绑定右值
int&& rr2 = x + y; // OK
string&& rr3 = string("hi"); // OK
int a = 5;
// int&& rr4 = a; // 错误!右值引用不能直接绑定左值
使用std::move绑定左值
虽然右值引用不能直接绑定左值,但可以通过std::move将左值转换为右值引用:
cpp
int a = 10;
int&& rr = std::move(a); // OK,std::move将a转换为右值引用
std::move的本质
std::move并不真的"移动"任何东西,它只是一个类型转换,将左值强制转换为右值引用:
cpp
template <class T>
typename remove_reference<T>::type&& move(T&& arg)
{
return static_cast<typename remove_reference<T>::type&&>(arg);
}
2.4 重要的属性规则
这是一个非常重要但容易混淆的概念:
变量表达式都是左值属性
即使一个变量被声明为右值引用类型,当它作为表达式使用时,它的属性仍然是左值!
cpp
int&& rr1 = 10;
// rr1是一个右值引用类型的变量
// 但rr1这个变量表达式本身是左值!
cout << &rr1 << endl; // OK,可以取地址,说明是左值
int& r = rr1; // OK,左值引用可以绑定rr1
// int&& rr2 = rr1; // 错误!右值引用不能绑定左值
int&& rr2 = std::move(rr1); // 必须使用move
为什么这样设计?
这个设计看起来很奇怪,但实际上非常合理。我们将在移动语义部分看到,这个设计保证了移动操作的安全性------只有当我们明确表示"这个对象可以被移动"时,才会发生移动。
三、左值和右值的函数匹配
3.1 函数重载的优先级
C++11以后,我们可以针对左值引用、const左值引用和右值引用分别重载函数:
cpp
void f(int& x)
{
cout << "左值引用重载 f(" << x << ")" << endl;
}
void f(const int& x)
{
cout << "const左值引用重载 f(" << x << ")" << endl;
}
void f(int&& x)
{
cout << "右值引用重载 f(" << x << ")" << endl;
}
匹配规则
- 左值优先匹配左值引用版本
- const左值优先匹配const左值引用版本
- 右值优先匹配右值引用版本
- 如果没有右值引用版本,右值会退而求其次匹配const左值引用版本
cpp
int main()
{
int i = 1;
const int ci = 2;
f(i); // 调用 f(int&)
f(ci); // 调用 f(const int&)
f(3); // 调用 f(int&&)
f(std::move(i)); // 调用 f(int&&)
// 右值引用变量的匹配
int&& rr = 10;
f(rr); // 调用 f(int&),因为rr是左值!
f(std::move(rr)); // 调用 f(int&&)
return 0;
}
3.2 实际输出分析
运行上面的代码,输出如下:
bash
左值引用重载 f(1)
const左值引用重载 f(2)
右值引用重载 f(3)
右值引用重载 f(1)
左值引用重载 f(10)
右值引用重载 f(10)
这个输出完美印证了我们前面说的规则:右值引用类型的变量表达式仍然是左值。
四、移动语义的实现与应用
4.1 为什么需要移动语义
在讲移动语义之前,让我们先看一个性能问题:
cpp
string func()
{
string str("hello world, this is a very long string!");
return str; // 这里会发生拷贝
}
int main()
{
string s = func(); // 这里又会发生拷贝
return 0;
}
在C++98中,上面的代码理论上会发生两次深拷贝(虽然编译器可能优化)。问题在于:
func内部的str是一个即将销毁的临时对象- 我们把这个临时对象的内容拷贝出来
- 然后临时对象被销毁,它的资源也被释放
- 这是一种资源浪费------我们完全可以"窃取"临时对象的资源,而不是拷贝
移动语义的核心思想
既然临时对象马上就要销毁了,我们为什么不直接"拿走"它的资源,而要费力去拷贝呢?这就是移动语义的核心思想。
4.2 实现移动构造函数
让我们基于之前实现的bit::string类,添加移动构造函数:
cpp
namespace bit
{
class string
{
public:
typedef char* iterator;
// 构造函数
string(const char* str = "")
: _size(strlen(str))
, _capacity(_size)
{
cout << "string(const char*) - 构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷贝构造函数
string(const string& s)
: _str(nullptr)
{
cout << "string(const string&) - 拷贝构造" << endl;
string tmp(s.c_str());
swap(tmp);
}
// 移动构造函数
string(string&& s)
: _str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&&) - 移动构造" << endl;
swap(s); // 直接交换资源
}
// 析构函数
~string()
{
cout << "~string() - 析构" << endl;
delete[] _str;
_str = nullptr;
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
const char* c_str() const { return _str; }
size_t size() const { return _size; }
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
移动构造的实现分析
移动构造函数接受一个右值引用参数string&& s。它的实现非常简单:
- 将自己的成员初始化为空/零
- 调用
swap,将参数s的资源交换过来 - 此时,
s变成了空状态,但没关系,因为s是一个即将销毁的临时对象
这个过程没有任何内存分配或数据拷贝,只是指针的交换,效率极高!
4.3 实现移动赋值运算符
cpp
// 拷贝赋值
string& operator=(const string& s)
{
cout << "string& operator=(const string&) - 拷贝赋值" << endl;
if (this != &s)
{
string tmp(s);
swap(tmp);
}
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&&) - 移动赋值" << endl;
swap(s);
return *this;
}
移动赋值同样简单:直接交换资源即可。
4.4 测试移动语义
cpp
int main()
{
bit::string s1("hello");
// 拷贝构造
bit::string s2 = s1;
cout << "-------------------" << endl;
// 构造临时对象 + 移动构造(编译器可能优化为直接构造)
bit::string s3 = bit::string("world");
cout << "-------------------" << endl;
// 移动构造
bit::string s4 = std::move(s1);
cout << "-------------------" << endl;
return 0;
}
输出分析
bash
string(const char*) - 构造
string(const string&) - 拷贝构造
-------------------
string(const char*) - 构造
string(string&&) - 移动构造
~string() - 析构
-------------------
string(string&&) - 移动构造
-------------------
~string() - 析构
~string() - 析构
~string() - 析构
可以看到,使用右值或std::move时,调用的是移动构造而不是拷贝构造,效率大大提升!
五、移动语义解决传值返回问题
5.1 问题场景
之前我们提到,左值引用解决了大部分场景的拷贝问题,但有些场景无法使用引用返回:
cpp
string addStrings(string num1, string num2)
{
string str;
// ...复杂的计算...
return str; // 局部对象,不能返回引用
}
这种情况下,str是一个局部对象,函数结束后就会销毁,我们不能返回它的引用。C++98只能接受拷贝的代价,或者使用输出型参数。
5.2 移动语义的解决方案
有了移动语义后,返回局部对象时会发生什么呢?
场景一:构造接收对象
cpp
bit::string func()
{
bit::string str("hello world");
cout << "---返回前---" << endl;
return str;
}
int main()
{
bit::string ret = func();
cout << ret.c_str() << endl;
return 0;
}
不同编译器的处理
在vs2019 debug模式下(关闭优化):
bash
string(const char*) - 构造
---返回前---
string(string&&) - 移动构造 // str移动构造临时对象
~string() - 析构 // str析构
string(string&&) - 移动构造 // 临时对象移动构造ret
~string() - 析构 // 临时对象析构
hello world
~string() - 析构 // ret析构
在vs2019 release模式或vs2022中(编译器优化):
bash
string(const char*) - 构造 // 直接构造ret
---返回前---
hello world
~string() - 析构
编译器将连续的移动操作优化成了直接构造!这种优化叫做返回值优化(RVO)。
场景二:赋值给已存在对象
cpp
int main()
{
bit::string ret;
ret = func();
cout << ret.c_str() << endl;
return 0;
}
输出(debug模式):
bash
string(const char*) - 构造 // ret的默认构造
string(const char*) - 构造 // func内的str
---返回前---
string(string&&) - 移动构造 // str移动构造临时返回对象
~string() - 析构 // str析构
string& operator=(string&&) - 移动赋值 // 临时对象移动赋值给ret
~string() - 析构 // 临时对象析构
hello world
~string() - 析构 // ret析构
即使在这种情况下,编译器不进行优化,我们使用的也是移动而不是拷贝,效率仍然很高。
5.3 编译器优化的层次
编译器对返回值的优化有不同的层次:
层次一:没有移动语义(C++98)
- 需要两次深拷贝
- 效率最低
层次二:有移动语义,但未优化
- 两次移动操作
- 比拷贝快很多,但仍有优化空间
层次三:返回值优化(RVO)
- 直接在目标位置构造对象
- 完全避免移动和拷贝
- 效率最高
现代编译器通常会自动进行RVO优化,但即使没有优化,有了移动语义也比C++98的拷贝要高效得多。
六、移动语义在容器中的应用
6.1 STL容器的接口升级
C++11之后,STL容器的push_back和insert系列接口都增加了右值引用版本:
cpp
// vector的push_back
void push_back(const value_type& val); // 左值版本
void push_back(value_type&& val); // 右值版本
// vector的insert
iterator insert(const_iterator position, const value_type& val);
iterator insert(const_iterator position, value_type&& val);
使用示例
cpp
#include <list>
using namespace std;
int main()
{
list<bit::string> lt;
// 场景1:插入左值(拷贝)
bit::string s1("111111111111");
lt.push_back(s1);
cout << "-------------------" << endl;
// 场景2:插入右值(移动)
lt.push_back(bit::string("222222222222"));
cout << "-------------------" << endl;
// 场景3:字面值隐式转换(先构造临时对象,再移动)
lt.push_back("333333333333");
cout << "-------------------" << endl;
// 场景4:move左值(移动)
lt.push_back(std::move(s1));
cout << "-------------------" << endl;
return 0;
}
输出分析
bash
string(const char*) - 构造
string(const string&) - 拷贝构造 // 场景1:拷贝s1
-------------------
string(const char*) - 构造
string(string&&) - 移动构造 // 场景2:移动临时对象
~string() - 析构
-------------------
string(const char*) - 构造 // 场景3:先构造临时对象
string(string&&) - 移动构造 // 再移动
~string() - 析构
-------------------
string(string&&) - 移动构造 // 场景4:移动s1
-------------------
~string() - 析构
~string() - 析构
~string() - 析构
~string() - 析构
~string() - 析构
6.2 在list中实现移动语义
让我们为之前实现的list添加移动语义的支持:
cpp
namespace bit
{
template<class T>
struct ListNode
{
ListNode<T>* _next;
ListNode<T>* _prev;
T _data;
// 左值构造
ListNode(const T& data = T())
: _next(nullptr)
, _prev(nullptr)
, _data(data)
{}
// 右值构造
ListNode(T&& data)
: _next(nullptr)
, _prev(nullptr)
, _data(std::move(data))
{}
};
template<class T>
class list
{
public:
// 左值版本的push_back
void push_back(const T& x)
{
insert(end(), x);
}
// 右值版本的push_back
void push_back(T&& x)
{
insert(end(), std::move(x));
}
// 左值版本的insert
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* newnode = new Node(x);
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
// 右值版本的insert
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* newnode = new Node(std::move(x));
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
};
}
七、引用折叠与完美转发
7.1 引用的引用问题
在C++中,我们不能直接定义引用的引用:
cpp
int x = 10;
int& r = x;
// int& & rr = r; // 编译错误!
但是,通过模板或typedef,可能会间接产生"引用的引用"的情况。这时,C++11引入了引用折叠规则来处理。
7.2 引用折叠规则
规则很简单
- 所有的右值引用折叠到右值引用上仍然是一个右值引用
- 所有的其他引用类型之间的叠加都将变成左值引用
换句话说:只有右值引用 + 右值引用 = 右值引用,其他情况都是左值引用
cpp
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n; // int& & 折叠为 int&
lref&& r2 = n; // int& && 折叠为 int&
rref& r3 = n; // int&& & 折叠为 int&
rref&& r4 = 1; // int&& &&折叠为 int&&
7.3 万能引用
模板中的T&&
在函数模板中,T&&是一个特殊的存在:
cpp
template<class T>
void f(T&& x)
{
// ...
}
这里的T&&不一定是右值引用!它被称为万能引用 或转发引用:
如下:我们定义了一个整型变量a
- 如果传递的实参是左值,
T被推导为int&,经过引用折叠后T&&变成int& - 如果传递的实参是右值,
T被推导为int,T&&就是int&&
cpp
template<class T>
void f(T&& x) {}
int main()
{
int a = 10;
f(a); // T推导为int&, T&&折叠为int&
f(10); // T推导为int, T&&就是int&&
f(std::move(a)); // T推导为int, T&&就是int&&
return 0;
}
7.4 完美转发
问题的提出
考虑下面的代码:
cpp
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const右值引用" << endl; }
template<class T>
void Function(T&& t)
{
Fun(t); // 问题:t总是匹配左值版本!
}
int main()
{
Function(10); // 传入右值
int a = 5;
Function(a); // 传入左值
Function(std::move(a)); // 传入右值
const int b = 10;
Function(b); // 传入const左值
Function(std::move(b)); // 传入const右值
return 0;
}
输出结果
bash
左值引用
左值引用
左值引用
const左值引用
const左值引用
问题分析
虽然Function能够通过万能引用接收任意类型的参数,但在函数内部调用Fun(t)时,由于t是一个变量表达式,它的属性总是左值,所以总是匹配左值版本的Fun。
这样就失去了我们传递右值的意义------我们希望如果传入的是右值,就调用右值版本的Fun;如果传入的是左值,就调用左值版本的Fun。
std::forward的作用
C++11提供了std::forward来解决这个问题,它能够完美转发参数的值类别:
cpp
template<class T>
void Function(T&& t)
{
Fun(std::forward<T>(t)); // 完美转发
}
int main()
{
Function(10); // 调用Fun(int&&)
int a = 5;
Function(a); // 调用Fun(int&)
Function(std::move(a)); // 调用Fun(int&&)
const int b = 10;
Function(b); // 调用Fun(const int&)
Function(std::move(b)); // 调用Fun(const int&&)
return 0;
}
输出结果
bash
右值引用
左值引用
右值引用
const左值引用
const右值引用
完美!现在参数的值类别被正确地保持并转发了。
std::forward的实现原理
两个重载
cpp
template <class T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {
return static_cast<T&&>(t);
}
template <class T>
T&& forward(typename std::remove_reference<T>::type&& t) noexcept {
static_assert(!std::is_lvalue_reference<T>::value, "bad forward");
return static_cast<T&&>(t);
}
forward的原理也是利用引用折叠:
- 如果
T是int&(左值情况),T&&折叠为int&,返回左值引用 - 如果
T是int(右值情况),T&&就是int&&,返回右值引用
完美转发的典型应用场景
cpp
template<class T>
class vector
{
public:
// 使用完美转发实现通用的emplace_back
template<class... Args>
void emplace_back(Args&&... args)
{
// 完美转发参数包给元素的构造函数
new (_finish) T(std::forward<Args>(args)...);
++_finish;
}
private:
T* _start;
T* _finish;
T* _endofstorage;
};
八、值类别的细化分类
8.1 C++11的值类别体系
C++11对值类别进行了更细致的划分:
bash
表达式
/ \
glvalue rvalue
/ \ / \
lvalue xvalue prvalue
三种基本类别
-
左值(lvalue):传统意义上的左值,有持久的存储位置
- 变量名、函数名
- 返回左值引用的函数调用
- 字符串字面值
- 前置递增/递减运算符的返回值
-
纯右值(prvalue - pure rvalue):传统意义上的右值
- 字面值常量(除字符串外):
42、true、nullptr - 返回非引用类型的函数调用
- 后置递增/递减运算符的返回值
- 算术表达式、逻辑表达式的结果
- 字面值常量(除字符串外):
-
将亡值(xvalue - expiring value):即将被移动的对象
- 返回右值引用的函数调用:
std::move(x) - 转换为右值引用的类型转换:
static_cast<X&&>(x)
- 返回右值引用的函数调用:
两种混合类别
- 泛左值(glvalue - generalized lvalue):左值 + 将亡值
- 右值(rvalue):纯右值 + 将亡值
8.2 实际应用中的理解
在实际编程中,我们通常只需要区分:
- 左值:可以取地址的持久对象
- 右值:临时对象或即将销毁的对象
将亡值的概念主要是为了在类型系统层面更精确地描述移动语义。
九、总结与最佳实践
9.1 核心概念回顾
左值与右值
- 左值:有持久状态,可以取地址
- 右值:临时对象,不能取地址
- 判断标准:能否取地址
左值引用与右值引用
- 左值引用(
&):绑定左值 - 右值引用(
&&):绑定右值 - const左值引用:可以绑定任何值类别
- 变量表达式都是左值,即使它的类型是右值引用
移动语义
- 移动构造:
T(T&& other) - 移动赋值:
T& operator=(T&& other) - 核心思想:窃取资源而不是拷贝
- 使用场景:传值返回、容器操作
引用折叠与完美转发
- 引用折叠:只有
&&+&&=&&,其他都是& - 万能引用:模板中的
T&& - 完美转发:
std::forward<T>(t)保持参数的值类别
9.2 实践建议
何时实现移动语义
- 类管理动态资源(堆内存、文件句柄等)
- 类的拷贝成本较高
- 类会被用作容器的元素类型
实现移动函数的注意事项
- 移动后的对象应处于有效但未指定的状态
- 移动操作应该是noexcept的(提升性能)
- 移动后源对象的析构函数仍会被调用
cpp
class Buffer
{
public:
// 移动构造应该标记为noexcept
Buffer(Buffer&& other) noexcept
: _data(other._data)
, _size(other._size)
{
other._data = nullptr; // 让源对象进入安全状态
other._size = 0;
}
~Buffer()
{
delete[] _data; // 源对象的析构仍会执行
}
private:
char* _data;
size_t _size;
};
使用std::move的场景
cpp
// 场景1:传递给容器
vector<string> vec;
string str = "hello";
vec.push_back(std::move(str)); // str被移动,之后不应再使用
// 场景2:返回成员变量
class Widget
{
string _data;
public:
string getData() && // 右值限定符
{
return std::move(_data); // 对象是右值时可以移动
}
};
// 场景3:实现移动赋值
Widget& operator=(Widget&& other)
{
_data = std::move(other._data);
return *this;
}
不要过度使用std::move
cpp
// 错误示例:不必要的move
string func()
{
string str = "hello";
return std::move(str); // 多余!编译器会自动优化
}
// 正确做法
string func()
{
string str = "hello";
return str; // 返回局部变量时自动视为右值
}
9.3 性能对比
让我们看一个实际的性能测试:
cpp
#include <iostream>
#include <vector>
#include <chrono>
class BigObject
{
public:
BigObject() : _data(new int[1000000]) {}
// 拷贝构造
BigObject(const BigObject& other)
: _data(new int[1000000])
{
std::copy(other._data, other._data + 1000000, _data);
}
// 移动构造
BigObject(BigObject&& other) noexcept
: _data(other._data)
{
other._data = nullptr;
}
~BigObject() { delete[] _data; }
private:
int* _data;
};
int main()
{
using namespace std::chrono;
// 测试拷贝
auto start = high_resolution_clock::now();
{
std::vector<BigObject> vec;
BigObject obj;
for (int i = 0; i < 1000; ++i)
{
vec.push_back(obj); // 拷贝
}
}
auto end = high_resolution_clock::now();
cout << "拷贝耗时: "
<< duration_cast<milliseconds>(end - start).count()
<< "ms" << endl;
// 测试移动
start = high_resolution_clock::now();
{
std::vector<BigObject> vec;
for (int i = 0; i < 1000; ++i)
{
vec.push_back(BigObject()); // 移动
}
}
end = high_resolution_clock::now();
cout << "移动耗时: "
<< duration_cast<milliseconds>(end - start).count()
<< "ms" << endl;
return 0;
}
十、练习与思考
10.1 练习题
练习1:实现移动语义
为下面的动态数组类实现移动构造和移动赋值:
cpp
class DynamicArray
{
public:
DynamicArray(size_t size)
: _data(new int[size])
, _size(size)
{}
// TODO: 实现拷贝构造
// TODO: 实现移动构造
// TODO: 实现拷贝赋值
// TODO: 实现移动赋值
~DynamicArray() { delete[] _data; }
private:
int* _data;
size_t _size;
};
练习2:使用万能引用
实现一个工厂函数,能够完美转发参数:
cpp
template<class T, class... Args>
unique_ptr<T> make_unique(Args&&... args)
{
// TODO: 使用完美转发创建对象
}
练习3:性能优化
优化下面的代码,使用移动语义减少不必要的拷贝:
cpp
vector<string> getStrings()
{
vector<string> result;
result.push_back(string("hello"));
result.push_back(string("world"));
return result;
}
10.2 思考题
为什么右值引用变量的表达式是左值?这样设计的好处是什么?
在什么情况下编译器不会自动生成移动构造函数?
std::move和std::forward有什么区别?什么时候用哪个?移动后的对象必须保持什么样的状态?为什么?
十一、下一篇预告
在下一篇文章中,我们将学习C++11的高级特性:
- 可变参数模板:编译期递归,参数包展开
- lambda表达式:匿名函数,捕获列表的奥秘
- function与bind:函数包装器,参数绑定
- emplace系列接口:就地构造的魔法
这些特性会让你的C++代码更加简洁、优雅和高效!
通过本文的学习,我们深入理解了右值引用和移动语义这个C++11中最重要的特性。移动语义不仅提升了性能,更重要的是它改变了我们思考资源管理的方式。掌握这些知识,你就掌握了现代C++的精髓!
以上就是C++11右值引用与移动语义的全部内容,希望这篇文章能够帮助你深入理解这个强大的特性!如有疑问,欢迎在评论区交流讨论!❤️
