概述
從 C++ 11 中開始,該語言支持兩種類型的分配:復制賦值和移動賦值。其中的內部細節是咋樣的呢?今天跟蹤了一下,是個蠻有趣的過程。下面我們以一個簡單的類來做個分析。
#ifndef HASPTR_H #define HASPTR_H #include <string> class HasPtr { public: friend void swap(HasPtr&, HasPtr&); HasPtr(const std::string& s = std::string()); HasPtr(const HasPtr& hp); HasPtr(HasPtr&& p) noexcept; HasPtr& operator=(HasPtr rhs); // HasPtr& operator=(const HasPtr &rhs); // HasPtr& operator=(HasPtr &&rhs) noexcept; ~HasPtr(); private: std::string* ps; int i; }; #endif // HASPTR_H #include "hasptr.h" #include <iostream> inline void swap(HasPtr& lhs, HasPtr& rhs) { using std::swap; swap(lhs.ps, rhs.ps); swap(lhs.i, rhs.i); std::cout << "call swap" << std::endl; } HasPtr::HasPtr(const std::string& s) : ps(new std::string(s)), i() { std::cout << "call constructor" << std::endl; } //這里的i+1只是為了方便調試的時候看過程,實際是不用加1的 HasPtr::HasPtr(const HasPtr& hp) : ps(new std::string(*hp.ps)), i(hp.i + 1) { std::cout << "call copy constructor" << std::endl; } HasPtr::HasPtr(HasPtr&& p) noexcept : ps(p.ps), i(p.i) { p.ps = 0; std::cout << "call move constructor" << std::endl; } HasPtr& HasPtr::operator=(HasPtr rhs) { swap(*this, rhs); return *this; } HasPtr::~HasPtr() { std::cout << "call destructor" << std::endl; delete ps; }
主函數
int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); HasPtr hp1("hello"), hp2("World"), *pH = new HasPtr("World"); hp1 = hp2; hp1 = std::move(*pH); return a.exec(); }
下面我們開始調試:
輸出:
我們通過構造函數構造了三個變量,他們的值和
address | ps | i | |
hp1 | 0x28fe64 | "hello" | 0 |
hp2 | 0x28fe5c | "World" | 0 |
pH | 0x28fe9c | "World" | 0 |
復制賦值
我們接着單步走:
可以發現首先調用了復制構造函數,構造了一個和hp2一樣的臨時變量
address | ps | i | |
this | 0x28fe2c | "World" | 1 |
hp2 | 0x28fe5c | "World" | 0 |
下一步:
到這里才開始進行賦值運算,我們對比一下數據:
address | ps | i | |
this | 0x28fe2c | "hello" | 0 |
rhs | 0x28fe8c | "World" | 1 |
這里的rhs就是我們剛剛分配的臨時變量,那么this就是hp1,所以最終是我們的臨時變量和hp1交換,我們接着走:
這里lhs的地址就是:0x28fe64,就是hp1的地址,交換之后:
到此hp1和臨時變量的值就完全交換過來了,也就是說hp1 = hp1了。
address | ps | i | |
lhs | 0x28fe64 | "World" | 1 |
rhs | 0x28fe8c | "hello" | 0 |
可是我們接着運行,發現進入了一個析構函數:
看一下地址是0x28fe8c以及其值,這是臨時變量,臨時變量不用了,所以被銷毀了,至此我們的復制賦值運算就結束了。
移動賦值
我們看一下移動賦值賦值:
首先進入移動函數,這里只是使指針指向了pH的數據,並未構造新的數據,變量右值引用了pH,只是相當於換了個名字。
接下來開始進入賦值運算:
這里兩個交換的值是hp1和pH,和復制賦值不同,它是和臨時變量交換數據,
后面進入析構函數:
它釋放掉了pH的數據。
可以看出來,pH的值被釋放掉了。
總結
調試過后,我們發現,賦值運算的過程並非像想象中那么簡單,是不是?復制賦值還是開辟一個臨時變量用於轉化,這個耗費了額外的空間資源。
移動賦值就可以避免這個問題,但是需要注意的是,移動賦值使用的是右值,用完之后就被銷毀了,所以,如果想把一個左值當做右值來用,必須確保這個左值在這之后不需要使用了。
參考:
- 《C++ primer》