C++ 类内默认成员函数

2 分钟阅读

C++ 类内默认成员函数

当我们在创建一个类时, 如果你没有主动定义六个默认函数的话,编译器将为你自动创建。 如下面两个类完全等价

class object {};

class object {
public:
  object() = default;
  ~object() = default;
  object(const object& other) = default;
  object(object&& other) = default;
  object& operator=(const object& other) = default;
  object& operator=(object&& other) = default;
};

下面代码示例默认函数的调用场景

class object {
public:
  object() = default; ///< 构造函数 #1
  ~object() = default;  ///< 析构函数 #2
  object(const object& other) = default;  ///< 拷贝构造函数 #3
  object(object&& other) = default; ///< 移动构造函数 #4
  object& operator=(const object& other) = default; ///< 拷贝赋值符 #5
  object& operator=(object&& other) = default;  ///< 移动赋值符 #6
};

int main() {
  /// 构造函数#1示例
  object x; ///< 调用默认构造函数#1

  /// 析构函数#2示例
  {
    object x2;
  } ///< 超出作用域临时变量x2,调用析构函数#2

  object* p3 = new object();
  delete p3;  ///< 调用析构函数#2

  /// 拷贝构造函数#3示例
  object base;
  object copy1(base);  ///< 调用拷贝构造函数 #3
  object copy2 = base;  ///< 调用拷贝构造赋值符#3

  /// 移动构造函数#4示例
  object base_move;
  object copy_move(std::move(base_move)); ///< 由于入参为右值, 调用移动构造函数#4

  /// 拷贝构造赋值符#5示例
  object base_operator;
  object copy_operator;
  copy_operator = base_operator;  ///< 调用拷贝构造赋值符#5

  /// 移动构造赋值符#6示例
  object move_base;
  object move_operator = std::move(move_base);  ///< 调用移动构造赋值符#6
  return 0;
}

特殊情况

当一个类的析构函数被定为private或是delete时,该类只能构造而无法析构。

class object1 {
private:
  ~object() = default;
};


int main() {
  object1* p = new object1; ///< 编译通过
  /// delete p; ///< 编译错误
  /// object1 obj;  ///< 编译错误
  return 0;
}

下面这个类object2等同于上面object1

class object2 {
public:
  ~object() = delete;
};

各个默认合成函数的生成关系

构造析构与拷贝构造与拷贝赋值运算符的默认生成关系

原则: 需要析构函数的类也需要拷贝和赋值操作。 需要拷贝和赋值操作函数的类,不一定需要析构函数。 需要拷贝操作的类也需要赋值操作,反之亦然。 如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。

原因:

一个成员有删除的或不可访问的析构函数会导致合成的默认拷贝和拷贝构造函数被定义为删除的,为了防止创建出无法销毁的对象。

class a
{
private:
  ~a() {}
};

int main()
{
    a obj0; ///< 没有默认构造函数, 编译错误
}

对于具有引用成员或无法默认构造的const成员的类,编译器不会为其合成默认构造函数。

class a
{
  const int i;   ///< 未赋值的 const 变量
  int& j;        ///< 未指向的引用变量
};

int main()
{
    /// a obj0; ///< 没有默认构造函数, 编译错误
}

如果一个类有const成员,则它不能使用合成的拷贝复制运算符,因为const成员被创建后无法再次赋值。

class a
{
  const int i = 0;
};

int main()
{
  a obj0;
  a obj1;
  obj0 = obj1;  ///< 编译错误
}

对于具有引用成员的类, 其合成拷贝构造函数也是被删除的。因为无法使引用改变指向。

管理类外资源的类必须定义拷贝控制成员。

赋值运算符有两个要求:

  • 如果将一个对象赋予它自身,赋值运算符必须能正常工作。
  • 大多数赋值运算符组合了析构函数和拷贝构造函数。
class object{
  std::string* p;
  public:
  object& operator=(const object& other) {
    if(&other != this) {
      delete p;
      p = new std::string(*other.p);  ///< 当传入对象就是自己时会出错。
    }
    return *this;
  }
};

如果我们不声明自己的拷贝构造函数或拷贝赋值运算符,编译器总会为我们合成这些操作,而编译器不会为某些类合成移动操作。特别是,当一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符。 如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝操作来代替移动操作

只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时编译器才会为合成移动构造函数或移动赋值运算符。

与拷贝操作不同,移动操作永远不会隐式定义为删除的函数。但是,如果我们显式要求编译器生成=default的移动操作,且编译器不能移动所有成员移动所有成员,则编译器会将移动操作定义为删除的函数。

  • 与拷贝构造函数不同,移动构造函数被定义为删除的函数的条件是:有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值运算符的情况类似。

  • 类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的。

    Snipaste_2020-12-07_14-14-02

  • 类似拷贝赋值运算符,如果有类成员是const的或是引用,则类的移动赋值运算符被定义为删除的。

    Snipaste_2020-12-07_14-16-31

定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地定义为删除的。

class object {
public:
  object() = default;
  object&& operator=(object&& other) {}
  object(object&& other) {}
};

int main() {
  object obj;
  auto obj_copy1 = std::move(obj);
  auto obj_copy2 = obj;
  return 0;
}

Snipaste_2020-12-07_11-15-47

下面代码中#1拷贝构造函数由于接收的是const的引用,所以也能够匹配右值入参。

class object{
  public:
  object() = default;
  object(const object& obj) = default;  ///< #1
};

int main(){
  object obj1;
  object obj2(std::move(obj1));
}

如果一个类有一个拷贝构造函数但未定义移动构造函数时,编译器不会合成移动构造函数。但是函数匹配规则保证该类型的对象会被拷贝,即使我们试图使用std::move来移动他们时也是如此。

class a
{
public:
  ///  默认构造函数
  a() {}
  /// 拷贝构造函数
  a(const a& other){}
};

int main()
{
  a obj0;
  a obj1 = std::move(obj0); ///< 此处会调用拷贝构造函数
}

特殊成员函数之间的依赖关系

编译器隐式声明

  默认构造函数 析构函数 拷贝构造函数 拷贝赋值 移动构造函数 移动赋值
全部不声明 预置 预置 预置 预置 预置 预置
任意构造函数 不声明 预置 预置 预置 预置 预置
默认构造函数 用户声明 预置 预置 预置 预置 预置
析构函数 预置 用户声明 预置 预置 不声明 不声明
拷贝构造函数 不声明 预置 用户声明 预置 不声明 不声明
拷贝赋值 预置 预置 预置 用户声明 不声明 不声明
移动构造函数 不声明 预置 弃置 弃置 用户声明 不声明
移动赋值 预置 预置 弃置 弃置 不声明 用户声明

更新时间: