文章目录
0、类型 和 值类别
类型描述了表达式或对象的数据性质(比如int,std::string,注:引用不是独立的类型,而是类型的修饰符)
值类别描述的是一个表达式的评估方式,即表达式的”值“在程序中如何表现的。C++中的值类别分为三大类:
- 左值
- 纯右值
- 将亡值
其中,纯右值和将亡值合称为右值。
1、左值
-
定义:左值是指向内存位置的表达式,它代表了一个对象的身份。换句话说,左值是有命名或地址的对象,能够在内存中持久存在,可以被修改。
-
特点:
- 可以出现在赋值操作符的左边或右边。
- 左值可以取地址(&)
- 通常是变量、数组元素、引用等。代表可以读取和修改的内存区域
实例:
int x = 10; // 这里的 x 是左值 x = 20; // x 出现在赋值符号的左边,是左值 int* p = &x; // 可以取 x 的地址,因为 x 是左值
2、右值
2.1 纯右值
纯右值(prvalue,pure rvalue)是那些表示临时的、不具备持久存储的值。
纯右值是不能被修改的常量、临时值、字面量,或者函数的返回值(非引用类型)。
特点:
- 纯右值是不能赋值的临时值。
- 通常纯右值用于计算和传递数据。
- 例如:字面量、算术表达式的结果等。
int a = 5 + 3; // 5 + 3 产生一个纯右值
int b = 42; // 42 是一个纯右值
2.2 将亡值
将亡值(xvalue,expiring value)是表示即将被销毁的对象的值类别。
它通常表示一个已经没有必要继续拥有其原本的对象身份的对象。它仍然占有内存,但它即将被销毁或转移。
特点:
- 将亡值是表示临时对象的值,它即将被销毁。
- 在使用移动语义时,常见的表达式会产生将亡值。
- 例如:
std::move()
产生的值就是将亡值。
std::string foo() {
return std::string("hello");
}
std::string str = std::move(foo()); // std::move 产生的亡值
3、左值引用 和 右值引用
声明:左值引用和右值引用是一种类型修饰符,修饰一个已有的类型,使得该类型变成引用类型
- 左值引用:通过
&
声明,绑定到左值。它只能绑定到左值对象,表示对象的引用。 - 右值引用:通过
&&
声明,绑定到右值。它用于移动语义(Move Semantics)中,帮助提高性能。右值引用允许我们捕获和操作临时对象。
左值引用
左值引用的特性
- 必须绑定到左值:左值引用只能绑定到左值(有明确存储位置的对象),不能绑定到右值(如字面量、临时对象等)。
int x = 10;
int& ref = x; // 正确,x 是左值
int& ref2 = 100; // 错误,不能将右值 100 绑定到左值引用
- 引用的对象可以被修改:通过左值引用可以修改引用的对象。
int a = 10;
int& ref = a;
ref = 20; // 修改了 a 的值
- 引用是指向对象的别名:左值引用是对象的别名,操作引用时实际上是在操作引用的对象。
常量左值引用的特性
- 绑定到右值:编译器为右值
100
创建了一个临时变量,并让s1
引用这个临时变量,确保在s1
的作用域内,临时变量的生命周期得以延续。
const int& ref = 100; // 正确,常量左值引用可以绑定右值
右值引用
- 右值引用就是对一个右值进行引用的类型。
- 因为右值是匿名的,所以我们只能通过引用的方式找到它。
- 无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。
通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
关于右值引用的使用,参考代码如下:
#include <iostream>
using namespace std;
int&& value = 520;
class Test
{
public:
Test()
{
cout << "construct: my name is jerry" << endl;
}
Test(const Test& a)
{
cout << "copy construct: my name is tom" << endl;
}
};
Test GetTestObj()
{
return Test(); // 这里面的是一个右值
}
int main()
{
int a1;
int &&a2 = a1; // error
Test& t = GetTestObj(); // error
Test && t = GetTestObj();
const Test& t = GetTestObj();
return 0;
}
-
在上面的例子中int&& value = 520;里面520是纯右值,value是对字面量520这个右值的引用。
-
在int &&a2 = a1;中a1虽然写在了=右边,但是它仍然是一个左值,使用左值初始化一个右值引用类型是不合法的。
-
在Test& t = GetTestObj()这句代码中语法是错误的,右值不能给普通的左值引用赋值。
-
在Test && t = GetTestObj();中GetTestObj()返回的临时对象被称之为将亡值,t是这个将亡值的右值引用。
-
const Test& t = GetTestObj()这句代码的语法是正确的,常量左值引用是一个万能引用类型,它可以接受左值、右值、常量左值和常量右值。
4、&&的特性
4.1 函数重载
&&
可以用于区分重载函数,这在需要处理左值和右值时非常有用。我们可以通过定义不同的函数版本来分别处理左值和右值。
另外还有一点需要额外注意const T&&表示一个右值引用,不是未定引用类型。
实例:
template<typename T>
void process_1(T&& param);
void process_2(const T&& param);
int main()
{
process_1(10); // T&& 表示右值引用
int x = 10;
process_1(x); // T&& 表示左值引用
process_2(x); // error, x是左值
process_2(10); // ok, 10是右值
}
由于上述代码中存在T&&或者auto&&这种未定引用类型,当它作为参数时,有可能被一个右值引用初始化,也有可能被一个左值引用初始化,在进行类型推导时右值引用类型(&&)会发生变化,这种变化被称为引用折叠。
在C++11中引用折叠的规则如下:
-
通过右值推导
T&&
或者auto&&
得到的是一个右值引用类型 -
通过左值推导
T&&
或者auto&&
得到的是一个左值引用类型 -
判断左值引用还是右值引用是需要判断它的值类别,而不是类型
int&& a1 = 5;
auto&& bb = a1;
auto&& bb1 = 5;
int a2 = 5;
int &a3 = a2;
auto&& cc = a3;
auto&& cc1 = a2;
const int& s1 = 100;
const int&& s2 = 100;
auto&& dd = s1;
auto&& ee = s2;
const auto&& x = 5;
/*第2行:a1是左值,推导出的bb为左值引用类型
第3行:5为右值,推导出的bb1为右值引用类型
第7行:a3为左值,推导出的cc为左值引用类型
第8行:a2为左值,推导出的cc1为左值引用类型
第12行:s1为常量左值引用,推导出的dd为常量左值引用类型
第13行:s2为常量右值引用,推导出的ee为常量左值引用类型*/
5、转移和完美转发
5.1 std::move
std::move
的作用:
-
std::move()
是一个标准库函数,它将一个左值强制转换为右值,从而可以绑定到右值引用上。 -
使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。
-
它本质上不会移动任何东西,而只是告诉编译器:“我不再需要这个对象的值了,你可以安全地移动它。”
示例:
#include <iostream>
#include <vector>
#include <utility> // std::move
int main() {
std::vector<int> v1 = {1, 2, 3, 4, 5};
std::vector<int> v2 = std::move(v1); // 将 v1 的内容移动到 v2
// v1 现在是空的,资源已经被转移
std::cout << "v1 size: " << v1.size() << std::endl;
std::cout << "v2 size: " << v2.size() << std::endl;
return 0;
}
5.2 剖析move的实现
std::remove_reference::type
std::remove_reference<T>::type
检查类型 T
是否是左值引用(T&
)或右值引用(T&&
)。如果 T
是引用类型,它会去掉这个引用并返回基本类型;如果 T
不是引用类型,则返回 T
本身。
具体行为:
- 如果
T
是左值引用(T&
),则返回去除左值引用的基本类型T
。 - 如果
T
是右值引用(T&&
),则返回去除右值引用的基本类型T
。 - 如果
T
既不是左值引用也不是右值引用,直接返回T
。
// 左值引用类型
using T1 = int&;
using BaseT1 = std::remove_reference::type<T1>; // BaseT1 是 int
// 右值引用类型
using T2 = int&&;
using BaseT2 = std::remove_reference::type<T2>; // BaseT2 是 int
// 非引用类型
using T3 = int;
using BaseT3 = std::remove_reference::type<T3>; // BaseT3 仍然是 int
std::move 的函数原型定义:
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type &&>(t);
}
std::move实现,首先,万能引用传递模板实现,利用引用折叠原理将右值经过T&&
传递类型保持不变还是右值,而左值经过T&&
变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。然后我们通过static_cast<>
进行强制类型转换返回T&&右值引用,而static_cast
之所以能使用类型转换,是通过remove_refrence::type
模板移除T&&
,T&
的引用,获取具体类型T(模板偏特化)。
5.3 forward
一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值,并不是原来的类型了。如果需要按照参数原来的类型转发到另一个函数,可以使用C++11提供的std::forward()函数,该函数实现的功能称之为完美转发。
// 函数原型
template <class T> T&& forward (typename remove_reference<T>::type& t) noexcept;
template <class T> T&& forward (typename remove_reference<T>::type&& t) noexcept;
// 精简之后的样子
std::forward<T>(t);
//当T为左值引用类型时,t将被转换为T类型的左值
//当T不是左值引用类型时,t将被转换为T类型的右值
示例:
#include <iostream>
using namespace std;
template<typename T>
void printValue(T& t)
{
cout << "l-value: " << t << endl;
}
template<typename T>
void printValue(T&& t)
{
cout << "r-value: " << t << endl;
}
template<typename T>
void testForward(T && v)
{
printValue(v);
printValue(move(v));
printValue(forward<T>(v));
cout << endl;
}
int main()
{
testForward(520);
int num = 1314;
testForward(num);
testForward(forward<int>(num));
testForward(forward<int&>(num));
testForward(forward<int&&>(num));
return 0;
}
测试打印结果:
l-value: 520
r-value: 520
r-value: 520
l-value: 1314
r-value: 1314
l-value: 1314
l-value: 1314
r-value: 1314
r-value: 1314
l-value: 1314
r-value: 1314
l-value: 1314
l-value: 1314
r-value: 1314
r-value: 1314
-
testForward(520);
实参为右值,初始化后被推导为一个右值引用
printValue(v);
已命名的右值v,编译器会视为左值处理,实参为左值
printValue(move(v));
已命名的右值编译器会视为左值处理,通过move又将其转换为右值,实参为右值
printValue(forward<T>(v));
forward的模板参数为右值引用,最终得到一个右值,实参为``右值`
-
testForward(num);
函数的形参为未定引用类型T&&,实参为左值,初始化后被推导为一个左值引用printValue(v);
实参为左值
printValue(move(v));
通过move将左值转换为右值,实参为右值
printValue(forward<T>(v));
forward的模板参数为左值引用,最终得到一个左值引用,实参为左值
-
testForward(forward<int>(num));
forward的模板类型为int,最终会得到一个右值,函数的形参为未定引用类型T&&被右值初始化后得到一个右值引用类型printValue(v);
实参为左值
printValue(move(v));
v是左值,通过move又将其转换为右值,实参为右值
printValue(forward<T>(v));
forward的模板参数为右值引用,最终得到一个右值,实参为右值
-
testForward(forward<int&>(num));
forward的模板类型为int&,最终会得到一个左值,函数的形参为未定引用类型T&&被左值初始化后得到一个左值引用类型-
printValue(v);
实参为左值 -
printValue(move(v));
通过move将左值转换为右值,实参为右值
-
printValue(forward<T>(v));
forward的模板参数为左值引用,最终得到一个左值,实参为左值
-
-
testForward(forward<int&&>(num));
forward的模板类型为int&&,最终会得到一个右值,函数的形参为未定引用类型T&&被右值初始化后得到一个右值引用类型-
printValue(v);
已命名的右值v,编译器会视为左值处理,实参为左值
-
printValue(move(v));
已命名的右值编译器会视为左值处理,通过move又将其转换为右值,实参为右值
-
printValue(forward<T>(v));
forward的模板参数为右值引用,最终得到一个右值,实参为右值
-