C++引用与移动语义

news2024/11/24 12:11:37

目录

一.引用分类

1.名词解释

1).左右值 

二.引用(左值引用)

1.左值引用(Lvalue Reference):

2.本质

3.形式

4.注意

5.示例

1)引用做左值

2)引用做函数返回值

三.右值引用

1.右值引用绑定一个常量

2.右值引用绑定一个变量

3.move移动语义

四.const引用

1.作为只读变量(常量)

2.作为只读变量的引用

3.const引用绑定将亡对象

五.auto与引用折叠

1.引用折叠

2.auto&

3.auto &&

六.引用传递失效与完美转发


一.引用分类

1.名词解释

1).左右值

         左值是表达式结束后仍然存在的持久对象。比如:变量、函数或数据成员的名字
返回左值引用的表达式,如 ++x、x = 1、cout << ' '、字符串字面量如 "hello world"

         右值是指表达式结束时就不存在的临时对象。返回非引用类型的表达式,如 x++、x + 1、make_shared(42),除字符串字面量之外的字面量,如 42、true
 

区分左值和右值的便捷方法是看能不能对表达式取地址,如果能则为左值,否则为右值;

2).将亡值

  将亡值是C++11新增的、与右值引用相关的表达式,比如:将要被移动的对象、T&&函数返回的
值、std::move返回值和转换成T&&的类型的转换函数返回值。

C++11中的所有的值必将属于左值、将亡值、纯右值三者之一,将亡值和纯右值都属于右值
 


     

    C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高了程序性能。根据左右值使用可分为左值引用,右值引用,以及常量左值引用。这些引用类型在C++中用于不同的场景,允许对对象进行不同级别的操作和访问控制。左值引用和右值引用还涉及到移动语义,用于提高资源管理的效率。

   

二.引用(左值引用)

1.左值引用(Lvalue Reference):

左值引用是最常见的引用类型。它通过在变量名前加上 & 符号来声明,用于创建已存在对象的别名。左值引用可以用于读取和修改已存在对象的值

2.本质

引用是为已存在的变量取了一个别名,引用和引用的变量共用同一块内存空间。

3.形式

int &b = a;

4.注意

  • 引用创建的时候必须初始化,不能指定为NULL。
  • 引用必须与一个合法的单元连用
  • 引用并不是创建一个数据类型,它不是一个独有的类型
  • 引用不能返回局部变量
  • 引用在只在定义的时候加&
  • 引用只是一个别名,不占内存(编译器处理)
  • 引用在形式上是指针常量,引用以后指向不能发生改变。
  • 一个变量可以有多个引用,不同的引用不同的空间

5.示例

1)引用做左值

int num= 42;
int& ptrNum = num;

2)引用做函数返回值

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

using namespace std; 

int & Function(int & a)
{
	return a;
}
int main()
{
	int x1=5;
	cout<< x1<<endl;
	int &P =  Function(x1);
	cout<< x1<<endl;
	return 0;
}

需要注意以前几点:

  • 引用作为函数的返回值时,必须在定义函数时在函数名前将&。

  • 用引用作函数的返回值的最大的好处是在内存中不产生返回值的副本。

  • 函数返回值可以是一个对象引用,在这种情况下函数中的return语句必须返回一个变量或可以做为左值的表达式。不要返回局部变量的引用,原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为”无所指”的引用,程序进入未知状态。(可以使用右值引用避免这种情况)

三.右值引用

根据左右值定义,常量只能做右值,而变量做右值时仅会读取。按照这个定义来理解,“右值引用”就是对“右值”的引用了,而右值可能是常量,也可能是变量,那么右值引用自然也是分两种情况来不同处理。与左值引用不同的是,右值引用并不是为了让引用的对象只能做右值。否则为什么不能这样呢?(int a=2,int &&b = a;)const引用的提出已经能解决此问题。右值引用本意是可以做为左值的

1.右值引用绑定一个常量

    和const引用一样,常量没有地址,没有存储位置,只有值,因此,要把这个值保存下来的话,同样得按照“新定义变量”的形式,因此,当右值引用绑定常量时,相当于定义了一个普通变量

int &&a = 5;
// 等价于
int a = 5; // a就是个普通的int变量而已,并不是引用

因此,右值一旦引用,就相当于一个左值! 

因此,不能以下这样写法!

