C++语法—引用

news2024/9/28 1:34:28

引用变量

概念

简单理解就是对一个已存在的变量起别名,与那个已存在的变量共用一块内存空间。
用法:已存在变量的类型 & 引用变量名 = (引用实体)已存在变量

int main()
{
	int a = 1;
	int& b = a;
	return 0;
}

在上面这个示例代码中,b是a的引用变量,我们可以通过a去输出1,也可以通过b来输出1,相当于给张三这个人起了个绰号叫张山,无论是叫张山还是张三代指都是一个人。

语法规定

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,再不能引用其他实体

引用与指针的区别

虽然在语法概念上引用就是一个,没有独立空间,和其引用实体共用同一块空间,但是底层实现是有空间,因为引用是按照指针方式来实现的

int main()
{   
    int a = 1;
    int& ra = a;
    int b = 2;
    ra = b;
    //这里只是把b的值赋值给ra,不是改变引用对象
    //&ra = &b;//引用一旦给定初值无法改变方向
    cout << b << endl;
    cout << ra << endl;

    //而指针可以改变指向
    int* pa = &a;
    pa = &b;
    (*pa)++;
    cout << b << endl;
    return 0;
}

引用和指针的不同点:

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
  2. 引用在定义时必须初始化,指针没有要求
  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  4. 没有NULL引用,但有NULL指针
  5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  7. 有多级指针,但是没有多级引用
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  9. 引用比指针使用起来相对更安全

引用的使用场景

传参

在这里插入图片描述swap1是传值传参,在swap1中,a和b 不过是main函数的拷贝,所以改变swap1的a和b,不能改变main函数的a和b。而swap2是传址传参,在swap2中,a和b是main函数中a和b的别名,所以能够改变他,而且传引用传参不会发生拷贝构造,也会提高效率

常引用参数、临时变量

其实由上面的例子来看,swap1的传值引用会产生临时匿名对象,然后再拷贝到函数参数中,会调用该类的拷贝构造。
但是在某些传引用传参也会产生临时匿名对象(在被const修饰的前提之下,为什么?因为临时匿名对象具有常性,所以只有加const才能引用他)

  1. 实参的类型正确,但实参是右值
  2. 发生隐式类型转换

比如:

double refcube(const double &ra)
{
	return ra*ra*ra;
}
double side = 3.0;
long edge = 5L;
double c1 = refcube(side); // ra is side 
double c2 = refcube(edge);// edge是long类型,发生隐式类型转换
double c3 = refcube(7.0);// 7.0是字面量是右值
double c7 = refcube(side + 10.0); // side + 10.0是将亡值 也是右值的一种

总结:哪几种场景会产生临时变量??

  1. 隐形类型转换 比如:intb=0 float a=b b在赋值到a时候编译器会产生临时变量再去赋值给a
  2. 函数传值传参
  3. 函数返回值

函数返回

int& Add1(int a, int b)
{
    int c = a + b;
    return c;
}

int Add2(int a, int b)
{
    int c = a + b;
    return c;
}

int main()
{
    int& ret1 = Add1(1, 2);//ret1是Add1中c的引用
    int ret2 = Add1(1, 2);//隐式类型转换

    int ret3 = Add2(1, 2);//返回的时候把Add2的临时里面对象给拷贝到ret3
    const int& ret4 = Add2(1, 2);//把他的临时匿名对象给引用了
    cout << "ret1 is :" << ret1 << endl;
    cout << "ret2 is :" << ret2 << endl;
    cout << "ret3 is :" << ret3 << endl;
    cout << "ret4 is :" << ret4 << endl;
    return 0;
}

在这里插入图片描述
为什么得出以上结果?
ret1是Add1中c的引用,c是add函数中的变量,生命周期会在离开add函数的时候销毁,所以引用的虽然是c,但是c已经在函数返回的时候还给内存了,所以是随机值。
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
ret2是3 本质上其实是 int c = 3 返回变量 int temporary& = c; ret2 = temporary。其实也是拷贝构造
ret3是典型的传值返回,返回的时候把Add2的临时里面对象给拷贝到ret3
ret4是引用类型,它把Add2的返回值引用了,因为临时匿名对象具有常性,所以要用const修饰

右值引用

字面量、常量、变量

字面量:顾名思义就是我们人在读这个变量的,可以里面懂的他代指的含义。const int a = 16 16就是字面量,而被const修饰的只读的变量就是常量。如果a没有被const修饰那就是变量。

左值、右值、将亡值

左值:是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。

int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;
	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
	return 0;
}

右值:也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。

int main()
{
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);
	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
	10 = 1;
	x + y = 1;
	fmin(x, y) = 1;
	return 0;
}

将亡值:即将还给内存的值,一些临时匿名对象。

概念

