【高级程序设计语言C++】右值引用

1. 左值引用和右值引用

  1. 什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋

,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左

值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名

  1. 什么是右值?什么是右值引用?

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引

用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能

取地址。右值引用就是对右值的引用,给右值取别名。

  1. 当我们讨论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;
}

上面的代码会输出什么呢?

输出结果:

img

那为什么会这样子呢?

这是因为在将字面量10绑定到右值引用rr1时,编译器会创建一个临时的整数对象,并将其值设置为10。这个临时的整数对象具有与rr1相同的生命周期,因此我们可以通过指针修改它的值。然而,需要注意的是,这种修改右值的方式并不是右值引用的主要用途。右值引用主要用于实现移动语义和完美转发等特性,以提高性能和灵活性。在实际中,我们很少直接修改右值,而是将其用于特定的语义操作。如果不想rr1被修改,可以用const int&& rr1 去引用。

3. 左值引用和右值引用的比较

3.1. 左值引用总结

  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,无法被左值引用接受,因为左值引用只能引用左值

所以,当我们尝试传递右值给左值引用时,编译器会报错

运行结果:

img

  1. 但是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左值引用可以引用左值和右值的特性。这在某些情况下是很有用的,例如当我们希望以只读方式访问参数时。

运行结果:

img

3.2. 右值引用总结

  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,可以被右值引用接受,并在函数内部被修改。

所以,当我们尝试传递左值给右值引用时,编译器会报错

  1. 但是右值引用可以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成为了悬空引用,访问它将导致未定义行为。**但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。**使用传值返回,会产生拷贝的问题,影响效率。

img

所以此时右值引用就有了它的价值

int&& getRef() 
{
    int x = 5;
    return move(x); // 返回右值引用,延长生命周期
}

int main() 
{
    int&& ref = getRef(); // 正确,延长生命周期
    // ...
}

在这个例子中,getRef函数返回了一个右值引用,通过使用move函数将局部变量x转换为右值引用,并延长了其生命周期。因此,在main函数中,我们可以安全地使用右值引用ref。

img

下面来看一段代码,看看右值引用在拷贝方面的好处。

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!。这证明了在拷贝时使用右值引用可以避免不必要的拷贝操作,并且不会影响最终结果。

运行结果:

img

  1. 48行的代码,传的是左值,调用的是拷贝构造函数

img

  1. 49行的代码,传的是右值,调用的是移动构造函数

img

从调试的角度看一看str的变化。

img

img

然后str从左值变为右值,传给myStr2的移动构造函数,造成了str的消亡。

img

此时没有产生拷贝,而是将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,实现了完美转发。

总之,完美转发允许我们以一种通用的方式将参数转发给其他函数,无论参数是左值还是右值。这可以提高代码的重用性和灵活性,避免了为每种可能的类型编写不同的转发函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值