int &&a = 1;
int &&b = a;

2.右值引用绑定一个变量

先看此段代码:

#include <iostream>
using namespace std;
class A
{ 
public:
    A() :m_ptr(new int(0)) 
    {
        cout << "constructor A" << endl;
    } 
    ~A()
    {
        cout << "destructor A, m_ptr:" << m_ptr << endl;
        delete m_ptr;
        m_ptr = nullptr;
    }
    private:
    int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
    A a;
    A b;
    cout << "ready return" << endl;
    if (flag)
        return a;
    else
        return b;
} 
int main()
{
    {
        A a = Get(false); // 运行报错
    } 
    cout << "main finish" << endl;
    return 0;
}

在解读这段代码之前,我们先来复习以下拷贝构造

拷贝构造函数(const 类名& 引用名){ … }

在以下三种情况下拷贝构造函数会自动被调用

1.已经创建完毕的对象初始化一个新的对象。

2.值传递方式给函数传参

3.以值方式返回局部对象

上述代码属于第三种情况导致的拷贝构造。

而根据规则,当用户没有定义拷贝构造的时候,C++会执行默认拷贝构造函数,进行浅拷贝(直接将原内容的地址交给要拷贝的类,两个类共同指向同一空间),这样执行上述代码,则会造成两次析构。

[root@fedora quote]# ./a.out 
constructor A
constructor A
ready return
destructor A, m_ptr:0x19522e0
destructor A, m_ptr:0x1951eb0
destructor A, m_ptr:0x19522e0
free(): double free detected in tcache 2
已放弃(核心已转储)

   因此我们应该提供深拷贝操作

A(const A& a) :m_ptr(new int(*a.m_ptr)) 
{
    cout << "copy constructor A" << endl;
}

    面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对
象 b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。因此,右值引用(移动构造)作用就体现出来了。

A(A&& a) :m_ptr(a.m_ptr) 
{
    a.m_ptr = nullptr;
    cout << "move constructor A" << endl;
}

        与拷贝构造不同的是,拷贝构造函数A(A&& a)则是创建一个与原始对象相等的新对象,它会复制资源。移动构造函数A(A&& a)用于将资源从一个对象移动到另一个对象,而不复制资源,从而提高性能。

        此外,移动构造函数使用了右值引用(&&),表示可以接收临时变量或被 std::move() 调用的对象;而复制构造函数使用了左值引用(&),表示只能接收常规变量的引用。

3.move移动语义

    move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义(左值转换为右值),没有内存拷贝。换句话说就是把一个对象强制转换为一个将亡对象。

#include <iostream>
#include <vector>
#include <string.h>

class MyString {
public:
    MyString(const char* str) {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }

    // 移动构造函数,使用右值引用
    MyString(MyString&& other) noexcept {
        data = other.data;
        length = other.length;
        other.data = nullptr; // 防止资源重复释放
        other.length = 0;
    }

    // 移动赋值运算符,使用右值引用
    MyString& operator=(MyString&& other) noexcept {
        if (this != &other) {
            delete[] data; // 释放当前对象的资源

            data = other.data;
            length = other.length;
            other.data = nullptr; // 防止资源重复释放
            other.length = 0;
        }
        return *this;
    }

    ~MyString() {
        delete[] data;
    }

    void print() const {
        std::cout << data << std::endl;
    }

private:
    char* data;
    size_t length;
};

int main() {
    MyString str1("Hello, World!");

    // 使用 std::move 来移动 str1 到 str2
    MyString str2(std::move(str1));  //强制让buf1将亡,那么右值引用就可以接收

    // 注意:此时 str1 不再有效,因为它的资源已经被移动到 str2

    // str1.print(); // 这将导致未定义行为,因为 str1 的资源已被移动

    str2.print(); // 输出 "Hello, World!"

    // 使用移动赋值运算符来将 str2 移动给 str3
    MyString str3 = std::move(str2);

    // 同样,str2 不再有效,其资源已经被移动到 str3

    // str2.print(); // 这将导致未定义行为

    str3.print(); // 输出 "Hello, World!"

    return 0;
}

值得注意的是,move语义并不是真的将变量转换为“将亡变量”。只是将引用(左值)强制转换为右值引用罢了。
总结,右值引用作用主要是两个:

        避免深拷贝,与move语义来连用提升性能。

        延长临时变量生命周期,避免不必要的复制。