之前学习的引用都是左值引用,而右值引用就是对右值起别名
右值引用语法: 引用类型 && 引用名 = 右值

右值引用的作用

其实左值引用也可以对右值取别名,比如:a+b表达式是右值,我们可以这样写const int& ret2 = (a + b); 因为临时匿名对象具有常性,我们加上const就可以对他引用了,也可以使用右值引用 int&& ret2 = (a + b); 如果你要使用右值引用引用左值的话可以使用move函数,他会返回一个右值给引用变量int a = 1; int&& b = move(a);
其实对于已经存在左值引用的情况下,为什么要弄一个右值引用呢?

void func(const int& a)
{
	cout << "void func(int& a)" << endl;
}

//void func(int&& a)
//{
//	cout << "void func(int&& a)" << endl;
//}

int main()
{
	int a = 0;
	int b = 1;
	func(a);
	func(a + b);
}
void func(int& a)
{
	cout << "void func(int& a)" << endl;
}

void func(int&& a)
{
	cout << "void func(int&& a)" << endl;
}

int main()
{
	int a = 0;
	int b = 1;
	func(a);
	func(a + b);
}

右值引用的第一个意义是在之前只有左值引用的时候,如果我们对右值引用要在前面+const 去修饰参数,可读性不强,难区分左右值传参,而现在多了右值引用我们可以利用传参不同构成函数重载,来增加明确的可读性。
再看一段代码:

int main()
{
	Jaxsen::string s1("hello");
	Jaxsen::string ret1 = s1;
	Jaxsen::string ret2 = (s1+'!');
	return 0;
}

ret1 = s1上,本质是s1调用了拷贝构造把ret1深拷贝到ret1中,然后我们再去看ret2 = (s1 + '!' ),首先s1 +'!'是一个右值,对于这个右值我们是否有必要对他进行深拷贝呢?在ret1 = s1我们之所以要做深拷贝是因为避免同一个变量指向同一块内存,导致我改变ret1,s1也会跟着改变。但是在s1 +'!'这个右值表达式中产生出来的结果是一个将亡值,它的生命周期将会在这个表达式运算完就还给内存了,我们根本不用去担心访问冲突的关系。所以移动拷贝就出来了。

移动拷贝

什么是移动拷贝?
简单的理解就是把那些临时匿名对象,没有名字的变量,用右值引用变量管理那块要还给操作系统的内存。省去深拷贝,提高了效率。
注意:右值引用的根本作用不是减少拷贝,大部分的减少拷贝左值引用已经完成了

string operator+(char ch)
{
	string tmp(*this);
	tmp += ch;
	return tmp;
}

在这里插入图片描述
在上面代码执行的时候,可以观察出ret2的地址和operator+中的tmp是一个地址。
左值引用解决直接减少拷贝,相当于传指针,不过没有指针那么复杂。而左值引用没有解决的是函数内的局部对象不能用引用返回的问题。
看下面场景:

class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> vv;
        vv.resize(numRows);//给定开辟有多少个vector<int>
        //给每一个vector<int>开辟空间
        for(int i = 0;i<vv.size();i++)
        {
            vv[i].resize(i+1);
        }
        //把杨辉三角的每一行的首元素和尾元素置为1
        for(int i = 0;i<numRows;i++)
        {
            vv[i][0] = 1;
            vv[i][vv[i].size()-1] =1;
        }
        //执行逻辑:上一行的前一位+上一行的当前位 =当前位
        for(int i = 2;i < numRows;i++)
        {
            for(int j = 1;j < vv[i].size() - 1;j++)
            {
                vv[i][j] = vv[i-1][j-1]+vv[i-1][j];
            }
        }
        return vv;   
    }
};

在这样的一个杨辉三角问题中,我们需要在generate函数中构建临时对象vv,然后还传值返回,在传值返回的时候深拷贝构建临时匿名对象,去复制给外面的接收又要深拷贝,而且这返回值根本不用担心访问冲突的问题,没有必要做深拷贝。所以右值这就是引用的第二个意义。

move

int main()
{
	string s1("hello");
	cout << "第一次赋值" << endl;
	string s2 = s1;
	cout << s1 << endl;
	cout << s2 << endl;

	cout << "第二次赋值" << endl;
	string&& s3 = move(s1);
	cout << s1 << endl;
	cout << s3 << endl;

	cout << "第三次赋值" << endl;
	string s4 = move(s1);
	cout << s1 << endl;
	cout << s4 << endl;
	return 0;
}

