[c++] 右值引用

1 左值和左值引用

在说右值引用之前,需要先说一下左值引用。当然,在右值引用出现之前,左值引用也不叫左值引用,就叫引用。现在一般也直接将左值引用称作引用,将右值引用称作右值引用。左值引用类似于 c 语言中经常使用的指针,c++ 引入左值引用,在一定程度上可以代替指针。

什么是左值,简单来说,左值是等号左边的值,是可以取地址的。左值引用就是左值的别名,左值引用和左值指向同一块内存;左值引用就像人的小名,指的是同一个人,这个人长高了 5cm,那么说这个人的大名和小名的时候,都长高了 5cm。

如下代码,是左值引用的例子:

(1)左值引用需要在声明的时候初始化

(2)左值引用不能指向一个右值,但是 const 左值引用可以用一个右值进行初始化

#include <iostream>
#include <string>

int main() {
  int a = 10;
  int &b = a;

  printf("a = %d, b = %d\n", a, b);
  b = 20;
  printf("a = %d, b = %d\n", a, b);
  // int &c; // 编译错误,左值引用在声明的时候必须初始化

  // int &d = 10; // 编译错误,左值引用不能使用一个右值初始化
  const int &e = 10;
  printf("&a = %p, &b = %p, &e = %p\n", &a, &b, &e);
  return 0;
}

下图打印出了 a 和 e 的地址,从地址可以看出来,对于 e 来说,虽然是用一个右值进行初始化,但是这个右值也是保存在栈上的。const 左值引用,类似于如下两句代码,只不过栈上这块内存是编译器内部分配的,开发者不感知。

int temp = 10;

int &e = temp ;

const 左值引用能使用一个右值进行初始化,这种情况在调用函数传参的时候也会遇到。如果函数形参是一个非 const 左值引用的话,那么调用函数的时候,如果直接传右值,会报编译错误;形参是 const 左值引用的话,那么可以直接传右值。当我们看到一个函数的形参是 const 左值引用的时候,总是习以为常,其实 const 左值引用还起到这样的作用,即可以直接传右值。

#include <iostream>
#include <string>

void Test(const int &data, int a, int b) {
  int la = 10;
  printf("data = %d\n", data);
  int lb = 20;
  printf("&data = %p, &a = %p, &b = %p\n", &data, &a, &b);
  printf("&la = %p, &lb = %p\n", &la, &lb);
}

int main() {
  int a = 10;
  Test(a, 10, 20);
  printf("----------------\n");
  Test(100, 30, 40); // 如果形参不是 const 左值引用的话,那么这句话编译会报错
  int b = 20;
  printf("main, &a = %p, &b = %p\n", &a, &b);
  return 0;
}

代码运行结果如下,从运行结果来看,当传值 100 的时候,实参保存在了 main 函数的栈里,而不是和另外两个形参 a 和 b 挨着保存。

2 右值和右值引用

右值是不能取地址的,等号右边的值。

如下代码,说明如下:

(1)Test() 函数的第一个形参是右值引用,这样的话在调用函数的时候,第一个实参可以直接传右值,而不需要像左值引用那样,形参必须是 const 左值引用,才可以传右值。

#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#include <string>

void Test(int &&a, int b, int c) {
  int la = 100;
  printf("a = %d, b = %d, c = %d\n", a, b, c);
  printf("&a = %p, &b = %p, &c = %p\n", &a, &b, &c);
  int lb = 100;
  printf("&la = %p, &lb = %p\n", &la, &lb);
  return;
}

int main() {
  int a = 100;
  Test(1, 2, 3);
  Test(4, 5, 6);
  Test(7, 8, 9);
  int b = 200;
  printf("\n--------------------------------\n");
  printf("main, &a = %p, &b = %p\n", &a, &b);

  int &&c = 10;
  printf("&c = %p\n", &c);
  c = 100;
  printf("c = %d, &c = %p\n", c, &c);
  return 0;
}