四.const引用

由于C++保留了C的const关键字,其主旨更想希望表达其“不可变”的含义。既然不可变,就有常量和只读变量之分。所以const引用分为以下两个方面含义:

1.作为只读变量(常量)

对于const引用,其实根本不是引用,就是一个普通的只读变量

const int &a = 8;
// 等价于
const int a = 8; // a其实就是个独立的变量,而并不是谁的引用

2.作为只读变量的引用

当用一个const引用来接收一个变量的时候,这时的引用是真正的引用,其实在p1内部保存了a的地址,当我们操作r的时候,会通过解指针的语法来访问到a

const int a = 5;

const int &p1 = a;
// 等价于
const int *p1 = &a; // 引用初始化其实是指针的语法糖

3.const引用绑定将亡对象

  const引用同样可以让将亡对象延长生命周期,但其实设计初衷是const引用更倾向于“引用一个不可变的量。

struct Test {
  int a, b;
};

Test GetAnObj() {
  Test t {1, 2}; 
  return t; // t会复制给临时空间
}

void Demo() {
  const Test &t1 = GetAnObj(); // 我设法引用这片临时空间,并且让他不要立刻释放
  // 临时空间被t1引用了,并不会立刻释放
}

五.auto与引用折叠

1.引用折叠

当套用模板或者某些情况下出现下述表达式,就会造成引用折叠。

void f(int & &t);
void f(int && &t);

引用折叠最终推导结果:

& + & -> &
& + && -> &
&& + & -> &
&& + && -> &&

2.auto&

   原则:由于&比&&优先级高,因此auto &一定推出左值引用,如果用auto &绑定常量或将亡对象则会报错

看示例:

auto &r1 = 5;  //错误

上述表达式中,5属于临时非常量对象,他没有具体的内存位置,因此不能绑定到一个非常量引用上。可以修改为 const auto &r1 = 5;

auto &r2 = GetAnObj(); //错误

左值引用不能绑定将亡对象

int &&b = 1;
auto &r3 = b; //OK

 左值引用可以绑定右值引用(因为右值引用一旦绑定后,相当于左值)

3.auto &&

原则:auto&&遇到左值会推导出左值引用,遇到右值才会推导出右值引用。

当出现下述场景的时候:

auto &&r1 = 5; 

5是右值,所以等价于int &&r1 = 5.

int a;
auto &&r2 = a; 

a是左值, 推导出int &

int &&b = 1;
auto &&r3 = b; 

 等价int b = 1,相当于普通变量,b是左值。推导出int &r3=b;

六.引用传递失效与完美转发

在第3.1节曾提到过:

int &&a = 10;
int &&b = a; //错误

右值一旦绑定,就会变成左值。test1(1)调用后变成左值,不满足test(&&)传参,右值引用传递时会失去右性,编译提示报错,如果解决呢?

 使用std::forward()完美转发

std::forward() 是 C++ 语言中的一个函数模板,用于实现完美转发。它主要用于在函数模板中保持传递参数时的值类别和引用限定符的完整性。在 C++ 中,当我们将参数传递给另一个函数时,我们通常会使用引用来避免不必要的拷贝。然而,传递参数时,参数的值类别(lvalue 或 rvalue)和引用限定符(const 或 non-const)会被编译器推断并保留。在某些情况下,我们希望将这些信息保留并传递给另一个函数,这就是完美转发的目的。

std::forward() 函数模板的定义位于 <utility> 头文件中。它接受一个参数,并将该参数转发给另一个函数。该函数模板的定义如下:

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

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

std::forward() 函数模板有两个重载版本,分别用于左值和右值引用。它们通过引用折叠规则来保持原始参数的值类别和引用限定符。传递给 std::forward() 的参数是一个引用,然后 std::forward() 返回一个相同类型的引用。这种转发可以确保参数被保留并以相同的方式传递。

经过修改编译就可以通过了:

int &&a = 10;
int &&b = std::forward<int>(a);

再看下面示例:

#include <iostream>
using namespace std;
template <class T>

void Print(T &t)
{
    cout << "L" << t << endl;
} 

template <class T>
void Print(T &&t)
{
    cout << "R" << t << endl;
} 

template <class T>
void func(T &&t)
{
    Print(t);
    Print(std::move(t));
    Print(std::forward<T>(t));
}

