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

C++11在左值引用的基础上增加右值引用

文章目录

  • 前言
  • 右值引用的出现
  • 左值和右值
  • 右值引用的作用
  • 引用类型的对比
    • 左值引用
    • 常量引用
    • 右值引用
  • 右值引用的示例
    • 常量引用实现
    • 改为左值引用实现报错
    • 改为右值引用实现
    • std::move() 函数
  • 万能引用
  • 左值引用和右值引用判定的函数
  • 总结

前言

右值引用这个词是最开始是学习 easylogging++ 这个日志开源项目的时候遇到的,当时遇到 && 这样的写法先是一愣,还有这种写法?难道是引用的地址?结果查询资料才明白这叫做右值引用。

右值引用的出现

其实右值引用是在 C++11 时增加的新内容,在此之前,引用是没有左值和右值之分的,只存在一种引用,也就是后来 C++11 标准中的左值引用,而右值引用的提出主要是为了解决之前左值引用出现的一些尴尬的问题。

左值和右值

说到右值引用需要先了解下左值和右值,这也是我自己学习的过程,之前在 《简单聊聊C/C++中的左值和右值》 这篇笔记中总结过,可以简单理解左值就是放在 = 左边,可以取到地址,可以被赋值的表达式,而右值通常是放在 = 右侧,不能取地址,只能被当成一个“值”的表达式。

右值引用的作用

右值引用的出现并不是为了取代左值引用,也不是和左值引用形成对立,而是充分利用右值内容来减少对象构造和析构操作,以达到提高程序代码效率的目的。

也就是说增加右值引用这个特性是为了提高效率,之前的总结中也提到过,在 C++11 中还引入了 std::move() 函数,并用这个函数改写了 std::remove_if() 函数,这就是提高效率的例子。

使用 std::move() 函数意味着放弃所有权,对于一个左值,如果我们明确放弃对其资源的所有权,则可以通过 std::move() 来将其转为右值引用,放弃所有权的这个操作不一定都是方便的,比如 std::auto_ptr 这个第一代的智能指针,就是因为转移了所有权,使用起来不太方便,才在最新标准中被废弃的。但如果你明确要转移所有权,并且合理使用,有时可以有效的提高程序效率。

引用类型的对比

在学习使用右值引用之前先复习一下左值引用,对比学习更有利于我们的记忆。

左值引用

int i = 22;
int& j = i;

j = 11;

上面这几行代码就是最常见左值引用的例子,变量 j 引用了变量 i 的存储位置,修改变量 j 就修改了变量 i 的值,但是如果引用一个值会怎么样呢?比如下面这行代码:

int& j = 22;

编译这行代码会得到一个编译错误:

error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
int& j = 22;

像上面这种问题,可以使用常量引用来解决。

常量引用

针对上面的编译错误,改成常量引用就可以通过编译了,就像这样:

const int& j = 22;

使用常量引用来引用数字常量22,可以编译通过是因为内存上产生了临时变量保存了22这个数据,这个临时变量是可以进行取地址操作的,因此变量 j 引用的其实是这个临时变量,相当于下面的这两句:

const int temp = 22;
const int &j = temp;

看到这里我们发现常量引用可以解决引用常量的问题,那么为什么非得新增一个右值引用呢?那是因为使用常引用后,我们只能通过引用来读取数据,无法去修改数据,这在很多情况下是很不方便的。

右值引用

常量引用可以使用右值引用来改写,改写之后可以正常编译,并且还可以进行修改:

int&& j = 22;

这句代码有两个需要注意的点,第一是右值引用是 C++11 中才增加的,所以需要增加 --std=c++11 这个编译选项才能正常编译,第二是右值引用的两个地址符需要连着写成 &&, 如果中间有空格写成 & & 会被认为是引用的引用而导致编译错误,这是不符合语法的。

右值引用的示例

前面对引用类型进行了对比,但是还没有发现右值引用的好处,接下来用一个例子来展示一下增加右值引用之前的写法,和使用右值引用的写法,通过对比来了解一下右值引用究竟有什么好处。

我们来实现一个自定义缓冲区,先使用最常见的方法来实现拷贝构造函数和拷贝赋值函数,简单实现如下,功能不太完整,但是可以说明右值引用的作用:

