跳转至

12 特殊的成员函数 & 13 移动语义⚓︎

1249 个字 144 行代码 预计阅读时间 8 分钟

概览⚓︎

每个类默认都有六个特殊的成员函数(special member functions,以下简称 SMF,当它们被调用的时候就会自动生成(或者可以自定义这些函数

  • 默认构造函数(default constructor:不接收任何参数,创建一个新的对象
  • 析构函数(destructor:当对象离开它的作用域时调用该函数
  • 拷贝构造函数(copy constructor:通过按成员拷贝另一个对象的成员,创建一个新的对象
  • 拷贝赋值运算符(copy assignment operator:将已经存在的一个对象赋给另一个对象
  • 移动构造函数(move constructor:通过移动一个已经存在的对象的内容来创建新对象
  • 移动赋值运算符(move assignment operator:将一个对象的内容移动到另一个对象里

例子

class Widget
{
    public:
        Widget();                             // default construtor
        Widget (const Widget& w);             // copy constuctor
        Widget& operator = (const Widget& w); // copy assignment operator
        ~Widget();                            // destrutor
        Widget (Widget&& rhs);                // move constructor
        Widget& operator = (Widget&& rhs);    // 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,即创建一个完整的、独立的拷贝对象,可以理解为不是成为它的影子,而是获得它整个的实体。要实现深拷贝,就需要自己重新定义这些 SMF,定义方法同一般的成员函数。

默认、删除⚓︎

下面是一个关于密码管理的一个类:

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;
    vector<int> vec2(3);
    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;                        
}
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 显式声明,用法同前。

Widget(Widhet&&) = default;
Widget& operator = Widget&& = default;

例子

HumanGenome.h
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 表示出现错误时不会抛出错误信息
  • && 表示右值引用(左值引用为 &
HumanGenome.cpp
// ...
// 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:如果我们在类中定义了拷贝构造函数和拷贝赋值运算符,那么我们也应该定义移动构造函数和移动赋值运算符。

评论区

如果有什么问题或想法,欢迎大家在下方留言~