1. 程式人生 > >深層次的理解 c++ 11 std::move和右值傳參

深層次的理解 c++ 11 std::move和右值傳參

記得以前看csdn上的文章,看到過一篇文章的作者寫過這樣一句話:

你以為你理解和你真正理解是有差別的,這中間的差別就是幾個小時的痛苦.

所以現在遇到很多我自以為簡單的知識點,就算自己以為理解了,也會嘗試著去進行實踐,並去想下這背後的深層次的原因。

是用來解決什麼問題,問題是怎麼來的?

第一部分:問題是怎麼來的?

c++中有2種賦值方式,一種是直接賦值,另一種是拷貝構造賦值 .
int a = 5 ;//拷貝賦值
int a (5) ;//直接賦值

如果我們沒有自己重寫拷貝建構函式,那麼系統會預設的幫我們建立一個拷貝建構函式,拷貝建構函式的過程就是把記憶體完整的拷貝給新的物件,這樣如果我們的類中含有指標的話就很容易出問題.

class HashPtrMem {
public:
    HashPtrMem():d(new int(0)){}

    HashPtrMem(HashPtrMem& other){
        std::cout << "HashPtrMem copy function" << std::endl;
        d = new int (*other.d);
    }

    ~HashPtrMem() {
        std::cout << "~HashPtrMem dealloc" << std::endl;
        delete
d; } int *d ; };

我們在main函式中,這樣呼叫:

int main(int argc, const char * argv[]) {
HashPtrM a ;
    HashPtrM b = a;
}

HashPtrM b = a;會呼叫預設的拷貝建構函式(叫做淺拷貝,如果自己重寫了拷貝建構函式,那麼就叫做深層拷貝)

指標在我們記憶體中儲存的是什麼?? 是地址,這個時候拷貝的時候,其實也會把地址完整的拷貝到新的物件中。

在main函式即將結束的時候,a和b都會析構,但是因為指標d的地址其實是同一個,所以最後會報錯誤 :

malloc: *** error for
object 0x1005010d0: pointer being freed was not allocated *** set a breakpoint in malloc_error_break to debug

解決的方案之一就是:

我們採用深度拷貝的方式,自己重寫拷貝建構函式:

    HashPtrMem(HashPtrMem& other){
        std::cout << "HashPtrMem copy function" << std::endl;
        d = new int (*other.d);
    }

什麼時候呼叫拷貝建構函式呢?

1.一個物件需要通過另一個物件進行初始化,比如我們的方式:
HashPtrM b = a;

2.函式引數以傳值的方式進行傳入

void GetTempPtr(HashPtrM b){
}

想想這裡,其實對基礎的掌握還是要求挺高的.

老鐵們,在我們拷貝建構函式的時候,實際上是把堆記憶體拷貝一份給新物件,然後在函式結束後,又釋放掉新建立的記憶體塊。

所以這是問題點,在拷貝建構函式的過程中,記憶體複製2份,析構也是double。

OK,現在我們這裡有這樣一個需求:

在一個函式內部,有一個臨時變數,這給函式即將結束,我們需要把這個臨時變數的內容賦值給類的同類型的變數.

這給時候我們如果使用拷貝建構函式的話,那麼就是記憶體拷貝一份,然後臨時變數析夠.

第二部分,那麼我們應該如何優化呢????

一個類可能有int型別,指標型別,類型別各種型別,除了完整的記憶體拷貝,我們還可以這樣做:

int等基本型別,直接賦值,也只能這麼做.
對於指標,我們直接採用下面程式碼的操作:

類似這樣的操作 :

int *p = new int (5);
int *q = p;
p = NULL;

小知識:

The C++ language guarantees that delete p will do nothing if p is equal to NULL。

這樣新物件的指標也指向了實體的資料,然後原指標變為了NULL,變為NULL後,析構的時候 delete 是不會觸發任何事件的.

類中的類型別,其實可以遞迴這樣的方式

這給就是我們傳右值的核心思想,資料只有一份,從原有的值就這樣”移動了”新值中.

注意:怎麼對右值進行操作的行為,是由程式設計者自己決定的,也就是我們自己決定的.

說起來抽象,舉個例子:

//
//  main.cpp
//  0304
//
//  Created by zhangkai on 2018/9/6.
//  Copyright © 2018年 zhangkai. All rights reserved.
//

#include <iostream>

class person {
public:

    person()
    {
        age = 0;
        name = new char [10]{'a','b','c'};
    }

    //拷貝建構函式
    person(const person& other)
    {
        printf("the address of other is:%p \n" , &other);
         std::cout << "person copy" << std::endl;
        this->age = other.age;


        size_t len = strlen(other.name);
        name = new char [len + 1];
        memcpy(name, other.name, len);
        name[len] = '\0';
    }

    //返回 person 主要是為了實現鏈式表示式
    person operator=(person& other){
        std::cout << "person operator == " << std::endl;
        this->age = other.age;


        size_t len = strlen(other.name);
        name = new char [len + 1];
        memcpy(name, other.name, len);
        name[len] = '\0';

        return *this;
    }

    //注意行為都是由我們自己來決定的.s
    //移動建構函式
    person(person&& other){
         std::cout << "person move" << std::endl;
        this->age = other.age;
        this->name  = other.name;

        other.name = NULL;
    }



    void copy(person other){
        printf("other address:%p \n" , &other);
    }

    ~person()
    {
        std::cout << "person deconstructor" << std::endl;
        if(name == NULL)
        {
            std::cout << "name is null" << std::endl;
        }
        delete[] name;
    }

    int age;
    char *name;
};


//class Family {
//public:
//    Family(person son): son_(son){
//
//    }
//private:
//    person son_;
//};


int main(int argc, const char * argv[]) {
    // insert code here...

    person son;//預設建構函式
    son.age = 10;

   //拷貝建構函式

    printf("the address of son is:%p \n" , &son);

    //中間會產生臨時變數
    //類似:person temp = son;然後會把son的記憶體完整的拷貝到臨時變數temp中
    //這個時候地址是不一樣的
    person son1 (son); // 和 person son1 = son 等價!

    //中間也會產生臨時變數,但是person(person&& other)程式會執行這裡
    //臨時變數的內容都是son的,中間沒有涉及到記憶體拷貝
    person son2 (std::move(son));



    return 0;
}

std::move只是把型別變為右值,當然類自己必須提供對右值對支援,編譯器是自己不會處理右值的.

預設的複製建構函式由於必須新增 const,所以其實其行為已經定死,就只能是拷貝建構函式.

注意:

    person(person&& other){
         std::cout << "person move" << std::endl;
        this->age = other.age;
        this->name  = other.name;

        other.name = NULL;
    }

這裡other.name必須置為null,不然析夠的時候會問題.