首页 > 编程语言 >C++ 中的左值引用和右值引用

C++ 中的左值引用和右值引用

时间:2023-11-13 10:04:18浏览次数:27  
标签:std 右值 move 左值 C++ 引用 构造函数

最近看拷贝复制部分内容的时候看到移动构造函数和移动赋值运算符的声明中有个 && 符号,另外在有些库里也看到了这个符号,所以把这个右值引用集中学习了一下,同时做了一些输出,希望也可以帮助到大家。

C 语言中的左/右值和 C++ 中的左/右值是不一样的,C 语言中的左值可以位于赋值语句的左侧,右值不能,比较直观,但 C++ 中的左值和右值里面的内容就比较多一些。

1. 左值和右值

在 C++ 中,左值(Lvalue)是可以被赋值的表达式,通常具有内存地址,可以被引用和修改。例如,变量、数组元素和对象成员等都是左值。

右值(Rvalue)则是临时的、无法被赋值的表达式,通常是计算结果或临时对象。右值不能被引用或修改,因为它们没有明确的内存地址。例如,常量、字面量和临时对象都是右值。

左值和右值的主要区别在于内存持久性,左值有持久的状态,比如变量,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。

《C++ Primer》 中对左值和右值有个形象的描述:当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。

所以当一个左值被当成右值使用时,实际使用的是它的内容(值)。在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。比如取地址符 &,就是对一个左值取地址,取出来的地址是个右值,因为右值只有内容,在内存中没有位置。而对一个地址解引用 *p,或者对一个数组取下标 arr[0],就获得了左值。有时候左值不一定可以放在表达式左边,因为有些左值不能被赋值,比如数组名和常量。

2. 左值引用和右值引用

C++11 引入了右值引用 && 的概念,允许将右值绑定到一个引用上,并且可以修改其内容,这提供了更多的灵活性和效率。

右值引用指向将要被销毁的对象,比如一个表达式。

int i = 10;
int& j = i;           // 正确:左值引用
int& k = i * 1;       // 错误:左值引用不能绑定右值
int&& m = i * 1;      // 正确:右值引用
int&& n = i;          // 错误:右值引用不能绑定左值
const int& p = i * 1; // 正确:const左值引用可以绑定右值

如果说变量是左值,那么问题来了,右值引用的变量也是变量,这个变量是左值么,比如这里的 m

答案为左值,所以下面这个表达式是错误的:

int&& q = m;          // 错误:右值引用不能绑定左值,即使这个左值是右值引用类型的变量

虽然直接把右值引用类型的变量绑定到变量上,但可以使用 move 来获取绑定到左值上的右值引用。

int&& q = std::move(m);    // 正确:std::move可以将左值转换为右值

3. 使用场景

3.1 移动构造函数和移动赋值运算符

看《C++ Primer》这本书到的拷贝控制这一章的时候,就会经常碰到右值引用符号 && ,它 常常用在移动构造函数和移动赋值运算符上。

C++ 中的左值引用和右值引用_构造函数

移动构造函数和移动赋值运算符要求传入右值,这时需要使用 std::move 获取右值。

#include <iomanip>
#include <iostream>
#include <string>
#include <utility>

using namespace std;

class MyClass {
public:
    std::string data;

    explicit MyClass(std::string _data = "")
        : data(std::move(_data)){};

    MyClass(MyClass&& other) noexcept
    {
        cout << "移动构造函数  " ;
        data = std::move(other.data);
    }

    MyClass& operator=(MyClass&& other) noexcept
    {
        cout << "移动赋值运算符  " ;
        if (this != &other) {
            data = std::move(other.data);
        }
        return *this;
    }
};

int main()
{
    MyClass A("SHERlocked93");

    MyClass D = std::move(A); // 移动构造
    cout << D.data << endl;

		//    MyClass E(std::move(D)); // 移动构造
		//    cout << E.data << endl;

    MyClass G;
    G = std::move(D); // 移动赋值
    cout << G.data << endl;
}

// 打印:
// 移动构造函数  SHERlocked93
// 移动赋值运算符  SHERlocked93

顺便说一下,如果一个类有拷贝构造函数而没有移动构造函数,那么原来使用移动构造函数的场景就使用比如 MyClass B(move(A)) 就会调用拷贝构造函数。拷贝赋值运算符和移动赋值运算符的情况也类似。

3.2 移动语义

上面移动构造函数和移动赋值运算符这个例子中,其实是右值引用在移动语义的一个使用场景,目的是减少不必要的拷贝操作,减少资源的不必要复制、销毁的性能开销,提高性能。使用移动语义提高性能的做法非常普遍,做实际项目的时候会经常用到。

常见的用法比如向一个数组中增加一个元素:

vec.push_back(std::move(obj));   // 减少obj对象的复制,或者直接用emplace_back
vec.emplace_back(obj);

或者来个具体的,比如合并两个数组,如果希望使用移动语义减少拷贝操作,可以使用移动迭代器 std::make_move_iterator 实现:

#include <iostream>
#include <string>
#include <utility>
#include <vector>

using namespace std;

class MyClass {
public:
    std::string data;

    explicit MyClass(std::string _data = "")
        : data(std::move(_data)){};

    MyClass(const MyClass& other)
    {
        cout << "拷贝构造函数  ";
        data = other.data;
    }

    MyClass(MyClass&& other) noexcept
    {
        cout << "移动构造函数  ";
        data = std::move(other.data);
    }

    MyClass& operator=(MyClass&& other) noexcept
    {
        cout << "移动赋值运算符  ";
        if (this != &other) {
            data = std::move(other.data);
        }
        return *this;
    }

