【C++入门到精通】右值引用 | 完美转发 C++11 [ C++入门 ]

news2024/11/20 0:36:46

在这里插入图片描述

阅读导航

  • 引言
  • 一、左值引用和右值引用
    • 1. 什么是左值?什么是左值引用?
    • 2. 什么是右值?什么是右值引用?
    • 3. move( )函数
  • 二、左值引用与右值引用比较
  • 三、右值引用使用场景和意义
  • 四、完美转发
    • std::forward 函数
    • 完美转发实际中的使用场景
  • 温馨提示

引言

当谈到C++的高级特性时,右值引用是一个不可忽视的重要概念。作为一种在C++11标准中引入的语言特性,右值引用为我们提供了更加灵活和高效的内存管理方式。它不仅可以优化代码性能,还可以改善对象拷贝行为,使得我们能够更好地处理临时对象和移动语义。通过深入理解右值引用的原理和使用方法,我们可以在C++编程中发挥出更大的威力,提升代码的效率和可维护性。本文将全面介绍右值引用的概念、用法和相关的重要概念,帮助读者更好地理解和应用这一关键特性。无论您是初学者还是有经验的程序员,都将从本文中获得对右值引用的深入认识,并能够在实际项目中灵活运用。让我们一起探索C++中右值引用的奇妙世界吧!🥰

一、左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,总的来说就是无论左值引用还是右值引用,都是给对象取别名。

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

在C++中,左值是指表达式结束后依然存在的数据对象,它可以出现在赋值操作的左边或右边。通常来说,变量、函数返回的引用、解引用操作等都是左值。简言之,左值可以被赋值,可以取地址。

左值引用是指对左值进行引用的方式。它使用&符号声明,可以绑定到一个左值上,从而允许我们通过引用修改原始的左值对象。左值引用就是给左值的引用,给左值取别名。左值引用在函数参数传递和函数返回值中经常被使用,能够避免不必要的复制,并且可以实现对原始对象的直接操作。左值引用也为后续引入右值引用打下了基础,是C++语言中非常重要的概念之一。

int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;

	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
	return 0;
}

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

在C++中,右值是指表达式结束后即将被销毁的临时数据对象,它通常不能出现在赋值操作的左边。字面上来说,右值就是“赋值运算符=右边的值”。比如,常量、临时对象、表达式的计算结果等都可以是右值。

右值引用是C++11引入的新特性,使用双&&符号声明,它可以绑定到一个右值或将要销毁的对象上。右值引用的引入使得我们能够实现移动语义,即将资源(如内存)的所有权从一个对象转移到另一个对象,而不需要进行深层的复制操作,从而提高了代码的效率和性能。右值引用还为移动构造函数和移动赋值运算符的实现提供了基础,这些特性在处理大型数据结构时非常有用。右值引用的引入使得C++语言能够更好地支持移动语义,从而更好地适应现代编程的需求。

int main()
{
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);
	
	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	
	// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
	10 = 1;
	x + y = 1;
	fmin(x, y) = 1;
	return 0;
}

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

int main()
{
	double x = 1.1, y = 2.2;
	int&& rr1 = 10;
	const double&& rr2 = x + y;
	
	rr1 = 20;
	rr2 = 5.5; // 报错
	return 0;
}

3. move( )函数

std::move() 是C++11引入的一个函数模板,位于头文件 <utility> 中。它用于将传入的对象转换为右值引用,从而支持移动语义。在移动语义中,对象的资源所有权可以从一个对象转移到另一个对象,而不需要进行深层的复制操作,这可以提高程序的性能和效率。

std::move() 的定义如下:

template <class T>
constexpr remove_reference_t<T>&& move(T&& t) noexcept;

其中,t 是一个通用引用,它可以绑定到左值或右值。std::move()t 转换为右值引用并返回,即使 t 是一个左值,也可以通过 std::move() 转为右值引用。