常量引用实现

#include <iostream>
#include <cstring>
using namespace std;


class CBuffer
{
public:
    // 构造函数
    CBuffer(int size = 1024): m_size(size)
    {
        cout << "CBuffer(int)" << endl;
        m_buffer = new char[size];
    }

    // 析构函数
    ~CBuffer()
    {
        cout << "~CBuffer()" << endl;
        delete[] m_buffer;
        m_buffer = nullptr;
        m_size = 0;
    }

    // 拷贝构造
    CBuffer(const CBuffer &origin): m_size(origin.m_size)
    {
        cout << "CBuffer(const CBuffer&)" << endl;
        m_buffer = new char[origin.m_size];
        memcpy(m_buffer, origin.m_buffer, m_size);
    }

    // 赋值重载
    CBuffer& operator=(const CBuffer &origin)
    {
        cout << "operator=(const CBuffer&)" << endl;
        if (this == &origin) return *this;

        delete[] m_buffer;

        m_size = origin.m_size;
        m_buffer = new char[origin.m_size];
        memcpy(m_buffer, origin.m_buffer, m_size);

        return *this;
    }

    int get_size()
    {
        return m_size;
    }

    static CBuffer gen_buffer(const int size)
    {
        CBuffer temp_buffer(size);
        return temp_buffer;
    }

private:
    char *m_buffer;
    int m_size;
};

int main()
{
    CBuffer b1;
    CBuffer b2(b1);
    cout << "b1.size = " << b1.get_size() << endl;
    cout << "b2.size = " << b2.get_size() << endl;

    b2 = CBuffer::gen_buffer(100);
    return 0;
}

运行结果是:

CBuffer(int)
CBuffer(const CBuffer&)
b1.size = 1024
b2.size = 1024
CBuffer(int)
operator=(const CBuffer&)
~CBuffer()
~CBuffer()
~CBuffer()

这个例子不具有实用性,只为了说明问题,CBuffer 这个类定义为了拷贝构造函数并且重载了 = 运算符,两个函数参数均使用常量引用的类型,这就是一般的写法。

但是这样实现有一个问题,因为参数是常量引用,所以没办法修改原对象的值,我们看到拷贝构造和赋值重载两个函数中都有申请空间和拷贝的操作,这种操作在操作内存较大的对象是比较耗时,所以应该尽量避免,我们想到可以使用新对象的指针指向旧对象指针来解决,这样就不用拷贝了,可是这样修改会导致两个对象指向同一块内存,这个问题需要解决。

改为左值引用实现报错

如果两个对象指向同一块内存,那么对象在析构的时候就会将一块内存释放两次导致奔溃,这时考虑在拷贝构造或者赋值重载时,将原来对象的指针设置成空就可以了,但是参数是常量没有办法修改啊,那我们将 const 关键字去掉试试,将两个函数改成这样:

    // 拷贝构造
    CBuffer(CBuffer &origin): m_size(origin.m_size)
    {
        cout << "CBuffer(CBuffer&)" << endl;
        m_buffer = origin.m_buffer;

        origin.m_buffer = nullptr;
        origin.m_size = 0;
    }

    // 赋值重载
    CBuffer& operator=(CBuffer &origin)
    {
        cout << "operator=(CBuffer&)" << endl;
        if (this == &origin) return *this;

        m_buffer = origin.m_buffer;

        origin.m_buffer = nullptr;
        origin.m_size = 0;

        return *this;
    }

看起来没有什么问题,但是编译的时候会报错:

error: invalid initialization of non-const reference of type ‘CBuffer&’ from an rvalue of type ‘CBuffer’
b2 = CBuffer::gen_buffer(100);
^
note: initializing argument 1 of ‘CBuffer& CBuffer::operator=(CBuffer&)’
CBuffer& operator=(CBuffer &origin)

这个错误是什么意思呢?其实说的就是在调用 CBuffer::gen_buffer(100); 函数时,会产生一个临时对象,这个临时对象在赋值给 b2 是会调用
CBuffer& operator=(CBuffer &origin) 函数,但是这个函数的参数是一个左值引用类型,而临时对象是一个右值,无法绑定到左值引用上,所以报错了。