int main()
{
    cout << "-- func(1)" << endl;
    func(1); //L R L
    int x = 10;
    int y = 20;
    cout << "-- func(x)" << endl;
    func(x); // x本身是左值 L R L
    cout << "-- func(std::forward<int>(y))" << endl;
    func(std::forward<int>(y)); //L R R
return 0;
}

输出结果:

其实对于前几个,大家应该都能推断出来。重点说一下最后一个(func(std::forward<int>(y));),也说一下forward的本质。

实际上,std::forward 的行为是根据模板参数的类别来确定的,而不是根据传递给它的参数的值类别。

在这个表达式中,func(std::forward<int>(y))的y是一个左值,但实际调用过程中,参数模板类型int是根据y的值决定的。并不是根据你所传输的模板int决定的。在这种情况下:func(std::forward<int>(y)) 中的 T 推断为 int&&

程序调用了接受右值引用的 Print() 版本,所以最终执行结果是R20

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

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

相关文章

代码随想录算法训练营day|139.单词拆分|多重背包基础力理论| 背包总结

139.单词拆分 力扣题目链接 给定一个非空字符串 s 和一个包含非空单词的列表 wordDict&#xff0c;判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。 说明&#xff1a; 拆分时可以重复使用字典中的单词。 你可以假设字典中没有重复的单词。 示例 1&#xff1a…

centos 端口被占用的快速排查方式

问题笔记 centos 端口被占用的快速排查方式 centos 端口被占用的快速排查方式 这里说一个我刚刚遇到的问题&#xff0c;解决步骤用来记录&#xff0c;方便以后自己查询。 nginx配置完index.html测试文件&#xff0c;发现一直显示的404页面。 我跑到服务器上想重启一下nginx …

如何从Git上拉取项目

1.Git的概念 Git是一个开源的分布式版本控制系统&#xff0c;可以有效、高速的处理从很小到非常大的项目版本管理。它实现多人协作的机制是利用clone命令将项目从远程库拉取到本地库&#xff0c;做完相应的操作后再利用push命令从本地库将项目提交至远程库。 2.Git的工作流程 …

Meshmixer在数字牙科的实践

数字牙科&#xff08;Digital Dentistry&#xff09;在口腔健康领域获得越来越多的空间&#xff0c;如何使用 Meshmixer 软件在数字牙科中创建 3D 模型对于该领域的专业人士来说是一项宝贵的技能。 在本文中&#xff0c;拟将学习如何掌握这个强大的工具并创建令人惊叹的 3D 模型…

本地MQTT服务器搭建(EMQX)

一、下载EMQX 下载地址&#xff1a;EMQ (emqx.com) 打开官网后&#xff0c;选择右边的免费试用按钮 然后单击EMQX Enterprise标签&#xff0c;然后选择下面的EMQX开源版&#xff0c;选择开源版的系统平台为Windows&#xff0c;单击免费下载。 在新页面下单击立即下载 二、安装…

第1章_瑞萨MCU零基础入门系列教程之单片机程序的设计模式

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写&#xff0c;需要的同学可以在这里获取&#xff1a; https://item.taobao.com/item.htm?id728461040949 配套资料获取&#xff1a;https://renesas-docs.100ask.net 瑞萨MCU零基础入门系列教程汇总&#xff1a; ht…

华为云云耀云服务器L实例评测|安装Java8环境 配置环境变量 spring项目部署 【!】存在问题未解决

目录 引出安装JDK8环境查看是否有默认jar上传Linux版本的jar包解压压缩包配置环境变量 上传jar包以及运行问题上传Jar包运行控制台开放端口访问失败—见问题记录关闭Jar的方式1.进程kill -92.ctrl c退出 问题记录&#xff1a;【!】未解决各种方式查看端口情况联系工程师最后排查…

自学Python05-学会Python中的函数定义

亲爱的同学们&#xff0c;今天我们将开始学习 Python 中的函数。函数就像一个魔法盒子&#xff0c;可以让我们在程序中执行一段代码&#xff0c;并且可以反复使用。这样&#xff0c;我们的程序就可以变得更加简洁和易于理解。现在&#xff0c;让我们一起来学习如何使用函数吧&a…

无涯教程-JavaScript - OCT2HEX函数

