当前位置: 首页 > news >正文

【C++11】右值引用详解

文章目录

  • 前言
  • 1. 左、右值的概念
    • 1.1 左值
    • 1.2 右值
    • 1.3 右值引用
  • 2. 右值引用的价值和使用场景
    • 2.1 左值引用的价值和缺陷
    • 2.2 右值引用的价值和使用场景
    • 2.3 小结
  • 3. 完美转发
  • 4. 类的移动构造和移动赋值

前言

在C++11之前,面对C++11之前出现的临时对象的传参构造,都只有老老实实进行深拷贝一份。但是C++11引入一个新的概念和语法特性(右值和右值引用),进而解决临时对象的深拷贝问题等等。

本文章小编主要从以下几个方面来带读者认识右值:

  1. 左、右值的概念
  2. 右值引用的价值和应用场景
  3. 完美转发
  4. 类的移动构造和移动赋值

注:小编的代码环境VS2022。

1. 左、右值的概念

1.1 左值

  • 左值:

    左值是指具有明确存储位置(内存地址)的表达式,通常可以出现在赋值语句的左侧。左值的特点是持久性,即其生命周期超出表达式求值的范围。

    两个特征

    1. 可以取地址。
    2. 可以被赋值。(常变量不可被修改)

例1:

int main()
{int p = 10; //普通整型变量int* a = new int(10); //普通整型指针const int c = 10; //普通常变量"1111111"; //字符串变量return 0;
}

上面代码中的p,a,c,"1111111"都是左值。它们的共同特点都是:可以被取地址

1.2 右值

  • 右值

    右值是指临时对象或没有持久存储位置的表达式,通常只能出现在赋值语句的右侧。右值的特点是短暂性,其生命周期通常仅限于当前表达式。

    两个特征:

    1. 不能取地址。
    2. 不能出现在表达式左边。(特例《C++ Primer》中有提到)

例2:

int main()
{int x = 0, y = 1;
// --------------------10;'c';x + y; //表达式计算结果min(x, y); //函数返回结果return 0;
}

类似于上面的10,'c',x + y, min(x, y)这样的常量或者表达式求值都是属于右值。它们的特点都是:不能被取地址,即没有具体的存储位置!

1.3 右值引用

认识了右值以后,我们来认识右值引用

左值小编已经出过一片文章了:左值引用。这里就不再过多赘诉了。

  • 右值引用:就是对右值取别名。
  • 语法形式:&&。例如:int&& p = 10;这个p就是10的别名。

例3:

int main()
{int a = 10;int& ra = a; //左值引用int&& p1 = 10; //右值引用double x = 1.1, y = 3.3;double&& p2 = x + y; //右值引用return 0;
}

引用都是一样的,都是为左值或者右值取别名(这里有,我们后面在完美转发小节的时候会填)

  • 现在我们思考一个问题

    左值引用可以引用右值吗?右值引用可以引用左值吗?

例4:

#include<iostream>
using namespace std;
int main()
{const int& a = 10; //const左值引用可以引用右值int x = 0;int&& b = std::move(x); //C++标准库中的move函数可以将一个左值返回一个右值return 0;
}

move的语法词典

move这个函数可以将左值转化为右值返回。std::move本质上是一个静态类型转换(static_cast),不涉及任何运行时计算或内存操作。其典型实现如下:

template <typename T>
typename std::remove_reference<T>::type&& move(T&& arg) noexcept {return static_cast<typename std::remove_reference<T>::type&&>(arg);
}

模板中的T&&代表万能引用,即既可以又来引用左值、也可以用来引用右值。

  • 结论:可以左值引用引用右值,右值引用引用左值。

2. 右值引用的价值和使用场景

进入右值引用的价值之前,我们先来回顾一下左值引用的价值。进而找出左值引用的缺陷,然后再引入右值引用的价值

  • 我们先来看一个前置知识。方便后续的理解

例5:

#include<iostream>
using namespace std;
void func(const int& x)
{cout << "void func(const int& x)" << endl;
}void func(int&& x)
{cout << "void func(int&& x)" << endl;
}int main()
{int x = 10;func(10);func(x);return 0;
}

上面的代码正确吗?即问题是:左值和右值引用能够构成重载吗?

在这里插入图片描述

显然:这里的const T&T&&构成函数重载而且没有调用歧义。在调用的时候,编译器会调用类型更匹配的函数

  • 前置知识:拥有右值属性的值会调用右值属性的参数的函数

2.1 左值引用的价值和缺陷