还有拷贝构造函数也是有相同的问题,当写出类似 b2 = CBuffer(CBuffer(1000)) 类型会产生临时对象的语句时,同样会因为左值引用不能绑定到右值上而报错,这时候就要请出右值引用了。

改为右值引用实现

对于赋值重载函数,我们使用右值引用将其改写为:

    // 赋值重载
    CBuffer& operator=(CBuffer &&origin)
    {
        cout << "operator=(CBuffer&&)" << endl;
        if (this == &origin) return *this;

        m_buffer = origin.m_buffer;

        origin.m_buffer = nullptr;
        origin.m_size = 0;

        return *this;
    }

这时可以正常通过编译,并且只是修改了指针的指向,并没有申请和拷贝另外一份内存。

std::move() 函数

如果我们将拷贝构造函数的参数也改成右值引用的形式:

    // 拷贝构造
    CBuffer(CBuffer &&origin): m_size(origin.m_size)
    {
        cout << "CBuffer(CBuffer&)" << endl;
        m_buffer = origin.m_buffer;

        origin.m_buffer = nullptr;
        origin.m_size = 0;
    }

编译时就会发现编译错误:

error: use of deleted function ‘constexpr CBuffer::CBuffer(const CBuffer&)’
CBuffer b2(b1);
^
note: ‘constexpr CBuffer::CBuffer(const CBuffer&)’ is implicitly declared
as deleted because ‘CBuffer’ declares a move constructor or move assignment operator
class CBuffer

其本质问题就是主函数中 CBuffer b2(b1); 这一句引起的,因为变量 b1 是一个左值,但是拷贝构造函数接受的是右值引用,所以类型不匹配导致了编译错误,这时可以使用 std::move() 函数改成这条语句为 CBuffer b2(std::move(b1)); 就可以正常编译运行了,运行结果为:

CBuffer(int)
CBuffer(CBuffer&)
b1.size = 0
b2.size = 1024
CBuffer(int)
operator=(CBuffer&&)
~CBuffer()
~CBuffer()
~CBuffer()

查看运行结果会发现 b1.size = 0,因为 b1 调用了 std::move() 函数,转移了资源的所有权,内部已经被“掏空”了,所以在明确所有权转移之后,不要再直接使用变量 b1 了。

万能引用

听到这个名字就感觉很厉害,什么是万能引用,其实就是可以同时接受左值和右值的引用类型,但是这种完能引用只能发生在推导的情况下,下面给出了一个例子:

#include <iostream>
using namespace std;

template<typename T>
void func(T&& val)
{
    cout << val << endl;
}

int main()
{
    int year = 2020;
    func(year);
    func(2020);
    return 0;
}

这段代码中 T&& val 就是万能引用,因为是在模板中,类型需要推导,如果是在普通函数中 T&& val 这个形式就是右值引用。

左值引用和右值引用判定的函数

文中多次提到左值和右值,可能刚学习这块内容的小伙伴会有些懵,其实 C++ 中提供了判定左值引用和右值引用的函数,头文件为 <type_traits>,函数名为 is_referenceis_rvalue_referenceis_lvalue_reference,看名字就可以知道他们的用途,看下面的例子就更清楚了。

#include <iostream>
#include <type_traits>
using namespace std;

int main()
{
    int i = 22;
    int& j = i;
    int&& k = 11;

    cout << "i is_reference: " << is_reference<decltype(i)>::value << endl;
    cout << "i is_lvalue_reference: " << is_lvalue_reference<decltype(i)>::value << endl;
    cout << "i is_rvalue_reference: " << is_rvalue_reference<decltype(i)>::value << endl;


    cout << "j is_reference: " << is_reference<decltype(j)>::value << endl;
    cout << "j is_lvalue_reference: " << is_lvalue_reference<decltype(j)>::value << endl;
    cout << "j is_rvalue_reference: " << is_rvalue_reference<decltype(j)>::value << endl;


    cout << "k is_reference: " << is_reference<decltype(k)>::value << endl;
    cout << "k is_lvalue_reference: " << is_lvalue_reference<decltype(k)>::value << endl;
    cout << "k is_rvalue_reference: " << is_rvalue_reference<decltype(k)>::value << endl;
    return 0;
}

