1.左值和右值
左值可以取地址、位于等号左边;而右值没法取地址,位于等号右边。
比如一个对象就是左值,一个常量就是右值.
int a = 10;
/*
* a 是一个左值,可以取地址
* 10 是一个右值,不可以取地址
*/
// ---------------------------------
class test;
test a = test();
/*
* a 是一个左值,可以取地址
* test() 是一个右值,临时对象不可以取地址
*/
2.引用
2.1 左右值引用
int a = 10;
// --------
int& b = a; //左值引用
int&& c = 10; //右值引用
2.2 const 引用的特殊性
引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。但是常量可以左值引用常量,属于特殊形式.
const int& d = 10;
const左值引用不会修改指向值,因此可以指向右值,这也是为什么要使用const T&作为函数参数的原因之一,例如 std::vector::push_back
void push_back (const value_type& val);
所以,const T& 可以接受引用传递和常量/临时值传递。
2.3 右值引用
再看下右值引用,右值引用的标志是&&,顾名思义,右值引用专门为右值而生,可以指向右值,不能指向左值:
int &&ref_a_right = 5; // ok
int a = 5;
int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值
ref_a_right = 6; // 右值引用的用途:可以修改右值
3.左右值转换
将左值转换为右值,通过 std::move 接口
int a = 5; // a是个左值
int &ref_a_left = a; // 左值引用指向左值
int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向
右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值
int &&ref_a = 5;
ref_a = 6;
// 等同于以下代码:
int temp = 5;
int &&ref_a = std::move(temp);
ref_a = 6;
被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。
// 形参是个右值引用
void change(int&& right_value) {
right_value = 8;
}
int main() {
int a = 5; // a是个左值
int &ref_a_left = a; // ref_a_left是个左值引用
int &&ref_a_right = std::move(a); // ref_a_right是个右值引用
// -----------------------
change(a); // 编译不过,a是左值,change参数要求右值
change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值
change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值
// -----------------------
change(std::move(a)); // 编译通过
change(std::move(ref_a_right)); // 编译通过
change(std::move(ref_a_left)); // 编译通过
change(5); // 当然可以直接接右值,编译通过
cout << &a << ' ';
cout << &ref_a_left << ' ';
cout << &ref_a_right;
// 打印这三个左值的地址,都是一样的
}
右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值。作为函数返回值的 && 是右值,直接声明出来的 && 是左值。
- 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
- 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
- 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
void f(const int& n) {
n += 1; // 编译失败,const左值引用不能修改指向变量
}
void f2(int && n) {
n += 1; // ok
}
int main() {
f(5);
f2(5);
}
3.1 移动语义
右值引用和std::move被广泛用于在STL和自定义类中实现移动语义,避免拷贝,从而提升程序性能。std::move 的兄弟 std::forward 可以支持将左右值转换为左右值
int a = 0;
int&& ref_a = std::forward<int&&>(a); // 左值换右值
// --------------------------------
void test(int&& a){
int& p = std::forward<int&>(a); //右值换左值
}
4.深度解析
右值引用就是Rust中的所有权转移,对于请看下面的代码
class test
{
public:
// 默认初始化
test() : size(0)
{}
// 有参构造函数
test(int* _data,int _size):size(_size){
data = new int[size];
for (int i = 0; i < size; i++)
{
*(data + i) = *(_data + i);
}
}
// 深拷贝
test(const test &a){
size = a.size;
data = new int[size];
for (int i = 0; i < size; i++)
{
*(data + i) = *(a.data + i);
}
}
// 移动构造函数
test(test&& a){
data = a.data;
size = a.size;
// 防止二次释放数据
a.data =nullptr;
}
~test(){
// 删除内存
if(data !=nullptr){
// 其实判断语句也不用了,现在delete可以自动判断是否为空指针了
delete[] data;
}
}
// 测试打印
int print()
{
if(data ==nullptr) return -1;
std::cout << "the size" << this->size << std::endl;
for(int i = 0 ;i<size;i++){
std::cout<<"data : "<<i<<" th :"<<*(data + i)<<std::endl;
}
return 1;
}
private:
int* data = nullptr;
int size;
};
int main(int argc, char *argv[])
{
int data[4] = {1,2,3,4};
test a(data,4);
a.print(); // 断点
test b(std::move(a));
b.print(); //断点
return 1; //断点
}
如图
![[编程与系统/编程语言/C++/C++语法/C++标准/C++11/image/image.png]]
第一个断点时,a 的内存还在,b 的内存还未开辟,看下一个断点
![[编程与系统/编程语言/C++/C++语法/C++标准/C++11/image/image-1.png]]
可以看到,b 完全的将 a 的指针数据和size 数据转移到自身上,而a也在没有数据了,这就是所有权转移,通过移动构造函数和 std::move 将一个变量的所有权转移给另一个所有权.
5.应用
右值的应用通常有
- 所有权转移,如 4
- 移动构造函数
- 移动赋值=运算符重载
- 返回值优化
返回值优化,是当函数内部创建数据,将数据以值传递的方式返回时,外部接收数据并不采用创建数据,复制值传递数据的方式,而是采用将值传递数据的地址直接移动给外部接收数据,减少创建数据、复制数据、删除数据的过度浪费。
#include <iostream>
struct Noisy{
Noisy() { std::cout << "constructed at " << this << '\n'; }
Noisy(const Noisy&) { std::cout << "copy-constructed\n"; }
Noisy(Noisy&&) { std::cout << "move-constructed\n"; }
~Noisy() { std::cout << "destructed at " << this << '\n'; }
};
Noisy f(){
// 初始化
Noisy v;
// 值
return v;
}
void g(Noisy arg){
std::cout << "&arg = " << &arg << '\n';
}
int main(){
Noisy v = f();
std::cout << "&v = " << &v << '\n';
g(f());
return 0;
}
其输出如下
constructed at 0x99d5bff6ae
&v = 0x99d5bff6ae
constructed at 0x99d5bff6af
&arg = 0x99d5bff6af
destructed at 0x99d5bff6af
destructed at 0x99d5bff6ae
在构造的时候采用初始化,然后通过值传递返回,发现函数f内部构造的对象和返回的对象的地址是一样的,类似于移动构造函数;同样对于传入参数的 g 函数来讲也是一样的,通过 f 内部构造对象,以值传递返回,之后以值传递返回,传入到 g(Noisy) 中,这样 g 的输入参数就会采用返回值优化。
5.1 支持移动语义
右值引用(T&&)允许函数识别临时对象(即将被销毁的对象),从而“窃取”其资源而非进行深拷贝。典型应用:移动构造函数和移动赋值运算符,高效转移资源(如动态内存、文件句柄等)。
void process(std::string&& str) {
// 可以安全地“移动”str的资源,避免拷贝
std::string internal = std::move(str);
}
5.2 明确拒绝左值(非临时对象)
右值参数强制调用者传递临时对象或显式使用std::move转换左值,避免意外修改左值。
void consume(Resource&& r); // 只接受临时对象或显式移动的左值
5.3 完美转发
结合模板和std::forward,右值引用可以保留参数的原始值类别(左值/右值),用于泛型代码(如工厂函数)。
template<typename T>
void relay(T&& arg) {
other_function(std::forward<T>(arg)); // 保持arg的左右值属性
}
5.4 优化性能
避免不必要的拷贝,直接重用临时对象的资源。例如
std::vector<int> create_data();
void use_data(std::vector<int>&& data); // 直接接管create_data()返回的临时vector
6.总结
对于传值有以下心得
- 对于类对象最好使用左值引用传递或者指针传递
- 对于临时对象最好使用右值引用传递
- 对于常数,使用值传递