文章目录
- 左值
- 左值引用
- 右值
- 右值引用
- 左值引用和右值引用
- 左值引用和右值引用总结
- 右值引用使用场景和意义
- 左值引用的使用场景
- 左值引用的缺点
- 右值引用
- 移动构造
- 移动赋值
- 右值引用的其他使用场景
- 万能引用
- 完美转发
- 完美转发的实际应用场景
C++11之前就有了引用的语法,而C++11中新增了的右值引用语法特性,所以在C++11之前的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
首先认识一下左值和右值,在来认识左值引用和右值引用。
左值
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,也可以出现在赋值符号的右边。
比如下面a,p,*p,b变量都是左值:
int a = 0;
int* p = new int(1);
const int b = 0;
左值引用
引用就是给变量取别名,左值引用就是给左值取别名。
左值引用符号&
比如下面refa,refp,pvalue,refb都是左值引用
//a,p,b都是左值
int a = 0;
int* p = new int(1);
const int b = 0;
//refa,refp,pvalue,refb都是左值引用
int& refa = a;//左值a的引用
int*& refp = p;//左值p的引用
int& pvalue = *p;//左值*p的引用
const int& refb = b;//左值b的引用
更多关于引用的知识在之前C++入门的博客中有详细的介绍C++入门。
这篇文章主要介绍C++11新引入的右值引用
右值
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(临时对象)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
比如下面10,a+b,fun(a+b)都是右值。
int fun(int a)
{
return a;
}
int main()
{
//a,p,b都是左值
int a = 0;
int* p = new int(1);
const int b = 0;
//10,a+b,fun(a+b)都是右值
10;
a + b;
fun(a + b);
return 0;
}
注意右值不能出现在=左边,
比如上面的右值出现在=左边时:
a + b = 1;//error编译错误,"="左边的操作数必须是左值
右值引用
右值引用就是对右值的引用,给右值取别名。
右值引用符号&&
比如下面ref1,ref2,ref3都是右值引用
//10,a+b,fun(a+b)都是右值
10;
a + b;
fun(a + b);
//ref1,ref2,ref3都是右值引用
int&& ref1 = 10;//右值10的引用
int&& ref2 = a + b;//右值a+b的引用
int&& ref3 = fun(a + b);//右值fun(a+b)的引用
左值引用和右值引用
左值引用可以引用右值吗?右值引用可以引用左值吗?
先说结论:可以。
右值是一些字面量和一些表达式和函数返回值的临时对象,而临时对象具有常性
。所以const左值引用也可引用右值。
比如下面ref就是左值引用对右值的引用:
int fun(int a)
{
return a;
}
int main()
{
int a = 1, b = 1;
//右值 这里的fun(a+b)是临时变量,生命周期只有自己所在的这一行
fun(a + b);
//左值引用引用右值
const int& ref = fun(a + b);//这里因为临时变量具有常性,所以要加const才可以。
}
右值引用也可以引用左值。
比如下面ref就是右值引用对左值的引用:
int main()
{
//左值a
int a = 0;
//右值引用引用左值,必须使用move函数来完成
int&& ref = move(a);
return 0;
}
关于move函数简单的认为就是右值引用引用左值必须要使用的。
左值引用和右值引用总结
-
左值引用
- 左值引用只能引用左值,不能引用右值
- const左值既可以引用左值,也可以引用右值
-
右值引用
- 右值引用只能引用右值,不能引用左值
- 但是右值引用可以move以后的左值
右值引用使用场景和意义
正片开始,上面主要介绍了左值引用和右值引用,通过前面可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?下面就来看看左值引用的短板,右值引用是如何补齐这个短板的!
更好的观察到现象,这里提供一个简易版的stirng类,通过控制台输出信息更好的观察程序运行情况和结果。
namespace ding
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
// 默认构造
string(const char* str = "")
:_size(strlen(str))
,_capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) --- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
//移动构造
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 operator=(const string& s)
{
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
void swap( string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
左值引用的使用场景
介绍左值引用的短板的时候,先看一下左值引用的使用场景。
- 做函数参数和返回值都可以提高效率。
比如:
下面代码全部会调用上面的string类,可以打印信息,方便观察结果。
void fun1(ding::string str)
{}
void fun2(ding::string& str)//左值引用做函数参数
{}
int main()
{
ding::string s1("hello world");
fun1(s1);
fun2(s1);
return 0;
}
运行结果:
- 对于fun1函数来说,是一个左值做函数参数,main函数中的s1传给fun1函数参数str时,会自动调用拷贝构造函数,生成一份s1传给str。mian函数中的s1对象和fun1函数参数str是两块不同的地址空间。在fun1函数中修改str不会影响到main函数中的s1对象。
- 对于fun2函数来说,是一个左值引用左函数参数,fun2函数中的str就是对main函数中的s1取别名,他俩是同一块地址空间。在fun2函数中修改str对象会影响到mian函数中的s1对象。
- 可以看出,左值引用做函数参数时,会减少拷贝,如果一个函数中相对外部对象做修改,那么传引用效率会更高,比传指针还高。因为指针还要占用4个字节大小空间。当然4个字节的空间不是很多,但是引用总比指针操作简单。
左值引用的缺点
左值引用做返回值,并不能完全避免函数返回对象时的拷贝。
当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回, 只能传值返回。这就是左值引用的短板。
下面用一个例子来说明:
ding::string fun()
{
ding::string str;//函数局部对象
return str;
}
int main()
{
ding::string s1 = fun();
return 0;
}
主函数调用fun函数,fun函数里面的局部变量做返回值,这里不能使用传引用返回,因为str是一个局部变量,出了这个函数后就被销毁了。只能使用传值返回,传值返回一定会在str析构之前调用拷贝构造函数来生成一份临时对象,然后临时对象在调用拷贝构造赋值给主函数的s1对象。
临时对象具有常性,cosnt左值引用可以引用,而拷贝构造函数的参数就是const左值引用类型。这里就会调用拷贝构造来完成。而拷贝构造函数又是一次深拷贝。这里编译器会优化,本来两次的拷贝构造编译器直接优化成了一次拷贝构造来完成。
这里还不能使用左值引用左返回值,只能使用值传递,值传递又会导致一次拷贝构造,C++11引入了右值引用来解决这一问题。
运行结果如下:
(这里我用的是vs2017专业版的,如果是新一点的编译器会优化,比如22版,会优化,一次调用也没有。如果是老一点的编译器,可能会调用两次。)
在Linux平台下使用g++(4.8.5)编译也是会优化,一次都不会调用。
下面的测试环境都在vs2017专业版下面进行测试了。
右值引用
移动构造
右值引用解决上面的问题就是给string类提供一个移动构造。
函数如下:
//移动构造(右值引用做为函数参数)
string(string&& s)
:_str(nullptr)
{
cout << "string(const string&& s) --- 移动拷贝" << endl;
swap(s);
}
移动构造函数不再调用构造函数取初始化,不涉及资源申请,直接交换两个对象即可。效率会比拷贝构造更高。
此时同样的代码,运行结果如下:
此时编译器会调用移动构造,来完成资源的移动,而不是像深拷贝一样,释放资源之前先拷贝一份临时资源在进行释放。效率更高。移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不 用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
移动赋值
不仅仅有移动构造,还有移动赋值,他们的本质都是一样的,不再申请新的资源,直接将之前的资源窃取过来,不用在做深拷贝,提高了效率。移动赋值函数如下:
//移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s)---移动赋值" << endl;
swap(s);
return *this;
}
比如下面程序就会调用移动赋值
ding::string fun()
{
ding::string str;
return str;
}
int main()
{
ding::string s1;
s1 = fun();//调用移动赋值
return 0;
}
运行结果:
如果不提供移动赋值,就会去调用赋值运算完成深拷贝。大大的提高了效率。
在C++11之后,STL库中的容器都支持了移动构造和移动赋值,
比如string类
总结:
通过上面的例子可以看出,cosnt左值引用去引用右值也是有价值的,比如没有移动构造的时候,右值析构之前会调用拷贝构造函数来完成资源的保存,但是会重新申请空间保存资源。在C++11之后,有了右值引用之后,右值直接转移资源,不再涉及资源申请的问题。提高了效率。这也是左值引用无法解决的问题。
右值引用的其他使用场景
C++11更新了右值引用之后,STL库中除了增加移动赋值和移动构造,有些插入函数还新增了右值引用版本。比如
list的push_back接口
下面就研究一下 右值引用做为插入函数接口参数的意义。
当一个链表中存放的是上面自己模拟实现简易的sting类时,向list中push_back元素:如下
int main()
{
ding::string s1("Hello World");
list<ding::string> l1;
//调用string的拷贝构造(深拷贝)
l1.push_back(s1);
//调用string的移动构造
l1.push_back("xxx");
//调用string的移动构造
l1.push_back(ding::string("xxxx"));
//调用string的移动构造
l1.push_back(move(s1));
return 0;
}
上面代码中,s1是左值,会调用左值引用版本的push_back();val就是左值引用,push_back时就会调用拷贝构造来构造string对象。此时val就是深拷贝。
后面的三个push_back传的都是右值,会去调用右值版本的push_back。val就是右值引用。调用移动构造来完成结点的插入。这样效率就会提高很多。
万能引用
模板中的万能引用。
在模板中&&符号是万能引用,即可以当做右值引用,也可以当做左值引用。
比如:
template<class T>
void fun(T&& data)
{
//....
}
函数参数data既不是左值引用也不是右值引用,而是万能引用。模板的万能引用只提供了同时接收左值和右值的能力,但是引用类型唯一作用就是限定了接收的类型,后续使用中都退化成了左值。比如上面fun函数中的data参数,万能引用只提供了同时接收左值和右值的功能,但是在函数体内后续使用data,data都只是左值。如果想让data继续保持原有属性,就要用到完美转发。
完美转发
完美转发主要解决的是模板中万能引用后退化成左值的问题。
比如下面代码
template<class T>
void Func(T& data)
{
cout << "左值引用" << endl;
}
template<class T>
void Func(const T& data)
{
cout << "const 左值引用" << endl;
}
template<class T>
void Func(T&& data)
{
cout << "右值引用" << endl;
}
template<class T>
void Func(const T&& data)
{
cout << "const 右值引用" << endl;
}
template<class T>
void PerfectForward(T&& data)
{
Func(data);
}
int main()
{
double a = 0;
PerfectForward(a);//左值
const int b = 1;
PerfectForward(b);//const左值
PerfectForward(10);//右值
PerfectForward(move(b));//const 右值
return 0;
}
运行结果:
可以发现,全部调用左值和const左值的函数了,而右值也调用左值版本的Func函数了。
这个原因是因为先调用PerfectForward函数,在PerfectForward函数中在调用Func函数。
而在PerfectForward函数中,向data传的不论是左值还是右值,data在后续使用过程中都是左值,所以上面程序运行的结果全是左值版本的Func函数。
如果想让data继续保持原有的属性,解决方式如下
template<class T>
void PerfectForward(T&& data)
{
Func(forward<T>(data));
}
在PerfectForward函数体内将data完美转发,保持其原有的属性
此时运行结果如下:
完美转发的实际应用场景
简易实现一个STL库中的list,提供push_back的右值引用版本
namespace ding
{
template<class T>
struct ListNode
{
ListNode(const T& data = T())
:_next(nullptr)
,_prev(nullptr)
,_data(data)
{}
ListNode<T>* _next;
ListNode<T>* _prev;
T _data;
};
template<class T>
class list
{
typedef ListNode<T> Node;
public:
list()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
~list()
{
Node* cur = _head->_next;
while (cur != _head)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
delete _head;
_head = nullptr;
}
//左值引用版本insert
void Insert(Node* pos,const T& data)
{
cout << "void Insert(Node* pos,const T& data)" << endl;
Node* newnode = new Node(data);
Node* prev = pos->_prev;
prev->_next = newnode;
newnode->_next = pos;
newnode->_prev = prev;
pos->_prev = newnode;
}
//右值引用版本insert
void Insert(Node* pos, const T&& data)
{
cout << "void Insert(Node* pos, const T&& data)" << endl;
Node* newnode = new Node(data);
Node* prev = pos->_prev;
prev->_next = newnode;
newnode->_next = pos;
newnode->_prev = prev;
pos->_prev = newnode;
}
//左值版尾插
void Push_back(T& data)
{
Insert(_head,data);
}
//右值版尾插
void Push_back(T&& data)
{
Insert(_head, forward<T>(data));
}
private:
Node* _head;
};
}
list简单实现了一个尾插功能,并且提供了左值版和右值版的尾插,尾插调用insert函数复用。
int main()
{
ding::list<int> ls;
int a = 0;
//左值
ls.Push_back(a);
//右值
ls.Push_back(1);
ls.Push_back(2);
ls.Push_back(3);
ls.Push_back(4);
return 0;
}
对于上面代码,除了第6行会调用左值版的push_back,其余的按理来说会调用右值版的push_back。然后右值版的push_back再调用右值版的insert。但是运行结果如下:
结果是右值版的也调用了左值版的insert。原因上面已经说过了。解决方式就是用完美转发。右值版的push_back修改如下:
void Push_back(T&& data)
{
//Insert(_head, data);
Insert(_head, forward<T>(data));
}
右值版的insert不用修改,如果修改完后,insert中的new就无法调用ListNode的构造函数了。