描述 OCT2HEX函数将八进制数转换为十六进制。 语法 OCT2HEX (number, [places])争论 Argument描述Required/OptionalNumber 您要转换的八进制数。 数字不得超过10个八进制字符(30位)。数字的最高有效位是符号位。其余的29位是幅度位。 负数使用二进制补码表示。 RequiredPl…

报考浙江工业大学MBA项目如何选择合适的辅导班?

浙江工业大学MBA项目每年有数百人报考&#xff0c;在浙江省内除了浙大以外算是人数比较多的一个项目。2023级的招生中第一志愿也通过复试刷掉了百来人&#xff0c;在省内其实作为第一志愿报考的风险在逐渐增大&#xff0c;考生们如果坚持报考&#xff0c;则在针对联考初试的备考…

B站:AB Test 知识全解

AB Test的实质&#xff1a;假设检验&#xff0c;主要有以下几个步骤&#xff1a; 1、在实验开始前&#xff0c;找产品、项目经理等确认&#xff1a;实验需要验证的改动点&#xff08;一次只能看一个&#xff01;&#xff01;&#xff01;&#xff09; 2、数据分析师设计需要去观…

day55 补

392.判断子序列 力扣题目链接(opens new window) 给定字符串 s 和 t &#xff0c;判断 s 是否为 t 的子序列。 字符串的一个子序列是原始字符串删除一些&#xff08;也可以不删除&#xff09;字符而不改变剩余字符相对位置形成的新字符串。&#xff08;例如&#xff0c;&quo…

【MFC】Button控件美化(自绘)

在MFC中Button控件不能通过OnCtlColor&#xff08;&#xff09;函数对外观做太多的改变。 HBRUSH C按钮控件自绘Dlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) {HBRUSH hbr CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor);switch (pWnd->GetDlgCtrlID()){case ID…

【已解决】ORA-00001: unique constraint (USR_JXZX_DSJKF_MODEL.SYS_C00912833) violated

已解决&#xff1a; nested exception is java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (USR_JXZX_DSJKF_MODEL.SYS_C00912833) violated 问题 学号主键ID重复了 思路 在 Oracle 中&#xff0c;可以使用以下方法找出重复的主键&#…

Vue中实现3D得球自动旋转

具体实现 安装echarts 在终端下安装echarts npm install -D echarts 安装echarts-gl 在终端下安装echarts-gl npm install -D echarts-gl earth3D组件 earth3D.vue <template><div class"globe3d-earth-container" ><div class"globe3d-earth&qu…

桉木做的建筑模板质量评价

桉木作为一种常见的建筑模板材料&#xff0c;以其独特的特性在建筑行业中得到广泛应用。然而&#xff0c;评估桉木建筑模板的质量是确保建筑项目成功的关键因素之一。本文将对桉木建筑模板的质量进行评价&#xff0c;从材料特性、强度和耐久性等方面进行分析。 首先&#xff0c…

Pinyin4j介绍和简单使用

前言 Pinyin4j是一个Java库&#xff0c;用于将汉字转换为拼音。它是由中国清华大学的Tsinghua University和中国科学院计算技术研究所的研究人员开发的。Pinyin4j可以用于Java应用程序中&#xff0c;以便在需要时将汉字转换为拼音。例如&#xff0c;它可以用于中文输入法、文本…

大模型tokenizer流式响应解决词句连贯性问题

大模型tokenizer词句连贯性问题 现象 from transformers import LlamaTokenizerFast import numpy as nptokenizer LlamaTokenizerFast.from_pretrained("heilerich/llama-tokenizer-fast") origin_prompt "Hi, Im Minwoo Park from seoul, korea." id…

使用 multiprocessing 多进程处理批量数据

示例代码 import multiprocessingdef process_data(data):# 这里是处理单个数据的过程return data * 2# 待处理的数据 data [1, 2, 3, 4, 5]def normal_func():# 普通处理方式result []for obj in data:result.append(process_data(obj)return resultdef parallel_func():# …

JavaScript编程语法作业

目录 目录 前言 思维导图 1&#xff0c;作业资源 2&#xff0c;if语句练习 2.1代码解读: 2.2,结果展示: 3&#xff0c;switch语句练习 3.1,代码解读: 3.2,结果展示: 4.while循环练习 4.1,代码解读: 4.2.结果展示: 5.do-while循环练习 5.1,代码解读: 5.2,结果展…