C++ 学习(1)---- 左值 右值和右值引用

这里写目录标题

      • 左值
      • 右值
      • 左值引用和右值引用
      • 右值引用和移动构造函数
        • std::move 移动语义
        • 返回值优化
        • 移动操作要保证安全
      • 万能引用
      • std::forward 完美转发
          • 传入左值
          • 传入右值

左值

左值是指可以使用 & 符号获取到内存地址的表达式,一般出现在赋值语句的左边,比如变量、数组元素和指针等。

下面是左值的举例:

int i = 42; // i 是一个 left value
int *p = &i // i 是一个左值,可以通过 & 获取内存地址
int& ldemoFoo() {return i;
}ldemoFoo() = 42;//  这里使用函数返回引用的形式, ldemoFoo 是一个左值
int *p1 = &ldemoFoo() 

左值(lvalue)代表的是对象的身份和它在内存中的位置,它一般出现在赋值语句的左侧,左值通常是可修改的(可以修改的左值)。
Notice : 左值代表一个具体的对象位置,在内存中有明确位置,通常是可以修改的

左值的特点包括:

  • 可寻址性:左值可以取得地址,即你可以使用 & 运算符来取得一个左值的地址。
  • 可赋值性:左值可以出现在赋值语句的左侧,你可以将一个值赋给它。

下面类型的值都是左值:

  • 变量名:如 int x;,x是一个左值。
  • 数组元素:如 arr[0],arr[0] 是一个数组的左值元素。
  • 结构体或类的成员:如 obj.member ,obj.member 是一个对象的左值成员。
  • 解引用的指针:如 *ptr,*ptr 是通过指针访问的对象的左值。

并非所有的左值都是可修改的。例如,const 限定的左值就不应该被修改。

右值

在C++中,右值(rvalue)是指哪些不代表内存的中具体位置, 不能被赋值和取地址的值 。
一般出现在赋值操作符的右边,表达式结束就不存在的临时变量。

右值的典型例子包括 字面量、临时对象以及某些表达式的结果。
右值主要用来表示数据值本身,而不是数据所占据的内存位置。

右值的关键特性包括:

  • 不可寻址性:右值不能取得地址,尝试对右值使用 & 运算符会导致编译错误。
  • 可移动性: 由于右值不代表持久的内存位置,因此可以安全地 “移动” 它们的资源到另一个对象,而无需进行复制。这就是为什么右值经常与移动语义一起使用。
  • 临时性:许多右值是临时对象,它们在表达式结束后就会被销毁。

Notice:右值必须要有一个包含内存地址变量去接收这个指,否则就会丢弃

C++ 中右值的例子有:

  • 字面量:比如整数10、字符’A’、浮点数3.14。
  • 函数返回的临时值:如 getRandomNumber() 返回的随机数,注意函数也有可能返回左值。
  • 由运算符产生的值:比如表达式 a + b 的结果,假设 a 和 b 是数值类型的变量。
  • 空指针常量:nullptr。
  • 字符串字面量:比如"hello world"。
  • 类的右值构造函数或移动构造函数生成的临时对象:如 MyClass() 创建的临时对象。
  • 通过std::move()转换得到的右值:std::move(myObject),其中 myObject 是一个左值。
  • 数组下标的表达式:如果数组是右值,那么数组下标也是右值,例如 arr[0],其中arr是一个临时数组。
  • 类成员的右值访问:如果类有一个返回右值的成员函数,那么该函数返回的结果是右值。
// 10 'A' 都是右值字面量
int a = 10;
char b = 'A';//  generateResult 返回值是一个临时对象,也就是右值
int a = generateResult(20, 10);
// a + b 产生的结果也是右值
int m = a + b;
// "hello world "是右值
const char *pName = "hello world";
// nullptr 是右值
int32_t *p = nullptr;DemoClass p = DemoClass();

注意函数返回值不一定只能是右值,也有可能是左值,比如返回引用的情形

int& testlvaluefuncyion() {int i;return i;
}int testrvaluefuncyion() {int i = 5;return i;
}{testlvaluefuncyion();// 正确,函数返回值可作为左值testlvaluefuncyion() = 10;int *p1 = &testlvaluefuncyion();std::cout << "function return value as leftvalue" << std::endl;
}// 函数返回值是int类型,此时只能作为右值
{testrvaluefuncyion();//testrvaluefuncyion() = 10;std::cout << "function return value as rightvalue" << std::endl;
}

左值引用和右值引用