(2)运行结果如下,在函数 Test() 中打印了形参 a、b、c 的地址,发现 b 和 c 的存储是挨着的,a 和它俩并不是挨存储的。a 存储在 main() 函数的栈里,并且多次调用的时候,使用的是一块相同的地址。多次调用复用一块地址。

使用右值引用的时候,稍有不慎就很容易出现 bug。c++ 中引用的出现本来就是为了代替 c 语言中的指针,简化使用的,但是随着版本的迭代,引用却越来越复杂。

本人在工作中就遇到过右值引用使用不当导致的 bug,链接放在下边。

[c++] 记录一次引用使用不当导致的 bug

(3)右值引用,只不过是 c++ 中的一个语法,右值引用的底层实现和 const 左值引用是类似的,也是有自己的存储内存的,上边的例子就是保存在栈上,只不过这块内存是编译器来分配的,开发者不感知;右值引用和 const 左值引用的区别是,前者可以修改,后者不可以修改。

(4)右值引用本身是一个左值,因为右值引用在 = 左边,可以取地址

如下代码,左值引用可以使用 a 进行初始化,a 和 b 指向同一块内存区域。

#include <iostream>
#include <string>

int main() {
  int &&a = 10;
  int &b = a;
  printf("a = %d, b = %d\n", a, b);
  b = 100;
  printf("a = %d, b = %d, &a = %p, &b = %p\n", a, b, &a, &b);
  return 0;
}

(5)右值引用不能使用左值进行初始化

类似于左值引用在声明的时候需要初始化,右值引用在声明的时候也需要初始化。 

非 const 左值引用不能使用右值初始化,同样的,右值引用也不能使用左值进行初始化。如下面的代码 int &&c = a; int &&d = b,这两行代码都会导致编译错误。可以使用 std::move() 将左值转换为右值,从打印地址来看,c 和 a 指向同一个地址,d 和 b 指向同一个地址。

#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#include <string>

int main() {
  int a = 10;
  int &&b = 20;

  // int &&c = a; // cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’
  // int &&d = b; // cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’
  printf("a = %d, b = %d, &a = %p, &b = %p\n", a, b, &a, &b);
  int &&c = std::move(a);
  int &&d = std::move(b);
  c = 100;
  d = 200;
  printf("a = %d, b = %d, &a = %p, &b = %p\n", a, b, &a, &b);
  printf("c = %d, d = %d, &c = %p, &d = %p\n", c, d, &c, &d);
  return 0;
}

从 std::move 的官方说明也能看出来,std::move() 可以将变量类型转变为右值引用类型。

3 为什么要引入右值引用

右值引用,理解和使用起来,都有点难度,并且使用时容易出错,但是在某些场景下,那为什么还要引入右值引用呢,使用右值引用对代码能带来什么优化 ?

3.1 移动语义

右值引用,最明显的意义是支持移动语义。在右值引用之前,c++ 中是不支持移动语义的,比如我要移动一个对象,那么会首先进行拷贝,将旧对象的数据拷贝到新对象中,然后旧对象销毁。这种方式我们习以为常,但这种方式的效率却是比较低下的。假想我们搬家的场景,也是我们的生活物品移动的过程,我们会先把物品再重新买一份,然后把旧物品销毁吗,当然不会的。

c++ 中移动语义通过 std::move() 来实现,移动的意思就是将对象中的内容移动到新对象中了,那么旧对象就不再使用了。

如下代码,是将 std::string 进行移动,将 str 移动到 str1 中。

#include <iostream>
#include <string>
#include <stdlib.h>
#include <stdio.h>

int main() {
  std::string str = "hello world, hello world";
  std::cout << "1, str " << str << ", str size " << str.size() << std::endl;
  printf("str addr = %p\n", str.c_str());
  std::string str1 = std::move(str);
  std::cout << "2, str " << str << ", str size " << str.size() << ", capacity " << str.capacity() << std::endl;
  printf("str1 addr = %p, str addr = %p\n", str1.c_str(), str.c_str());

  std::string str2 = str1;
  printf("str2 addr = %p\n", str2.c_str());
  return 0;
}