补充减少拷贝多用于自定义类型中,内置类型拷贝消耗不大。主要考虑自定义类型的拷贝

  • 价值

    1. 做函数参数,减少拷贝。
    2. 做返回值,减少拷贝。
  • 缺陷

    1. 当我们一个函数需要返回一个临时对象的时候,这个时候我们不能返回一个临时对象的左值引用!!!

      结果是未知的。

2.2 右值引用的价值和使用场景

我们再看左值引用的使用缺陷:无法返回一个具有临时性的对象

右值不就是用来引用一个临时性的对象吗?

大致我们能够明白了:右值引用的语法是用来解决左值引用没有解决的历史性问题的!!!

接下来,我们详细探讨一个右值引用的价值(自定义类型string为例)

下面是手写的一个string.h文件,便于我们观察拷贝的细节。

namespace Er //防止命名冲突
{class string{public:string(const char* str = ""):_size(strlen(str)), _capacity(_size){cout << "string(char* str)" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}// s1.swap(s2)void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}~string(){delete[] _str;_str = nullptr;}const char* c_str() const{return _str;}private:char* _str;size_t _size;size_t _capacity; // 不包含最后做标识的\0};
}
  • 我们先聚焦于这两个成员函数
    在这里插入图片描述
    来看下面的场景,例6:
#include"String.h"
#include<iostream>
using namespace std;
Er::string func()
{Er::string str("xxxxx");return str;
}int main()
{Er::string ret;ret = func();return 0;
}

上面代码中如果没有编译器的优化,会进行两次深拷贝(抛开我们实现的swap之外)这样的开始是十分恐怖的!原因就在于我们函数返回的参数是一个右值。我们不得不进行深拷贝。

在这里插入图片描述

对于上面的右值我们有新概念

  • 对于内置类型的右值:纯右值
  • 对于自定义类型的右值:将亡值

如果我们更想减少拷贝,反正将亡值都快要消失了,那么我们是不是可以将将亡值的成员直接给我交换过来?我的数据和其交换,这样效率是否会大大提高?是的,这就是移动语义的构造和赋值的主要思想!!!

此时,String.h中添加两个函数:

// 移动构造
string(string&& s) noexcept:_str(nullptr), _size(0), _capacity(0)
{cout << "string(string&& s) -- 移动语义" << endl;swap(s);
}
// 移动赋值
string& operator=(string&& s) noexcept
{cout << "string& operator=(string&& s) -- 移动语义" << endl;swap(s);return *this;
}

noexcept是表示该函数不会抛异常。swap是自己实现的成员函数Er::string::swap。
由于s是一个将亡值,所以我们直接用this指针指向的内容进行交换,不需要过多的拷贝!!
极大地提高了效率

再来运行上面的例6:

在这里插入图片描述

对面图片中提出的这个问题,如果我们需要右值返回,那么我们返回的值是否需要是一个右值呢?是的,但是str并不是一个右值,相反它是一个左值!!

但是为什么调用了移动构造了呢?
先前也有例子,这里再谈一遍:因为函数返回不是返回的变量,而是返回的是一个临时变量!!
函数返回值是一个临时变量,是一个右值并不是左值

所以总结一下:

  • 使用场景一:右值引用可以用于做函数参数,可用于将亡值对于对象的构造或者赋值。
  • 价值:在接受函数返回值、临时变量的时候减少深拷贝。

例7:

//头文件已包含
int main()
{Er::string s; //正常构造Er::string tmp("aaaaa"); //语句一s = "xxxxxx"; //语句二s = std::move(tmp); //右值的移动赋值return 0;
}

来看运行结果:
在这里插入图片描述

二者都是移动语义

下面进行补充

  • 补充一,关于到底是调用什么拷贝或者赋值函数:

例8:

//头文件已包含
int main()
{Er::string str1("aaaaa"); //语句一//Er::string str2(move("aaaaa")); //语句二 不要对常量进行moveEr::string tmp = "xxxxx";move(tmp);Er::string str3(tmp); //语句三str1 = "xxxxx"; //语句四return 0;
}
  • 前置:字符串常量是一个左值类型。类型为:const char* const

关于运行结果,小编给出提示,读者下来自己理解:

  1. 函数重载,参数传入类型更匹配的地方。解决语句一,语句三。
  2. 隐式类型转化导致的参数发生变化。解决语句四。
  • 补充二,关于move

例9:
在这里插入图片描述

  • 我们应该认识到:move的返回值是一个右值引用,并不是将传入的参数改为右值……

  • 验证:
    在这里插入图片描述