C++中的引用是一种别名,代表的就是变量的地址本身,可以通过一个变量别名访问一个变量的值。
int &a = b 表示可以通过引用 a 访问变量 b , 注意引用实际就是指向变量 b,等于是变量 b 的别名
左值引用是指对左值进行引用的引用类型,通常使用 & 符号定义
右值引用是指对右值进行引用的引用类型,通常使用 && 符号定义

C++11引入了右值引用,允许我们将右值绑定到引用上。这在 移动语义完美转发 等高级功能中非常有用。

class DemoClass {...};
// 接收一个左值引用
void foo(X& x);
// 接收一个右值引用
void foo(X&& x);X x;
foo(x); // 传入参数为左值,调用foo(X&);X bar();
foo(bar()); // 传入参数为右值,调用foo(X&&);

通过重载左值引用和右值引用两种函数版本,满足在传入左值和右值时触发不同的函数分支。 注意 void foo(const X& x); 同时接受左值和右值传参。

void foo(const X& x);
X x;
foo(x); // ok, foo(const X& x)能够接收左值传参X bar();
foo(bar()); // ok, foo(const X& x)能够接收右值传参// 新增右值引用版本
void foo(X&& x);
foo(bar()); // ok, 精准匹配调用foo(X&& x)

定义右值引用的方法:

int a = 10;
// 定义左值引用
int &lvalue_ref = a; // 定义右值引用
int &&rvalue_ref = 10 + 20; 

右值引用和移动构造函数

假设定义一个类 DemoContainerClass,包含一个指针成员变量 p,该指针指向了另一个成员变量 DemoBasicClass,假设 DemoBasicClass 占用了很大的内存,创建和复制 DemoBasicClass 都需要很大的开销。

