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类,包含构造函数、拷贝构造、拷贝赋值和析构函数。代码如下:

#include <iostream>#include <string.h>#include <vector>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新特性:右值引用移动语义完美转发的主要内容,如果未能解决你的问题,请参考以下文章

C++ C++11新特性--右值引用

C++ C++11新特性--右值引用

C11新特性右值引用&&

C++11新特性:19—— C++11右值引用

如何评价 C++11 的右值引用(Rvalue reference)特性?

C++ 11特性