天天看点

C++右值引用的示例

解释

通常来说左值是指非零时对象,而右值是指临时对象。我们知道c++在进行临时对象的拷贝时会调用拷贝构造函数:

vector<string> v;
v.push_back(string("1234");
           

产生的行为就是先构造一个“1234”的临时string,然后调用拷贝拷贝构造函数拷贝临时“1234”到vector里面,然后销毁临时的“1234”。在这样的语义里面临时“1234”明显是一个右值,想一想它从产生到销毁居然就是为了被拷贝一次?那不等价于直接拿它的内容去用就好了嘛,是的c++的右值引用的出现帮我们解决了这个问题,我们看个栗子:

没有用右值引用的栗子

#include <iostream>
#include <vector>
using namespace std;

extern "C" {
#include <string.h>
}

class MyClass {
    private:
        char* str;
    public:
        MyClass(const char* s) {
            cout<<"construct MyClass\n";
            str = new char[strlen(s)];
            strcpy(str, s);
        }

        ~MyClass() {
            cout<<"~MyClass\n";
            delete str;
        }

        MyClass(const MyClass& mc) {
            cout<<"copy MyClass\n";
            delete str;
            str = new char[strlen(mc.str)];
            strcpy(str, mc.str);
        }

        MyClass& operator= (const MyClass& mc) {
            cout<<"operator=(&)\n";
            if(this != &mc) {
                delete str;
                str = new char[strlen(mc.str)];
                strcpy(str, mc.str);
            }
        }

        friend ostream& operator << (ostream& os, MyClass& mc) {
            os<<mc.str;
            return os;
        }
};

vector<MyClass> makeMcVec() {
    vector<MyClass> v;
    v.reserve();
    v.push_back(MyClass("Hello World"));
    v.push_back(MyClass("\n"));
    for(auto it = v.begin(); it != v.end(); it++) {
        cout<<*it;
    }
    return v;
}
           

输出为:

construct MyClass
copy MyClass
~MyClass
construct MyClass
copy MyClass
~MyClass
Hello World
after makeMcVec
~MyClass
~MyClass
           

加入右值引用的区分

我们看到我们为了用两个string,产生了四次堆区的new和delete,把堆拷贝一次然后销毁,那为什么不直接用就好了呢,这明显性能不好,其实我们只要两次就可以了。好我们加入两个成员函数:

MyClass& operator = (MyClass&& mc) {
            cout<<"operator = &&\n";
            if(this != &mc) {
                this->str = mc.str;
                mc.str = NULL;
            }
        }

        MyClass(MyClass&& mc) {
            cout<<"MyClass &&\n";
            this->str = mc.str;
            mc.str = NULL;
        }
           

输出:

construct MyClass
MyClass &&
~MyClass
construct MyClass
MyClass &&
~MyClass
Hello World
after makeMcVec
~MyClass
~MyClass
           

我们看到,拷贝构造函数不被调用到了,这里发生了什么呢,当我们声明了“&&”右值引用符号之后,对待右值时会调用右值引用的函数,这样子我们就能针对右值引用做处理了,我们把MyClass(MyClass&& mc)的形式称为转移构造函数,我们看到我们对待str的时候是直接“转移过来了”而不是再拷贝一次,这样就不用再去重复分配一次堆内存了,高效了。当然编译器不会自动帮我们产生一个拷贝构造函数的,因为它无法知道我们想干什么,需要我们手动声明。

你可能说那我直接在拷贝构造函数里面直接转移不就行了了嘛何必多此一举,如果拷贝的不是一个右值而是一个左值呢,你直接拷过来别人还想用啊,这样不就出问题了嘛,明显对待左值这么做不对,所以需要区分。

move

vector<MyClass> makeMcVec1() {
    vector<MyClass> v;
    v.reserve();
    MyClass a("Hello World");
    MyClass b("\n");
    v.push_back(a);
    v.push_back(b);
    return v;
}
           

输出

construct MyClass
construct MyClass
copy MyClass
copy MyClass
~MyClass
~MyClass
after makeMcVec
Hello World
~MyClass
~MyClass
           

在这种情况下其实我们实际上a可以当作右值,因为a以后也不会被用到了,但是编译器无法推导,有没有办法让a被当作右值对待呢,加入move转变成右值:

vector<MyClass> makeMcVec1() {
    vector<MyClass> v;
    v.reserve();
    MyClass a("Hello World");
    MyClass b("\n");
    v.push_back(move(a));
    v.push_back(move(b));
    return v;
}
           

输出

construct MyClass
construct MyClass
MyClass &&
MyClass &&
~MyClass
~MyClass
after makeMcVec
Hello World
~MyClass
~MyClass
           

继续阅读