使用 std::move() 主要用于以下两个场景:

  1. 在实现移动构造函数和移动赋值运算符时,可以使用 std::move() 将成员变量转换为右值引用,从而实现资源的转移而非复制。
  2. 在标准库中,例如容器的 insertemplace 方法中,使用 std::move() 可以将对象的所有权转移到容器中,避免不必要的复制操作。

需要注意的是,std::move() 仅仅是将对象转换为右值引用,它本身并不会进行实际的资源移动操作。因此,在使用 std::move() 后,程序员仍需谨慎处理对象的生命周期,以避免悬挂指针或对象被多次释放等问题。

二、左值引用与右值引用比较

⭕左值引用总结:

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值
int main()
{
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra1 = a; // ra为a的别名
	//int& ra2 = 10; // 编译失败,因为10是右值
	
	// const左值引用既可引用左值,也可引用右值。
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}

⭕右值引用总结:

  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以move以后的左值.
int main()
{
	// 右值引用只能右值,不能引用左值。
	int&& r1 = 10;
	// error C2440: “初始化”: 无法从“int”转换为“int &&”
	// message : 无法将左值绑定到右值引用
	int a = 10;
	int&& r2 = a;
	
	// 右值引用可以引用move以后的左值
	int&& r3 = std::move(a);
	return 0;
}

三、右值引用使用场景和意义

  1. 移动语义:右值引用的最重要的使用场景之一就是实现移动语义。通过移动语义,可以避免不必要的深层复制操作,提高程序的性能和效率。移动语义通常在以下情况下使用:
    • 移动构造函数和移动赋值运算符:通过将资源的所有权从一个对象转移到另一个对象,而非进行深层的复制操作,来提高效率。
    • 标准库中的容器和算法:许多标准库中的容器和算法都利用了移动语义,例如移动构造和移动赋值来提高性能。

例如在下面这段代码中,使用了右值引用来实现移动语义,从而避免不必要的深层复制操作,提高了对象的构造和赋值效率。

移动构造函数的定义如下:

string::string(string&& s)
    : _str(nullptr), _size(0), _capacity(0)
{
    cout << "string(string&& s) -- 移动语义" << endl;
    swap(s);
}

在移动构造函数中,接收一个右值引用作为参数,通过 && 标识符表示。在函数体内部,输出一条信息以表明这是移动构造函数,并且调用了 swap() 函数来交换资源,实现了移动语义。

移动赋值运算符的定义如下:

string& string::operator=(string&& s)
{
    cout << "string& operator=(string&& s) -- 移动语义" << endl;
    swap(s);
    return *this;
}

在移动赋值运算符中,同样接收一个右值引用作为参数。在函数体内部,输出一条信息以表明这是移动赋值运算符,并且调用了 swap() 函数来交换资源,实现了移动语义。

  1. 完美转发:右值引用与通用引用(universal reference)结合使用时,可以实现完美转发。完美转发允许将函数参数按原样传递给其他函数,无论原始参数是左值还是右值。这对于泛型编程以及实现转发函数(forwarding function)非常有用。

  2. 优化临时对象:临时对象是在表达式求值过程中创建的临时值,它们的生命周期很短暂,并且通常在表达式结束后立即销毁。通过使用右值引用,可以避免不必要的拷贝构造和析构操作,提高代码的性能和效率。

  3. 移动语义和资源管理:右值引用在资源管理方面非常有用,例如管理动态分配的内存、文件句柄、网络连接等。通过使用右值引用,可以实现资源的移动而非复制,从而提高程序的性能和可维护性。

  4. 避免不必要的拷贝构造和析构:当需要返回临时对象时,通过使用右值引用可以避免不必要的拷贝构造和析构,提高代码的效率。

四、完美转发

模板中的&& 万能引用

void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发

std::forward 函数

std::forward 是C++标准库中的一个函数模板,位于 <utility> 头文件中。它用于实现完美转发,将传入的参数以原样转发给其他函数。

std::forward 的函数模板定义如下:

