C++基础(插入1)——C++11新特性:右值引用移动语义完美转发
Posted 木叶藤花的五亩地
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++基础(插入1)——C++11新特性:右值引用移动语义完美转发相关的知识,希望对你有一定的参考价值。
因为最近想准备春招,所以想看一些之前没怎么掌握的东西,所以在C++基础中插入那么一章。大家可以以后学到再配合着看。
在我学习以上三个C++11的新特性的时候,会发现这三个是环环相扣的。
右值引用:
首先说明左值,按照我的理解,有具体名字并且在这个表达式结束后依旧存在的对象叫做左值;与之相反的就是右值。
int a = 1;
//其中a就是左值,1就是右值
这里再补充一下,函数返回值如果没有添加引用,在main函数中返回就是右值,因为它是临时变量。
int test01()
{
int a = 1;
return a;
}
int main()
{
int a = test01();//这里a就是左值,而test01()就是右值
return 0;
}
接下来引出左值引用和右值引用:
int a = 1;
int& ref = a;//左值引用,就是我们常见的引用,相当于给a取了别名
int& b = 1;//而此处,1是右值,就不能用&的形式来引用,因此会报错(编译报错?求指教)
C++11中定义了右值引用
int&& b = 1;//右值引用
本来“1”在这个等式运行完生命就结束了,但是使用右值引用,相当于给1取了一个别名,它就会一直存活下去。
补充一个知识:在移动语义里会用到:
左值引用只能绑定左值,右值引用只能绑定右值。
但是常量左值引用可以绑定所有(常量左值、非常量左值、右值)。
移动语义
移动语义先从一个例子看起:大家可以自己写一个string类,包含构造函数、拷贝构造、拷贝赋值和析构函数。代码如下:
using namespace std;
class String
{
public:
//构造函数
String(const char *str = 0)
{
con_num ++;
if(!str)
{
data = new char[1];
*data = '\0';
}
else
{
data = new char[strlen(str) + 1];
strcpy(data, str);//使用strcpy要包含头文件string.h
}
}
//拷贝构造函数
String(const String& str);
//赋值操作
String& operator=(const String& str);//形参是常量左值引用
//析构函数
~String();
char *data;
static int con_num; //构造函数调用次数
static int copy_num; //拷贝构造函数调用次数
};
int String::con_num = 0;
int String::copy_num = 0;
//拷贝构造
String::String(const String& str)
{
copy_num ++;
data = new char[strlen(str.data) + 1];
strcpy(data, str.data);
}
//赋值
String& String::operator=(const String& str)
{
//检测赋值给自己
if(this == &str)
{
return *this;
}
else
{
delete[] this->data;
this->data = new char[strlen(str.data) + 1];
strcpy(this->data, str.data);
return *this;
}
}
String::~String()
{
delete[] data;
}
我们可以验证一下当我们在main函数中实例化一个类对象的时候,调用的是哪些构造函数。
int main()
{
vector<String> vstr;
String str1("Hello");
//调用一次构造函数
cout << "con_num=" << String::con_num <<" copy_num=" << String::copy_num << endl;
String str2(str1);
//调用拷贝构造函数
cout << "con_num=" << String::con_num <<" copy_num=" << String::copy_num << endl;
vstr.reserve(200);//当没有这个reserve提前预留空间时,下面的拷贝构造调用的次数是超过100次的,因此每次超过vector预留的空间大小,都会再重新开辟空间
for(int i = 0; i < 200; i ++)
{
vstr.push_back("Hello");
//调用了100次构造函数和100次拷贝构造函数
//100次构造函数是将自带的string构造成自己写的String类型
vstr.push_back(str2);
//调用100次拷贝构造函数
}
cout << "con_num=" << String::con_num <<" copy_num=" << String::copy_num << " move_num=" << String::move_num << endl;
return 0;
大家可以尝试运行一下这个程序,来判断一下实例化对象时使用了哪些构造函数。可以通过con_num、copy_num来进行输出判断。
我们会发现在进行vstr.push_back("Hello");这个步骤时,进行了100次的构造函数和拷贝构造。每次进行push_back()操作时,先会构建String类型,因此使用构造函数,然后使用拷贝构造,将“Hello”赋给vstr里的String类型。(此处是我自己验证的,欢迎指正探讨)。
验证思路如下,我非常疑惑vstr.push_back("Hello");为什么会调用构造函数与拷贝构造函数,于是我在main函数中,将vector<String> vstr;改成了vector<String> vstr(1);再输出con_num会发现调用了一次构造函数,因此我推断,实际上vstr.push_back("Hello");的操作,等价于vector<String> vstr(1);vstr[0]="Hello";。以上。
转回去继续探讨这个循环了100的往数组中push_back的操作,我们可以看到自己写的拷贝构造的代码。创建了一个数组去进行strcpy的操作。每进行一次push,都会new一个data。
所以,明明已经有了“Hello”,但是却还是要再new,然后strcpy,十分地耗费时间和空间,有没有什么办法可以直接将已经构建的data的指针直接指向已经存在的“Hello”呢?
于是,便有了移动语义。
移动构造的函数这么写,记得在类内添加这个函数和表示移动构造次数的move_num。
//移动构造
String::String(String&& str):data(str.data)
{
str.data = nullptr;
move_num ++;
}
可以看到,移动构造函数的形参是一个右值引用(&&),主函数中的“Hello”是一个右值,因此会优先调用这个移动构造函数。直接就将this->data赋值为str.data了,不需要再new一个数组了。因此非常节省内存和时间。
而且在次数,直接换指针的指向没关系,因此str是一个右值,生命周期很短,就算新new一个数组去strcpy它,它之后也会析构掉,不如直接用一个指针去指向它。
总结一下:移动语义不需要new数组去strcpy,而是直接将指针指向这个右值。避免对含有资源的对象发生无畏的拷贝。
移动构造函数充分利用了资源,有些左值是局部变量,生命周期也很短,因此,可以将其使用std::move()的方法,将这个局部左值转变为右值,这样就可以使用移动构造函数了。
例如可以在for里面构建一个tmp,tmp的作用域就是for循环的这个大括号中,超出了这个大括号,这个tmp就不存在了,而如果直接传入tmp调用的是拷贝构造函数,而使用move()函数,调用的就是移动构造。
for(int i = 0; i < 100; i ++)
{
String tmp("Hello");
vstr.push_back(move(tmp)); //100次构造,vstr构造String的类型,接受“Hello”,使用拷贝构造将“Hello”拷贝进去
}
并且,补充一点,就像之前说的那样,移动拷贝构造将原来的内容以一个新的指针指向,并且将原指针指向了nullptr,因此,在使用移动拷贝构造后,在调用原来的内容会产生错误。
像在我自己的编译器里,是运行不出来下面这个cout的结果的,因为str1的内容被str3指向了。不同的编译器可能会有不同的结果,欢迎大家和我说一下你们的结果是什么,我也学习一下。
String str3(move(str1));
cout << str1.data << endl;
通用引用( universal references),随口提一句,就是模板下的&&,例如T && t。这里可以既表示左值引用,也可以表示右值引用。就看T推导出来是int还是int&(此处以int举例)。
完美转发
完美转发就是:
有两个同名函数,比如:
void process(int &a)
{
cout << "process(int &a) = " << a << endl;
}
void process(int &&a)
{
cout << "process(int &&a)" << a << endl;
}
void test01(int &&a)
{
process(a);
cout << "process" << endl;
}
当你使用:
void test()
{
test01(1); //传进去的是右值
}
结果为:
也就是说明明传进去的是右值,但是却在下一步的函数中被转换成了左值。完美转发要解决的就是这个问题。
C++中提出了std::forward()函数来解决这个问题。用法为forward<T>();
将test01()函数中改为:
void test01(int &&a)
{
process(forward<int>(a));
cout << "process" << endl;
}
可以得到结果为:
也就是它调用的是形参为右值的那个同名函数。
而此处仍然不可以传递左值,因为我写的test01()的形参是一个右值引用,而如何保证我们能够传递左值呢,这就需要前面提了一句的通用引用。
将test01()改成模板类:
template<typename T>
void test01(T && a)
{
cout << "process" << endl;
process(forward<T>(a));
}
之后再在test()函数中:
void test()
{
int a = 1;
test01(a); //此处推断出T为int&
test01(1); //此处推断出T为int
}
结果为:
很完美。
参考网站:https://www.jianshu.com/p/d19fc8447eaa
此网站最后介绍了一个emplace函数,很优美。
如果大家有看到错误的和看不懂的,欢迎随时给我发消息。我也是刚刚看并总结分享了一下,如有误导,万分抱歉,欢迎指正。
以上是关于C++基础(插入1)——C++11新特性:右值引用移动语义完美转发的主要内容,如果未能解决你的问题,请参考以下文章