一个面试题引发的思考——类的特种成员函数

2024-07-18 13:36:31 浏览数 (3)

之前问面试者“定义一个空类,并声明该类的多个对象,为什么对象间可以相互赋值?”本意是希望面试者能够回答编译期默认生成的构造函数、拷贝构造函数和拷贝赋值运算符函数。但是并没有回答到点子上。进一步引导到,“类的特种成员函数有哪些?”,也没有回答上来。有可能是我没有问清楚,也有可能是面试者由于紧张懵住了。今天刚好拿出这个问题来讨论下。

类的特种成员函数

关于特种成员函数,C 11前有四个:默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符。C 11开始新增了移动赋值运算符和移动构造函数,即C 11起存在6个特种成员函数。

  • 这些成员函数只有在代码中用到且没有声明时才会生成默认,即没有用到则不会生成。(见示例代码1)
  • 关于默认构造函数:当类中不存在构造函数时,才会生成默认构造函数。(见示例代码1)
  • 生成的默认函数时inline、非虚且为public访问级别,继承自父类的虚析构除外。(见示例代码1)
  • 移动构造和移动赋值会针对其非静态的成员(含基类部分)执行移动构造和移动赋值,但是移动构造和移动赋值只是移动请求;针对不可移型别将执行对应的拷贝动作
  • 拷贝构造函数和拷贝赋值运算符的生成相互独立,两者并无影响。(见示例代码2)
  • 移动构造和移动赋值相互影响,声明了其中一个就会阻止编译器生成另一个。声明移动构造会抑制移动赋值的生成;声明移动赋值运算符函数会抑制移动构造函数的生成。
  • 声明移动操作,则会阻止生成拷贝操作;声明拷贝操作,则会阻止生成移动操作(见示例代码3)
代码语言:javascript复制
//////////////////  示例代码1 begin /////////////////////
#include<iostream>

class NullClass {};

void test_null()
{
  NullClass a, b;

  a = b;
  NullClass c = a;

  NullClass d = std::move(a);
  d = std::move(b);
}

//编译器生成的函数如下
class NullClass
{
  public: 
  // inline constexpr NullClass() noexcept = default;
  // inline constexpr NullClass(const NullClass &) noexcept = default;
  // inline constexpr NullClass(NullClass &&) noexcept = default;
  // inline NullClass & operator=(const NullClass &) noexcept = default;
  // inline NullClass & operator=(NullClass &&) noexcept = default;
};
////////////////////  示例代码1 end /////////////////////

//////////////////  示例代码2 begin /////////////////////
class CopyClass {
public:
  CopyClass()=default;
  CopyClass(const CopyClass& rhs){}
};

void test_copy()
{
  CopyClass a, b;

  a = b;
  CopyClass c = a;

  CopyClass d = std::move(a);
  d = std::move(b);
}
//编译器生成的函数如下:
class CopyClass
{
  
  public: 
  inline constexpr CopyClass() noexcept = default;
  inline CopyClass(const CopyClass & rhs){
  }
  
  // inline CopyClass & operator=(const CopyClass &) noexcept = default;
};
////////////////////  示例代码2 end /////////////////////


//////////////////  示例代码3 begin /////////////////////
class MoveClass {
public:
  MoveClass()=default;
  MoveClass(const MoveClass&& rhs){}
};

void test_move()
{
  MoveClass a, b;

  //a = b;//声明移动操作,禁止了拷贝赋值
  //MoveClass c = a;/声明移动操作,禁止了拷贝构造

  MoveClass d = std::move(a);
  //d = std::move(b);/声明移动构造,禁止了移动赋值
}
//编译器生成的函数如下:
class MoveClass
{
  
  public: 
  inline constexpr MoveClass() noexcept = default;
  inline MoveClass(const MoveClass && rhs){
  }
  
  // inline constexpr MoveClass(const MoveClass &) /* noexcept */ = delete;
  // inline MoveClass & operator=(const MoveClass &) /* noexcept */ = delete;
};

////////////////////  示例代码3 end /////////////////////

Rule of Three

Rule of Three讲,如果类声明了拷贝构造函数、拷贝赋值运算符、析构函数中的任何一个,就得同时声明这三个。默认理解为声明这三个中的一个必定涉及到了资源管理,所以默认的拷贝操作也就不再适宜,所以均需要用户自定义。

C 11 标准指出,存在拷贝操作或析构函数的条件下,仍然自动生成拷贝操作是废弃行为(见如下示例代码)。虽然当前的编译器仍然支持自动生成另一个拷贝操作,但强烈建议遵守大三律,程序员同时显示声明这三个函数。

代码语言:javascript复制
class CopyClass {
public:
  CopyClass() = default;
  ~CopyClass()=default;
  CopyClass(const CopyClass& rhs) {}
};

void test_copy()
{
  CopyClass a, b;

  a = b;
  CopyClass c = a;

  CopyClass d = std::move(a);
  d = std::move(b);
}
//编译器生成的函数如下:
class CopyClass
{
  
  public: 
  inline constexpr CopyClass() noexcept = default;
  inline ~CopyClass() /* noexcept */ = default;
  inline CopyClass(const CopyClass & rhs){
  }
  
  // inline constexpr CopyClass & operator=(const CopyClass &) noexcept = default;
};

Rule of Five

Rule of Five讲,如果类声明了拷贝构造函数、拷贝赋值运算符、析构函数、移动构造函数、移动赋值运算符中的任何一个,就得同时声明这五个。结合Rule of Three很容易理解。

如果声明了拷贝构造函数、拷贝赋值运算符、析构函数中的任何一个,必须同时声明这三个。

如果声明了移动操作中的一个则会抑制另一个的生成,因此,需要同时声明他们两个。

如果声明了拷贝操作则会抑制移动操作的生成,所以声明了拷贝操作,则必须声明移动操作。反之亦然。

同时生成这五个函数的代码示例如下:

代码语言:javascript复制
class CopyFive {
public:
  CopyFive() = default;
  ~CopyFive()=default;
  CopyFive(const CopyFive& rhs) =default;
  CopyFive(CopyFive&& rhs) = default;

  CopyFive& operator=(const CopyFive& rhs) = default;
  CopyFive& operator=(CopyFive&& rhs) = default;
};

总结

本文结合C 特种成员函数的生成限制,讲解了Rule of Three和Rule of Five,建议我们都遵守Rule of Five,同时声明这5个函数。

0 人点赞