运行结果如下,从运行结果我们可以得到以下几点:

(1)str 通过 std::move() 被移动到 str1 之后,str 中就没有字符串了,size 是 0

(2)打印出了 str 和 str1 的字符串的地址,可以看到两者是相等的,这也能说明,在 str 移动到 str1 的过程中,没有进行拷贝,而是将字符串的地址赋值给了 str1;str2 使用 str1 进行赋值,没有使用 std::move(),那么就时通过拷贝来完成的。

(3)上边的代码,字符串长度大于 15,这样才能观察到移动的现象;如果字符串长度不大于 15 的话,每个 std::string 都会有一个自带的数组来保存字符串,这种情况下仍然会拷贝,而不是移动,因为当字符串不大于 15 的时候,std::string 不会申请内存,而是使用自带的字符串。

3.2 移动构造

移动构造也是一种构造方式,形参是右值引用数据类型。移动构造在如下 3 种情况下会调用:

(1)显示使用 std::move() 构造一个新对象时

(2)使用 std::move() 进行函数传参时

(3)函数返回对象值的时候。这种情况下默认会有返回值优化,看不到移动的过程,如果编译的时候将返回值优化关闭,则可以看到移动过程

#include <iostream>
#include <string>

class Test {
public:
    Test(int data) : data_(data) {
        std::cout << "constructor " << data_ << std::endl;
    }

    Test(const Test& o) : data_(o.data_) {
        std::cout << "copy constructor " << data_ << std::endl;
    }

    Test(Test&& o) noexcept : data_(o.data_) {
        std::cout << "move constructor " << data_ << std::endl;
    }

    void PrintData() {
      std::cout << "data = " << data_ << std::endl;
    }

private:
    int data_;
};

void func1(Test t) {
  t.PrintData();
}

Test func2(Test t) {
  return t;
}

Test func3() {
  Test t(10);
  return t;
}

int main() {
    Test t1(1);
    Test t2(std::move(t1)); // 显示调用 std::move() 进行构造
    func1(std::move(t2)); // 显式调用 std::move() 进行传参

    Test t3(3);
    func2(t3); // 函数返回 t 对象

    Test t4 = func3(); // 默认有返回值优化,只构造一次,如果编译时使用 -fno-elide-constructors 关闭返回值优化,则在函数内构造一次,返回和赋值的时候后 move 两次
    return 0;
}

移动构造函数在什么情况下才会默认生成 ? 移动构造默认生成的限制条件是比较多的,不想拷贝构造函数,用户不显式声明的话,编译器就会隐式生成。以下条件都满足的情况下,编译器才会隐式生成移动构造函数。

(1)用户没有定义拷贝构造函数。如果用户定义了拷贝构造函数,那么编译器不会生成移动构造函数。

(2)用户没有定义拷贝赋值运算符。如果用户定义了拷贝赋值运算符,那么编译器不会生成移动构造函数。

(3)用户自定义了移动构造函数。如果用户自定义了移动构造函数,那么编译不再默认生成。

(4)用户没有定义析构函数。如果用户定义了析构函数,那么编译器不会生成默认的移动构造函数。

std::vector 中的数据使用一个动态数组来维护的。如果一直向 vector 中  push 数据的话,那么就会频繁出现数组扩容的情况,数组扩容的话,就是申请一块新的内存,然后将旧数组中的数据拷贝到新数组中,然后销毁旧数组。

如果对象支持移动构造的话,那么在 std::vector 扩容的时候就会使用移动构造来移动数组中的元素,而不是使用拷贝加销毁的方式来移动元素。

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

class Test {
public:
    Test(int data) : data_(data) {
        std::cout << "constructor " << data_ << std::endl;
    }

    Test(const Test& o) : data_(o.data_) {
        std::cout << "copy constructor " << data_ << std::endl;
    }

    Test(Test&& o) noexcept : data_(o.data_) {
        std::cout << "move constructor " << data_ << std::endl;
    }

    void PrintData() {
      std::cout << "data = " << data_ << std::endl;
    }

private:
    int data_;
};

