之前问面试者“定义一个空类,并声明该类的多个对象,为什么对象间可以相互赋值?”本意是希望面试者能够回答编译期默认生成的构造函数、拷贝构造函数和拷贝赋值运算符函数。但是并没有回答到点子上。进一步引导到,“类的特种成员函数有哪些?”,也没有回答上来。有可能是我没有问清楚,也有可能是面试者由于紧张懵住了。今天刚好拿出这个问题来讨论下。
类的特种成员函数
关于特种成员函数,C 11前有四个:默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符。C 11开始新增了移动赋值运算符和移动构造函数,即C 11起存在6个特种成员函数。
- 这些成员函数只有在代码中用到且没有声明时才会生成默认,即没有用到则不会生成。(见示例代码1)
- 关于默认构造函数:当类中不存在构造函数时,才会生成默认构造函数。(见示例代码1)
- 生成的默认函数时inline、非虚且为public访问级别,继承自父类的虚析构除外。(见示例代码1)
- 移动构造和移动赋值会针对其非静态的成员(含基类部分)执行移动构造和移动赋值,但是移动构造和移动赋值只是移动请求;针对不可移型别将执行对应的拷贝动作。
- 拷贝构造函数和拷贝赋值运算符的生成相互独立,两者并无影响。(见示例代码2)
- 移动构造和移动赋值相互影响,声明了其中一个就会阻止编译器生成另一个。声明移动构造会抑制移动赋值的生成;声明移动赋值运算符函数会抑制移动构造函数的生成。
- 声明移动操作,则会阻止生成拷贝操作;声明拷贝操作,则会阻止生成移动操作(见示例代码3)
////////////////// 示例代码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个函数。