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

在这里插入图片描述

阅读导航

  • 引言
  • 一、左值引用和右值引用
    • 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++、算法和编程的奥秘。祝您生活愉快,排便顺畅!
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SVG的viewBox、width和height释义, 示例及代码

svg的是没有边界的&#xff0c;svg画布只是用于展示svg世界中某一个范围的内容&#xff0c;而对于超过了svg画布范围的内容&#xff0c;则会被遮挡。默认svg画布默认显示世界坐标下原点坐标的width*height面积的矩形视野。 ​ 我们可以通过viewBox来修改默认的显示配置&#…

图新地球地图导入操作步骤

1、下载图源&#xff0c;如下&#xff1a; 2、将其全部复制或部分复制&#xff0c;然后回到桌面&#xff0c;打开文件所在位置&#xff0c;如下&#xff1a; 3、将复制的数据粘贴到文件夹下&#xff0c;具体如下&#xff1a; 4、复制到路径如下&#xff1a; 5、复制结果如下&am…

Spring Boot - filter 的顺序

定义过滤器的执行顺序 1、第一个过滤器 import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; impor…

linux如何使用shell远程连接

简介&#xff1a;本文的一切条件基于redhat的linux操作系统。 1、创建虚拟机&#xff1a; 如有需要&#xff0c;请转至【linux基础】在VMware上安装RHEL9详细教程_融社的博客-CSDN博客 &#xff08;如若侵权&#xff0c;该篇立删&#xff09; 2、使用命令查看网段信息 打…

LeetCode【4】寻找两个正序数组中位数

题目&#xff1a; 思路&#xff1a; https://blog.csdn.net/a1111116/article/details/115033098 代码&#xff1a; public double findMedianSortedArrays(int[] nums1, int[] nums2) {int[] ints Arrays.copyOf(nums1, nums1.length nums2.length);System.arraycopy(nums2…

Linux网络——HTTP

一.应用层 我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层. 我们上一次写的网络版本计算器就是一个应用层的网络程序。 我们约定了数据的读取&#xff0c;一端发送时构造的数据, 在另一端能够正确的进行解析, 就是ok的. 这种约定, 就是应…

java 访问sqlserver 和 此驱动程序不支持jre1.8错误

sqlserver数据如下&#xff1b; TestSQL.java&#xff1b; import java.sql.*;public class TestSQL {public static void main(String[] args) throws ClassNotFoundException, SQLException {String driverName "com.microsoft.sqlserver.jdbc.SQLServerDriver";…

Golang起步篇(Windows、Linux、mac三种系统安装配置go环境以及IDE推荐以及入门语法详细释义)

Golang起步篇 Golang起步篇一. 安装Go语言开发环境1. Wondows下搭建Go开发环境(1). 下载SDK工具包(2). 解压下载的压缩包&#xff0c;放到特定的目录下&#xff0c;我一般放在d:/programs下(路径不能有中文或者特殊符号如空格等)(3). 配置环境变量步骤1&#xff1a;先打开环境变…

基于STM32的外部中断(EXTI)在嵌入式系统中的应用

外部中断&#xff08;External Interrupt&#xff0c;EXTI&#xff09;是STM32嵌入式系统中常见且重要的功能之一。它允许外部事件&#xff08;例如按键按下、传感器触发等&#xff09;通过适当的引脚触发中断&#xff0c;从而应用于各种嵌入式系统中。在STM32微控制器中&#…

Vulkan渲染引擎开发教程 一、开发环境搭建

一 安装 Vulkan SDK Vulkan SDK 就是我们要搞的图形接口 首先到官网下载SDK并安装 https://vulkan.lunarg.com/sdk/home 二 安装 GLFW 窗口库 GLFW是个跨平台的小型窗口库&#xff0c;也就是显示窗口&#xff0c;图形的载体 去主页下载并安装&#xff0c;https://www.glfw.…

搭建内部知识库,解决企业内部琐碎信息问题

企业内部面临着大量琐碎的信息&#xff0c;这些信息可能分散在各个部门、员工之间&#xff0c;导致查找和共享变得困难。这种情况下&#xff0c;搭建一个内部知识库可以解决这一问题。通过内部知识库&#xff0c;企业可以将琐碎的信息整理、分类&#xff0c;并提供一个集中存储…

Learning Perception Module

参考文章&#xff1a;自动驾驶开发者说|框架|如何单独运行apollo相机感知模块&#xff1f; - 知乎引言文章主要尝试了apollo框架下&#xff0c;视觉感知模块的单独运行&#xff0c;并利用离线的数据包进行检测实时展示结果。过程相对来说比较顺利。在加上已经用VScode搭建的单步…

nodejs微信小程序-慢性胃炎健康管理系统的设计与实现-安卓-python-PHP-计算机毕业设计

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…

Gem5模拟器学习之旅——翻译自官网

文章目录 安装并使用gem5 模拟器支持的操作系统和环境依赖在 Ubuntu 22.04 启动(gem5 > v21.1)Docker获取代码用 SCons 构建用法首次构建 gem5gem5 二进制类型调试opt快速 常见错误错误的 gcc 版本Python 位于非默认位置未安装 M4 宏处理器Protobuf 3.12.3 问题 安装并使用g…

redis安装(Windows和linux)

如何实现Redis安装与使用的详细教程 Redis 简介 Redis是一个使用C语言编写的开源、高性能、非关系型的键值对存储数据库。它支持多种数据结构&#xff0c;包括字符串、列表、集合、有序集合、哈希表等。Redis的内存操作能力极强&#xff0c;其读写性能非常优秀&#xff0c;且…

gRPC 四模式之 双向流RPC模式

双向流RPC模式 在双向流 RPC 模式中&#xff0c;客户端以消息流的形式发送请求到服务器端&#xff0c;服务器端也以消息流的形式进行响应。调用必须由客户端发起&#xff0c;但在此之后&#xff0c;通信完全基于 gRPC 客户端和服务器端的应用程序逻辑。 为什么有了双向流模式…

Android resource/drawable转换成Uri,Kotlin

Android resource/drawable转换成Uri&#xff0c;Kotlin private fun convertResource2Uri(resId: Int): Uri {return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE "://" resources.getResourcePackageName(resId) / resources.getResourceTypeName(resI…

开源WIFI继电器之方案介绍

一、实物 1、外观 2、电路板 二、功能说明 输出一路继电器常开信号&#xff0c;最大负载电流10A输入一路开关量检测联网方式2.4G Wi-Fi通信协议MQTT配网方式AIrkiss&#xff0c;SmartConfig设备管理本地Web后台管理&#xff0c;可配置MQTT参数供电AC220V其它一个功能按键&…

进程概述

文章目录 计算机算机组成因特尔CPU型号摩尔定律衡量CPU的指标指令&#xff08;Instruction)操作系统&#xff08;Operating System&#xff09;虚拟地址空间&#xff08;Virtual Address Space&#xff09;进程(Process/task)进程管理(PCB - 进程控制块)进程控制块&#xff08;…

某60区块链安全之重入漏洞实战记录

区块链安全 文章目录 区块链安全重入漏洞实战实验目的实验环境实验工具实验原理实验内容 重入漏洞实战 实验目的 学会使用python3的web3模块 学会以太坊重入漏洞分析及利用 实验环境 Ubuntu18.04操作机 实验工具 python3 实验原理 以太坊智能合约的特点之一是能够调用和…