学过C++的应该都知道C++中独有的概念—引用。
int b=1;
int& a = b;
大家都知道引用是一个地址的别名。C++11中将引用分为左值引用和右值引用。
左值引用就是上面那个例子,大家应该都了解这里不再详谈,这篇文章重点讨论右值引用:
C++11左值,纯右值和将亡值
通俗理解上,左值就是可以放在等号左边的值,右值就是放在等号右边的值。
更专业的说法是:
左值:左值指定了一个函数或对象,是一个可以取地址的表达式(能取地址就行)
右值:不和对象相关联的值(字面量)或者求其值结果是字面量或者一个匿名的临时对象
从定义上看左值,右值最重要的区别是能不能进行&x(取地址)操作。
C++11中将右值分成了纯右值和将亡值:
将亡值:是指进行右值引用操作之后的右值。它是一个产生于右值引用的概念。
C++11:右值引用
了解了三种值的概念我们现在来聊一聊右值引用。
参考左值引用的概念,右值引用就是给右值起个别名。这有什么意义,为什么我称呼他为C++11的荣光。下面我们来看这样一段程序:
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
int mCtor = 0;
int cCtor = 0;
int dtor = 0;
class myString{
public:
myString(const char* str = 0){
if (str) {
m_data = new char[strlen(str)+1];
strcpy(m_data, str);
}
else {
m_data = new char[1];
*m_data = '\0';
}
cout<<"myString无参构造函数"<<endl;
mCtor ++;
}
myString(const myString &str){
cout<<"myString复制构造函数"<<endl;
cCtor ++;
this->m_data = new char[strlen(str.m_data)+1];
strcpy(this->m_data,str.m_data);
}
~myString(){
delete[] m_data;
dtor++;
cout<<"myString析构函数"<<endl;
}
private:
char* m_data;
};
int main()
{
myString str1(myString("hello"));
cout<<mCtor<<endl;
cout<<cCtor<<endl;
cout<<dtor<<endl;
}
这是C++中string类的一个实现,在C++标准中匿名对象是一个纯右值,也就是当语句执行完之后就会被析构。
myString str1(myString("hello"));
这段代码运行的过程中首先生成一个内容为“hello”的匿名对象期间生成一个"hello"大小的字符串空间,然后在调用str1的拷贝构造函数这个期间又生成了一个"hello"大小的字符串空间,然后再将匿名对象的空间释放掉。(当然你可能现在看不到这个场景,上面的代码输出也可能不是这个情形,这是因为现在的编译器功能比较强大,像这种套娃类的定义可能会进行优化即把匿名对象和str1看成是一个。这个优化是可以关闭的自己百度就行了)
可能有人会说那我不这么 定义不就行了吗,大家要明白我上面的例子只是方便我解释右值引用,很有可能你在编程的时候需要定义一个返回值类型为类的函数:
myString returnMyString(){
return myString();
}
void getMyString(myString str){
}
getMyString(returnMyString());
print函数在调用的过程中调用了两次构造函数:第一次就是myString()生成一个匿名的对象,第二次是调用拷贝构造函数构造函数的返回值内容为生成的匿名对象。就相当于**myString(myString())**这是函数的返回值。可能大家不理解为什么:C++标准中匿名对象是一个纯右值,在执行完return myString()这条语句之后就会自动调用析构函数销毁内存空间,也就是说我们不能用它,这样就只能在生成一个和他一摸一样的匿名对象去执行下一条语句。
当然这个现象你也看不到编译器帮你优化好了hh.
大家都懂编译器是个什么东西啊,有时帮你优化有时就不优化。所以我们还是自己搞懂这个东西比较好。
说回正题我想生成一个内容为"hello"的字符串为什么要连续申请两个内容一摸一样的内存地址呢,而且生成之后还要销毁一个。这就很拖运行速度。
鉴于此右值引用的第一个意义——移动构造就诞生了
移动构造
myString(myString &&str):m_data{str.m_data} {
cout<<"myString移动复制构造函数"<<endl;
csCtor ++;
str.m_data = nullptr;
}
正如左值引用可以提高函数的调用性能一样,我们也希望可以用右值引用来避免申请和销毁一个没有实际意义的内存空间。
如果我们用右值引用将语句执行完就销毁的右值变成将亡值也就是延长它的生存周期。
调用移动构造函数,我们就可以不用再申请一份相同内容的内存空间,而是用一个别名继续使用上面提到的myString()这个匿名对象所申请的内存空间。这样就少调用了一个拷贝构造函数和虚构函数。极大地提高了性能。
std::move()
看到这里你会发现这不是偷吗:既然你不能用了,我就拿过来当我的空间了
这种方法好像很有意思,那我们可不可以用同样的方式将一些生命周期短的左值里的内容也偷过来呢。
C++11中提供了std::move()方法来将左值转换为右值:它的作用是告诉编译器我是个左值但是不调用拷贝构造函数而是调用移动构造函数。
myString str1("string");
myString str2(std::move(str1));//调用移动构造函数
注意:
这里str2偷取了str1的内容之后,str1还没有析构,他仍然是离开作用于之后才调用析构函数。
我们不能继续用str1来访问"string"这个内容,会产生错误(虽然不会报错)。
通用引用
引用有这么多好处,那能不能定义一个方法左值和右值我都可以用引用:
C++中可以将引用与模板相结合:
模板里的&&是一个未定义的引用类型,被称为通用引用必须被初始化,左值引用还是右值引用取决于初始化的对象左值还是右值。
template<typename T>
void f( T&& param){
}
f(10);
f(x);
注意:只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个通用指针。
完美转发
C++中右值引用还有一个重要的意义就是完美转发。
先说说什么是转发:
转发是指函数将自己的参数转发给其他函数进行处理也就是返回的对象是自己接受的参数。
什么是完美转发
完美转发是指转发之后参数的类型,特征保持不变。
可能你会想保持特征不变那我就用一个模板加通用指针不就行了吗。
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
void process(int& i){
cout << "process(int&):" << i << endl;
}
void process(int&& i){
cout << "process(int&&):" << i << endl;
}
template<typename T>
void myforward(T&& i){
cout << "myforward(int&&):" << i << endl;
process(i);
}
int main()
{
int a = 0;
process(a); //a被视为左值 process(int&):0
process(1); //1被视为右值 process(int&&):1
process(move(a)); //强制将a由左值改为右值 process(int&&):0
myforward(2); //右值经过forward函数转交给process函数,却成为了一个左值,
//原因是该右值有了名字 所以是 process(int&):2
myforward(move(a)); // 同上,在转发的时候右值变成了左值 process(int&):0
// forward(a) // 错误用法,右值引用不接受左值
}
运行这段代码会发现myforward并不能完美的将参数传给process,因为当我们将一个右值传给myforward时经过右值引用得到了名字,变成了左值。
那怎样才能做到完美转发呢?
C++11提供了一个方法
std::forward
void myforward(T&& i){
cout << "myforward(int&&):" << i << endl;
process(std::forward<T>(i));
}
用std::forward和通用引用就可以做到完美转发。
总结
1.值分为左值和右值,右值可以通过右值引用成为将亡值
2.右值引用过后的别名属于左值
3.模板中的&&是通用引用,初始化为左值就是左值引用,初始化为右值就是右值引用。
4.右值引用两个重要意义:
第一:通过移动语义(移动构造函数,移动赋值函数)避免了不必要的内存拷贝,提高了性能。
第二:配合std::forward和通用引用实现了完美转发。
5.std::move可以将左值转换为右值