析构函数 (destructor) 是一种以 ~
为前缀、以类名为后缀的成员函数。它的形参列表为空,没有返回类型,用于析构 (destroy) 对象。
class Foo {
public:
~Foo(); // destructor
};
一个对象被析构时,先执行析构函数函数体中的语句,再隐式地逐个析构其(非静态)数据成员。 数据成员被析构的顺序与它们被构造的顺序相反,即:与它们在类的定义中出现的顺序相反。
如果析构函数没有被显式地声明,那么编译器会隐式地定义一个默认的版本,称为合成的 (synthesized) 析构函数。 C++11 允许显式地生成合成的析构函数,只需要在定义时在形参列表后紧跟 = default;
即可。
合成的析构函数,只会逐个析构数据成员,这意味着不会对原始指针成员调用 delete
运算符。
拷贝构造函数 (copy constructor) 是一类特殊的构造函数:
const
对象的引用。explicit
。class Foo {
public:
Foo(const Foo&); // copy constructor
};
拷贝赋值运算符 (copy assignment operator) 是对赋值运算符的重载,函数签名几乎总是如下形式:
const
对象的引用。const
对象的引用。class Foo {
public:
Foo& operator=(const Foo&); // copy assignment operator
};
有些类型的对象不应支持拷贝操作(例如 std::iostream
)。 自 C++11 起,实现该语义只需将拷贝操作(拷贝构造函数和拷贝赋值运算符)标注为删除的 (deleted) :
class Foo {
public:
Foo(const Foo&) = delete;
Foo& operator=(const Foo&) = delete;
};
合成的拷贝操作(拷贝构造函数和拷贝赋值运算符)会逐个拷贝数据成员。 这意味着只会对内置指针进行浅拷贝 (shallow copy),即:只拷贝该指针的值(所指对象的地址),而不拷贝所指对象。
如果含有数组成员,则合成的拷贝操作会逐个拷贝成员数组的每一个元素。
如果一个类含有无法拷贝的 (non-copyable) 数据成员,则这个类本身也应当是无法拷贝的,此时合成的拷贝操作将是删除的。
简单来讲:
类型名后紧跟 &&
表示定义一个对该类型对象的右值引用 (rvalue reference):
<utility>
中的函数模板 std::move<>()
可以将左值表达式变为右值表达式。int i = 42;
int& r = i; // ✅ 将 左值引用 绑定到 左值表达式
int& r2 = i * 42; // ❌ 普通左值引用 无法绑定到 右值表达式
const int& r3 = i * 42; // ✅ 指向常量的左值引用 可以绑定到 右值表达式
int&& rr = i; // ❌ 右值引用 无法直接绑定到 左值表达式
int&& rr2 = i * 42; // ✅ 将 右值引用 绑定到 右值表达式
int&& rr3 = std::move(rr2); // ✅ std::move 将 左值表达式 变为 右值表达式
移动构造函数 (move constructor) 是一类特殊的构造函数:
template <typename T>
class Vector {
public:
Vector(Vector&& rhs) noexcept // 不抛出异常
// 接管 移动源对象 的数据成员:
: head_(rhs.head_), free_(rhs.free_), tail_(rhs.tail_) {
rhs.head_ = rhs.free_ = rhs.tail_ = nullptr; // 确保 析构 rhs 是安全的
}
private:
T* head_; // 指向 首元
T* free_; // 指向 第一个自由元
T* tail_; // 指向 过尾元
};
移动赋值运算符 (move assignment operator) 是对赋值运算符的重载,函数签名几乎总是如下形式:
const
对象的左值引用。template <typename T>
class Vector {
public:
Vector& operator=(Vector&& rhs) noexcept { // 不抛出异常
if (this == &rhs) {
// 自己给自己赋值,不做任何事
} else {
free(); // 析构 被赋值对象 中的元素,释放内存
// 接管 移动源对象 的数据成员:
head_ = rhs.head_;
free_ = rhs.free_;
tail_ = rhs.tail_;
rhs.head_ = rhs.free_ = rhs.tail_ = nullptr; // 确保 析构 rhs 是安全的
}
return *this; // 返回 左值引用
}
private:
void free(); // 析构元素,释放内存
T* head_; // 指向 首元
T* free_; // 指向 第一个自由元
T* tail_; // 指向 过尾元
};
noexcept
移动操作(移动构造函数和移动赋值运算符)一般只涉及赋值操作,不需要分配动态内存,因此不会抛出异常,应当在形参列表与函数体之间用 noexcept
标注。
标准库容器类型(例如 std::vector<T>
)在重新分配 (reallocation) 的过程中,需要将所存储的元素逐个搬运到新分配的内存空间里。 如果 T
的移动构造函数被标注为 noexcept
,则容器会利用它来搬运元素; 否则,容器将不得不用 T
的拷贝构造函数来搬运元素。