C++程序设计——右值引用

一、右值引用概念

引用:

        C++98中提出了引用的概念,引用即别名,引用变量与其引用实体共用同一块内存空间,而引用的底层是通过指针来实现,因此使用引用,可以提高程序的可读性。

右值引用(类型&& 右值):

(1)为了提高程序的运行效率,C++11中引入了右值引用;

(2)右值引用也是别名;

(3)只能对右值进行引用。

(4)右值有了名字后,就成了一个普通变量,也就是左值。

二、左值与右值

        左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式。一般认为:可以放在=的左边,或能够取地址的称为左值;只能放在=右边的,或不能取地址的称为右值。但不一定完全正确。

一般认为:

(1)普通类型的变量,因为有名字,可以取地址,都认为是左值。

(2)const修饰的常量,不可修改,只读类型,理论上应该按照右值对待,但因为其可以取地址,C++11认为其是左值。

(3)如果表达式的运行结果是一个临时变量或者对象,认为是右值。

(4)如果表达式运行的结果或单个变量是一个引用,认为是左值。

C++11对右值进行了严格区分:

(1)C语言中的纯右值,比如:10,a+b;

(2)将亡值,比如:表达式的中间结果、函数按照值的方式进行返回。

三、引用与右值引用比较

C++98中的普通引用与const引用,在其引用实体上的区别:

(1)普通引用只能引用左值,不能引用右值;

(2)const引用既可以引用左值,也可以引用右值。

C++程序设计——右值引用_第1张图片

C++ 11中右值引用:

(1)只能引用右值;

(2)一般情况不能直接引用左值。

 (3)右值引用引用左值方法:使用move函数进行转换。

C++程序设计——右值引用_第2张图片

四、值形式返回对象的缺陷

        如果一个类中涉及到资源的管理,用户必须显示的提供拷贝构造函数、赋值运算符重载、析构函数,否则编译器将会自动生成默认的成员函数。而当遇到拷贝对象或对象之间相互赋值的情况时,就会出现一些问题,比如:

class String {
public:
	String(const char* str = "") {
		if (nullptr == str) {
			str = "";
		}
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}
	String(const String& s)
		: _str(new char[strlen(s._str) + 1])
	{
		strcpy(_str, s._str);
	}

	String& operator=(const String& s) {
		if (this != &s) {
			char* temp = new char[strlen(s._str) + 1];
			strcpy(temp, s._str);
			delete[] _str;
			_str = temp;
		}
		return *this;
	}
	String operator+(const String& s) {
		char* temp = new char[strlen(_str) + strlen(s._str) + 1];
		strcpy(temp, _str);
		strcpy(temp + strlen(_str), s._str);
		String result(temp);
		delete[] temp;
		return result;
	}

	~String() {
		if (_str) {
			delete[] _str;
			_str = nullptr;
		}
	}

public:
	char* _str;
};

int main() {
	String s1("hello ");
	String s2("world");
	String s3;
	s3 = s1 + s2;
}

        在执行s3 = s1 + s2时,其中operator+是以值形式返回。那么result在返回前,就必须创建一个临时对象,临时对象创建好后,result就被销毁了,然后使用返回的临时对象去赋值s3,赋值完成后,临时对象就被销毁掉。

        可以发现,result、返回的临时对象、s3每个对象创建后,都有自己独立的空间,且它们存放的内容也相同,相当于创建了三个内容完全相同的对象,对于空间是一种浪费,程序的效率也较低。

优化思路:

        如果可以将result的空间转移给临时对象,临时对象在赋值s3时,再将空间转移给s3,这样就相当于只开辟了一次空间。

实现方法:

        可以发现,result相对于临时对象,临时对象相对于s3来说,都是将亡值,也就是右值。所以我们只需要实现一个移动构造函数和移动赋值函数即可,这样对于右值,则会自动调用移动函数,采用转移的方式进行构造或赋值。

C++程序设计——右值引用_第3张图片

注意:

(1)移动构造和移动赋值函数的参数不能设置为const类型的右值引用,否则资源无法转移导致移动语义失效。

(2)C++11之后,类中默认的成员函数就新添加了移动赋值、移动构造两个成员函数,同理若用户自己没有实现,编译器会默认生成,只不过默认生成的也是采用浅拷贝的方式实现的,所以当类中涉及到资源的管理时,也需要自己显示定义实现。

五、右值引用引用左值

        按照语法定义,右值引用只能引用右值,但有些情况下,也需要用右值去引用左值实现移动语义。

        当需要用右值引用引用左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数,它并不是什么搬移功能,唯一的功能就是将一个左值强制转化为右值,然后实现移动语义。

应用场景示例:

C++程序设计——右值引用_第4张图片

        比如以上代码,移动构造中,我们希望的是将s中的资源 全部转移到新对象中,但是s虽然是右值,但编译器并不认为s中的_name、_sno是右值,因为它们有自己的名字、可以对它们取地址,所以在初始化列表初始化时,并不会直接转移,而是会调用String中的拷贝构造函数。

        所以此时就需要我们使用move函数,显示的告诉编译器将s中的_name、_sno当中右值处理,这样在初始化时就会选中去调用String的移动构造函数。

C++程序设计——右值引用_第5张图片 

应用反例:

六、完美转发

1. 概念

        完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。

所谓完美:

        函数模板在向其他函数传递自身参数时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。

2. 目的

        保留其他函数对转发来的参数的左右值属性,进行不同的处理。

比如:

C++程序设计——右值引用_第6张图片

        函数func针对参数的左右值属性不同,有不同的处理方式,所以希望函数模板repost在转发参数时,保留其左右值属性,但通过以上写法是不能达到目的的,除法针对左右值属性,编写两个函数模板,但是这又降低了代码的复用性。

        所以希望能够有一种方法,能够实现repost在转发参数时,保留其左右值属性,即完美转发。

3. 实现

(1)将模板函数(包括类模板和函数模板)的参数书写成“T&& 参数名”的形式,这样模板函数既可以接收左值,也可以接收右值引用。(注意:普通函数不行)

(2)C++11提供了模板函数std::forward(参数),用于转发参数。如果参数是左值,转发后仍是左值,如果参数是右值引用,转发后仍是右值引用。

C++程序设计——右值引用_第7张图片

你可能感兴趣的:(C++程序设计,c++,开发语言)