12 特殊的成员函数 & 13 移动语义⚓︎
约 1249 个字 144 行代码 预计阅读时间 8 分钟
概览⚓︎
每个类默认都有六个特殊的成员函数(special member functions,以下简称 SMF
- 默认构造函数(default constructor
) :不接收任何参数,创建一个新的对象 - 析构函数(destructor
) :当对象离开它的作用域时调用该函数 - 拷贝构造函数(copy constructor
) :通过按成员拷贝另一个对象的成员,创建一个新的对象 - 拷贝赋值运算符(copy assignment operator
) :将已经存在的一个对象赋给另一个对象 - 移动构造函数(move constructor
) :通过移动一个已经存在的对象的内容来创建新对象 - 移动赋值运算符(move assignment operator
) :将一个对象的内容移动到另一个对象里
例子
拷贝、拷贝赋值⚓︎
回顾以下:当我们创建构造函数时,我们想用它来为对象的成员变量初始化,这时我们会用到初始化列表(initializer list),相比为每个成员变量单独赋值方便得多:
template <typename T>
vector<T>::vector<T> : _size(0), _capacity(kInitialSize), _elems(new T[kInitialSize]) { }
然而,有时候这种默认的 SMF 不能满足我们的需要。以拷贝构造函数为例,它默认会拷贝所有的成员变量,如果变量是一个指针,这样的拷贝只能创建一个指向同一块内存的指针,而不是拷贝整个内存块到一个新的内存块,这样的拷贝称为浅拷贝(shallow copy
我们希望做到深拷贝(deep copy
默认、删除⚓︎
下面是一个关于密码管理的一个类:
class PasswordManager
{
public:
PasswordManager();
~PasswordManager();
// ...
PasswordManager(const PasswordManager& rhs);
PasswordManager& operator = (const PasswordManager& rhs);
private:
// ...
}
我们知道,每个类都有这六种 SMF ,有两个函数与拷贝有关。鉴于安全性的考虑,我们打算删除这两个函数(也就是上面列出的最后两个函数delete
,只要按下面的代码修改即可:
PasswordManager(const PasswordManager& rhs) = delete;
PasswordManager& operator = (const PasswordManager& rhs) = delete;
除了手动 ban 掉 SMF 外,我们还可以手动允许某些特殊函数的使用 —— 使用关键词 default
,用法与 delete
一致。
class PasswordManager
{
public:
PasswordManager() = default;
PasswordManager(const PasswordManager& pm) = default;
~PasswordManager();
// ...
PasswordManager(const PasswordManager& rhs) = delete;
PasswordManager& operator = (const PasswordManager& rhs) = delete;
private:
// ...
}
Quiz
你能否判断出以下函数属于什么类型的吗?
using std::vector;
// 它也是拷贝构造函数!
vector<int> func(vector<int> vec0)
{
// 默认构造函数
vector<int> vec1;
// 不是 SMF,一个带参的构造函数
vector<int> vec2(3);
// 不是 SMF,使用了初始化列表
vector<int> vec3{3};
// 函数声明
vector<int> vec4();
// 拷贝构造
vector<int> vec5(vec2);
// 默认构造函数
vector<int> vec{};
// 拷贝构造函数
vector<int> vec{vec3 + vec4};
// 也是拷贝构造函数
vector<int> vec8 = vec4;
// 拷贝赋值运算符
vec8 = vec2;
return vec8;
}
移动语义⚓︎
移动构造函数、移动赋值运算符⚓︎
假如我们需要将下面的 StringTable
类的对象(存储一个字符串映射表)拷贝到另一个对象上,而且之后我们不需要被拷贝的对象,那么这样的拷贝操作实属有些浪费内存了 ......
class StringTable
{
public:
StringTable() {}
StringTable(const StringTables& st) {}
// ...
// no move/dtor functionality
private:
std::map<int, std::string> values;
}
这时我们需要用到移动操作 —— 移动构造函数和移动赋值运算符,它们能够实现“按成员的移动”操作。
警告
只有在以下情况下,移动构造函数和移动赋值运算符才会生成:
- 没有声明拷贝运算
- 没有声明(自定义的)移动运算
- 没有声明析构函数
需要移动操作时,可以添加关键词 default
显式声明,用法同前。
例子
class HumanGenome
{
private:
std::vector<char> data;
public:
// move constructor
HumanGenome(HumanGenome&& other) noexcept:
data(std::move(other.data))
{
std::cout << "HumanGenome moved into stage." << std::endl;
}
}
- 关键词
noexcept
表示出现错误时不会抛出错误信息 &&
表示右值引用(左值引用为&
)
// ...
// They are copy constuctors
HumanGenome stage1(HumanGenome genome)
{
genome.process();
return genome;
}
HumanGenome stage1(HumanGenome genome)
{
genome.process();
return genome;
}
HumanGenome stage1(HumanGenome genome)
{
genome.process();
return genome;
}
// In the main function:
std::vector<char> initialData = {'A', 'T', 'G', 'C'};
HumanGenome genome(initialData);
// Pipelines are independent of each other
// 我的理解是 stage1 - stage3 是管道的三个阶段。
// genome 每移动到下一个阶段就会被加工一次,也就是对同一个 genome 对象加工三次,
// 而不是对它的副本进行加工,体现了“移动”的过程。
genome = stage1(std::move(genome));
genome = stage2(std::move(genome));
genome = stage3(std::move(genome));
std::move()⚓︎
在上面的例子中,我们多次用到了 std::move()
,它的作用是将一个左值转化为一个 x-value。当我们不再需要原来的对象时,可以用 std::move()
进行转移操作而非拷贝。
目前关于 x-value 的概念我还是不太理解,二刷的时候再补上!
注意
我们应避免在主程序代码中使用 std::move()
,它一般用于类的定义里(比如构造函数和运算符
int main()
{
HumanGenome genome_one;
HumanGenome genome_two;
genome_one.add_base('A');
// 这是一个拷贝赋值运算符,否则最后的 add_base 函数无法正确执行
genome_two = genome_one;
genome_one.add_base('T');
}
SMF 规则⚓︎
使用 SMF 时往往遵循以下规则:
- 零号规则(Rule of 0
) :如果默认 SMF 的功能足够自己使用(或者压根不需要用到它们) ,请不要对它们自定义(或者使用它们) 。- 可以自定义的情况:拷贝一份动态分配的内存(指针)
- 三号规则(Rule of 3
) :若要自定义析构函数、拷贝构造函数或者拷贝赋值运算符,则需要同时这三者。因为自定义这些函数意味着我们需要手动处理某些数据,那么我们应该对处理这些数据的各种操作负责。 - 五号规则(Rule of 5
) :如果我们在类中定义了拷贝构造函数和拷贝赋值运算符,那么我们也应该定义移动构造函数和移动赋值运算符。
评论区