1. 程式人生 > 其它 >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 = '\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識別到的型別,避免使用函式過載
}