template <typename T>
T&& forward(typename std::remove_reference<T>::type& arg) noexcept;

template <typename T>
T&& forward(typename std::remove_reference<T>::type&& arg) noexcept;

这个函数模板有两个重载版本,接受一个通用引用作为参数。它使用了 typename std::remove_reference<T>::type 来移除参数的引用限定符,以保持参数的值类别(左值或右值)。

当传入一个左值时,std::forward 返回一个左值引用;当传入一个右值时,std::forward 返回一个右值引用。这样就可以保持参数在转发过程中的值类别不变。

std::forward 的主要应用场景是在模板函数中进行完美转发,将参数原样传递给其他函数。通过使用 std::forward,可以避免不必要的拷贝和移动操作,提高代码的性能和效率。

以下是使用 std::forward 进行完美转发的示例:

void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }

void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }

// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>

void PerfectForward(T&& t)
{
	Fun(std::forward<T>(t));
}

int main()
{
	PerfectForward(10); // 右值
	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b); // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

完美转发实际中的使用场景

template<class T>
struct ListNode
{
    ListNode* _next = nullptr;
    ListNode* _prev = nullptr;
    T _data;
};

template<class T>
class List
{
    typedef ListNode<T> Node;

public:
    List()
    {
        // 创建一个头节点,并将头节点的_next和_prev都指向自身,表示链表为空
        _head = new Node;
        _head->_next = _head;
        _head->_prev = _head;
    }

    void PushBack(T&& x)
    {
        // 在链表尾部插入一个右值
        Insert(_head, std::forward<T>(x));
    }

    void PushFront(T&& x)
    {
        // 在链表头部插入一个右值
        Insert(_head->_next, std::forward<T>(x));
    }

    void Insert(Node* pos, T&& x)
    {
        // 在指定位置之前插入一个右值

        // 获取pos节点的前一个节点
        Node* prev = pos->_prev;

        // 创建一个新的节点
        Node* newnode = new Node;
        
        // 使用完美转发将右值x赋值给新节点的_data
        newnode->_data = std::forward<T>(x);

        // 调整链表中的指针
        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = pos;
        pos->_prev = newnode;
    }

    void Insert(Node* pos, const T& x)
    {
        // 在指定位置之前插入一个左值

        // 获取pos节点的前一个节点
        Node* prev = pos->_prev;

        // 创建一个新的节点
        Node* newnode = new Node;
        
        // 将左值x赋值给新节点的_data
        newnode->_data = x;

        // 调整链表中的指针
        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = pos;
        pos->_prev = newnode;
    }

private:
    Node* _head;
};

int main()
{
    List<bit::string> lt;
    lt.PushBack("1111");
    lt.PushFront("2222");
    return 0;
}


上面这段代码是一个简化的链表实现,包括了节点结构 ListNode 和链表类 List。其中,链表类中的 PushBackPushFrontInsert 函数用于在链表中插入元素。

Insert 函数中,有两个重载版本,分别用于插入右值引用和左值引用。关键位置是对节点的 _data 成员赋值的地方。

对于右值引用版本,使用 std::forward<T>(x) 将参数 x 原样转发,保持其原始值类别。这样做可以避免不必要的拷贝操作,提高性能和效率。

对于左值引用版本,直接将参数 x 赋值给节点的 _data 成员。因为左值引用已经是一个具名对象,没有必要进行移动或拷贝操作。

在主函数中,创建了一个 List<bit::string> 类型的链表对象 lt,并通过 PushBackPushFront 函数向链表中插入元素。

总的来说,这段代码展示了如何使用完美转发和模板来实现一个简单的链表,并在插入元素时考虑了右值引用和左值引用的情况,以提高代码的灵活性和效率。

温馨提示

感谢您对博主文章的关注与支持!另外,我计划在未来的更新中持续探讨与本文相关的内容,会为您带来更多关于C++以及编程技术问题的深入解析、应用案例和趣味玩法等。请继续关注博主的更新,不要错过任何精彩内容!