运行结果如下,满足返回1,否则返回0:

i is_reference: 0
i is_lvalue_reference: 0
i is_rvalue_reference: 0
j is_reference: 1
j is_lvalue_reference: 1
j is_rvalue_reference: 0
k is_reference: 1
k is_lvalue_reference: 0
k is_rvalue_reference: 1

总结

  • 右值引用的写法为 T&& val,两个地址符要挨在一起,在模板中被称为万能引用
  • 注意左值引用和右值引用的使用区别,其实本质都是为了减少无效的拷贝
  • std::move() 函数会转移对象的所有权,转移操作之后将左值转为右值引用,原对象不可再直接使用
  • 可以使用 is_referenceis_rvalue_referenceis_lvalue_reference 来判断引用类型

陪伴是最长情的告白,等待是最极致的思念
五一离家返工了,心里有些不是滋味,为了家出来奋斗却将“家”抛在了身后,珍惜眼前人吧~

相关文章:

  • 汇编指令入门级整理
  • 使用c++filt命令还原C++编译后的函数名
  • 配置Beyond Compare 4作为git mergetool来解决git merge命令导致的文件冲突
  • git在回退版本时HEAD~和HEAD^的作用和区别
  • 对称加密、非对称加密、公钥、私钥究竟是个啥?
  • 认证、HTTPS、证书的基本含义
  • 码龄10年工作6年的搬砖小哥,最常访问的学习网站都在这里了
  • C++中的std::lower_bound()和std::upper_bound()函数
  • 根证书的应用和信任基础
  • Shell脚本中获取命令运行结果、特殊变量使用、条件判断等常用操作
  • gdb调试解决找不到源代码的问题
  • GDB调试指北大全
  • 小白眼中的docker究竟是个什么东西
  • GDB调试指北-启动GDB并查看说明信息
  • Redis源码-BFS方式浏览main函数
  • [rust! #004] [译] Rust 的内置 Traits, 使用场景, 方式, 和原因
  • 07.Android之多媒体问题
  • canvas 绘制双线技巧
  • CSS进阶篇--用CSS开启硬件加速来提高网站性能
  • java第三方包学习之lombok
  • k个最大的数及变种小结
  • Linux链接文件
  • React系列之 Redux 架构模式
  • Swoft 源码剖析 - 代码自动更新机制
  • Travix是如何部署应用程序到Kubernetes上的
  • 聚类分析——Kmeans
  • 力扣(LeetCode)21
  • 浏览器缓存机制分析
  • 强力优化Rancher k8s中国区的使用体验
  • 深入体验bash on windows,在windows上搭建原生的linux开发环境,酷!
  • 移动互联网+智能运营体系搭建=你家有金矿啊!
  • kubernetes资源对象--ingress
  • ​RecSys 2022 | 面向人岗匹配的双向选择偏好建模
  • #我与Java虚拟机的故事#连载13:有这本书就够了
  • $(selector).each()和$.each()的区别
  • (arch)linux 转换文件编码格式
  • (C语言)共用体union的用法举例
  • (翻译)Quartz官方教程——第一课:Quartz入门
  • (十二)devops持续集成开发——jenkins的全局工具配置之sonar qube环境安装及配置
  • (算法)Travel Information Center
  • (转) RFS+AutoItLibrary测试web对话框
  • (转)C语言家族扩展收藏 (转)C语言家族扩展
  • (转)nsfocus-绿盟科技笔试题目
  • .Net 6.0 处理跨域的方式
  • .net 7 上传文件踩坑
  • .NET Core 项目指定SDK版本
  • .NET Framework .NET Core与 .NET 的区别
  • .net framework 4.0中如何 输出 form 的name属性。
  • .Net 代码性能 - (1)
  • .netcore 如何获取系统中所有session_如何把百度推广中获取的线索(基木鱼,电话,百度商桥等)同步到企业微信或者企业CRM等企业营销系统中...
  • .NET程序员迈向卓越的必由之路
  • .Net程序猿乐Android发展---(10)框架布局FrameLayout
  • .NET平台开源项目速览(15)文档数据库RavenDB-介绍与初体验
  • .NET企业级应用架构设计系列之技术选型
  • @NestedConfigurationProperty 注解用法