在这里插入图片描述
为什么会有这样的输出,请解释赋值语句的含义?
第一次赋值语句是将对象s1赋值给s2,发生深拷贝把s1的值拷贝一份给s2,相当于在内存中开多一块空间存放与s1一样的值,不过是s2管。
第二次赋值语句是将对象s1move成了右值,然后用右值引用变量去给该值起别名,s3和s1都有管理这片内存的权限,和左值引用没有什么区别。
第三次是s1变成右值交给s4去管理,发生了移动拷贝,把s1原本管理的内存交给s4去管理,s1被置为了null。所以不能轻易的把左值move右值然后又赋值给其他变量,这样原本的那个左值会失去对该内存的管理。

完美转发

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;
}

在这里插入图片描述
为什么是这个输出结果?
再看一个案例

int main()
{
	int&& rr1 = 10;
	cout << &rr1 << endl;
	rr1++;
	cout << rr1 << endl;
	return 0;
}

在这里插入图片描述
10是字面量而rr1右值引用变量引用之后就可以取地址和做++运算,得出结论:当右值被右值引用变量引用之后就转化为了左值,或者说右值引用变量是左值。
为什么要这样设计呢?因为只有这样才可以对右值进行操作(移动构造)。
我们再回到上面那一题目,我们可以得出结论,在我们将右值作为函数参数传递给右值引用的时候,那个参数就变成了左值,所以输出的就是左值。

forward