再次感谢您的支持和关注。期待与您建立更紧密的互动,共同探索C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1225765.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring接入Metric+Graphite+Grafana搭建监控系统

环境搭建 Metric 主要是记录操作记录&#xff0c;把数据传给Graphite&#xff0c;这个只需要引入依赖就可以了 日志收集系统&#xff0c;可以支持很多的监控系统一般在Spring项目中用其收集数据&#xff0c;可以发送到Graphite等监控系统中一般使用Merter和Timer分别记录成功…

[黑马程序员SpringBoot2]——开发实用篇1

目录&#xff1a; 手工启动热部署自动启动热部署热部署范围配置关闭热部署功能第三方bean属性绑定松散绑定常用计量单位应用bean属性校验进制数据转换规则加载测试专用属性加载测试专用配置测试类中启动web环境发送虚拟请求匹配响应执行状态匹配响应体匹配响应体(json)匹配响应…

信息系统项目管理师 第四版 第1章 信息化发展

1.信息与信息化 信息是指音讯、消息、信息系统传输和处理的对象&#xff0c;泛指人类社会传播的一切内容。来自P1 信息化是指在国家宏观信息政策指导下&#xff0c;通过信息技术开发、信息产业的发展、信息人才的配置&#xff0c;最大限度地利用信息资源以满足全社会的信息需…

一种用于脑肿瘤和组织分割的具有体积特征对齐的三维跨模态特征交互网络

A 3D Cross-Modality Feature Interaction Network With Volumetric Feature Alignment for Brain Tumor and Tissue Segmentation 一种用于脑肿瘤和组织分割的具有体积特征对齐的三维跨模态特征交互网络背景贡献实验方法Cross-Modality Feature Interaction ModuleVolumetric …

UE 程序化网格 计算横截面 面积

首先在构造函数内加上程序化网格&#xff0c;然后复制网格体到程序化网格组件上&#xff0c;将Static Mesh&#xff08;类型StaticMeshActor&#xff09;的静态网格体组件给到程序化网格体上 然后把StaticMesh&#xff08;类型为StaticMeshActor&#xff09;Instance暴漏出去 …

Apache Hive源码阅读环境搭建

前置软件&#xff1a; JDK 1.8 Maven 3.3.9 1 下载源码 # 下载源码 git clone https://github.com/apache/hive.gitcd hive# 查看标签 git tag# 切换到要阅读的指定版本的tag git checkout rel/release-2.1.02 编译源码 mvn clean install -DskipTests执行报错 日志如下 E…

C#,数值计算——插值和外推,曲线插值(Curve_interp)的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { /// <summary> /// Object for interpolating a curve specified by n points in dim dimensions. /// </summary> public class Curve_interp { private int dim { get; s…

消息积压了如何处理?

欢迎大家到我的博客阅读这篇文章。消息积压了如何处理&#xff1f; - 胤凯 (oyto.github.io)在系统中使用消息队列的时候&#xff0c;消息积压这个问题也经常遇到&#xff0c;并且这个问题还不太好解决。 消息积压的直接原因通常是&#xff0c;系统中的某个部分出现了性能问题…

初刷leetcode题目(2)——数据结构与算法

&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️Take your time ! &#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️&#x1f636;‍&#x1f32b;️…

YOLO目标检测——无人机检测数据集下载分享【含对应voc、coco和yolo三种格式标签】

实际项目应用&#xff1a;无人机识别数据集说明&#xff1a;无人机检测数据集&#xff0c;真实场景的高质量图片数据&#xff0c;数据场景丰富标签说明&#xff1a;使用lableimg标注软件标注&#xff0c;标注框质量高&#xff0c;含voc(xml)、coco(json)和yolo(txt)三种格式标签…

【Web】PHP反序列化的一些trick

