左值、右值与右值引用 & C++11中

我们先来谈谈C++11中对左右值的判断标准,以及左右值本身的一些细节,我想这应该是故事的开始。

   在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值。
   举个例子:在a=b+c;中,&a是允许的操作,但&(b+c)这样的操作则不会通过编译。因此a是一个左值,(b+c)是一个右值。

   其次,在C++11中右值又分为将亡值(xvalue,eXpiring Value)和纯右值(prvalue,Pure Rvalue)。
   其中纯右值的概念等同于我们在C98标准中所谓的右值概念,讲的是用于辨识临时变量和一些不跟对象关联的值
   比如class a{...};a fun(){...}函数返回的临时实例,比如a=1+3;中1+3产生的临时变量值,再比如a=true;中的true,上述这些都是纯右值。

   而将亡值是C++11中新提出的一个概念。
    这里读者最关心的应该是两个问题:1.将亡值概念的提出是为了解决什么问题?2.将亡值的定义到底是什么?

   我们来从问题的起源一步步讲起,希望在最后解答这两个问题。
   大家都知道,C++中一般函数或方法的实参以及返回值都是以副本的形式传递的(除非你特别指定了按引用来传递,而这也仅限于实参)。
   来看一段代码
#include <iostream>

using namespace std;

class A

{

	public:

		//构造函数(初始化p_int,并附上i的值)

		A(int i)

			p_int=new int(i);

			cout<<"call A(int i)"<<endl;
		}

		//==============================

		//拷贝构造函数(对p_int实现了深拷贝)

		A(const A &a)
		{ 

			p_int=new int(*(a.p_int));

			cout<<"call A(const A &a)"<<endl; 

		}

		//==============================

		//析构函数(释放p_int)

		~A()

			cout<<"call ~A()"<<endl; 

			delete p_int; 

		}

		//==============================

		int *p_int;

};

//传入一个A对象,并直接返回该对象

A fun(A a){return a;}

int main()

{

	A test(10);

	fun(test);

	return 1;

}
   运行结果
   代码分析
    首先我们关闭了g++编译器的自动优化功能,来看到c++最本质的运行状态,关闭自动优化的参数是-fno-elide-constructors。
  
 因为A test(10);代码,是我们调用了A的普通构造函数,获得了屏幕输出call A(int i)。
   因为将test传入fun()中使用了按副本传递,所以调用了A的拷贝构造函数,获得了屏幕输出call A(const A &a)。
   因为fun()中的return a;也是按副本传递的,所以再一次的调用了A的拷贝构造函数,获得了屏幕输出call A(const A &a)。
   至此程序运行的屏幕输出应该是清晰且没有任何疑问的。
    同时由于类的成员涉及指针类型,所以我们的拷贝构造函数使用了深拷贝。
   在这段代码中,我只显性的创建了一次A的对象,但是却产生了巨大的开销。
   为p_int开辟内存就达3次之多,如果是实际身材环境中,真实的类可能包含更多的成员涉及更多的内存。


   我们仔细思考一下,两次拷贝构造函数的调用都是由于程序产生了一个副本导致的。
    而这些副本对象其实并没有任何实际意义,而且转瞬即逝,在完成了自己的传递任务后就即可销毁了。这恰恰和将亡值想要表达的意思十分吻合,不是吗?
   第一个问题似乎已经有了答案,但将亡值的概念能为我们解决实际问题呢?

    如果已经可以明确,我的拷贝源是一个将亡值,那么我们其实并没有深拷贝的必要,而是只要将他的资源移位即用就可以。
   所以C++11提供了一个所谓的移动构造函数,接受一个将亡值作为拷贝源。

   来看一段代码
#include <iostream>

#include <iostream>

using namespace std;

class A

{

	public:

		//构造函数(初始化p_int,并附上i的值)

		A(int i)

			p_int=new int(i);

			cout<<"call A(int i)"<<endl;

		}

		//==============================

		//拷贝构造函数(对p_int实现了深拷贝)

		A(const A &a)

		{ 

			p_int=new int(*(a.p_int));

			cout<<"call A(const A &a)"<<endl; 

		}

		//==============================

		//移动构造函数(把将亡值的指针据为己用)

		A(A &&a):p_int(a.p_int)

		{

			a.p_int=nullptr;

			cout<<"call A(A &&a)"<<endl; 

		}

		//==============================

		//析构函数(释放p_int)

		~A()

		{ 

			cout<<"call ~A()"<<endl; 

			delete p_int; 

		}

		//==============================

		int *p_int;
};
//传入一个A对象,并直接返回该对象

Afun(A a){return a;}

