> 文章列表 > C++程序设计——右值引用

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

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++ 11中右值引用:

(1)只能引用右值;

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

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

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

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

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来说,都是将亡值,也就是右值。所以我们只需要实现一个移动构造函数和移动赋值函数即可,这样对于右值,则会自动调用移动函数,采用转移的方式进行构造或赋值。

注意:

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

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

五、右值引用引用左值

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

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

应用场景示例:

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

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

 

应用反例:

六、完美转发

1. 概念

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

所谓完美:

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

2. 目的

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

比如:

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

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

3. 实现

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

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