1 C++

1.1 C++类的构造函数和赋值运算符

一个完整的C++类会有以下几种特殊的函数,主要是构造函数和赋值运算符,其中构造函数包括构造函数、拷贝构造函数移动构造函数,赋值运算符包括拷贝赋值、移动赋值,

  • 构造函数 :ClassName()
  • 拷贝构造函数:ClassName(const ClassName&)
  • 拷贝赋值函数:ClassName& operator=(const ClassName&)
  • 移动构造函数:ClassName(ClassName &&)
  • 移动赋值函数:ClassName& operator=(ClassName&&)
  • 析构函数: ~ClassName()

1.2 C++类的完整示例代码

如果类名为Widget,则该类包含上述的所有特殊函数的代码为

#include <iostream>

class Widget
{
public:
    Widget():m_width(0), m_ptr_data(nullptr) // 默认构造函数
    {
        std::cout << "调用默认构造函数" << std::endl;
    }; 

    virtual~Widget()
    {
        std::cout << "调用析构函数" << std::endl;
        if (m_ptr_data)
        {
            delete m_ptr_data;
        }
    }

    Widget(int width_) :m_width(width_), m_ptr_data(nullptr) // 构造函数
    {
        std::cout << "调用有参构造函数" << std::endl;
    };

    Widget(const Widget& other): m_width(other.m_width), m_ptr_data(nullptr)  // 拷贝构造函数
    {
        std::cout << "调用拷贝构造函数" << std::endl;

        if (other.m_ptr_data != nullptr)
        {
            this->m_ptr_data = new int(*other.m_ptr_data);
        }
    }

    Widget& operator=(const Widget& other) // 拷贝赋值函数
    {
        std::cout << "调用拷贝赋值函数" << std::endl;
        if (this == &other) {
            return *this;
        }

        // 清理当前对象的资源
        if (this->m_ptr_data != nullptr)
        {
            delete this->m_ptr_data;
            this->m_ptr_data = nullptr;
        }

        // 复制另一个对象的数据
        if (other.m_ptr_data != nullptr)
        {
            this->m_ptr_data = new int(*other.m_ptr_data);
        }
        else
        {
            this->m_ptr_data = nullptr;
        }

        this->m_width = other.m_width;

        return *this;
    }

    Widget(Widget&& other):m_width(other.m_width), m_ptr_data(nullptr) // 移动构造函数
    {
        std::cout << "调用移动构造函数" << std::endl;

        // 如果另一个对象有内存需要移动
        if (other.m_ptr_data != nullptr)
        {
            this->m_ptr_data = other.m_ptr_data; // 移动内存到新对象,不需要删除旧内存
            other.m_ptr_data = nullptr; // 设置另一个对象指针为空,不需要删除旧内存
        }
    }

    Widget& operator=(Widget&& other) noexcept// 移动赋值函数
    {
        std::cout << "调用移动赋值函数" << std::endl;
        if (this != &other)
        {
            // 清理当前对象的资源
            if (this->m_ptr_data != nullptr)
            {
                delete this->m_ptr_data;
                this->m_ptr_data = nullptr;
            }

            // 交换成员变量
            this->m_ptr_data = other.m_ptr_data; // 更新当前对象的指针(不需删除旧内存)  
            this->m_width = other.m_width;

            // 设置other为空状态
            other.m_ptr_data = nullptr; // 设置另一个对象为空状态(不需删除旧内存)
        }

        return *this;
    }

private:
    int m_width;
    int* m_ptr_data;
};

int main()
{
    // 调用构造函数
    Widget my_widget(10); 

    // 调用拷贝构造函数
    Widget my_widget_copy_1(my_widget);
    Widget my_widget_copy_2 = my_widget;

    // 调用拷贝赋值函数
    Widget my_widget_2;
    my_widget_2 = my_widget;

    // 调用移动构造函数
    Widget my_widget_copy_3(std::move(my_widget));

    // 调用移动赋值函数
    Widget my_widget_3;
    my_widget_3 = std::move(my_widget);

    return 0;
}

程序输出

调用有参构造函数
调用拷贝构造函数
调用拷贝构造函数
调用默认构造函数
调用拷贝赋值函数
调用移动构造函数
调用默认构造函数
调用移动赋值函数
调用析构函数
调用析构函数
调用析构函数
调用析构函数
调用析构函数
调用析构函数

1.3 拷贝构造与拷贝赋值的区别,移动构造与移动赋值的区别

从上述代码我们可以看出拷贝构造函数与拷贝赋值运算符、移动构造函数与移动赋值运算符的作用很相似,但是两者还是有区别的。

拷贝构造函数与移动构造函数是使用已有的对象创建一个新的对象,而拷贝赋值运算符与移动赋值运算符主要是使用一个对象的值复制给另一个已经存在的对象,区别就是:是否有新的对象产生,如果有新的对象产生就是构造,没有就是赋值。

1.4 拷贝构造与移动构造的区别

1.4.1 功能不同

拷贝构造函数用于从一个已存在的对象创建一个新的对象,即复制构造函数。它会复制原始对象的所有成员变量的值,从而创建一个新的、与原始对象相同的对象。

移动构造函数用于从一个右值引用的临时对象创建一个新的对象。它会“窃取”原始对象的资源(例如指针或文件句柄),并将其移动到新对象中,从而避免复制大量数据,提高性能和减少内存使用。

1.4.2 形参类型不同

拷贝构造函数的参数通常是 const 引用类型的对象,例如:

MyClass(const MyClass& other);

移动构造函数的参数通常是右值引用类型的对象,例如:

MyClass(MyClass&& other);

1.4.3 调用时机不同

拷贝构造函数通常在以下情况下被调用:

  • 用一个对象初始化另一个对象
  • 传递一个对象作为参数到函数中
  • 从函数中返回一个对象

移动构造函数通常在以下情况下被调用:

  • 从一个临时对象创建一个新对象
  • 将一个临时对象作为参数传递到函数中
  • 从函数中返回一个临时对象

1.4.4 适用范围不同

拷贝构造函数适用于所有类型的对象,包括具有指针和资源的对象。

移动构造函数适用于具有可移动资源的对象,例如指针、文件句柄、unique_ptr 等,但不适用于具有固定位置内存的对象,例如数组、vector 等。

1.4.5 使用场景不同

拷贝构造函数适用于以下场景:

  • 对象需要被复制而不是被移动;
  • 对象的数据量较小,复制时的性能损耗可以接受;
  • 对象的状态在复制过程中不会发生变化

移动构造函数适用于以下场景:

  • 对象需要频繁地在不同的地方移动,而不是被拷贝;
  • 对象包含大量的数据或者内部资源,移动比复制更加高效;
  • 对象的拷贝构造函数会造成对象状态的不稳定,因为拷贝构造函数通常会执行深拷贝操作,而移动构造函数则不会