首页 > 代码库 > “复制赋值”和“移动赋值”的思考
“复制赋值”和“移动赋值”的思考
概述
从 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》
“复制赋值”和“移动赋值”的思考