ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2022-05-20 21:33:52  阅读:153  来源: 互联网

标签:函数 val 右值 对象 左值 引用 优化


这一节中主要讲了对象和函数在使用和调用过程中一些注意事项,比较重要的是右值引用和最后的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 = '\0';
        } 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识别到的类型,避免使用函数重载
}

标签:函数,val,右值,对象,左值,引用,优化
来源: https://www.cnblogs.com/woden3702/p/16293758.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有