从左值引用、右值引用到拷贝构造、完美转发

一、左值和右值的区别

要想弄明白左值引用和右值引用,首先要理解什么是左值,什么是右值。

1.左值

左值代表一个内存地址值,并且通过这个内存地址,就可以对内存进行读写。简单来讲,左值即为能取到地址的值。比如:

int a=3;//其中a为左值,可以对其取地址。”3“即为右值,其职责仅仅是将其值赋予a,用完就没了(消亡),无法对其进行取地址的操作
cout<<&a<<endl;//输出a的地址
int *b=&a;//同样,b也是左值

值得注意的是,此处的值不一定是单纯的一个“值”,还有可能是表达式或者函数返回对象,详情请看对右值的介绍。

2.右值

右值代表一个常量或者是与运算操作符结合的表达式,跟左值相反,右值取不到地址

int text()
{
	int c = 20;
	return c;
}
int main()
{
	int a1 = 10, a2 = 11;
	cout << &(a1 + a2) << endl;//a1+a2是表达式,取不到地址,为左值
	cout << &text() << endl;//此处的函数返回对象也是左值
	return 0;
}

image-20230330212911174

看到这里肯定会有人问,如果:

int a=10;
int b=a;

那此时a是左值还是右值??

在表达式b=a中,a所管理的内存空间中存放的值被拷贝到临时寄存器中,再将其赋值给b,此时a依然是个左值。换句话说,左值可以被左值和右值赋值。

二、左值引用与右值引用

声明出来的左值引用和右值引用都是左值

int b=10;
int &a=b;
int &&c=100;//a和c都是左值
1.左值引用

即绑定到左值的引用,作用是为对象起别名,避免对象的拷贝。通过&来创建左值引用。非常量左值引用只能绑定到非常量左值上;而常量左值引用可以绑定到非常量左值、常量左值、非常量右值、常量右值等所有的值类型。

  • 应用:函数传参,函数的返回值
int a=10;//a为左值
int &b=a;//把b绑定到a,b即为a的左值引用,此时a和b指向同一块内存地址
&b=10;//错误,非常量左值引用只能接受左值
const int &c=10;//正确,常量左值引用可以接受右值
const int &c=a;//正确,常量左值引用可以接受非常量左值
//总结:常量左值引用可以接受左值、右值、常量左值、常量右值,而非常量左值引用只能接受左值

在对象的拷贝构造中,我们往往会用到左值引用,以避免对象的深拷贝所带来的资源消耗,和浅拷贝所带来的内存泄漏的问题,比如在一个类的定义中:

#include <iostream>
#include <string>
using namespace std;
class numberTx {
private:
	int* m_ptr;
public:
	numberTx(int value)//有参构造
		: m_ptr(new int(value)) {
		cout << "Call numberTx(int value)有参" << endl;
	}
	numberTx(const numberTx& T)
	{
		this->m_ptr = T.m_ptr;
		cout << "Call numberTx(const numberTx& T)浅拷贝" << endl;
	}
	~numberTx() {
		if (m_ptr!=nullptr)
		{
			delete m_ptr;
			m_ptr = nullptr;
		}
		cout << "Call ~numberTx()析构" << endl;
	}
	int GetValue(void) { return *m_ptr; }
};

numberTx getNum()
{
	numberTx a(100);
	return a;
}
void text()
{
	numberTx num1(20);
	numberTx num2(num1);
	cout << num1.GetValue()<<endl;
	cout << num2.GetValue() << endl;
}
int main(int argc, char const* argv[]) {
	numberTx a(getNum());
	cout << "a=" << a.GetValue() << endl;
	cout << "-----------------" << endl;
	text();
	system("pause");
	return 0;
}

image-20230331140247674

这就是浅拷贝所带来的问题。当对象资源被释放时,对象成员变量(指针)所指向的内存空间的资源也被析构函数释放,而浅拷贝使两个指针指向堆区的同一块内存空间,造成堆区资源被重复释放。所以要用到深拷贝。

numberTx(const numberTx& T)
{
    this->m_ptr = new int(*T.m_ptr);
    cout << "Call numberTx(const numberTx& T)深拷贝" << endl;
}

image-20230331141044234

与浅拷贝不同的是,当用拷贝构造函数给对象赋值时,深拷贝重新开辟(new)了一块内存空间,这样就不会造成两个指针指向同一块内存空间而造成的堆区资源重复释放的问题。

那么问题来辽,在案例中我们被深拷贝的对象只是个int类型的内存空间,但如果是个占用内存超大的临时对象呢?我们要重新开辟一块内存空间,而临时对象在完成自己的使命以后资源就被释放掉了,这样深拷贝势必会造成资源的浪费。那可不可以让目标对象指向临时对象所占的内存,将临时对象资源的控制权转移给目标对象呢?

拷贝构造是这样的:

在这里插入图片描述
而移动构造是这样的

在这里插入图片描述

就是让这个临时对象它原本控制的内存的空间转移给构造出来的对象,这样就相当于把它移动过去了。从图中可以看到,这类似于浅拷贝,只不过原本由临时对象申请的堆内存,由新建对象a接管,临时对象不再指向该堆内存。

numberTx(numberTx& T)//改进的拷贝构造
{
    this->m_ptr = T.m_ptr;
    T.m_ptr=nullptr;
    cout << "Call numberTx(const numberTx& T)改进的拷贝构造" << endl;
}

