9.2对象中的动态内存分配
文章目录
1.Spreadsheet类
略
2.使用析构函数释放内存
略
3.处理复制和赋值
编译器生成的方法递归调用对象数据成员的拷贝构造函数或赋值运算符。然而对于基本类型,如 int、double 和指针,只是提供表层(或按位)复制或赋值;只是将数据成员从源对象直接复制或赋值到目标对象。当在对象内动态分配内存时,这样做会引发问题。
无论什么时候,在类中动态分配内存后,都应该编写自己的拷贝构造函数和赋值运算符,以提供深层的内存复制。
3.1Spreadsheet类的拷贝构造函数
略
3.2Spreadsheet类的赋值运算符
我们需要一种全有或全无的机制: 要么全部成功,要么该对象保持不变。为实现这样一个能安全处理异常的赋值运算符,要使用"复制和交换"惯用方法。交换每个数据成员的 swap()方法的实现使用标准库中提供的<utility>
中的 std::swap()工具函数,std::swap()可以高效地交换两个值。
class Spreadsheet{
public:
Spreadsheet& operator=(const Spreadsheet& rhs);
void swap(Spreadsheet& other) noexcept;
};
void swap(Spreadsheet& first, Spreadsheet& second) noexcept;
void Spreadsheet::swap(Spreadsheet& other) noexcept
{
std::swap(m_width, other.m_width);
// ...
}
void swap(Spreadsheet& first, Spreadsheet& second) noexcept
{
first.swap(second);
}
Spreadsheet& Spreadsheet::operator=(const Spreadsheet& rhs)
{
Spreadsheet temp {rhs};
swap(temp);
return *this;
}
- 第一阶段创建一个临时副本。这不修改当前 Spreadsheet 对象的状态,因此,如果在这个阶段
发生异常,不会出现问题。 - 第二阶段使用swap()函数,将创建的临时副本与当前对象交换。swap()永远不会抛出异常。
- 第三阶段销毁临时对象(由于发生了交换,现在包含原始对象)以清理内存。
3.3禁止赋值和按值传递
在函数声明添加=delete
即可
4.使用移动语义处理移动
对象的移动语义(move semantics)需要实现移动构造函数(move constructor)和移动赋值运算符(move assignment operator)。如果源对象是操作结束后会被销毁的临时对象,或显式使用 std::move()
时,编译器就会使用这两个方法。移动将内存和其他资源的所有权从一个对象移动到另一个对象。这两个方法基本上只对成员变量进行浅复制(shallow copy),然后转换已分配内存和其他资源的所有权,从而阻止悬空指针和内存泄漏。
移动构造函数和移动赋值运算符将数据成员从源对象移动到新对象,然后使源对象处于有效但不确定的状态。通常,源对象的数据成员被重置为空值,但这不是必需的。为了安全起见,不要使用任何已被移走的对象,因为这会触发未定义的行为。std::unique_ptr和 shared_ptr是例外情况。标准库明确规定,这些智能指针在移动时必须将其内部指针重置为 nullptr
,这使得从智能指针移动后可以安全地重用这些智能指针。
在实现移动语义前,需要学习右值(rvalue)和右值引用(rvalue reference)。
4.1右值引用
在C++中,左值Cvalue)是可获取其地址的一个量,例如一个有名称的变量。由于经常出现在赋值语句的左边,因此将其称作左值。另外,所有不是左值的量都是右值((rvalue),例如字面量、临时对象或临时值。通常右值位于赋值运算符的右边。
右值引用是一个对右值(rvalue)的引用。特别地,这是一个当右值是临时对象或使用std::move()
显式移动时才适用的概念。右值引用的目的是在涉及右值时提供可选用的特定重载函数, 通过右值引用, 某些涉及复制大量值的操作可通过简单地复制指向这些值的指针来实现。
函数可将&&
作为参数说明的一部分(例如 type&& name
),以指定右值引用参数。通常,临时对象被当作const type&
,但当函数重载使用了右值引用时,可以解析临时对象,用于该函数重载。
std::move()
做的唯一的事就是将左值转换为右值.
有名称的右值引用,如右值引用参数,本身就是左值,因为它具有名称!
4.2实现移动语义
移动语义是通过右值引用实现的。为了对类增加移动语义,需要实现移动构造函数和移动赋值运算符。移动构造函数和移动赋值运算符应使用noexcept
限定符标记,这告诉编译器,它们不会抛出任何异常。
class Spreadsheet{
public:
Spreadsheet(Spreadsheet&& src) noexcept;
Spreadsheet& operator=(Spreadsheet&& rhs) noexcept;
private:
void cleanup() noexcept;
void moveFrom(Spreadsheet& src) noexcept;
};
void Spreadsheet::cleanup() noexcept
{
for(size_t i{0}; i < m_width; i++){
delete[] m_cells[i];
}
delete[] m_cells;
m_cells = nullptr;
m_width = m_height = 0;
}
void Spreadsheet::moveFrom(Spreadsheet& src) noexcept
{
// Shallow copy of data
m_width = src.m_width;
m_height = src.m_height;
m_cells = src.m_cells;
// Reset the source object, because ownership has been moved
src.m_width = 0;
src.m_height = 0;
src.m_cells = nullptr;
}
// Move constructor
Spreadsheet::Spreadsheet(Spreadsheet&& src) noexcept
{
moveFrom(src);
}
// Move assignment operator
Spreadsheet& Spreadsheet::operator=(Spreadsheet&& rhs) noexcept
{
// Check for self-assignment
if(this == &rhs){
return *this;
}
// Free the old memory and move ownership
cleanup();
moveFrom(rhs);
return *this;
}
当你声明了一个或多个特殊成员函数(析构函数、拷贝构造函数、移动构造函数、拷贝赋值运算符和移动赋值运算符)时,通常需要声明所有这些函数,这称为"5规则"(Rule of Five)。可以为它们提供显式实现,也可以显式默认(=default)或删除(=delete)它们。
使用std::exchange
定义在<utility>
中的std::exchange()
, 可以用一个新的值替换原来的值, 并返回原来的值.
void Spreadsheet::moveFrom(Spreadsheet& src) noexcept
{
m_width = exchange(src.m_width, 0);
m_height = exchange(src.m_height, 0);
m_cells = exchange(src.m_cells, 0);
}
移动对象数据成员
略
用交换方式实现移动构造函数和移动赋值运算符
略
4.3测试 Spreadsheet移动操作
略
4.4用移动语义实现交换函数
略
4.5在返回语句中使用std::move
当从函数中返回一个局部变量或参数时,只要写return object;就可以了,不要使用 std::move()。
4.6向函数传递参数的最佳方法
对于不被复制的参数,通过const引用传递仍然是应使用的方法,值传递建议仅适用于函数无论如何都要复制的参数。在这种情况下,通过使用值传递语义,代码对于左值和右值都是最优的。如果传入一个左值,它只复制一次,就像const引用参数一样。如果传入一个右值,则不会进行复制,就像右值引用参数一样。
class DataHolder
{
public:
void setData(std::vector<int> data) { m_data = std::move(data) };
private:
std::vector<int> m_data;
};
5.零规则
"零规则"指出,在设计类时,应当使其不需要上述5个特殊成员函数。
析构函数、拷贝构造函数、移动构造函数、拷贝赋值运算符和移动赋值运算符