1.对象的引用优化,右值引用优化

1.对象的引用优化,右值引用优化

这一节中主要讲了对象和函数在使用和调用过程中一些注意事项,比较重要的是右值引用和最后的move和forward

对象的使用过程中调用了哪些方法?

对于以下这个测试类,列出了十几种不同的定义方式

class Test {
public:
    Test(int a = 4, int b = 10) : ma(a), mb(b) {
        cout << "Test()" << endl;
    }

    ~Test() {
        cout << "~Test()" << endl;
    }

    Test(const Test &src) {
        ma = src.ma;
        mb = src.mb;
        cout << "Test(const Test&)" << endl;
    }

    Test &operator=(const Test &src) {
        ma = src.ma;
        mb = src.mb;
        cout << "operator=(const Test&)" << endl;
        return *this;
    }

private:
    int ma;
    int mb;
};

实现结果如下:

有几个比较值得注意的点:

  • 对象赋值的情况会产生临时对象,临时对象在语句结束后会执行析构函数
  • 隐式生成的临时对象,如t2=60,编译器会找对象中有无合适的构造方法生成对象
  • 用指针保存临时对象,临时对象在语句结束后会被析构。安全的做法是通过引用指向对象
  • (50,50)这种形式的是逗号表达式,赋值的时候只看最后的数字

函数调用过程中背后调用的方法

函数调用的过程中,实参传递到形参需要重新初始化,函数的形参对象需要初始化,这个过程中会调用对象的拷贝构造方法

函数体内部返回的对象也要现在main栈帧中拷贝构造一个临时变量,才能在main作用域中访问这个对象。

函数体执行完毕后需要先析构函数体内构造的对象,然后再析构形参列表构造的对象

三条对象优化的规则

  1. 函数参数传递过程中,对象优先按引用传递,不要按值传递。
  2. 函数返回对象的时候,应该优先返回一个临时对象,而不要返回一个定义过的对象
  3. 接受返回值是对象的函数调用的时候,优先按初始化的方式接收,不要按赋值的方式接收

上图中的代码最后被优化为以下代码:

Test GetObject(Test &t){
	int val=t.getData();
	return Test(val);//定义临时对象 2.Test()
}
int main(){
	Test t1;//1.Test()
	Test t2=GetObject(t1);//用临时对象拷贝构造同类型的新对象,编译器会优化此过程 少了临时对象在main栈帧上的构造和析构
	return 0;
}
//3.~Test()
//4.~Test()

优化完只剩下4步构造析构的过程

之前String代码中的问题

class String {

    friend std::ostream &operator<<(std::ostream &os, const String &src);
    friend String operator+(const String &l, const String &r);

public:
    String(const char *src = nullptr) {
        if (src == nullptr) {
            _pstr = new char[1];
            *_pstr = "";
        } else {
            _pstr = new char[strlen(src) + 1];
            strcpy(_pstr, src);
        }
        std::cout<<"String(const char *src = nullptr)"<<std::endl;
    }

    ~String() {
        delete[] _pstr;
        _pstr = nullptr;
        std::cout<<"~String()"<<std::endl;
    }

    String(const String &src) {
        _pstr = new char[strlen(src._pstr) + 1];
        strcpy(_pstr, src._pstr);
        std::cout<<"String(const String &src)"<<std::endl;
    }

    bool operator>(const String &str) const {
        return strcmp(_pstr, str._pstr) > 0;
    }

    bool operator<(const String &str) const {
        return strcmp(_pstr, str._pstr) < 0;
    }

    bool operator==(const String &str) const {
        return strcmp(_pstr, str._pstr) == 0;
    }

    int length() const {
        return strlen(_pstr);
    }

    char &operator[](int index) {
        return _pstr[index];
    }

    char *c_str() const {
        return _pstr;
    }

private:
    char *_pstr;
};

String GetString(String& str){
    const char* pstr=str.c_str();
    String tmpStr(pstr);
    return tmpStr;//这一步要在main栈帧中拷贝构造一个临时变量,会重新划分一块内存
}

int main(){
   	String s1("assf");
    String s2;
    s2=GetString(s1);//调用赋值重载函数,会删除原有内存,重新划分一块内存
    cout<<s2.c_str()<<endl;
    //这一过程划分了两次内存,且都是无效的
}

在调用中出现了多次临时对象,产生一个临时对象就要在栈帧上拷贝赋值原来的内存,而使用一次就要删除,非常耗时

添加带右值引用参数的拷贝构造和赋值函数

一个右值引用变量本身是一个左值,所以一个定义好的右值引用变量不能赋值给右值引用

带右值引用参数的拷贝构造函数和赋值重载函数会指向临时对象开辟的内存,在整个过程中不会有无效的内存释放和开辟,大幅提高了运行效率

实例代码如下:

//带右值引用的拷贝构造函数
String(String &&src)  noexcept {
    std::cout<<"String(String &&)"<<std::endl;
    _pstr=src._pstr;
    src._pstr= nullptr;
}
//带右值引用的赋值重载函数
    String& operator=(String &&src) noexcept {
        std::cout<<"String& operator=(String &&)"<<std::endl;
        if(this==&src)
            return  *this;

        delete[] _pstr;

        _pstr=src._pstr;
        src._pstr= nullptr;
        return *this;
    }
//如果使用右值引用版本的拷贝重载函数就不需要内存的开辟和释放

输出结果如下:

带有左值引用的拷贝重载的对象中一般都有带右值引用的拷贝重载的版本。

自定义的String类在vector中的应用

在push_back的过程中,如果传入左值,匹配带有左值参数的临时对象,如果传入临时对象,会首先调用临时对象的构造函数,再调用带右值参数的拷贝构造函数。

为什么push_back会调用带有右值引用的拷贝构造函数?看下面 (Downarrow)

move移动语义和forward类型完美转发

move()是将左值转化为右值

forward()是指类型的完美转发,能够识别左值和右值类型

如果在自己定义的vector类里定义支持调用右值引用的push_back方法,首先要push_back的参数是一个右值引用的类型

第一种写法:使用函数重载,分别定义一个参数是左值引用的和一个参数是右值引用的函数

void push_back(T &val) {
    if (full()) {
        expend();
    }
    //*_last++ = val;
    _alloctor.construct(_last, val);
    _last++;
}

void push_back(T &&val) {
    if (full()) {
        expend();
    }
    _alloctor.construct(_last, std::move(val));
    _last++;
}

在函数钟调用了_alloctor.construct(),该函数传递了参数val,所以也需要函数重载接受右值引用和左值引用。

void construct(T *p, const T &val) {//负责对象构造
    new(p) T(val);//定位new
}

void construct(T *p, const T &&val) {//负责对象构造
    new(p) T(std::move(val));//定位new
}

第二种写法:使用函数模板的类型推演和引用折叠

首先说明引用折叠是什么意思。如果函数模板推演出的类型是Ty&& + &&(+后面的&&是参数中,属于必带的符号),引用折叠后的类型就就是Ty&&,是右值引用;如果函数模板推演出的类型是Ty& + &&,引用折叠后的类型就就是Ty&,是左值引用。使用forward可以识别Ty的左值或者右值的类型。

template<typename Ty>
void push_back(Ty &&val) {//Ty识别传入参数是左值还是右值,然后进行引用折叠
    if (full()) {
        expend();
    }
    _alloctor.construct(_last, std::forward<Ty>(val));//将val转换为Ty识别到的类型,避免使用函数重载
    _last++;
}
template<typename Ty>
void construct(T *p, Ty &&val) {//Ty识别传入参数是左值还是右值,然后进行引用折叠
    new(p) T(std::forward<Ty>(val));//将val转换为Ty识别到的类型,避免使用函数重载
}
hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » 1.对象的引用优化,右值引用优化