C++进阶 —— 右值引用(C++11新特性)

文章详细介绍了C++中的左值与右值概念,包括它们的区别和隐式转换。接着讲解了右值引用的引入,它是C++11的新特性,用于提高程序效率,特别是在涉及资源管理的对象拷贝和传递时。移动语义是通过右值引用实现的,允许资源在对象间的高效转移,减少了不必要的拷贝。最后,文章提到了完美转发的概念,它能保持参数的原始特性,优化函数调用。

目录

一,左值与右值

左值 loactor value

右值 read value

二,右值引用

以值的形式返回对象的缺陷

三,移动语义

move函数

四,完美转发

forward函数


一,左值与右值

        左值与右值是C语言中的概念,但并没有严格的区分方式;在C++中每个表达式都有两个独立属性:类型值分类;值类别决定表达式如何被使用,最主要的值类别为左值右值

        左右值根本的区别在于是否可通过取地址以获取对应内存地址;通俗来说,可以放在=左边的、或能取地址的称为左值,只能放在=右边的、或不能取地址的称为右值,但不完全正确;可看出,左值一定可以作为右值使用,右值一定不能为左值使用

         expression
          /      \
     glvalue    rvalue
      泛左值      右值
       /  \      /  \
   lvalue  xvalue   prvalue
    左值    将亡值    纯右值

左值 loactor value

有标识符(变量名),占据特定内存位置且可取地址,可出现在赋值运算符左边;

  • 普通类型的变量可取地址,都认为是左值;
  • 如单个变量是引用或表达式运行结果是引用,则认为是左值;
  • const修饰的常变量是不可修改的只读类型,但可取地址,C++11认为是左值(常量左值);
  • 字符串字面量为左值,有固定内存位置可取地址,类型为const char[N](常量左值);
  • 右值引用本身是左值,所有有名字的引用变量本身都是左值;
int a = 1; //普通类型变量a为左值,可取地址&a
int arr[10]; arr[0]=1;
string s="hello"; s[0]='H'; //operator[]返回左值引用,所以vec[0]是左值

int& c = a; //引用变量c为左值
int& fun(int& a) { return a; }
fun(a) = 10; //表达式fun(a)运行结果是引用为左值

const int b = 1; //常变量b认为是左值
const char* good_ptr = "Hello"; //"Hello"是左值,可取地址(&"Hello")

//std::move(str)返回参数右值引用,这个返回的表达式本身是将亡值
void process(string&& str) {
    // 在函数内部,str是有名字的变量,所以是左值!
    string local = std::move(str); // 需要std::move将左值转为右值
}

右值 read value

临时的,没有名字,即将销毁的值;

  • 纯右值Pure Right Value,无标识,通常用于初始化对象或计算表达式,生命周期仅限于当前表达式,绑定到右值引用会延长纯右值的生命周期,如:
    • 字面量(除了字符串字面量),100;
    • 算数表达式的结果,a+b;
    • 函数返回非引用类型,int add(int x, int y) { return x + y; };
    • lambda表达式,[](){ return 42; };
    • this指针;
100; 
int a,b;
int c = a + b; //隐式转换为右值
int add(int x, int y) { return x + y; } //函数返回的是值
string getName() { return "Alice"; } //函数返回临时对象
[](){ return 42; } 
this; //在成员函数中,this是纯右值
  • 将亡值Expiring Value,是有身份(有明确内存地址)但即将结束生命周期(资源可被窃取);
    • 函数返回的局部对象;
    • 通常由std::move或static_cast<T&&>产生;
      • 返回类型为右值引用的函数调用表达式是xvalue(将亡值),如std::move;
      • 转换为右值引用的类型转换表达式是xvalue(将亡值),如static_cast<T&&>;
//函数返回的局部对象
string createString() {
    string local = "Hello";
    return local;  // 返回时,local变成将亡值
}

string str = "Hello";
string new_str = std::move(str);
string&& rref = static_cast<string&&>(str);

注:三个隐式自动转换

  • 左值转换为右值,如i+3;
  • 数组名为常量左值,在表达式中会转换为首元素地址;
  • 函数名为常量左值,在表达式中转换为函数地址;

二,右值引用

        C++98提出了引用的概念,引用即别名,引用变量与其引用的实体公共同一块空间,且引用的底层是通过指针来实现的,使用引用可提高程序的可读性;通常说的引用指的是左值引用(T&);

void swap(int& left, int& right)
{
	int tmp = left;
	left = right;
	right = tmp;
}

        为了提高程序的运行效率,C++11引入了右值引用(T&&),右值引用也是别名,但其只能对右值引用;

  • 右值引用也必须立即进行初始化操作
  • 且只能使用右值进行初始化
  • 可延长生命周期并修改;
int&& a = 1; //对右值进行引用
a = 10; //还可对右值引用进行修改
const int&& b = 1; //常量右值引用,无实际用处