class DemoBasicClass {
public:DemoBasicClass() {std::cout << __FUNCTION__ "construct call" << std::endl;}~DemoBasicClass() = default;DemoBasicClass(const DemoBasicClass& ref) {std::cout << __FUNCTION__ "copy construct call" << std::endl;}
};class DemoContainerClass{
private:DemoBasicClass *p = nullptr;
public:DemoContainerClass() {p = new DemoBasicClass();}
~DemoContainerClass() {if( p != nullptr) {delete p;}
}
DemoContainerClass(const DemoContainerClass& ref) {std::cout << __FUNCTION__ "copy construct call" << std::endl;p = ref.p;
}DemoContainerClass& operator=(const DemoContainerClass& ref) {std::cout << __FUNCTION__ "operator construct call" << std::endl;DemoBasicClass* tmp = new DemoBasicClass(*ref.p);delete this->p;this->p = tmp;return *this;
}

上面定义了 DemoContianerClass 的赋值构造函数 ,现在假设有下面的场景

{DemoContainerClass p;DemoContainerClass q;p = q;
}

输出如下:

rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoContainerClass::operator =operator construct call
rValurRefDemo::DemoBasicClass::DemoBasicClasscopy construct call

DemoContainerClass pDemoContainerClass q 初始化时,都会执行 new DemoBasicClass,所以会调用两次 DemoBasicClassconstruct ,执行 p = q 时,会调用一次 DemoBasicClass 的拷贝构造函数,根据 ref 复制出一个新结果。
由于 q 在后面的场景还是可能使用的,为了避免影响 q,在赋值的时候调用DemoBasicClass 的构造函数复制出一个新的 DemobasicClass 给 p 是没有问题的。

但在下面的场景下,这样是没有必要的

static rValurRefDemo::DemoContainerClass demofunc() {return rValurRefDemo::DemoContainerClass();
}{DemoContainerClass p;p = demofunc();
}

这种场景下,demofunc 创建的那个临时对象在后续的代码中是不会用到的,所以我们不需要担心赋值函数中会不会影响到那个 DemobasicClass 临时对象,也就没有必要创建一个新的 DemoBasicClass 类型给 p,
更高效的做法是,直接使用 swap 交换对象的 p 指针,这样做有两个好处:

  1. 不需要调用 DemobasiClass 的构造函数,提高效率
  2. 交换之后,demofunc 返回的临时对象拥有 p 对象的 p 指针,在析构时可以自动回收,避免内存泄漏

这种避免高昂的复制成本,从而直接将资源从一个对象移动到另一个对象的行为,就是C++的 移动语义
哪些场景适合移动操作呢?无法获取内存地址的右值就很合适,我们不需要担心后续的代码会用到这个值。
添加移动赋值构造函数如下:

DemoContainerClass& operator=(DemoContainerClass&& rhs) noexcept {std::cout << __FUNCTION__ "move construct call" << std::endl;std::swap(this->p, rhs.p);return *this;
};

输出结果如下:

###############################################################
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoContainerClass::operator =move construct call
std::move 移动语义

C++提供了std::move函数,这个函数做的工作很简单:通过隐藏掉入参的名字,返回对应的右值。

std::cout << "#############################################" << std::endl;
{DemoContainerClass p;DemoContainerClass q;// OK 返回右值,调用移动赋值构造函数 q,但是 q 以后都不能正确使用了p = std::move(q);}std::cout << "#######################################" << std::endl;
{DemoContainerClass p;// OK 返回右值,调用移动赋值构造函数 效果和 demofunc 一样p = std::move(demofunc());
}

输出结果如下:

###############################################################
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoContainerClass::operator =move construct call
###############################################################
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoContainerClass::operator =move construct call

一个容易犯错的例子:

class Base {
public:// 拷贝构造函数Base(const Base& rhs);// 移动构造函数Base(Base&& rhs) noexcept;
};class Derived : Base {
public:Derived(Derived&& rhs)// wrong. rhs是左值,会调用到 Base(const Base& rhs).// 需要修改为Base(std::move(rhs)): Base(rhs) noexcept {...}
}
返回值优化

考虑下面的情形:

DemobasicClass foo() {DemobasicClass  x;return x;
};DemobasicClass  bar() {DemobasicClass  x;return std::move(x);
}

大家可能会觉得 foo 需要一次复制行为:从 x 复制到返回值;bar 由于使用了 std::move,满足移动条件,所以触发的是移动构造函数:从x移动到返回值。复制成本大于移动成本,所以 bar 性能更好。

实际效果与上面的推论相反,bar中使用std::move反倒多余了。现代C++编译器会有返回值优化。换句话说,编译器将直接在foo返回值的位置构造x对象,而不是在本地构造x然后将其复制出去。很明显,这比在本地构造后移动效率更快。

移动操作要保证安全

比较经典的场景是std::vector 扩缩容。当vector由于push_back、insert、reserve、resize 等函数导致内存重分配时,如果元素提供了一个 noexcept 的移动构造函数,vector 会调用该移动构造函数将元素移动到新的内存区域;否则 则会调用拷贝构造函数,将元素复制过去。

万能引用

完美转发是C++11引入的另一个与右值引用相关的高级功能。它允许我们在函数模板中将参数按照原始类型(左值或右值)传递给另一个函数,从而避免不必要的拷贝和临时对象的创建。

为了实现完美转发,我们需要使用 std::forward 函数和通用引用(也称为转发引用)。通用引用是一种特殊的引用类型,它可以同时绑定到左值和右值。通用引用的语法如下:

通用引用的形式如下:

template<typename T>
void foo(T&& param);

万能引用的ParamType是T&&,既不能是const T&&,也不能是std::vector&&

通用引用的规则有下面几条:

  1. 如果 expr 是左值, T 和 param 都会被推导为左值引用
  2. 如果 expr 是右值, T会被推导成对应的原始类型, param会被推导成右值引用(注意,虽然被推导成右值引用,但由于param有名字,所以本身还是个左值)。
  3. 在推导过程中,expr的const属性会被保留下来。

参考下面示例:

template<typename T>
void foo(T&& param);// x是一个左值
int x =2 7;
// cx 是带有const的左值
const int cx = x;
// rx 是一个左值引用
const int& rx = cx;// x是左值,所以T是int&,param类型也是int&
foo(x);// cx是左值,所以T是const int&,param类型也是const int&
foo(cx);// rx是左值,所以T是const int&,param类型也是const int&
foo(rx);// 27是右值,所以 T 是int,param类型就是 int&&
foo(27);

std::forward 完美转发

template<typename T, typename Arg> 
std::shared_ptr<T> factory_v4(Arg&& arg)
{ return std::shared_ptr<T>(new T(std::forward<Arg>(arg)));
}//  std::forward的定义如下
template<class S>
S&& forward(typename remove_reference<S>::type& a) noexcept
{return static_cast<S&&>(a);
}
传入左值

int p;
auto a = factory_v4§;

根据万能引用的推导规则,factory_v4中的 Arg 会被推导成 int&。这个时候factory_v4 和 std::forwrd等价于:

shared_ptr<A> factory_v4(int& arg)
{ return shared_ptr<A>(new A(std::forward<int&>(arg)));
}int& std::forward(int& a) 
{return static_cast<int&>(a);
}

这时传递给 A 的参数是 int&, 调用的是拷贝构造函数 A(int& ref), 符合预期

传入右值

auto a = factory_v4(3);

shared_ptr<A> factory_v4(int&& arg)
{ return shared_ptr<A>(new A(std::forward<int&&>(arg)));
}int&& std::forward(int&& a) 
{return static_cast<int&&>(a);
}

此时,std::forward作用与std::move一样,隐藏掉了arg的名字,返回对应的右值引用。
这个时候传给A的参数类型是X&&,即调用的是移动构造函数A(X&&),符合预期。
右值引用,移动构造
####重要参考
深浅拷贝和临时对象

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

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

相关文章

我的NPI项目之Android 安全系列 -- Android Strongbox 使能(一)

这里借用Android14高通相关的技术文档作为基础文档&#xff0c;该文档描述的是基于NFC的secure element. NFC型号为SN220. 有些概念的说明&#xff1a; 1. RoT 在我们目前的这个上下文中&#xff0c;首先RoT下几个内容&#xff0c;Bootinfo/ Additonal params(images hash) /…

洛谷C++简单题小练习day21—梦境数数小程序

day21--梦境数数--2.25 习题概述 题目背景 Bessie 处于半梦半醒的状态。过了一会儿&#xff0c;她意识到她在数数&#xff0c;不能入睡。 题目描述 Bessie 的大脑反应灵敏&#xff0c;仿佛真实地看到了她数过的一个又一个数。她开始注意每一个数码&#xff08;0…9&#x…

✅技术社区项目—Session/Cookie身份验证识别

session实现原理 SpringBoot提供了一套非常简单的session机制&#xff0c;那么它又是怎么工作的呢? 特别是它是怎么识别用户身份的呢? session又是存在什么地方的呢? 核心工作原理 借助cookie中的 JESSIONID 来作为用户身份标识&#xff0c;这个数据相同的&#xff0c;认…

【DAY04 软考中级备考笔记】数据结构基本结构和算法

数据结构基本结构和算法 2月25日 – 天气&#xff1a;晴 周六玩了一天&#xff0c;周天学习。 1. 什么是数据结构 数据结构研究的内容是一下两点&#xff1a; 如何使用程序代码把现实世界的问题信息化如何用计算机高效地处理这些信息从创造价值 2. 什么是数据 数据是信息的…

第十四章 Linux面试题

第十四章 Linux面试题 日志t.log(访问量)&#xff0c; 将各个ip地址截取&#xff0c;并统计出现次数&#xff0c;并按从大到小排序(腾 讯) http://192. 168200.10/index1.html http://192. 168.200. 10/index2.html http:/192. 168 200.20/index1 html http://192. 168 200.30/…

测试C#使用ViewFaceCore实现图片中的人脸遮挡

基于ViewFaceCore和DlibDotNet都能实现人脸识别&#xff0c;准备做个遮挡图片中人脸的程序&#xff0c;由于暂时不清楚DlibDotNet返回的人脸尺寸与像素的转换关系&#xff0c;最终决定使用ViewFaceCore实现图片中的人脸遮挡。   新建Winform项目&#xff0c;在Nuget包管理器中…

基于深度学习的子图计数方法

背景介绍 子图计数&#xff08;Subgraph Counting&#xff09;是图分析中重要的研究课题。给定一个查询图 和数据图 , 子图计数需要计算 在 中子图匹配的&#xff08;近似&#xff09;数目 。一般我们取子图匹配为子图同构语义&#xff0c;即从查询图顶点集 到数据图顶点集 的…

Excel的中高级用法

单元格格式&#xff0c;根据数值的正负分配不同的颜色和↑ ↓ 根据数值正负分配颜色 2-7 [蓝色]#,##0;[红色]-#,##0 分配颜色的基础上&#xff0c;根据正负加↑和↓ 2↑-7↓ 其实就是在上面颜色的代码基础上加个 向上的符号↑&#xff0c;或向下的符号↓ [蓝色]#,##0↑;[红色…

01背包问题:组合问题

01背包问题&#xff1a;组合问题 题目 思路 将nums数组分成left和right两组&#xff0c;分别表示相加和相减的两部分&#xff0c;则&#xff1a; left - right targetleft right sum 进而得到left为确定数如下&#xff0c;且left必须为整数&#xff0c;小数表示组合不存在&…

【Python从入门到进阶】49、当当网Scrapy项目实战(二)

接上篇《48、当当网Scrapy项目实战&#xff08;一&#xff09;》 上一篇我们正式开启了一个Scrapy爬虫项目的实战&#xff0c;对当当网进行剖析和抓取。本篇我们继续编写该当当网的项目&#xff0c;讲解刚刚编写的Spider与item之间的关系&#xff0c;以及如何使用item&#xff…

Qt QWidget 简约美观的加载动画 第二季

&#x1f603; 第二季来啦 &#x1f603; 简约的加载动画,用于网络查询等耗时操作时给用户的提示. 这是最终效果: 一共只有三个文件,可以直接编译运行 //main.cpp #include "LoadingAnimWidget.h" #include <QApplication> #include <QVBoxLayout> #i…

Chiplet技术与汽车芯片(一)

目录 1.摩尔定律放缓 2.Chiplet的优势 2.1 提升芯片良率、降本增效 2.2 设计灵活&#xff0c;降低设计成本 2.3 标准实行&#xff0c;构建生态 3.Chiplet如何上车 22年8月左右&#xff0c;Chiplet概念突然在二级市场火了起来&#xff0c;封测四小龙华天、长电、通富微电、…

智慧应急与物联网相结合:物联网技术如何提升智慧应急响应能力

目录 一、引言 二、智慧应急与物联网技术的结合 三、物联网技术提升智慧应急响应能力的途径 四、物联网技术在智慧应急中的应用案例 五、物联网技术在智慧应急中面临的挑战与解决方案 挑战一&#xff1a;技术标准与规范不统一 解决方案&#xff1a; 挑战二&#xff1a;…

复旦大学EMBA联合澎湃科技:共议科技迭代 创新破局

1月18日&#xff0c;由复旦大学管理学院、澎湃新闻、厦门市科学技术局联合主办&#xff0c;复旦大学EMBA项目、澎湃科技承办的“君子知道”复旦大学EMBA前沿论坛在厦门成功举办。此次论坛主题为“科技迭代 创新破局”&#xff0c;上海、厦门两地的政策研究专家、科学家、科创企…

三天学会阿里分布式事务框架Seata-Seata及分布式事务简介

锋哥原创的分布式事务框架Seata视频教程&#xff1a; 实战阿里分布式事务框架Seata视频教程&#xff08;无废话&#xff0c;通俗易懂版&#xff09;_哔哩哔哩_bilibili实战阿里分布式事务框架Seata视频教程&#xff08;无废话&#xff0c;通俗易懂版&#xff09;共计10条视频&…

Python算法题集_实现 Trie [前缀树]

Python算法题集_实现 Trie [前缀树] 题208&#xff1a;实现 Trie (前缀树)1. 示例说明2. 题目解析- 题意分解- 优化思路- 测量工具 3. 代码展开1) 标准求解【定义数据类默认字典】2) 改进版一【初始化字典无额外类】3) 改进版二【字典保存结尾信息无额外类】 4. 最优算法5. 相关…

自定义神经网络三之梯度和损失函数激活函数

文章目录 前言梯度概述梯度下降算法梯度下降的过程 optimize优化器 梯度问题梯度消失梯度爆炸 损失函数常用的损失函数损失函数使用原则 激活函数激活函数和损失函数的区别激活函数Relu-隐藏层激活函数Sigmoid和Tanh-隐藏层Sigmoid函数Tanh&#xff08;双曲正切&#xff09; &l…

Panalog大数据日志审计系统libres_syn_delete.php命令执行漏洞

声明 本文仅用于技术交流&#xff0c;请勿用于非法用途 由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;文章作者不为此承担任何责任。 1、产品简介 Panalog大数据日志审计系统定位于将大数据产品应用于高校…

Linux之vim的使用详细解析

个人主页&#xff1a;点我进入主页 专栏分类&#xff1a;C语言初阶 C语言进阶 数据结构初阶 Linux C初阶 算法 欢迎大家点赞&#xff0c;评论&#xff0c;收藏。 一起努力&#xff0c;一起奔赴大厂 目录 一.vim简介 二.vim的基本概念 三.vim的基本操作 3.1准备 …

STL - B树

1、常见的搜索结构 种类数据格式时间复杂度顺序查找无要求O(N&#xff09;二分查找有序O( )二叉搜索树无要求O(N)二叉平衡树(AVL树和红黑树)无要求O( )哈希无要求O(1) 以上结构适合用于数据量相对不是很大&#xff0c;能够一次性存放在内存中&#xff0c;进行数据查找的场景…