int main() {
  std::vector<Test> v;

  std::cout << "1, v size = " << v.size() << ", capacity = " << v.capacity() << std::endl;
  Test t1(1);
  v.push_back(t1);
  std::cout << "2, v size = " << v.size() << ", capacity = " << v.capacity() << std::endl;

  Test t2(2);
  v.push_back(t2);
  std::cout << "3, v size = " << v.size() << ", capacity = " << v.capacity() << std::endl;
  return 0;
}

运行结果如下,从运行结果可以看出来,在向数组中 push_back t2 之前,数组容量是 1,所以需要扩容,扩容的时候,对于数组中已经存在 t1,使用了移动构造的方式,而不是使用拷贝构造。

移动构造函数声明为 noexcept 的时候,编译器才会使用移动构造;如果没有 noexcept 修饰,那么仍然使用拷贝构造。

移动构造的形参是右值引用,右值引用的意思是这个值是一个临时值,函数返回之后,生命周期就结束了。std::move() 的意思也是,被 move 之后的对象只剩下一个空壳子了。std::move() 和右值引用结合起来使用,语义是一致的。

3.3 移动赋值运算符

移动赋值运算符,拷贝赋值运算符有些类似,当给一个已经创建的对象赋值的时候,才会调用赋值运算符;如果这个对象还不存在,那么调用的是拷贝构造函数或者移动构造函数。

在满足如下条件之后,编译器才会生成默认的移动赋值运算符。有一点需要注意,如果显式声明了移动构造函数,那么也不会默认生成移动赋值。

如下是官方的例子。

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

struct A
{
    std::string s;

    A() : s("test") {}

    A(const A& o) : s(o.s) { std::cout << "move failed!\n"; }

    A(A&& o) : s(std::move(o.s)) {
      std::cout << "move construct\n";
    }

    A& operator=(const A& other)
    {
         s = other.s;
         std::cout << "copy assigned\n";
         return *this;
    }

    A& operator=(A&& other)
    {
         s = std::move(other.s);
         std::cout << "move assigned\n";
         return *this;
    }
};

A f(A a) { return a; }

struct B : A
{
    std::string s2;
    int n;
    // implicit move assignment operator B& B::operator=(B&&)
    // calls A's move assignment operator
    // calls s2's move assignment operator
    // and makes a bitwise copy of n
};

struct C : B
{
    ~C() {} // destructor prevents implicit move assignment
};

struct D : B
{
    D() {}
    ~D() {} // destructor would prevent implicit move assignment
    D& operator=(D&&) = default; // force a move assignment anyway
};

int main()
{
    A a1, a2;
    std::cout << "Trying to move-assign A from rvalue temporary\n";
    a1 = f(A()); // move-assignment from rvalue temporary
    std::cout << "Trying to move-assign A from xvalue\n";
    a2 = std::move(a1); // move-assignment from xvalue, A 中显式定义了移动赋值运算符

    std::cout << "\nTrying to move-assign B\n";
    B b1, b2;
    std::cout << "Before move, b1.s = \"" << b1.s << "\"\n";
    b2 = std::move(b1); // calls implicit move assignment,B 中没有显式定义移动赋值运算符,也没有定义拷贝构造,移动构造,拷贝赋值,析构函数,所以会生成默认的移动赋值运算符
    std::cout << "After move, b1.s = \"" << b1.s << "\"\n";

    std::cout << "\nTrying to move-assign C\n";
    C c1, c2;
    c2 = std::move(c1); // calls the copy assignment operator,定义了析构函数,那么移动构造和移动赋值运算符都不会默认生成

    std::cout << "\nTrying to move-assign D\n";
    D d1, d2;
    d2 = std::move(d1); // 定义了析构函数,但是使用 default 强制生成了默认移动构造,所以会调用移动构造
}

4 参考

std::move - cppreferenc.com

Move constructors - cppreference.com

Move assignment operator - cppreference.com

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

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

相关文章

算法详解——图的深度优先遍历和广度优先遍历