引用与右值引用比较

  • T& 只能绑定左值,给对象起别名,作为函数参数可修改传入对象;
  • T&& 只能绑定右值,实现移动语义和完美转发;
  • const T& 即可绑定左值也可绑定右值,只读访问;

//普通类型只能引用左值,不能引用右值
int a = 10;
int& ra = a;
//int& rb = 10; 编译失败,10是右值

//常量左值引用,即可引用左值也可引用右值
const int& rc = a;
const int& rb = 10;
//右值引用只能引用右值,一般不能引用左值
int&& rd = 10;
//int&& re = a; 编译失败,a是左值
//int&& re = rd; 编译失败,rd是左值
//一下函数均可传入右值,int&& value会更优先因为更精确
void process(const int& value)
void process(int&& value) 

以值的形式返回对象的缺陷

        如一个类中涉及到资源管理,需显式提供拷贝构造、赋值运算符重载以及析构函数,否则拷贝对象或对象之间赋值就会出错(浅拷贝);

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* tmp = new char[strlen(s._str) + 1];
			strcpy(tmp, s._str);
			delete[] _str;
			_str = tmp;
		}
		return *this;
	}

	String operator+(const String& s)
	{
		char* tmp = new char[strlen(_str) + strlen(s._str) + 1];
		strcpy(tmp, _str);
		strcpy(tmp + strlen(_str), s._str);
		String strRet(tmp);
		return strRet;
	}

	~String() { delete[] _str; }
private:
	char* _str;
};

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

上述代码s1+s2调用operator+中,返回的是strRet值(将完值),s3创建过程为:

  • 创建一个临时对象接收然后strRet值,调用拷贝构造,String Tmp = strRet;
    • strRet出作用域将销毁;
  • 然后使用临时对象来构造s3,调用拷贝构造,String s3(Tmp);
    • 最后在销毁此临时对象Tmp;

        在这个过程中,strRet、Tmp,s3,都有自己独立的空间,相当于创建了三个内容完全相同的对象,浪费空间,程序效率低下,且临时对象作用不大;

三,移动语义

         C++11新增的特性,以提高对象传递和拷贝时的效率;即通过将资源所有权从一个对象转移到另一个对象,来避免不必要的内存分配和数据复制;具体表现为,通过“右值引用”将移动构造或移动赋值运算符设置为优先于常规的拷贝构造或拷贝赋值运算符;

  • 即将一个对象中资源移动到另一个对象中的方式;
  • C++11中如需实现移动语义,必须使用右值引用;
class A
{
public:
	A(int a) :_a(a) { cout << "A(int a)._a=" << _a << endl; } //构造,注意A(int&& a)
	A(A& a) :_a(a._a) { cout << "A(A& a)._a=" << _a << endl; } //拷贝构造
	A& operator=(const A& a) { _a = a._a; return *this; } //赋值重载

	A(A&& a)noexcept :_a(a._a) { cout << "A(A&& a)._a=" << _a << endl; } //移动构造
	A& operator=(A&& a)noexcept { _a = a._a; return *this; } //移动赋值重载
private:
	int _a;
};

int main()
{
	A a1(1), a2(2); //调用 A(int a)
	A a3(a1); //调用 A(A & a)
	a3 = a2; //调用 A& operator=(const A& a)

	A a4(move(a1)); //调用 A(A&& a)
	a4 = move(a2); //调用 A& operator=(A&& a)
	return 0;
}

上述代码s1+s2调用operator+中,引入移动构造,将strRet中资源转移到临时对象中;

String(String&& s)
	:_str(s._str)
{
	s._str = nullptr;
}

引入移动构造后,s3创建过程为:

  • 创建一个临时对象接收然后strRet值,调用移动构造,String Tmp = strRet;
    • 将strRet中资源转移到临时对象中,strRet出作用域将销毁;
  • 然后使用临时对象来构造s3,调用移动构造,String s3(Tmp);
    • 将临时对象中资源转移到s3中,最后在销毁此临时对象Tmp;

