跳到主要内容 C++左值与右值深度解析 | 极客日志
C++ 算法
C++左值与右值深度解析 本文深入解析 C++ 左值与右值的概念、分类及引用机制。涵盖纯右值、将亡值、左值引用的绑定规则,以及移动语义和完美转发的高级应用。通过对比拷贝与移动操作的性能差异,结合 std::move 和 std::forward 的使用示例,阐述了避免不必要的内存分配和优化资源管理的最佳实践。最后总结了常见陷阱如多次移动、const 对象移动等,并介绍了 C++ 标准演进对值类别的影响,帮助开发者构建完整的现代 C++ 知识体系。
C++左值与右值深度解析
1. 引言
在 C++ 的发展历程中,C++11 标准的发布是一个重要的里程碑。该标准引入了右值引用、移动语义等特性,极大地提升了程序的性能和表达能力。然而,要充分利用这些特性,必须深入理解左值和右值的概念。
本文将系统性地讲解左值与右值的理论基础和实践应用,涵盖从基础概念到高级特性的全部内容,帮助读者构建完整的知识体系。
2. 基本概念与定义
2.1 历史起源
左值(lvalue)和右值(rvalue)的命名最初来源于赋值表达式:
左值 :可以出现在赋值运算符左侧的表达式
右值 :只能出现在赋值运算符右侧的表达式
然而,这个简单的定义并不完全准确,现代 C++ 对这两个概念有更精确的界定。
2.2 现代定义
左值(lvalue) :具有明确内存地址、可以取地址(&)的表达式,其生命周期通常较为持久
右值(rvalue) :没有明确内存地址、不可取地址的表达式,通常表示临时对象或即将销毁的对象
2.3 判断标准 判断一个表达式是左值还是右值的最简单方法是:尝试对其取地址 。
int x = 10 ;
&x;
&10 ;
&(x + 5 );
3. 左值深度剖析
3.1 左值的特征 int a = 5 ;
double d = 3.14 ;
std::string str = "hello" ;
int & func () {
static int value = 10 ;
return value;
}
func () = 20 ;
int a = 10 ;
int * ptr = &a;
*ptr = 100 ;
3.2 左值的分类
4. 右值深度剖析
4.1 右值的分类
纯右值(prvalue - pure rvalue)
将亡值(xvalue - expiring value)
4.1.1 纯右值(prvalue) int func () {
return 10 ;
}
func ();
int a = 5 , b = 10 ;
a + b;
a * b;
a < b;
a && b;
4.1.2 将亡值(xvalue) int && func () {
static int value = 10 ;
return std::move (value);
}
func ();
std::string s = "hello" ;
std::move (s);
int a = 10 ;
static_cast <int &&>(a);
struct X {
int value;
};
X&& func () {
return X{42 };
}
func ().value;
4.2 为什么区分 prvalue 和 xvalue
纯右值 :没有身份标识,纯粹的临时值
将亡值 :有身份标识(指向某个对象),但该对象的资源可以被移动
std::string str1 = "hello" ;
std::string str2 = std::move (str1);
std::string str3 = std::string ("hello" ) + std::string ("world" );
5. 引用机制详解
5.1 左值引用
5.1.1 左值引用的基本规则 int x = 10 ;
int & ref = x;
ref = 20 ;
5.1.2 const 左值引用的特殊性 const 左值引用可以绑定到右值,这是一个特殊规则:
const int & cref = 10 ;
const int & cref2 = x + 5 ;
std::string s = "hello" ;
const std::string& cref3 = s + " world" ;
原因 :const 引用承诺不修改对象,因此可以安全地绑定到临时对象。
5.1.3 左值引用的应用场景 void processString (const std::string& str) {
std::cout << str << std::endl;
}
int & getElement (std::vector<int >& vec, size_t index) {
return vec[index];
}
std::vector<int > vec = {1 , 2 , 3 };
getElement (vec, 0 ) = 100 ;
std::vector<int > vec = {1 , 2 , 3 , 4 , 5 };
for (int & elem : vec) {
elem *= 2 ;
}
5.2 右值引用 右值引用是 C++11 引入的新特性,使用双&&符号声明。
5.2.1 右值引用的基本规则 int && rref = 10 ;
int && rref2 = 5 + 3 ;
int x = 10 ;
int && rref4 = std::move (x);
5.2.2 重要特性:右值引用本身是左值 int && rref = 42 ;
int && rref2 = std::move (rref);
原因 :右值引用变量有名字,可以取地址,因此它本身是左值。
5.3 引用绑定规则总结 引用类型 绑定左值 绑定右值 说明 T&✓ ✗ 左值引用只能绑定左值 const T&✓ ✓ const 左值引用可绑定任意值 T&&✗ ✓ 右值引用只能绑定右值 const T&&✗ ✓ const 右值引用(少用)
int x = 10 ;
int & lr1 = x;
const int & clr1 = x;
const int & clr2 = 10 ;
int && rr2 = 10 ;
int && rr3 = std::move (x);
const int && crr1 = 10 ;
6. 移动语义 移动语义是 C++11 引入的重要特性,它允许将资源从一个对象转移到另一个对象,而不是进行昂贵的拷贝操作。
6.1 为什么需要移动语义
6.1.1 拷贝的代价 class BigData {
int * data;
size_t size;
public :
BigData (size_t s) : size (s), data (new int [s]) {
std::cout << "构造,分配 " << s << " 个整数的内存\n" ;
}
BigData (const BigData& other) : size (other.size), data (new int [other.size]) {
std::cout << "拷贝构造,分配新内存并复制 " << size << " 个元素\n" ;
std::copy (other.data, other.data + size, data);
}
BigData& operator =(const BigData& other) {
std::cout << "拷贝赋值\n" ;
if (this != &other) {
delete [] data;
size = other.size;
data = new int [size];
std::copy (other.data, other.data + size, data);
}
return *this ;
}
~BigData () {
delete [] data;
std::cout << "析构,释放内存\n" ;
}
};
BigData createBigData () {
return BigData (1000000 );
}
BigData data = createBigData ();
在没有移动语义的情况下,返回的临时对象需要被拷贝到 data 变量中,这涉及大量的内存分配和数据复制。
6.2 移动构造函数和移动赋值运算符 class BigData {
int * data;
size_t size;
public :
BigData (size_t s) : size (s), data (new int [s]) {
std::cout << "构造\n" ;
}
BigData (const BigData& other) : size (other.size), data (new int [other.size]) {
std::cout << "拷贝构造\n" ;
std::copy (other.data, other.data + size, data);
}
BigData (BigData&& other) noexcept : size (other.size), data (other.data) {
std::cout << "移动构造,窃取资源,无需分配新内存\n" ;
other.data = nullptr ;
other.size = 0 ;
}
BigData& operator =(const BigData& other) {
std::cout << "拷贝赋值\n" ;
if (this != &other) {
delete [] data;
size = other.size;
data = new int [size];
std::copy (other.data, other.data + size, data);
}
return *this ;
}
BigData& operator =(BigData&& other) noexcept {
std::cout << "移动赋值,窃取资源\n" ;
if (this != &other) {
delete [] data;
data = other.data;
size = other.size;
other.data = nullptr ;
other.size = 0 ;
}
return *this ;
}
~BigData () {
delete [] data;
std::cout << "析构\n" ;
}
};
6.2.1 使用示例 BigData data1 (1000000 ) ;
BigData data2 = std::move (data1);
BigData data3 (500000 ) ;
data3 = createBigData ();
6.3 std::move 详解
6.3.1 std::move 的本质 template <typename T>
typename std::remove_reference<T>::type&& move (T&& t) noexcept {
return static_cast <typename std::remove_reference<T>::type&&>(t);
}
std::move不移动任何东西 !
它只是一个类型转换,将参数转换为右值引用
真正的'移动'发生在移动构造函数或移动赋值运算符中
6.3.2 std::move 的使用 std::string str1 = "hello" ;
std::string str2 = std::move (str1);
std::cout << str1;
6.3.3 std::move 后的对象 被 std::move 的对象仍然是有效的对象,可以:
std::string str = "hello" ;
std::string s = std::move (str);
str = "new value" ;
{
std::string temp = "temp" ;
std::string s = std::move (temp);
}
str.clear ();
std::cout << str.length ();
if (str.empty ()) {
}
7. 完美转发 完美转发(Perfect Forwarding)是 C++11 引入的高级特性,用于在模板函数中保持参数的值类别。
7.1 引用折叠规则
7.2 万能引用(Universal Reference) template <typename T>
void func (T&& param) {
}
int x = 10 ;
func (x);
func (10 );
template <typename T>
void func1 (T&& param) ;
template <typename T>
void func2 (std::vector<T>&& param) ;
int x = 10 ;
auto && var1 = x;
auto && var2 = 10 ;
7.3 std::forward 详解 std::forward 用于完美转发,保持参数的值类别。
7.3.1 问题的由来 void process (int & x) {
std::cout << "左值版本\n" ;
}
void process (int && x) {
std::cout << "右值版本\n" ;
}
template <typename T>
void wrapper (T&& arg) {
process (arg);
}
int a = 10 ;
wrapper (a);
wrapper (10 );
7.3.2 解决方案:std::forward template <typename T>
void wrapper (T&& arg) {
process (std::forward<T>(arg));
}
int a = 10 ;
wrapper (a);
wrapper (10 );
7.3.3 std::forward 的实现原理 template <typename T>
T&& forward (typename std::remove_reference<T>::type& t) noexcept {
return static_cast <T&&>(t);
}
7.4 完美转发的应用
template <typename T, typename ... Args>
std::unique_ptr<T> make_unique (Args&&... args) {
return std::unique_ptr <T>(new T (std::forward<Args>(args)...));
}
class MyClass {
public :
MyClass (int a, const std::string& b) {}
};
auto ptr = make_unique <MyClass>(42 , "hello" );
8. 实际应用场景
8.1 容器操作优化 std::vector<std::string> vec;
std::string str = "hello world" ;
vec.push_back (str);
vec.push_back (std::move (str));
vec.emplace_back ("hello world" );
8.2 工厂函数优化
BigData createData () {
BigData data (1000 ) ;
return data;
}
BigData data = createData ();
8.3 智能指针所有权转移 std::unique_ptr<int > ptr1 = std::make_unique <int >(10 );
std::unique_ptr<int > ptr2 = std::move (ptr1);
void takeOwnership (std::unique_ptr<int > ptr) {
}
takeOwnership (std::move (ptr2));
8.4 避免拷贝的函数返回
std::vector<int > generateLargeVector () {
std::vector<int > vec (1000000 ) ;
for (size_t i = 0 ; i < vec.size (); ++i) {
vec[i] = i;
}
return vec;
}
auto result = generateLargeVector ();
8.5 交换操作优化
template <typename T>
void old_swap (T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
template <typename T>
void new_swap (T& a, T& b) {
T temp = std::move (a);
a = std::move (b);
b = std::move (temp);
}
9. 常见陷阱与最佳实践
9.1 常见陷阱
9.1.1 陷阱 1:多次 std::move std::string str = "hello" ;
std::string s1 = std::move (str);
std::string s2 = std::move (str);
std::cout << str;
教训 :不要对同一对象多次使用 std::move。
9.1.2 陷阱 2:移动 const 对象 const std::string cstr = "hello" ;
std::string s = std::move (cstr);
教训 :const 对象无法被移动,因为移动操作需要修改源对象。
9.1.3 陷阱 3:返回局部变量时使用 std::move
std::string badFunc () {
std::string str = "hello" ;
return std::move (str);
}
std::string goodFunc () {
std::string str = "hello" ;
return str;
}
教训 :返回局部变量时不要使用 std::move,会阻止编译器优化。
9.1.4 陷阱 4:右值引用变量是左值 void process (std::string& s) {
std::cout << "左值版本\n" ;
}
void process (std::string&& s) {
std::cout << "右值版本\n" ;
}
void func (std::string&& str) {
process (str);
process (std::move (str));
}
教训 :右值引用变量本身是左值,需要 std::move 转换。
9.1.5 陷阱 5:忘记 noexcept class MyClass {
MyClass (MyClass&& other) noexcept {
}
MyClass& operator =(MyClass&& other) noexcept {
return *this ;
}
};
教训 :std::vector 等容器在重新分配内存时,只有当移动构造函数标记为 noexcept 时才会使用移动操作,否则会使用拷贝操作以保证异常安全性。
9.2 最佳实践
9.2.1 实践 1:移动操作标记 noexcept class MyClass {
public :
MyClass (MyClass&&) noexcept ;
MyClass& operator =(MyClass&&) noexcept ;
};
9.2.2 实践 2:遵循五法则/零法则 五法则 :如果需要自定义以下任何一个,通常需要自定义全部五个:
析构函数
拷贝构造函数
拷贝赋值运算符
移动构造函数
移动赋值运算符
零法则 :如果可能,不要自定义这些函数,使用默认实现。
class GoodClass {
std::unique_ptr<int []> data;
std::vector<std::string> names;
};
9.2.3 实践 3:使用=default 和=delete class MyClass {
public :
MyClass (const MyClass&) = default ;
MyClass (MyClass&&) = default ;
MyClass& operator =(const MyClass&) = delete ;
};
9.2.4 实践 4:合理选择参数传递方式
void func1 (int x) ;
void func2 (char c) ;
void func3 (const std::string& s) ;
void func4 (const std::vector<int >& v) ;
void func5 (std::string& s) ;
void func6 (std::string&& s) ;
template <typename T>
void func7 (T&& arg) ;
9.2.5 实践 5:优先使用 emplace 而非 insert/push std::vector<std::pair<int , std::string>> vec;
vec.push_back ({1 , "hello" });
vec.push_back (std::make_pair (2 , "world" ));
vec.emplace_back (3 , "efficient" );
9.2.6 实践 6:完美转发使用 std::forward template <typename T, typename ... Args>
void wrapper (Args&&... args) {
T obj (std::forward<Args>(args)...) ;
}
10. C++ 标准演进
10.1 C++11
右值引用(T&&)
移动构造函数和移动赋值运算符
std::move 和 std::forward
万能引用(转发引用)
std::vector<std::string> vec;
std::string str = "hello" ;
vec.push_back (std::move (str));
10.2 C++14
auto func () {
return std::vector<int >{1 , 2 , 3 };
}
auto lambda = [](auto && x) {
return std::forward<decltype (x)>(x);
};
10.3 C++17
保证的拷贝消除(Guaranteed Copy Elision)
对纯右值进行特殊处理
struct NonMovable {
NonMovable () = default ;
NonMovable (const NonMovable&) = delete ;
NonMovable (NonMovable&&) = delete ;
};
NonMovable create () {
return NonMovable ();
}
NonMovable obj = create ();
10.4 C++20 C++20 引入了概念(Concepts),可以约束值类别:
#include <concepts>
template <typename T>
concept Movable = std::movable<T>;
template <Movable T>
void func (T&& arg) {
}
template <typename T>
requires std::is_lvalue_reference_v<T>
void processLvalue (T&& arg) {
}
11. 总结
11.1 值类别体系 表达式 / \
glvalue rvalue
/ \ / \
lvalue xvalue xvalue prvalue
glvalue (泛左值):有身份的表达式
lvalue (左值):有身份,不可移动
xvalue (将亡值):有身份,可移动
rvalue (右值):可移动的表达式
prvalue (纯右值):无身份,可移动
11.2 核心概念总结 概念 定义 特点 左值 有明确内存地址的表达式 可取地址,持久存在 右值 临时对象或字面量 不可取地址,即将销毁 左值引用 使用&声明 绑定左值,const 可绑定右值 右值引用 使用&&声明 绑定右值,支持移动语义 std::move 类型转换工具 将左值转换为右值引用 std::forward 完美转发工具 保持参数的值类别 移动语义 资源转移机制 避免昂贵的拷贝操作 万能引用 模板中的 T&& 可绑定左值和右值
11.3 关键要点
左值 :有名字、可取地址、生命周期持久
右值 :临时对象、字面量、即将销毁的对象
左值引用 :绑定左值,const 左值引用可绑定右值
右值引用 :绑定右值,是移动语义的基础
std::move :只做类型转换,不执行移动操作
std::forward :用于完美转发,保持参数的值类别
移动语义 :通过转移资源所有权来避免拷贝
万能引用 :模板中的 T&& 可以绑定任意值类别
右值引用变量本身是左值 :这是最容易混淆的点
移动操作应标记 noexcept :对容器性能至关重要
11.4 实践建议
优先使用标准库工具 :std::move、std::forward
遵循五法则或零法则 :确保资源管理正确
移动操作标记 noexcept :提高容器性能
不要过度使用 std::move :尤其是返回局部变量时
优先使用 emplace 系列函数 :减少临时对象
理解值类别 :是掌握现代 C++ 的基础
11.5 结语 左值和右值是 C++ 类型系统的核心概念。随着 C++11 引入右值引用和移动语义,这些概念变得更加重要。深入理解这些概念不仅能帮助我们编写更高效的代码,还能更好地理解 C++ 标准库的设计思想。
移动语义的引入极大地提升了 C++ 程序的性能,特别是在处理大型对象和容器时。通过避免不必要的拷贝操作,我们可以显著降低程序的时间和空间开销。
完美转发则为编写通用的模板代码提供了强大的工具,使得我们能够在不丢失参数值类别信息的情况下进行函数调用转发。
掌握这些概念需要时间和实践,但这是成为 C++ 高级程序员的必经之路。希望本文能够帮助读者建立起对左值、右值及相关概念的全面理解。
参考文献
ISO/IEC 14882:2011 - C++11 Standard
ISO/IEC 14882:2014 - C++14 Standard
ISO/IEC 14882:2017 - C++17 Standard
ISO/IEC 14882:2020 - C++20 Standard
Scott Meyers, 'Effective Modern C++', O'Reilly Media, 2014
Nicolai M. Josuttis, 'C++17 - The Complete Guide', 2019
Bjarne Stroustrup, 'The C++ Programming Language (4th Edition)', 2013
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown 转 HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
HTML 转 Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online