目录 ①__wakeup绕过 ②加号绕过正则匹配 ③引用绕过相等 ④16进制绕过关键词过滤 ⑤Exception绕过 ⑥字符串逃逸 要中期考试乐(悲) ①__wakeup绕过 反序列化字符串中表示属性数量的值 大于 大括号内实际属性的数量时&#xff0c;wakeup方法会被绕过 &#xff08;php5-p…

基于海洋捕食者算法优化概率神经网络PNN的分类预测 - 附代码

基于海洋捕食者算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于海洋捕食者算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于海洋捕食者优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针…

SQL零基础入门教程,贼拉详细!贼拉简单! 速通数据库期末考!(八)

FULL OUTER JOIN 除了前面讲到的 INNER JOIN&#xff08;内连接&#xff09;、LEFT JOIN&#xff08;左连接&#xff09;、RIGHT JOIN&#xff08;右连接&#xff09;&#xff0c;还有另外一种关联方式&#xff0c;即 FULL OUTER JOIN&#xff08;全外连接&#xff09; FULL O…

移动端路径传参以数字的形式,写死的情况

页面1 async getListTransferAndApprova() { //把mark值拼接到路径的后面&#xff0c;定义一个变量&#xff0c;使得切换穿的mark都不一样let mark ;if (this.tabsCurrent 0) {mark 2;} else if (this.tabsCurrent 1) {mark 3;}else if (this.tabsCurrent 2) {mark 4;}…

【AD封装】芯片IC-SOP,SOIC,SSOP,TSSOP,SOT(带3D)

包含了我们平时常用的芯片IC封装&#xff0c;包含SOP,SOIC,SSOP,TSSOP,SOT&#xff0c;总共171种封装及精美3D模型。完全能满足日常设计使用。每个封装都搭配了精美的3D模型哦。 ❖ TSSOP和SSOP 均为SOP衍生出来的封装。TSSOP的中文解释为&#xff1a;薄的缩小型 SOP封装。SSO…

WMS重力式货架库位对应方法

鉴于重力式货架的特殊结构和功能&#xff0c;货物由高的一端存入&#xff0c;滑至低端&#xff0c;从低端取出。所以重力式货架的每个货位在物理上都会有一个进货口和一个出货口。因此&#xff0c;在空间上&#xff0c;对同一个货位执行出入库操作需要处于不同的位置。 比如对…

Os-hackNos-1

Os-hackNos-1 一、主机发现和端口扫描 主机发现 arp-scan -l端口扫描 nmap -P 192.168.80.141二、信息收集 访问80端口&#xff0c;可知目标是ubuntu系统&#xff0c;中间件是Apache 目录扫描&#xff0c;发现两个路径 dirsearch -u http://192.168.80.141/ -e *index.html路…

FISCO BCOS 3.0【03】配置和使用pythonSDK

官方技术文档&#xff1a;https://fisco-bcos-doc.readthedocs.io/zh-cn/latest/index.html 我们在官方技术文档的基础上&#xff0c;进行&#xff0c;对文档中一些不清楚的地方进行修正 依赖软件 Ubuntu sudo apt install -y zlib1g-dev libffi6 libffi-dev wget git初始化…

STM32硬件调试器不一定准确,proteus不一定准确

我在做实验的过程中&#xff0c;发现里面的那个变量ii一直都不变搞了很久没有发现问题&#xff0c; 然后怀疑是不是软件出了问题&#xff0c;然后直接只用单片机的一个灯泡来检测是否正常&#xff0c;发现&#xff1a;单片机里面正常&#xff0c;但是硬件调试的时候&#xff0…

后端面经学习自测(三)

文章目录 1、ArrayList和Linkedlist区别&#xff1f;2、ArrayList扩容机制&#xff1f;3、ArrayList和Linkedlist分别能做什么场景&#xff1f;4、事务特性&#xff1f;MySQL事务Redis事务Spring事务5、在Spring中事务失效的场景&#xff1f;6、Java泛型&#xff1f;7、泛型擦除…