需要注意的是,改构造函数将临时对象(源对象)的指针指向NULL了,那在text()中就无法取到num2的m_ptr

运行结果:

image-20230331143337115

由运行结果可以看出,当同时存在参数类型为常量左值引用numberTx(const numberTx& T)和左值引用numberTx(numberTx& T)的拷贝构造函数时,getNum()返回的临时对象(右值)只能选择前者,非匿名对象 num1(左值)可以选择后者也可以选择前者,系统选择后者是因为该情况后者比前者好。为什么getNum()返回的临时对象(右值)只能选择前者?这是因为常量左值引用可以接受左值、右值、常量左值、常量右值等所有值,而非常量左值引用只能接受左值。因此,如果源对象是一个右值(临时对象),那么只能调用类的深拷贝构造函数numberTx(const numberTx& T),而在深拷贝构造函数中由于const的存在,无法改变源对象指针的指向。又想避免浅拷贝带来的重复释放问题,又想避免深拷贝所带来的内存开销,所以,右值引用出现啦。

2.右值引用

右值引用即为绑定到右值的引用,他做到了左值引用做不到的事情,即将一个非常量左值绑定到右值上,该右值的生命周期延长

const int &a=10;//正确,常量左值引用可以接受右值
int b=10;
int &c=b;//正确,
int &d=10;//错误,非常量左值引用只能接受右值
int func()
{
	return 3;
}
int &n=func();//错误,非常量左值引用只能接受右值
int &&t=func();//正确,非常量右值引用可以(其实是只能,这个后面会讲)接受右值

const int &&c1=a;      //错误,a是一个非常量左值,不可以被常量右值引用绑定
const int &&c2=a1;     //错误,a1是一个常量左值,不可以被常量右值引用绑定
const int &&c3=a+a1;   //正确,(a+a1)是一个非常量右值,可以被常量右值引用绑定
const int &&c4=a1+a2;  //正确,(a1+a2)是一个常量右值,可以被常量右值引用绑定

在func中,返回值3的生命周期被延长,t成为3的右值引用

因此,我们的构造函数就有了更完美的写法:

numberTx(numberTx&& T)//移动构造,可以接受一个左值传入
{
    this->m_ptr = T.m_ptr;
    T.m_ptr=nullptr;
    cout << "Call numberTx(const numberTx& T)移动构造" << endl;
}

这在C++11中被称作完美转发

完整代码:

#include <iostream>
#include <string>

using namespace std;

class numberTx {
private:
	int* m_ptr;
public:
	numberTx(int value)//有参构造
		: m_ptr(new int(value)) {
		cout << "Call numberTx(int value)有参" << endl;
	}
	/*numberTx(const numberTx& T)
	{
		this->m_ptr = T.m_ptr;
		cout << "Call numberTx(const numberTx& T)浅拷贝" << endl;
	}*/
	numberTx(const numberTx& T)
	{
		this->m_ptr = new int(*T.m_ptr);
		cout << "Call numberTx(const numberTx& T)深拷贝" << endl;
	}
	numberTx(numberTx& T)//改进的拷贝构造
	{
		this->m_ptr = T.m_ptr;
		T.m_ptr = nullptr;
		cout << "Call numberTx(const numberTx& T)改进的拷贝构造" << endl;
	}
	numberTx(numberTx&& T)//移动构造,可以接受一个左值传入
	{
		this->m_ptr = T.m_ptr;
		T.m_ptr = nullptr;
		cout << "Call numberTx(const numberTx& T)移动构造" << endl;
	}
	~numberTx() {
		if (m_ptr)
		{
			delete m_ptr;
			m_ptr = nullptr;
		}
		cout << "Call ~numberTx()析构" << endl;
	}

	int GetValue(void) { return *m_ptr; }
};

numberTx getNum()
{
	numberTx a(100);
	return a;
}
void text()
{
	numberTx num1(20);
	numberTx num2(num1);
	numberTx num3(numberTx(30));
//	cout << num1.GetValue()<<endl;
	cout << num2.GetValue() << endl;
	cout << num3.GetValue() << endl;
}
int main(int argc, char const* argv[]) {
	numberTx a(getNum());
	cout << "a=" << a.GetValue() << endl;
	cout << "-----------------" << endl;
//	text();
	system("pause");
	return 0;
}

运行结果:

image-20230331150547941

呵呵,因为有编译器优化的存在,结果并不是跟预想的一样,会有“Call numberTx(const numberTx& T)移动构造”输出。关闭方法:

img

现在结果正确:

image-20230331151005306

3.move()

在移动构造中,右值引用只能接受右值,那我就想传个左值咋办??

右值引用通过std::move()来指向左值(move强制将左值转成右值,即该左值编成了将亡值,move了以后就无了),比如我们在text中把原来的拷贝构造改一改,那运行结果是这样的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0hFGzAdx-1680248239050)(null)]

三、总结:

绑定到右值的引用,通过&&来获得右值引用。非常量右值引用只能绑定到非常量右值上;常量右值引用可以绑定到非常量右值、常量右值上。左值引用和右值引用本质都是减少内存开销优化内存使用的一种方法,右值引用就是将那些产生的临时的变量或对象偷过来作为长生命周期的对象存在,避免了不必要的创建与销毁。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值