目录 一、图结构二、深度优先遍历2.1 图的遍历2.2 深度优先遍历过程2.3 深度优先遍历核心思想2.4 深度优先遍历实现 三、广度优先遍历3.1 广度优先遍历过程3.2 广度优先遍历核心思想3.3 广度优先遍历实现 参考文献 一、图结构 图结构指的是如下图所示的由节点和边组成的数据。 …

力扣刷题Days18-190颠倒二进制位(js)

目录 1&#xff0c;题目 2&#xff0c;代码 1&#xff0c;逐位颠倒 800001011 循环过程&#xff1a; 最终结果&#xff1a; 3&#xff0c;学习与总结 1&#xff0c;<< 位运算符 1&#xff0c;题目 颠倒给定的 32 位无符号整数的二进制位。 2&#xff0c;代码 1…

高频:spring知识

1、bean的生命周期&#xff1f; 主要阶段 初始化 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh 信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext67424e82: startup date []; root of context hi…

代码原理文献阅读(4)_3.12

4.2.经济调度问题(ED) 事实上&#xff0c;UC本质上也是一种ED问题。但随着电力电子技术的快速发展&#xff0c;越来越多的新型设备接入电力系统&#xff0c;调度逐渐变得"杂"、"散"、"灵活" 。此时&#xff0c;系统受到不确定性的影响更加强烈&a…

微信小程序 uniapp+vue学生考勤签到系统19j29

签到基本是没一个学生和教育工作者都要面对的一个事情&#xff0c;传统的签到都是需要单独找老师签到&#xff0c;或者老师课堂上单名来完成的&#xff0c;但是随着时代的发展这种比较落后的管理方式已经逐渐的被广大高校摒弃&#xff0c;教育工作者需要一种更加灵活且操作方便…

OCR-free相关论文梳理

⚠️注意&#xff1a;暂未写完&#xff0c;持续更新中 引言 通用文档理解&#xff0c;是OCR任务的终极目标。现阶段的OCR各种垂类任务都是通用文档理解任务的子集。这感觉就像我们一下子做不到通用文档理解&#xff0c;退而求其次&#xff0c;先做各种垂类任务。 现阶段&…

Linux系统架构----Tomcat 部署

一.Tomcat概述 Tomcat服务器是一个免费的开放式源代码的web应用服务器&#xff0c;属于轻量级应用级服务器&#xff0c;在中小型系统和并发访问用户不是很多的场合下被普遍使用&#xff0c;是开发和调试JSP程序的首首选。 一般来说&#xff0c;tomcat虽然和Apache或者Nginx这些…

CSS:实现择色器透明度条的两种方法(附赠一个在线图片转base64网站)