  • 使用场景二:作为函数参数,用于一些常用的接口中。

例如STL中C++11各个容器都添加了新的接口:
在这里插入图片描述
在这里插入图片描述
……

2.3 小结

右值引用的价值和使用场景:

  • 价值:解决了左值引用没有解决的临时对象返回的问题,大大地减少了深拷贝的消耗。

  • 使用场景:

    1. 移动构造和移动赋值
    2. 一些接口的设计

3. 完美转发

在前面1.3右值引用时提到了T&&代表完美引用。同时也可以解决上面在1.3留的一个坑。

来看下面一个场景:

例10:

//头文件已经正确包含
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; }template<typename T>
void PerfectForward(T&& t)
{Fun(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;
}
  • 来看运行结果:
    在这里插入图片描述

    结果全是左值?这是为什么呢???

关于这个问题,我们先回到1.3的例3:

int main()
{int&& p1 = 10; //语句一return 0;
}

对于语句一来说:p1是左值还是右值呢
在这里插入图片描述

右值引用居然是一个左值???

  • 不管是左值引用还是右值引用,形参的属性都是左值属性!!!

如果形参属性不是左值属性,那么我们之前代码所写的swap(s)这样的代码还能成功吗?这也是编译器做出的优化,将右值引用的属性改为左值属性,这样更有利于我们的设计。

那么对于这样的万能引用,我们应该如何来保持其原有属性呢?

这就要讲到我们所用的完美转发了:

  • 函数std::forward。forward词典
    在这里插入图片描述
  • 作用:在传参过程中保持对象原生属性

回到上面的例11:

//其余代码保持不变
template<typename T>
void PerfectForward(T&& t)
{Fun(std::forward<T>(t)); //即可
}

在这里插入图片描述

结果完美正确

所以现在再来看STL设计的容器接口,一定是运用到了完美转发!!!

4. 类的移动构造和移动赋值

  • 针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

    1. 如果你没有自己实现移动构造函数,且没有实现析构函数拷贝构造拷贝赋值重载中的任意一个那么编译器会自动生成一个默认移动构造

      默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

    2. 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。

      默认生成的移动赋值重载函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)

    3. 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值

简要概括:

(涉及深拷贝:实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个)、

  1. 不涉及深拷贝的类不需要写,编译器自动提供
  2. 涉及深拷贝的类需要写,编译器不提供。
  3. 一旦自己写了,编译器就不再提供。

完。

  • 小编希望这篇文章能够帮助你!
http://www.dtcms.com/a/275900.html

相关文章:

  • 同步、异步、阻塞、非阻塞之间联系与区别
  • 小皮面板搭建pikachu靶场并bp使用爆破模块破解
  • 传感器WSNs TheDataLinkLayer——X-MAC
  • vue3+vit+vue-router路由,侧边栏菜单,面包屑导航设置层级结构
  • Redis事件机制
  • 嵌入式领域编码合集(为什么中文会乱码)
  • Java开发八股文之基础篇
  • qt 正则表达式
  • [面试] 手写题-二分查找
  • linux学习第30天(线程同步和锁)
  • WordPress Ads Pro Plugin本地文件包含漏洞(CVE-2025-4380)
  • 计算机毕业设计springboot服装工厂移动报表软件设计 基于SpringBoot的服装企业移动端数据可视化系统 面向服装制造行业的移动报表与公告发布平台
  • 微服务环境下的灰度发布与金丝雀发布实战经验分享
  • 多路选择器的学习
  • 《Java Web程序设计》实验报告六 JSP+JDBC+MySQL实现登录注册
  • 【飞算JavaAI】一站式智能开发,驱动Java开发全流程革新
  • 20250712-1-Kubernetes 监控与日志管理-K8s日志管理与维护_笔记
  • Go语言中的Options模式
  • 【Go + Gin 实现「双 Token」管理员登录】
  • Linux驱动08 --- 数据库
  • MCU芯片的功能安全机制E2E的基本原理和应用实现
  • 解锁C++数据结构:开启高效编程之旅
  • IDEA+Eclipse+Lombok无效问题排查
  • Java 之字符串 --- String 类
  • 电脑上如何查看WiFi密码
  • 什么是Jaccard 相似度(Jaccard Similarity)
  • 蓝牙调试抓包工具--nRF Connect移动端 使用详细总结
  • 日志不再孤立!用 Jaeger + TraceId 实现链路级定位
  • 程序在计算机中如何运行?——写给编程初学者的指南
  • 12.使用VGG网络进行Fashion-Mnist分类