- 1. 左值引用和右值引用
- 2. 修改的右值
- 3. 左值引用和右值引用的比较
- 3.1. 左值引用总结
- 3.2. 右值引用总结
- 4. 右值引用使用场景和意义
- 5. 完美转发
1. 左值引用和右值引用
- 什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋
值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左
值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
- 什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引
用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能
取地址。右值引用就是对右值的引用,给右值取别名。
- 当我们讨论C++中的左值和右值时,可以将其类比为“物品”。
左值可以理解为“可修改的物品”。它是具有持久性和身份的物品,我们可以对其进行赋值、取地址和修改操作。就像我们拿着一个实际的物品,可以一直使用它,甚至可以改变它的状态。
右值可以理解为“临时的物品”。它是没有持久性和身份的物品,通常是临时产生的中间结果。我们不能对其进行赋值、取地址或修改操作,因为它们即将被丢弃。就像我们拿着一个临时使用的物品,用完之后就会丢弃掉。
举个例子来说明:
假设我们有一个盒子,里面装着一些物品。左值就像是我们拿起一个物品,可以一直使用它,甚至可以改变它的状态。右值就像是我们拿起一个临时使用的物品,用完之后就会丢弃掉。
在C++中,我们可以将一个左值赋值给左值引用,而右值则可以赋值给右值引用。这是因为左值引用期望一个持久的物品,而右值引用期望一个临时的物品。
总结起来,左值是可修改的物品,右值是临时的物品。这是一个简化的描述,但希望能帮助你理解左值和右值的基本概念。
void modifyValue(int& value)
{
value = 10; // 修改左值的值
}
int main()
{
int x = 5; // 左值 x
modifyValue(x); // 传递左值给函数进行修改
std::cout << "Modified value: " << x << std::endl;
int&& y = 20; // 右值 y
std::cout << "Original value: " << y << std::endl;
return 0;
}
在上面的代码中,我们定义了一个函数modifyValue,它接受一个左值引用作为参数,并修改传入的左值的值。在main函数中,我们声明了一个左值x,并将其传递给modifyValue函数进行修改。最后,我们输出修改后的值。另外,在main函数中,我们还声明了一个右值引用y,并将一个临时的右值20赋值给它。然后,我们输出原始的右 值。
通过这段代码,我们可以看到左值可以被修改,而右值通常是临时的、不可修改的。这展示了左值和右值的基本特性。
2. 修改的右值
在上面的描述中,我们知道右值通常是临时的、不可修改的。注意说的是通常!
来看这样一段代码:
int main()
{
int&& rr1 = 10;
int* address = &rr1;
*address = 20;
cout << rr1 << endl;
return 0;
}
上面的代码会输出什么呢?
输出结果:
那为什么会这样子呢?
这是因为在将字面量10绑定到右值引用rr1时,编译器会创建一个临时的整数对象,并将其值设置为10。这个临时的整数对象具有与rr1相同的生命周期,因此我们可以通过指针修改它的值。然而,需要注意的是,这种修改右值的方式并不是右值引用的主要用途。右值引用主要用于实现移动语义和完美转发等特性,以提高性能和灵活性。在实际中,我们很少直接修改右值,而是将其用于特定的语义操作。如果不想rr1被修改,可以用const int&& rr1 去引用。
3. 左值引用和右值引用的比较
3.1. 左值引用总结
- 左值引用只能引用左值,不能引用右值。
void modifyValue(int& value)
{
// 在函数内部修改value的值
value = 10;
}
int main()
{
int x = 5;
modifyValue(x); // 传递左值
modifyValue(20); // 错误,无法传递右值
return 0;
}
在这个示例中,我们定义了一个函数modifyValue,它接受一个左值引用参数value。在函数内部,我们将value的值修改为10。
在main函数中,我们分别调用modifyValue函数,并传递一个左值x和一个右值20作为参数。对于左值x,可以被左值引用接受,并在函数内部被修改。但是对于右值20,无法被左值引用接受,因为左值引用只能引用左值。
所以,当我们尝试传递右值给左值引用时,编译器会报错。
运行结果:
- 但是const左值引用既可引用左值,也可引用右值。
void modifyValue(const int& value)
{
// value是const左值引用,可以引用左值和右值
// 但是在函数内部无法修改value的值,因为它是const的
cout << "Value: " << value << endl;
}
int main()
{
int x = 5;
const int y = 10;
modifyValue(x); // 传递左值
modifyValue(20); // 传递右值
return 0;
}
在这个示例中,我们定义了一个函数modifyValue,它接受一个const左值引用参数value。在函数内部,我们将value的值输出到控制台上。
在main函数中,我们分别调用modifyValue函数,并传递一个左值x和一个右值20作为参数。无论是左值还是右值,都可以被const左值引用接受。在函数内部,我们无法修改value的值,因为它是const的。
这个示例展示了const左值引用可以引用左值和右值的特性。这在某些情况下是很有用的,例如当我们希望以只读方式访问参数时。
运行结果:
3.2. 右值引用总结
- 右值引用只能右值,不能引用左值。
void modifyValue(int&& value)
{
// 在函数内部修改value的值
value = 10;
}
int main()
{
int x = 5;
modifyValue(x); // 错误,无法传递左值
modifyValue(20); // 传递右值
return 0;
}
在这个示例中,我们定义了一个函数modifyValue,它接受一个右值引用参数value。在函数内部,我们将value的值修改为10。
在main函数中,我们尝试分别调用modifyValue函数,并传递一个左值x和一个右值20作为参数。对于左值x,无法被右值引用接受,因为右值引用只能引用右值。但是对于右值20,可以被右值引用接受,并在函数内部被修改。
所以,当我们尝试传递左值给右值引用时,编译器会报错。
- 但是右值引用可以move以后的左值
void modifyVector(vector<int>&& vec)
{
// 在函数内部修改vec的值
vec.push_back(10);
}
int main()
{
vector<int> v = { 1, 2, 3 };
modifyVector(move(v)); // 传递move后的左值
for (auto e : v)
{
cout << e << " ";
}
return 0;
}
在这个示例中,我们定义了一个函数modifyVector,它接受一个右值引用参数vec。在函数内部,我们使用push_back函数向vec中添加一个元素。
在main函数中,我们创建了一个名为v的vector对象,并初始化为{1, 2, 3}。然后,我们将v传递给modifyVector函数,通过调用move函数将v转换为右值。这样,我们可以将move后的左值传递给右值引用参数,并在函数内部修改它。
这个示例展示了右值引用可以引用move后的左值的特性。通过使用move函数,我们可以将左值转换为右值,并在函数内部修改它,而无需进行深拷贝操作。
4. 右值引用使用场景和意义
下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!
int& getRef()
{
int x = 5;
return x; // 返回局部变量的引用,悬空引用
}
int main()
{
int& ref = getRef(); // 悬空引用
// ...
}
在这个例子中,getRef函数返回了一个局部变量x的引用,但当getRef函数结束时,x的生命周期结束,引用ref成为了悬空引用,访问它将导致未定义行为。**但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。**使用传值返回,会产生拷贝的问题,影响效率。
所以此时右值引用就有了它的价值
int&& getRef()
{
int x = 5;
return move(x); // 返回右值引用,延长生命周期
}
int main()
{
int&& ref = getRef(); // 正确,延长生命周期
// ...
}
在这个例子中,getRef函数返回了一个右值引用,通过使用move函数将局部变量x转换为右值引用,并延长了其生命周期。因此,在main函数中,我们可以安全地使用右值引用ref。
下面来看一段代码,看看右值引用在拷贝方面的好处。
class MyString
{
public:
MyString(const std::string& str) : m_data(str)
{
std::cout << "拷贝构造函数" << std::endl;
}
MyString(std::string&& str) : m_data(move(str))
{
std::cout << "移动构造函数" << std::endl;
}
// 拷贝赋值运算符
MyString& operator=(const MyString& other)
{
std::cout << "拷贝赋值运算符" << std::endl;
if (this != &other)
{
m_data = other.m_data;
}
return *this;
}
// 移动赋值运算符
MyString& operator=(MyString&& other)
{
std::cout << "移动赋值运算符" << std::endl;
if (this != &other)
{
m_data = std::move(other.m_data);
}
return *this;
}
const std::string& getData() const
{
return m_data;
}
private:
std::string m_data;
};
int main()
{
std::string str = "Hello, world!";
MyString myStr1(str); // 使用拷贝构造函数,将str拷贝到myStr1中
MyString myStr2(std::move(str)); // 使用移动构造函数,将str的内容移动到myStr2中
std::cout << "myStr1: " << myStr1.getData() << std::endl; // 输出: Hello, world!
std::cout << "myStr2: " << myStr2.getData() << std::endl; // 输出: Hello, world!
return 0;
}
在这个例子中,我们定义了一个MyString类,它包含一个std::string成员变量来管理字符串数据。MyString类提供了拷贝构造函数、移动构造函数、拷贝赋值运算符和移动赋值运算符。
在main函数中,我们首先创建了一个std::string对象str,然后使用拷贝构造函数将其拷贝到myStr1对象中。这里的拷贝构造函数会创建一个新的std::string对象,并将str的内容拷贝到新对象中。
接下来,我们使用移动构造函数将str的内容移动到myStr2对象中。这里的移动构造函数使用了右值引用,并通过std::move将str转换为右值引用。这样做的好处是,移动构造函数不需要创建新的对象,而是直接将str的内容移动到myStr2的成员变量中。这样可以避免不必要的拷贝操作,提高性能和效率。
最后,我们输出myStr1和myStr2对象中的字符串内容,可以看到它们都是Hello, world!。这证明了在拷贝时使用右值引用可以避免不必要的拷贝操作,并且不会影响最终结果。
运行结果:
- 48行的代码,传的是左值,调用的是拷贝构造函数
- 49行的代码,传的是右值,调用的是移动构造函数
从调试的角度看一看str的变化。
然后str从左值变为右值,传给myStr2的移动构造函数,造成了str的消亡。
此时没有产生拷贝,而是将str的内容直接移动到了myStr2的成员中,相当于减少了拷贝,提高了效率。要知道C++的类型是很多的,如果是map或者是unordered_map的拷贝,将会影响效率。
注意:此时将str移动到myStr2的成员中去,str自己本身是会嘎了的。你就把str当成一个武林高手,但是时日无多了,刚好遇到了一个小伙myStr2,str临死前把一身武功都传给了myStr2,然后str就嘎了。
5. 完美转发
有时候,我们有一个函数,它接收一个参数,并且我们希望将这个参数传递给另一个函数,但我们不知道这个参数的具体类型。我们希望能够以一种通用的方式将参数转发给另一个函数,而不需要为每种可能的类型编写不同的转发函数。
这就是完美转发的概念。完美转发允许我们将参数以原样转发给另一个函数,无论参数是左值还是右值。它可以保留参数的值类别(左值或右值),并将其传递给适当的函数。
在C++中,我们可以使用模板和引用折叠来实现完美转发。引用折叠是一种特殊的规则,它允许我们在模板函数中保留参数的值类别。
template <typename T>
void forwardFunction(T&& arg)
{
anotherFunction(std::forward<T>(arg));
}
在这个例子中,我们定义了一个模板函数forwardFunction,它接收一个参数arg。这里的T&&是一个右值引用折叠的语法,它可以接收任意类型的参数,不管是左值还是右值,并保留参数的值类别。
在forwardFunction中,我们使用std::forward来实现完美转发。std::forward是一个模板函数,它接收一个参数,并将其转发为对应的左值引用或右值引用。这里的T是模板类型参数,它会根据参数的值类别来确定是转发为左值引用还是右值引用。
通过使用std::forward,我们可以保留参数的值类别,并将其传递给anotherFunction,实现了完美转发。
总之,完美转发允许我们以一种通用的方式将参数转发给其他函数,无论参数是左值还是右值。这可以提高代码的重用性和灵活性,避免了为每种可能的类型编写不同的转发函数。