整个过程只需创建一块堆内存即可,节省空间也提供运行效率;

    注:

    • 移动构造函数的参数,不要设置成const类型的右值引用,因为资源无法转移而导致移动语义失效;
    • 在C++11中编译器会为类默认生成一个移动构造,为浅拷贝,因此当类中涉及到资源管理时需显式定义移动构造;

    move函数

            按照语法右值引用只能引用右值,但有些场景可能需要用右值去引用左值实现移动语义;当需要用右值引用引用左值时,可通过move函数将左值转化为右值;

    • 辅助函数,其唯一的功能就是将一个左值强制转化为右值引用,以实现移动语义;
    • 标准库的许多组件实现了移动语义,允许在参数为右值时直接转移对象的资产和属性的所有权,而无需复制,如string,vector等;
    • 注意,在标准库中,被移出的对象处于有效但未指定的状态;在操作后,被移出的对象应被销毁或分配一个新值;否则访问会产生一个未指定的值;
    • 被转化的左值,其生命周期并没有随左值的转化而改变,即std::move转化的左值变量不会被销毁;
    • STL中也有另一个move函数,将一个范围的元素搬移到另一个位置;
    //等价于
    static_cast<remove_reference<decltype(arg)>::type&&>(arg)
    template<class _Ty>
    inline typename remove_reference<_ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
    {
        return ((typename remove_reference<_Ty>::type&&)_Arg);
    }
    int main()
    {
    	int a = 1;
    	int aa = move(a); 
    
    	string s = "abcd";
    	string ss = move(s); //实现移动语义,资源已转移,s已为空;
    
    	vector<int> v = { 1,2,3,4 };
    	vector<int> vv = move(v); //实现移动语义,资源已转移,v已为空;
        
        //std::move(str)是将亡值,但变量b是左值
        string&& b = std::move(str);  
    	return 0;
    }

    move函数的经典误用:

    String s1("hello world");
    String s2(move(s1));
    String s3(s2);
    

            move将s1转化为右值后,在实现s2的拷贝时会使用移动构造,此时s1的资源就被转移到s2中了,s1就会成为无效字符串;

    四,完美转发

            以下PerfectForword为转发的模板函数,Func为实际目标函数,但转发不算完美;完美转发是将参数按传递给PerfectForword函数的实际类型转发给目标函数,而不产生额外开销,就像不存在转发者一样;

    void Func(int x){cout << x << endl;}
    
    template<typename T>
    void PerfectForward(T t)
    {
        //目标函数
        Func(t);
    }

    完美转发是指在函数模板中,完全依照模板的参数类型,将参数传递给函数模板中调用的另一个函数,同时保持其值类别(lvalue或rvalue)和类型不变;

    所谓完美,即模板函数在向其他函数传递自身形参时:

    • 如相应实参是左值,就应被转发为左值;如相应实参是右值,就应该被转发为右值;
    • 为保留对转发而来的参数的左右值属性进行不同处理;
      • 如参数为左值时拷贝语义,参数为右值移动语义;

    通用引用,即可绑定左值也可绑定右值;

    • 如果传入左值结果为左值引用,T 推导为 T&T&& 折叠为 T&

    • 如果传入右值结果为右值引用,T 推导为 TT&& 保持 T&&

    // 右值引用 - 只能绑定到右值
    void process(string&& str) {}  // 明确的右值引用
    
    // 通用引用 - 可以绑定到左值和右值
    template<typename T>
    void PerfectForward(T&& t) {}  // 可能是左值引用或右值引用

    引用折叠规则

    • T& & 折叠为 T&
    • T& && 折叠为 T&
    • T&& & 折叠为 T&
    • T&& && 折叠为 T&&

    forward函数

    • 如arg不是左值引用,则返回arg的右值引用;
    • 如arg是左值引用,则返回arg(不修改其类型);
    • 辅助函数,将右值引用完美转发到其他函数;
      • 因为所有有名字的引用变量本身都是左值,即使是右值引用;
    • 任何实例化都必须显式指定T的类型(任何隐式推断的T都不匹配);
    //等价于
    static_cast<decltype(arg)&&>(arg)
    //C++11通过forward函数来实现完美转发!
    void Fun(int& x) { cout << "int& -> lvalue ref" << endl; }
    void Fun(int&& x) { cout << "int&& -> rvalue ref" << endl; }
    void Fun(const int& x) { cout << "const int& -> lvalue ref" << endl; }
    void Fun(const int&& x) { cout << "const int&& -> rvalue ref" << endl; }
    
    template<typename T>
    void PerfectForward(T&& t) //万能引用,即可传递左值也可传递右值
    {
    	Fun(std::forward<T>(t));
    }
    
    int main()
    {
    	PerfectForward(10); //10是右值,int&& -> rvalue ref
    
    	int a;
    	PerfectForward(a); //a是左值,int& -> lvalue ref
    	PerfectForward(std::move(a)); //将a转化为右值 int&& -> rvalue ref
    
    	const int b = 8;
    	PerfectForward(b); //b是const左值 const int& -> lvalue ref
    	PerfectForward(std::move(b)); //将b转化为const右值 const int&& -> rvalue ref
    	return 0;
    }

    右值引用作用

    C++98中引用作用,因为引用是别名,需要用指针操作的地方,可以使用引用来替代,可提高代码的可读性及安全性;

    C++11中右值引用主要有以下作用:

    • 给中间临时变量取别名;
    • 实现移动语义(移动构造或移动赋值);
    • 实现完美转发;

    评论
    成就一亿技术人!
    拼手气红包6.0元
    还能输入1000个字符
     
    红包 添加红包
    表情包 插入表情
     条评论被折叠 查看
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值