    MyClass& operator=(const MyClass& other)
    {
        cout << "拷贝赋值运算符  ";
        if (this != &other) {
            data = other.data;
        }
        return *this;
    }
};

template <typename T>
std::vector<T> concat(std::vector<T>& vec1,
                      std::vector<T>& vec2)
{
    std::vector<T> res;
    res.reserve(vec1.size() + vec2.size());

    res.insert(res.end(), std::make_move_iterator(vec1.begin()), std::make_move_iterator(vec1.end()));
    res.insert(res.end(), std::make_move_iterator(vec2.begin()), std::make_move_iterator(vec2.end()));

    return res;
}

int main()
{
    vector<MyClass> a;
    vector<MyClass> b;
    a.emplace_back(MyClass("SHERlocked93"));
    b.emplace_back(MyClass("hello wrold!"));

    cout << endl;
    auto r = concat(a, b);

    cout << endl;
    cout << r[0].data << "-" << r[1].data << endl;
}

// 打印:
// 移动构造函数  移动构造函数  移动构造函数  移动构造函数  
// SHERlocked93-hello wrold!

可以看到只调用了移动构造函数,而没有使用拷贝构造函数,如果将移动迭代器改为普通迭代器,就会使用拷贝构造函数了。

除了移动语义外,右值引用还被用在完美转发上,另外比如在函数返回一个大对象时,可以返回一个临时对象的右值引用,而不是进行拷贝操作,除此之外还有其他一些应用场景,以后遇到了再探讨一下。


网上的帖子大多深浅不一,甚至有些前后矛盾,在下的文章都是学习过程中的总结,如果发现错误,欢迎留言指出,如果本文帮助到了你,别忘了点赞支持一下哦,你的点赞是我更新的最大动力!

标签:std,右值,move,左值,C++,引用,构造函数
From: https://blog.51cto.com/u_5384160/8337113

相关文章

  • C++ 中 Linux 下 Socket 编程
    Socket套接字是网络间不同计算机上的进程通信的一种常用方法,利用三元组(ip地址,协议,端口)就可以唯一标识网络中的进程,网络中的进程通信可以利用这个标志与其它进程进行交互。Socket也是对TCP/IP协议族的一种封装,是应用层与TCP/IP协议族通信的中间软件抽象层。1.Socket基本概念......
  • 值类型与引用类型
    值类型和引用类型类型被分为两种:值类型(整数,boolstructchar⼩数)和引⽤类型(string数组⾃定义的类,内置的类)。值类型只需要⼀段单独的内存,⽤于存储实际的数据,(单独定义的时候放在栈中),默认值是0引⽤类型需要两段内存第⼀段存储实际的数据,它总是位于堆中,第⼆段是⼀个引⽤,指......
  • rust 程序设计笔记(2)所有权 & 引用
    所有权数据存储在栈和堆上,存放在栈上的数据都是已知所占据空间的突然的问题//内存中的栈是怎么存储数据的?好的,想象一下你有一摞盘子。你只能从上面放盘子,也只能从上面拿盘子,这就是栈的工作方式。在内存中,栈是用来存储数据的地方,它工作得就像这摞盘子。当你的程序运行......
  • C++U2-第12课-单元测评(二)
    上节课作业部分(点击跳转)单元测评2题目和答案解析1、 2、  3、不定长数组,不允许不初始化列,可以不初始化行;  4、比较常见的字符的ASCII码,空格为32,字符0为48,大写字母A为65,小写字母a为97  5、abs为绝对值函数,正数的绝对值是本身,负数的绝对值为相反数,ceil表示向......
  • c++ lambda表达式
    一、lambda语句介绍在cppreference中对lambda的解释是:一个能够捕获作用域中变量的未命名函数对象个人认为就是一个用于快速定义一个匿名函数的语句使用格式1.capture子句,lambda的核心,通过改变[]中的值,来设定捕获的范围2.参数列表,可选,用于确定捕获参数的类型3.mutable,可选......
  • C++ 内存分区
    C/C++内存管理C/C++内存分布转载https://www.coonote.com/note/cpp-memory-management.htmlC/C++程序内存分配的几个区域:栈区(stack)在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集......
  • c++ extern关键字
    @[TOC]什么是extern?extern是C++中的一个关键字,用于声明一个全局变量或函数,但并不分配内存或提供定义。它的主要作用是告诉编译器这个变量或函数在其他源文件中定义,编译器不应该分配内存空间或生成代码,而应该等待链接器来解析它。使用extern声明变量//在一个源文件中定义一个......
  • C++零基础教程(抽象类和接口)
    (文章目录)前言本篇文章来讲解抽象类和接口的概念,抽象类和接口都需要依靠我们之前讲解的虚函数来实现,那么我们就来看看如何使用虚函数来实现抽象类和接口吧。一、抽象类概念抽象类是一种不能直接实例化(即创建对象)的类,它被用作其他类的基类或接口。抽象类通过声明纯虚函数(没有......
  • c++ function使用
    一、function介绍funciotn是从c++11开始支持的特性,使用它需要包含<functional>头文件在cppreference中解释为:类模板std::function是一个通用的多态函数包装器。std::function的实例可以对任何可以调用的目标实体进行存储、复制、和调用操作,这些目标实体包括普通函数、Lambda表达......
  • C++ 采用get()和put()读写文件
    在某些特殊的场景中,我们可能需要逐个读取文件中存储的字符,或者逐个将字符存储到文件中。这种情况下,就可以调用get()和put()成员方法实现。C++ostream::put()成员方法通过《C++cout.put()》一节的学习,读者掌握了如何通过执行cout.put()方法向屏幕输出单个字符。我们知道,fstr......