C++11---右值引用(深度讲解)

目录

简要介绍

什么是左值 什么是右值

1.左值,左值引用

2.右值

右值引用

 左值引用和右值引用比较

左值引用总结:

右值引用总结:

为什么要有右值引用

从左值引用窥探

解决方案:

万能引用 完美转发

模板中的万能引用

std::forward完美转发


简要介绍

右值引用是C++11的新特性,无论左值引用还是右值引用,都是在给对象取别名

什么是左值 什么是右值

1.左值,左值引用

左值是一个数据的表达式(例如变量或者解引用后的指针),我们可以对其进行取地址和修改赋值,左值可以出现在赋值符号的左边,而右值不能出现在赋值符号的左边,而左值引用就是对左值起别名,

如图

2.右值

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引
用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能
取地址

右值引用

右值引用就是对右值的引用,给右值取别名

这几个都是常见的右值引用

需要注意的是,在给右值引用后,变量就会变成左值,为什么? 因为需要对其进行修改,如果需要再要将其变成右值,需要加上std::move()将其变成右值

右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是a引用后,可以对a取地
址,也可以修改rr1。如果不想a被修改,可以用const int&& a 去引用,是不是感觉很神奇,
这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要

 左值引用和右值引用比较

左值引用总结:

1. 左值引用只能引用左值,不能引用右值。
2. 但是const左值引用既可引用左值,也可引用右值


右值引用总结:

1. 右值引用只能右值,不能引用左值。
2. 但是右值引用可以move以后的左值。

为什么要有右值引用

从左值引用窥探

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引
用呢?是不是画蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!
注意以下采用右值引用的移动构造

#pragma once
class string
{
public:
	string(const char* str = "")
		:_size(strlen(str))
		, _capacity(_size)
	{
		//cout << "string(char* str)" << endl;
		_str = new char[_capacity + 1];
		strcpy(_str, str);
	}
	// s1.swap(s2)
	void swap(string& s)
	{
		::swap(_str, s._str);
		::swap(_size, s._size);
		::swap(_capacity, s._capacity);
	}
	// 拷贝构造
	string(const string& s)
		:_str(nullptr)
	{
		cout << "string(const string& s) -- 深拷贝" << endl;
		string tmp(s._str);
		swap(tmp);
	}
	// 赋值重载
	string& operator=(const string& s)
	{
		cout << "string& operator=(string s) -- 深拷贝" << endl;
		string tmp(s);
		swap(tmp);
		return *this;
	}
	// 移动构造
	string(string&& s)
		:_str(nullptr)
		, _size(0)
		, _capacity(0)
	{
		cout << "string(string&& s) -- 移动语义" << endl;
		swap(s);
	}
	// 移动赋值
	string& operator=(string&& s)
	{
		cout << "string& operator=(string&& s) -- 移动语义" << endl;
		swap(s);
		return *this;
	}
	~string()
	{
		delete[] _str;
		_str = nullptr;
	}
	char& operator[](size_t pos)
	{
		assert(pos < _size);
		return _str[pos];
	}
	void reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}
	void push_back(char ch)
	{
		if (_size >= _capacity)
		{
			size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
			reserve(newcapacity);
		}
		_str[_size] = ch;
		++_size;
		_str[_size] = '\0';
	}
	//string operator+=(char ch)
	string& operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}
	const char* c_str() const
	{
		return _str;
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity; // 不包含最后做标识的\0
};

左值引用的使用场景:
做参数和做返回值都可以提高效率

如下:

void func1(string s1)
{}
void func2(string& s1)
{}
int main()
{
    string s1("hello world");
    // func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
    func1(s1);
    func2(s1);
    // string operator+=(char ch) 传值返回存在深拷贝
    // string& operator+=(char ch) 传左值引用没有拷贝提高了效率
    s1 += '!';
    return 0;
}

左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回
只能传值返回。例如:string to_string(int value)函数中可以看到,这里只能使用传值返回,
传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

 这里可以看到,传值返回会至少导致一次拷贝构造(旧编译可能优化没有那么好就是两次了)

因为to_string的返回值是右值,利用这个右值构造ret,如果没有了移动构造,就会去调用拷贝构造,就会给上一个深拷贝,可以看见,这里是有优化空间的

出来吧!我的右值引用和移动语义

解决方案:

在string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不
用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己

string (string&& s)
	:_str(nullptr)
{
    cout<<"这是移动构造"<<endl;
	swap(s);
}
string (const string& s)
    :_str(nullptr)
{
    cout<<"拷贝构造深拷贝"<<endl;
    string tmp._str;
    swap(tmp,s);
}
int main()
{
    string ret2 = bit::to_string(-1234);
    return 0;
}

 再运行一下,就会发现你会调用移动构造,而移动构造里没有开空间,进行拷贝数据,所以效率提高了

(当移动构造和拷贝构造同时出现的时候,因为to_string的返回值是右值,所以编译器就会选择适合的->移动构造)

同样的道理,移动赋值也是这样

string& operator=(string&& s)
{
    cout << "string& operator=(string&& s) -- 移动语义" << endl;
    swap(s);
    return *this;
}
int main()
{
    string ret1;
    ret1 = to_string(1234);
    return 0;
}

// 运行结果:
// string(string&& s) -- 这是移动构造
// string& operator=(string&& s) -- 移动语义

 这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象
接收,编译器就没办法优化了,to_string函数中会先用str生成构造生成一个临时对象,但是
我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时
对象做为to_string函数调用的返回值赋值给ret1,这里调用的移动赋值

万能引用 完美转发

模板中的万能引用

void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
template<typename T>
void PerfectForward(T&& t)//万能引用 传左值的时候就变成 int& t 右值就是int&&
{
    Fun(t);
}
int main()
{
    PerfectForward(10); // 右值
    int a;
    PerfectForward(a); // 左值
    PerfectForward(std::move(a)); // 右值
    const int b = 8;
    PerfectForward(b); // const 左值
    PerfectForward(std::move(b)); // const 右值
    return 0;
}

std::forward完美转发

forward完美转发可以在传参的时候保留对象原生类属性

void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
    Fun(std::forward<T>(t));
}
int main()
{
    PerfectForward(10); // 右值
    int a;
    PerfectForward(a); // 左值
    PerfectForward(std::move(a)); // 右值
    const int b = 8;
    PerfectForward(b); // const 左值
    PerfectForward(std::move(b)); // const 右值
    return 0;
}

  • 22
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值