六一的部落格


关关难过关关过,前路漫漫亦灿灿。



类对象的拷贝, 赋值, 和销毁

如编译器在某些情况会提供默认构造函数的合成版本, 编译器也可以提供拷贝构造函数, 拷贝赋值运算符, 和析构函数的合成版本


合成版本的注意事项

讨论非删除 delete 的合成版本

C语言中存在浅拷贝和深拷贝的问题:

  • 申请动态内存由指针管理, 使用一个指针对另一个指针赋值时, 两个指针指向同一块内存区域, 而不是各自拥有一块内存区域, 存放相同的值. 此为浅拷贝
  • 为另一个指针申请动态内存, 使用指针所指对象为动态内存赋值, 此为深拷贝

initializer_list对象之间赋值, 亦是浅拷贝

管理动态内存的类通常不能使用编译器提供的合成版本

不过管理动态内存的方式是多样的, 比如类拥有vector或string类型的数据成员. 此时动态内存的管理交付给vector和string, 编译器提供的合成版本调用vector和string的拷贝控制成员, 规避了浅拷贝问题


拷贝构造函数

1class T
2{
3public:
4    T(const T &);
5    T(T &);
6};

赋值运算符重载: 拷贝赋值运算符

1class T
2{
3    T &operator=(const T &);
4    T &operator=(T &);
5};

移动构造函数

1class T
2{
3public:
4    T(T &&);
5};

赋值运算符重载: 移动赋值运算符

1class T
2{
3public:
4    T &operator=(T &&);
5};

析构函数

1class T
2{
3public:
4    ~T();
5};

拷贝初始化

copy initialization

使用一个对象初始化另一个对象

  1. 二者类型一致,拷贝
  2. 二者类型不一致,先经过一次允许的隐式转换

调用拷贝构造函数或移动构造函数


拷贝初始化发生的情景

  1. 使用赋值运算符初始化对象

    • 右侧运算对象和左侧运算对象类型相同

      1T var1;
      2T var2 = var1;
    • 显式调用构造函数临时构造一个与左侧运算对象类型相同的右侧运算对象

      1T var = T(args);
    • 使用隐式转换构造函数构造一个与左侧运算对象类型相同的右侧运算对象

      1T var = val; // 允许一次转换
      

    最终,右侧运算对象和左侧运算对象是同一类型

  2. 函数调用值传递: 将对象作为实参传递给一个非引用类型的形参

  3. 函数调用返回: 一个返回类型为非引用类型的函数返回一个对象

  4. 列表初始化数组中的元素,容器中的元素等

  5. 使用容器的insert操作,push操作

以上场景在没有定义移动构造函数时正确: 拷贝构造函数形参如果是const T &, 其可以接受右值; 如果存在移动构造函数, 其匹配优先级更高; 编译器有时可以跳过拷贝/移动构造函数, 直接创建对象


直接初始化

创建对象时使用参数列表匹配构造函数(包括拷贝/移动构造函数)

1T var(args);

显式构造函数只能用于直接初始化, 或者配合static_cast

1vector<int> v1(10);        // 直接初始化,匹配构造函数
2
3vector<int> v2 = 10;       // 错误:该构造函数是转换构造函数,但不支持隐式转换

直接初始化发生的其他情景

使用容器的emplace操作


拷贝控制成员


类对象的拷贝, 赋值, 和销毁

如编译器在某些情况会提供默认构造函数的合成版本, 编译器也可以提供拷贝构造函数, 拷贝赋值运算符, 和析构函数的合成版本


合成版本的注意事项

讨论非删除 delete 的合成版本

C语言中存在浅拷贝和深拷贝的问题:

  • 申请动态内存由指针管理, 使用一个指针对另一个指针赋值时, 两个指针指向同一块内存区域, 而不是各自拥有一块内存区域, 存放相同的值. 此为浅拷贝
  • 为另一个指针申请动态内存, 使用指针所指对象为动态内存赋值, 此为深拷贝

initializer_list对象之间赋值, 亦是浅拷贝

管理动态内存的类通常不能使用编译器提供的合成版本

不过管理动态内存的方式是多样的, 比如类拥有vector或string类型的数据成员. 此时动态内存的管理交付给vector和string, 编译器提供的合成版本调用vector和string的拷贝控制成员, 规避了浅拷贝问题


拷贝构造函数

1class T
2{
3public:
4    T(const T &);
5    T(T &);
6};

赋值运算符重载: 拷贝赋值运算符

1class T
2{
3    T &operator=(const T &);
4    T &operator=(T &);
5};

移动构造函数

1class T
2{
3public:
4    T(T &&);
5};

赋值运算符重载: 移动赋值运算符

1class T
2{
3public:
4    T &operator=(T &&);
5};

析构函数

1class T
2{
3public:
4    ~T();
5};

拷贝初始化

copy initialization

使用一个对象初始化另一个对象

  1. 二者类型一致,拷贝
  2. 二者类型不一致,先经过一次允许的隐式转换

调用拷贝构造函数或移动构造函数


拷贝初始化发生的情景

  1. 使用赋值运算符初始化对象

    • 右侧运算对象和左侧运算对象类型相同

      1T var1;
      2T var2 = var1;
    • 显式调用构造函数临时构造一个与左侧运算对象类型相同的右侧运算对象

      1T var = T(args);
    • 使用隐式转换构造函数构造一个与左侧运算对象类型相同的右侧运算对象

      1T var = val; // 允许一次转换
      

    最终,右侧运算对象和左侧运算对象是同一类型

  2. 函数调用值传递: 将对象作为实参传递给一个非引用类型的形参

  3. 函数调用返回: 一个返回类型为非引用类型的函数返回一个对象

  4. 列表初始化数组中的元素,容器中的元素等

  5. 使用容器的insert操作,push操作

以上场景在没有定义移动构造函数时正确: 拷贝构造函数形参如果是const T &, 其可以接受右值; 如果存在移动构造函数, 其匹配优先级更高; 编译器有时可以跳过拷贝/移动构造函数, 直接创建对象


直接初始化

创建对象时使用参数列表匹配构造函数(包括拷贝/移动构造函数)

1T var(args);

显式构造函数只能用于直接初始化, 或者配合static_cast

1vector<int> v1(10);        // 直接初始化,匹配构造函数
2
3vector<int> v2 = 10;       // 错误:该构造函数是转换构造函数,但不支持隐式转换

直接初始化发生的其他情景

使用容器的emplace操作