int main()
{
	A test(10);

	fun(test);

	return 1;

}
   运行结果
   代码分析
   和上面代码的区别是,编译器将fun()中的return a;判定为将亡值,所以在拷贝副本调用了A的移动构造函数,获得了屏幕输出call A(A &&a)。
   无疑这种判断是合理且符合我们预期的。
    同时,我们也知道了将亡值有自己的数据类型的格式,即A &&(这里的A能够被其他数据类型替代)。

   通过上面的例子,读者应该能够清楚地了解到将亡值概念的提出能够解决什么样的实际问题。
   那么一个值是否是将亡值,只能被动的依靠编译器裁决嘛?答案是否定的!
    我们可以明确的声明一个将亡值(更加专业的叫法是右值引用),甚至能将一个明确知道不会再使用的左值变为右值引用拷贝源。

   来看两段代码的比较。
   代码1


#include <iostream>

using namespace std;

class A

{
	public:

		//构造函数(初始化p_int,并附上i的值)

		A(int i)

			p_int=new int(i);

			cout<<"call A(int i)"<<endl;

		}

		//==============================

		//拷贝构造函数(对p_int实现了深拷贝)

		A(const A &a)

			p_int=new int(*(a.p_int));

			cout<<"call A(const A &a)"<<endl; 

		}

		//==============================

		//移动构造函数(把将亡值的指针据为己用)

		A(A &&a):p_int(a.p_int)

		{

			a.p_int=nullptr;

			cout<<"call A(A &&a)"<<endl; 

		}

		//==============================

		//析构函数(释放p_int)

		~A()

			cout<<"call ~A()"<<endl; 

			delete p_int; 

		}

		//==============================

		int *p_int;

};

//传入一个A对象,并直接返回该对象

A fun(A a){return a;}

int main()

{

	//test是一个左值(一次普通构造)

	A test(10);

	//test以纯右值副本1传入(一次拷贝构造),以右值引用副本2传出(一次移动构造),被左值a拷贝(一次移动构造)

	A a=fun(test);

	//到这里副本1和副本2都完成历史使命被析构了

	cout<<"###############"<<endl; 

	//a和test析构

	return 1;

}
   代码1执行结果

   代码2

#include <iostream>

using namespace std;

class A

{

	public:

		//构造函数(初始化p_int,并附上i的值)

		A(int i)

			p_int=new int(i);

			cout<<"call A(int i)"<<endl;

		}

		//==============================

		//拷贝构造函数(对p_int实现了深拷贝)

		A(const A &a)

			p_int=new int(*(a.p_int));

			cout<<"call A(const A &a)"<<endl; 

		}

		//==============================

		//移动构造函数(把将亡值的指针据为己用)

		A(A &&a):p_int(a.p_int)

		{

			a.p_int=nullptr;

			cout<<"call A(A &&a)"<<endl; 

		}

		//==============================

		//析构函数(释放p_int)

		~A()

			cout<<"call ~A()"<<endl; 

			delete p_int; 

		}

		//==============================

		int *p_int;

};

//传入一个A对象,并直接返回该对象

A fun(A a){return a;}

int main()

{

	//test是一个左值(一次普通构造)

	A test(10);

	//test以纯右值副本1传入(一次拷贝构造),以右值引用副本2传出(一次移动构造),a成为副本2的别名

	A &&a=fun(test);

	//到这里副本1完成了历史使命被析构了

	//A &&a=fun(test);语句本质上延长了副本2的生命期,所以副本2没有析构

	cout<<"###############"<<endl; 

	//test和副本2析构

	return 1;

}
   代码2执行结果
   代码分析
   两段代码的屏幕输出应该是清晰且没有异议的,具体的原因我们在代码注释中已经详细说明。
    在代码中我们明确的声明了右值引用类型变量a,并将它作为返回返回值副本的别名,延迟了副本的生命周期,减少了程序开销。

    下面这段代码,我们会展示当我们明确一个左值以后不会再被使用时,如何他它转换为右值引用拷贝源。

#include <iostream>

using namespace std;

class A

{

	public:

		//构造函数(初始化p_int,并附上i的值)

		A(int i)

			p_int=new int(i);

			cout<<"call A(int i)"<<endl;

		}

		//==============================

		//拷贝构造函数(对p_int实现了深拷贝)

		A(const A &a)

			p_int=new int(*(a.p_int));

			cout<<"call A(const A &a)"<<endl; 

		}

		//==============================

		//移动构造函数(把将亡值的指针据为己用)

		A(A &&a):p_int(a.p_int)

		{

			a.p_int=nullptr;

			cout<<"call A(A &&a)"<<endl; 

		}

		//==============================

		//析构函数(释放p_int)

		~A()

			cout<<"call ~A()"<<endl; 

			delete p_int; 

		}

		//==============================

		int *p_int;

};

//传入一个A对象,并直接返回该对象

A fun(A a){return a;}

int main()

{

	//test是一个左值(一次普通构造)

	A test1(10);

	//将test1作为右值引用拷贝源调用移动构造,当然move()不会改变test1本身,如果你在下面语句继续执行A test2(test1)仍然是拷贝构造;

	A test2(move(test1));

	cout<<"##################"<<endl; 

}
   执行结果
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值