深層次的理解 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,不然析夠的時候會問題.