2.2、用析构函数释放内存
每当完成动态分配内存时,都应该释放。如果在一个对象中动态分配内存,释放内存的地方就是析构函数。编译器保证当对象被破坏时会调用析构函数。下面就是Spreadsheet类定义中的析构函数:
export class Spreadsheet
{
public:
Spreadsheet(std::size_t width, std::size_t height);
~Spreadsheet();
// Code omitted for brevity
};
析构函数拥有与类(以及构造函数)同样的名字,前面加一个~。析构函数没有参数,只能有一个。析构函数不会抛出例外。
下面是Spreadsheet类析构函数的实现:
Spreadsheet::~Spreadsheet()
{
for (size_t i{ 0 }; i < m_width; ++i) {
delete[] m_cells[i];
}
delete[] m_cells;
m_cells = nullptr;
}
析构函数释放在构造函数中分配的内存。然而,没有规则要求你在析构函数中释放内存。可以在析构函数中写任何想写的代码,但是将其用于释放内存或者其它资源通常是一个好主意。
2.3、处理拷贝与赋值
如果你不写拷贝构造函数与拷贝赋值操作符,c++会为你写的。这些编译器生成的成员函数递归调用对象数据成员的拷贝构造函数或者拷贝赋值操作符。然而,对于原始类型,比如int,double,以及指针,它们提供了shallow或者说bitwise拷贝或者赋值:只是拷贝或者将数据成员从源对象直接赋值给目标对象。这避免了在对象动态分配内存时的总是。例如,下面的代码拷贝了spreadsheet s1,当s1被传递给printSpreadsheet()函数时初始化s:
import spreadsheet;
void printSpreadsheet(Spreadsheet s)
{
// code omitted for brevity
}
int main()
{
Spreadsheet s1{ 4, 3 };
printSpreadsheet(s1);
}
Spreadsheet包含一个指针变量:m_cells。Spreadsheet的一个shallow拷贝给出目标对象一个m_cells指针的拷贝,但不是其内部数据的拷贝。这样的话,结果就是s与s1具有指向同样数据的指针,如下图所示:
如果s改变了m_cells指向的一些东东,其变化也会在s1中显现。更糟的是,当printSpreadsheet()函数退出时,s的析构函数被调用,就会释放m_cells指向的内存。就会造成s1中的m_cells不再指向有效的内存,如下图所示。这叫做悬浮指针。
令人难以置信的是,这个问题在赋值时更加糟糕。假定有下面的代码:
Spreadsheet s1{ 2, 2 }, s2{ 4, 3 };
s1 = s2;
执行完第一行代码后,s1与s2的Spreadsheet对象都会被构建,内存结构如下所示:
当赋值语句执行完后,内存结构就变成下面的了:
现在,不光s1与s2中的m_cells指针指向同样的内存,前面s1指向的m_cells的内存也变成了孤儿。这就叫做内存渗露了。
需要明确的是,拷贝构造函数与拷贝赋值操作符必须做深层次的拷贝;也就是说,不光要拷贝指针数据成员,也要拷贝这些指针指向的真实数据。
可以看出来,依靠c++缺省的拷贝构造函数与缺省的拷贝赋值操作符并不总是一个好主意。
警告:每当在类中有动态分配资源时,应该自己写拷贝构造函数与拷贝赋值操作符来提供内存的深层次拷贝。
2.3.1、Spreadsheet拷贝构造函数
下面是Spreadsheet类的拷贝构造函数的声明:
export class Spreadsheet
{
public:
Spreadsheet(const Spreadsheet& src);
// Code omitted for brevity
};
定义如下:
Spreadsheet::Spreadsheet(const Spreadsheet& src)
: Spreadsheet { src.m_width, src.m_height }
{
// The ctor-initializer of this constructor delegates first to the
// non-copy constructor to allocate the proper amount of memory.
// The next step is to copy the data.
for (size_t i{ 0 }; i < m_width; ++i) {
for (size_t j{ 0 }; j < m_height; ++j) {
m_cells[i][j] = src.m_cells[i][j];
}
}
}
注意代理构造函数的使用。拷贝构造函数的构造函数初始化器首先代理非拷贝构造函数来分配适合大小的内存。拷贝构造函数体拷贝真实值。与之一起,该过程实现了m_cells动态分配二维数组的深层次拷贝。
不需要删除任何既有的m_cells,因为这是一个拷贝构造函数,因此在this对象中还不存在m_cells。