一、效果展示 二、实现方式 1.锥形渐变 .main{width: 600px;height: 20px;background: repeating-conic-gradient(rgba(1, 1, 1, 0.1) 0 25%,transparent 0 50%);background-size: 20px 20px;} 2.背景图 将一个四方格图片转为base64然后直接在css中使用 .main1 {width: 600p…

RabbitMQ 模拟实现【四】:虚拟主机设计

文章目录 虚拟主机设计虚拟主机分析交换机和虚拟主机之间的从属关系核心 API发布消息订阅消息应答消息消费者管理类 虚拟主机设计 虚拟主机分析 类似于 MySQL 的 database&#xff0c;把交换机&#xff0c;队列&#xff0c;绑定&#xff0c;消息…进⾏逻辑上的隔离&#xff0…

医学数据分析中缺失值的处理方法

医学数据分析中缺失值的处理方法 &#xff08;为了更好的展示&#xff0c;在和鲸社区使用代码进行展示&#xff09; 医学数据分析中&#xff0c;缺失值是不可避免的问题。缺失值的存在会影响数据的完整性和准确性&#xff0c;进而影响分析结果的可靠性。因此&#xff0c;在进…

php+vue+mysql公司员工薪酬工资管理系统

采用面向对象的思维方式&#xff0c;以符合实际的功能与性能要求&#xff0c;并进行了创新。为了提升小型企业工资管理的自动化和友善性的小型企业工资管理系统。 本文提出了一种基于面向对象的思想方法&#xff0c;以适应系统的实际功能与性能要求。为了使小型企业工资管理更具…

柚见第十期(后端队伍接口详细设计)

创建队伍 用户可以 创建 一个队伍&#xff0c;设置队伍的人数、队伍名称&#xff08;标题&#xff09;、描述、超时时间 P0 队长、剩余的人数 聊天&#xff1f; 公开 或 private 或加密 信息流中不展示已过期的队伍 请求参数是否为空&#xff1f;是否登录&#xff0c;未登录不…

决策树 | 分类树回归树:算法逻辑

目录 一. 决策树(Decision Tree)1. 决策树的构建1.1 信息熵(Entropy)1.1.1 信息量&信息熵 定义1.1.2 高信息熵&低信息熵 定义1.1.3 信息熵 公式 1.2 信息增益(Information Gain)1.2.1 信息增益的计算1.2.2 小节 2. 小节2.1 算法分类2.2 决策树算法分割选择2.3 决策树算…

Python应用数值方法:工程与科学实践指南

信息技术时代的挑战与机遇 我们正处在一个信息技术高速发展的时代&#xff0c;这是一个科技与创新蓬勃发展的时代。大数据与人工智能的崛起&#xff0c;正以前所未有的速度推动着传统技术的智能化变革。这种变革不仅带来了前所未有的机遇&#xff0c;也对科学和工程技术人员的…

什么时候要分库分表

对于一个日活用户在百万数量级的商城来说&#xff0c;每天产生的订单数量可能在百万级&#xff0c;特别在一些活动促销期间&#xff0c;甚至上千万。 假设我们基于单表来实现&#xff0c;每天产生上百万的数据量&#xff0c;不到一个月的时间就要承受上亿的数据&#xff0c;这…

水库大坝安全监测中需要注意的事项

随着经济和社会的发展&#xff0c;水资源的需求也在不断增加。因此&#xff0c;建设水库已成为保障水资源的主要方式之一。然而&#xff0c;随着水库规模的增大和工程的复杂性的增加&#xff0c;水库大坝的安全问题也日益引起重视。为此&#xff0c;需要对水库大坝进行安全监测…

2024年云服务器ECS价格表出炉——阿里云

2024年阿里云服务器租用费用&#xff0c;云服务器ECS经济型e实例2核2G、3M固定带宽99元一年&#xff0c;轻量应用服务器2核2G3M带宽轻量服务器一年61元&#xff0c;ECS u1服务器2核4G5M固定带宽199元一年&#xff0c;2核4G4M带宽轻量服务器一年165元12个月&#xff0c;2核4G服务…

变量的本质和命名规则

变量的本质 内存:计算机中存储数据的地方&#xff0c;相当于一个空间变量本质:是程序在内存中申请的一块用来存放数据的小空间 变量命名规则与规范 规则: 不能用关键字 关键字:有特殊含义的字符&#xff0c;JavaScript 内置的一些英语词汇。例如:let、var、if、for等>只…

2024阿里技术官重磅推出“Java进阶必备宝典” 5大专题 6000字解析

5.JVM实战 CPU占用过高案例实战 内存占用过高案例实战 15种方式编写高效优雅Java程序实战 6.JVM底层技术 亿级流量高井发下GC预估与调优 JHSDB工具透视L ambda底层实现 JVM(HotSpot)核心源码解读 JVM核心模块(GC算法)手写实战 核心三&#xff1a;网络编程与高效IO 1.网络…

人形双臂机器人重大进展!顶刊公布业界首个双臂通用协同操作架构

图1&#xff1a;人居环境下的人形双臂机器人系统 通用人形机器人作为近年来机器人与AI交叉领域的研究热点和技术竞争高地&#xff0c;因其具备在非结构化人居环境中承担各种琐碎家务的潜力而得到广泛关注。人形双臂系统直接承载着人形机器人操作任务的执行能力&#xff0c;通用…