#include<iostream> #include<vector> #include<string> using namespace std; class Test { public: Test(const string& s = "hello world") :str(new string(s)) { cout << "默认构造函数" << endl; }; Test(const Test& t); Test& operator=(const Test& t); Test(Test&& t) noexcept; Test& operator=(Test&& t) noexcept; ~Test(); public: string * str; }; Test::Test(const Test& t) { str = new string(*(t.str)); cout << "拷贝构造函数" << endl; } Test& Test::operator=(const Test& t) { cout << "拷贝赋值运算符" << endl; return *this; } Test::Test(Test&& t)noexcept { str = t.str; t.str = nullptr; cout << "移动构造函数" << endl; } Test& Test::operator=(Test&& t)noexcept { cout << "移动赋值运算符" << endl; return *this; } Test::~Test() { cout << "析构函数" << endl; } int main() { vector<Test> vec(1); Test t("what"); vec.push_back(std::move(t)); return 0; }
运行结果截图如下:
首先说说为什么会这样输出:
1、第一个 “默认构造函数” 是因为vector<Test> vec(1) , 所以事先使用默认构造函数构造了一个Test对象
2、第二个 “默认构造函数” 是因为Test t ,使用默认构造函数构造了一个对象
3、第三个 “移动构造函数” 大多数人会以为是 vec.push_back(std::move(t)) ,push_back 导致对象的移动而输出的。具体的原因其实是由于重新分配内存而导致的,我们的 vector 对象 vec 初始的容量只有 1 ,且里面已经有一个对象了,就是vector<Test> vec(1)的时候创建的,所以再向vec里面添加Test对象时,就会导致vec重新分配内存。由于vec中的对象定义了移动构造函数且是可用的(因为我们将其声明为了noexcept),所以就会调用移动构造函数将vec中原始的那个对象移动到新的内存中,从而输出 “移动构造函数”。
4、第四个 “移动构造函数” 才是因为Test 对象 t 被移动到vector 对象 vec 新的空间而输出的
5、第五个 “析构函数” 是因为重新分配内存后,原来的内存将被销毁,所以输出一个“析构函数”
6、后面三个 “析构函数” 是因为执行了return 0, 内存被释放,vec 和 t 都被析构,所以输出三个 “析构函数”
将 Test.h 和 Test.cpp 文件中的noexcept 都删去,输出的结果变成了:
更改之后的输出只有第四行的输出变了,其余行输出原因与上面是一样的
第四行的输出由 “移动构造函数” 变成了 “拷贝构造函数” ,原因是:
由于我们的移动构造函数没有声明为noexcept,所以我们的移动构造函数就会被认为是可能抛出异常,所以在重新分配内存的过程中,vec 对象就会使用拷贝构造函数来“移动”对象(这里说的移动其实是拷贝,并不是移动),所以就输出了“拷贝构造函数”。
此外,移动构造函数何时会被调用?在《C++ primer》上有一句话是这样说的:
为了避免这种潜在的问题,除非vector知道元素类型的移动构造函数不会抛出异常,否则在重新分配内存的过程中,它就必须使用拷贝构造函数而不是移动构造函数。如果希望在vector重新分配内存这类情况下对我们自定义类型的对象进行移动而不是拷贝,就必须显示的告诉标准库我们的移动构造函数可以安全使用。我们通过将移动构造函数(及移动赋值运算符)标记为noexcept来做到这一点。