指针成员与拷贝构造
编写C++程序有一条必须注意的规则,就是在类中包含了一个指针成员的话,要特别小心拷贝构造函数的编写,因为一不小心,就会出现内存泄漏。
class HasPtrMem {
public:
HasPtrMem() : d(new int(0)) {}
HasPtrMem(const HasPtrMem &h) : d(new int(*h.d)) {}
~HasPtrMem() {
delete d;
}
int *d;
};
#include <iostream>
#include "HasPtrMem.h"
using namespace std;
int main() {
HasPtrMem a;
HasPtrMem b(a);
cout << *a.d << endl;
cout << *b.d << endl;
}
我们为 HasPtrMem 添加了一个拷贝构造函数,拷贝构造函数从堆中重新分配内存,将该分配来的内存的指针交还给 d,又使用 *(h.d) 对 *d 进行了初始化,避免了悬挂指针的困扰。
移动语义
有些时候,我们不需要拷贝构造语义。
#include <iostream>
using namespace std;
class HasPtrMem {
public:
HasPtrMem() : d(new int(0)) {
cout << "Construct: " << ++n_cstr << endl;
}
HasPtrMem(const HasPtrMem &h) : d(new int(*h.d)) {
cout << "Copy Construct: " << ++n_cptr << endl;
}
~HasPtrMem() {
cout << "Destruct: " << ++n_dstr << endl;
delete d;
}
int *d;
static int n_cstr;
static int n_dstr;
static int n_cptr;
};
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
HasPtrMem GetTemp() { return HasPtrMem(); }
int main() {
HasPtrMem a = GetTemp();
}
注意关闭编译优化:add_compile_options(-fno-elide-constructors)
输出:
Construct: 1
Copy Construct: 1
Destruct: 1
Copy Construct: 2
Destruct: 2
Destruct: 3
- 构造函数调用了一次,GetTemp 函数中 HasPtrMem() 表达式显示地调用了构造函数。
- 拷贝构造函数调用了两次,一次是从 GetTemp 函数中 HasPtrMem() 生成的变量上拷贝构造出一个临时值,以用作 GetTemp 的返回值;另一次则是由临时值构造出 main 中变量 a 调用的。
- 相应的析构函数调用了 3 次。
如果 HasPtrMem 的指针指向非常大的堆内存数据的话,那么拷贝构造的过程就会非常昂贵。
让我们把目光再次聚集在临时对象上,按照 C++ 的语义,临时对象将在语句结束后被析构,会释放它所包含的堆内存资源。而 a 在拷贝构造的时候,又会被分配堆内存,这样一去一来似乎没有太大的意义。
移动构造函数
在构造时使得 a.d 指向临时对象的堆内存资源,同时保证临时对象不释放所指向的堆内存,那么在构造完成后,临时对象被析构,a 就从中 “偷” 到了临时对象所拥有的堆内存资源。
在C++11中,这样的“偷走”临时变量中资源的构造函数,就被称为“移动构造函数”,而这样的“偷”的行为,则称之为“移动语义”(move semantices)。
#include <iostream>
using namespace std;
class HasPtrMem {
public:
HasPtrMem() : d(new int(0)) {
cout << "Construct: " << ++n_cstr << endl;
}
HasPtrMem(const HasPtrMem &h) : d(new int(*h.d)) {
cout << "Copy Construct: " << ++n_cptr << endl;
}
HasPtrMem(HasPtrMem &&h):d(h.d) {
h.d = nullptr;
cout << "Move construct: " << ++n_mvtr << endl;
}
~HasPtrMem() {
cout << "Destruct: " << ++n_dstr << endl;
delete d;
}
int *d;
static int n_cstr;
static int n_dstr;
static int n_cptr;
static int n_mvtr;
};
HasPtrMem GetTemp() {
HasPtrMem h;
cout << "Resource from " << __func__ << ": " << hex << h.d << endl;
return h;
}
int main() {
HasPtrMem a = GetTemp();
cout << "Resource from " << __func__ << ": " << hex << a.d << endl;
}
输出:
Construct: 1
Resource from GetTemp: 0x600001e88030
Move construct: 1
Destruct: 1
Move construct: 2
Destruct: 2
Resource from main: 0x600001e88030
Destruct: 3
移动构造函数使用了参数 h 的成员 d 初始化了本对象的成员d(而不是像拷贝构造函数一样需要分配内存,然后将内存依次拷贝到新分配的内存中),而 h 的成员 d 随后被置为指针控制 nullptr。
可以看到,这里没有调用拷贝构造函数,而是调用了两次移动构造函数,移动构造的结果是 GetTemp 中的 h 的指针成员 h.d 和 main 函数中的 a 的指针成员 a.d 的值是相同的,即 h.d 和 a.d 都指向了相同的堆地址内存。
那移动构造函数何时会被触发?
左值、右值与右值引用
可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值。
在C++1中,右值是由两个概念构成的,一个是将亡值(xvalue, eXpiring Value),另一个是纯右值(prvalue, Pure Rvalue)。
纯右值:
- 非引用返回的函数返回的临时变量值
- 一些运算表达式,如 1 + 3 产生的临时变量
- 不跟对象关联的字面量值,如 2、'c'、true
将亡值:
- 返回右值引用 T&& 的函数返回值
- std::move 的返回值
- 转换为 T&& 的类型转换函数的返回值
在C++11中,右值引用就是对一个右值进行引用的类型。
T &&a = ReturnRvalue();
ReturnRvalue 函数返回的右值在表达式语句结束后,其生命也就终结了,而通过右值引用的声明,该右值又”重获新生“,其生命周期将与右值引用类型变量 a 的生命周期一样。
所以相比于一下语句的声明方式
T b = ReturnRvalue();
右值引用变量声明,就会少一次对象的析构以及一次对象的构造。
参考
一文读懂C++右值引用和std::move
C++ moves for people who don’t know or care what rvalues are