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.总结

对于传值有以下心得

  • 对于类对象最好使用左值引用传递或者指针传递
  • 对于临时对象最好使用右值引用传递
  • 对于常数,使用值传递

添加新评论