含义:在传参的过程中保留对象原生类型属性
使用方法:forward<原生类型>(变量)
上面修改后:

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(forward<T>(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;
}

在这里插入图片描述
注意:在使用forward()的时候,要从最开始层层往下写下来。

移动赋值

和移动构造一样的原理:右值传参对该参数做移动,把这个生命周期极短的临时变量交给右值引用变量管理。
在这里插入图片描述

总结

右值引用解决了,左值引用中函数内变量不能引用返回需要强行深拷贝的场景和传右值作为参数,仍然使用深拷贝的场景。使用右值引用减少了很多没有必要的深拷贝,大大提高了效率

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

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

相关文章

minio 快速入门+单机部署+集群

目录 原理 概念 名词解释 Set /Drive 的关系 MinIO部署 单机 单机单盘 单机多盘 集群 多机单盘 多机多盘 配置负载均衡 调优 原理 MinIO是一个S3兼容的高性能对象存储&#xff0c;其主要特点如下&#xff1a; 适合存储大容量非结构化的数据&#xff0c;如图片&…

骨传导耳机品牌排行榜分享:360度实测分析10款抢手骨传导耳机!

随着科技的不断进步和人们生活方式的变化&#xff0c;骨传导耳机以其独特的传声方式和开放式设计&#xff0c;逐渐成为运动爱好者、户外活动家以及听力障碍人士的新宠。不同于传统耳机将声音直接导入耳道&#xff0c;骨传导耳机通过振动颅骨将声音传递至内耳&#xff0c;不仅能…

数据结构与算法——Java实现 20.习题——二叉树层序遍历

认真的人改变自己&#xff0c;执着的人改变命运 —— 24.9.27 102. 二叉树的层序遍历 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;[[3],[9,20],[15,7]]示例 2&#xff1a; 输入&#xff1a;root [1] 输出&#xff1a;[[1]]示例 3&…

Java单体服务和集群分布式SpringCloud微服务的理解

单体应用存在的问题 1.随着业务的发展开发变得越来越复杂。 2.修改或者新增&#xff0c;需要对整个系统进行测试、重新部署。 3.一个模块出现问题&#xff0c;很可能导致整个系统崩溃。 4.多个开发团队同时对数据进行管理&#xff0c;容易产生安全漏洞。 5.各个模块使用同…

Spring Session学习

系列文章目录 JavaSE基础知识、数据类型学习万年历项目代码逻辑训练习题代码逻辑训练习题方法、数组学习图书管理系统项目面向对象编程&#xff1a;封装、继承、多态学习封装继承多态习题常用类、包装类、异常处理机制学习集合学习IO流、多线程学习仓库管理系统JavaSE项目员工…

如何在算家云搭建MVSEP-MDX23(音频分离)

一、MVSEP-MDX23简介 模型GitHub网址&#xff1a;MVSEP-MDX23-music-separation-model/README.md 在 main ZFTurbo/MVSEP-MDX23-音乐分离模型 GitHub 上 在音视频领域&#xff0c;把已经发布的混音歌曲或者音频文件逆向分离一直是世界性的课题。音波混合的物理特性导致在没有…

快消品海外仓应该如何选择合适WMS仓储系统?

快消品的“快”属性天然契合海外仓&#xff0c;快消品大部分是必需品&#xff0c;库存周转快&#xff0c;需保持一定量的安全库存&#xff0c;及时、稳定补货尤为重要&#xff1b;快消品最适合全渠道销售&#xff0c;线上线下等&#xff0c;比较考验备货和统筹能力&#xff1b;…

基于RustDesk自建远程桌面服务

最近向日葵越来越难用了&#xff0c;官方好像限制了免费用户的带宽&#xff0c;但是限制的有点过头了&#xff0c;卡的基本没法用。 向日葵的平替todesk对于免费用户又有时长限制&#xff0c;对于经常用的小伙伴不大友好。 咱也不是说非得白嫖&#xff0c;但是向日葵和todesk这…

观测云链路追踪分析最佳实践

背景 如果要在开发、运维和工程层面持续改进一个涉及多服务的应用&#xff0c;以链路追踪、日志检索、指标收集、用户体验监测、性能剖析、关联分析等作为代表性技术的可观测性必不可少&#xff0c;这一看法已成为共识&#xff0c;但在采用这项技术的过程中&#xff0c;如何分…

msvcr100.dll丢失的解决方法,六种解决msvcr100.dll丢失使用技巧

在使用计算机的过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“msvcr100.dll丢失”。这个问题可能会让我们感到困惑和无助&#xff0c;但是不用担心&#xff0c;本文将为大家介绍六种实用的解决方法&#xff0c;帮助你轻松解决这个问题。 一&#xff…

【JAVA报错已解决】Java.lang.NullPointerException

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

朋友圈信息流广告投放,曝光成本是多少?

微信作为国内最流行的社交平台之一&#xff0c;其朋友圈广告凭借精准的用户画像和强大的社交属性&#xff0c;成为了众多品牌商家进行市场推广的重要渠道。云衔科技推出了专业的微信朋友圈广告开户及代运营服务&#xff0c;旨在帮助企业轻松跨越技术门槛&#xff0c;精准触达目…

【Verilog学习日常】—牛客网刷题—Verilog企业真题—VL64

时钟切换 描述 题目描述&#xff1a; 存在两个同步的倍频时钟clk0 clk1,已知clk0是clk1的二倍频&#xff0c;现在要设计一个切换电路&#xff0c;sel选择时候进行切换&#xff0c;要求没有毛刺。 信号示意图&#xff1a; 波形示意图&#xff1a; 输入描述&#xff1a; …

D20【python接口自动化学习】-python基础之内置数据类型

day20 内置数据类型的常见错误 学习日期&#xff1a;20240927 学习目标&#xff1a;内置数据类型--29 内置数据类型的常见错误 学习笔记&#xff1a; 访问错误 不同数据类型之间的操作报错 解决错误的方法 对只读类型进行写入报错 解决错误的方法 引用错误 解决错误的方法 …

产销皆下行,造势口碑遭“反噬”,魏建军能否重振长城汽车?

今年以来&#xff0c;长城汽车可谓多次被打在舆论聚光灯下&#xff0c;既有“一把手”魏建军一反此前低调务实作风而在今年多次于公共场合慨慷激昂频出“金句”宏观层面的&#xff1b;也有旗下多款车型销量数据出现下滑的微观层面。 近日&#xff0c;长城汽车披露了2024年1-8月…

python的 __name__和__doc__属性

__name__属性 __name__属性 用于判断当前模块是不是程序入口&#xff0c;如果当前程序正在使用&#xff0c;__name__的值为__main__。 在编写程序时&#xff0c;通常需要给每个模块添加条件语句&#xff0c;用于单独测试该模块的功能。 每个模块都有一个名称&#xff0c;当一…

ArduSub程序学习(11)--EKF实现逻辑①

1.read_AHRS() 进入EKF&#xff0c;路径ArduSub.cpp里面的fast_loop()里面的read_AHRS(); //从 AHRS&#xff08;姿态与航向参考系统&#xff09;中读取并更新与飞行器姿态有关的信息 void Sub::read_AHRS() {// Perform IMU calculations and get attitude info//----------…

WinForm程序嵌入Web网页

文章目录 前言一、三方库或控件的选择测试二、Microsoft Edge WebView2安装、使用步骤1.安装2.使用 前言 由于此项目需要winform客户端嵌入web网页并于JAVA端交互数据&#xff0c;所以研究了一下嵌入web网页这部分&#xff0c;趟了一遍雷&#xff0c;这里做下记录。 一、三方库…

软件设计之Maven(2)

软件设计之Maven(2) 路线图推荐&#xff1a; 【Java学习路线-极速版】【Java架构师技术图谱】 尚硅谷新版Maven教程&#xff08;高效入门maven&#xff0c;上手又快又稳&#xff09; 资料可以去尚硅谷官网免费领取 学习内容&#xff1a; 依赖管理版本统一及维护依赖范围Buil…

Comfyui 学习笔记1

如果图像输出被裁剪&#xff0c;则需要使用PrepImageForClipVision&#xff0c;来设置图像距离上边沿的位置. 决定绘画的作用区域&#xff0c;后面的KSample只作用到 mask标记的范围。 图像位置偏移了&#xff0c;可以考虑通过Image crop 裁剪 IPAdapter face 提取时&…