默认函数控制 =default 与 =delete
= default
和 = delete
是C++11引入的两个新特性,它们分别用于显示地指定默认的和删除的特殊成员函数。
类与默认函数
当我们定义一个类而没有显式提供特定的特殊成员函数时,C++编译器会自动生成一些默认的特殊成员函数。这些默认生成的函数通常包括:
- 默认构造函数(Default Constructor): 如果你没有提供任何构造函数,编译器会生成一个默认构造函数。这个构造函数执行默认的成员初始化,对于内置类型,不进行显式初始化。
- 析构函数(Destructor): 如果你没有提供析构函数,编译器会生成一个默认的析构函数。这个析构函数负责销毁类的成员和基类。
- 拷贝构造函数(Copy Constructor): 如果你没有提供拷贝构造函数,编译器会生成一个默认的拷贝构造函数。这个函数执行浅拷贝,对于指针等资源,只进行指针的复制而不是深拷贝。
- 拷贝赋值运算符(Copy Assignment Operator): 如果你没有提供拷贝赋值运算符,编译器会生成一个默认的拷贝赋值运算符。这个函数也执行浅拷贝。
- 移动构造函数(Move Constructor): 如果你没有提供移动构造函数,编译器会生成一个默认的移动构造函数。这个函数执行移动语义,将右值引用的资源所有权转移到新对象。
- 移动赋值运算符(Move Assignment Operator): 如果你没有提供移动赋值运算符,编译器会生成一个默认的移动赋值运算符。这个函数也执行移动语义。
这些函数被称为特殊成员函数,它们在需要时由编译器自动生成,但是如果你显式地提供了其中的某一个,编译器就不再生成它。如果你想控制这些函数的行为,可以显式提供自定义的实现或者使用 = default
来保持编译器生成的默认行为。同样,你可以使用 = delete
来阻止编译器生成某个特殊成员函数。
需要注意:
- 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,c++不会再提供其他构造函数
示例:
#include <iostream>
using namespace std;
class Base {
public:
Base(int x): m_X(x){}
int m_X;
};
void test01() {
Base b(10);
// Base a; 报错
Base a(b); // 提供了有参构造, 还是会提供默认拷贝构造
Base c(move(b)); // 也会提供默认移动构造
cout << b.m_X << endl;
}
int main() {
test01();
return 0;
}
#include <iostream>
using namespace std;
class Base {
public:
// Base(int x): m_X(x){}
Base(const Base& b) {
this->m_X = b.m_X;
}
int m_X;
};
void test01() {
// Base b(10); error
// Base a; error
}
int main() {
test01();
return 0;
}
= default 与 = delete
在C++11标准中称= default修饰的函数为显式默认【缺省】(explicit defaulted)函数,而称=delete修饰的函数为删除(deleted)函数或者显示删除函数。
C++11引入显式默认和显式删除是为了增强对类默认函数的控制,让程序员能够更加精细地控制默认版本的函数。
= default
上篇文章提到了POD类型, 一旦声明了自定义版本的构造函数,则有可能导致我们定义的类型不再是POD类型,我们便不再能够享受POD类型为我们带来的便利
对于上面提到的这些,我们无需过度担心,因为C++11非常贴心地为我们提供了解决方案,就是使用=default
类内部指定函数为默认函数
一般情况下,我们可以在定义类的时候直接在类内部指定默认函数,如下所示:
class Base
{
public:
Base() = default;
Base(const Base& obj) = default;
Base(Base&& obj) = default;
Base& operator= (const Base& obj) = default;
Base& operator= (Base&& obj) = default;
~Base() = default;
};
使用 =defaut 指定的默认函数和类提供的默认函数是等价的
在类外部指定函数为默认函数
// 类定义
class Base
{
public:
Base();
Base(const Base& obj);
Base(Base&& obj);
Base& operator= (const Base& obj);
Base& operator= (Base&& obj);
~Base();
};
// 在类定义之外指定成员函数为默认函数
Base::Base() = default;
Base::Base(const Base& obj) = default;
Base::Base(Base&& obj) = default;
Base& Base::operator= (const Base& obj) = default;
Base& Base::operator= (Base&& obj) = default;
Base::~Base() = default;
如果程序猿对C++类提供的默认函数(上面提到的六个函数)进行了实现,那么可以通过 =default 将他们再次指定为默认函数,不能使用 =default 修饰这六个函数以外的函数。
有参构造也不能修饰!
二者区别: 当在类内使用=default时,函数将隐式地声明为内联,如果不希望是内联函数,就将函数在类外定义
= delete
=delete 表示显示删除,显式删除可以避免用户使用一些不应该使用的类的成员函数,使用这种方式可以有效的防止某些类型之间自动进行隐式类型转换产生的错误。下面举例说明:
禁止使用默认生成的函数
class Base
{
public:
Base() = default;
Base(const Base& obj) = delete;
Base& operator= (const Base& obj) = delete;
};
int main()
{
Base b;
Base tmp1(b); // error
Base tmp = b; // error
return 0;
}
第5行:禁用拷贝构造函数
第6行:禁用 = 进行对象复制
第12行:拷贝构造函数已被显示删除,无法拷贝对象
第13行:复制对象的赋值操作符重载函数已被显示删除,无法复制对象
禁止使用自定义函数
class Base
{
public:
Base(int num) : m_num(num) {}
Base(char c) = delete;
void print(char c) = delete;
void print()
{
cout << "num: " << m_num << endl;
}
void print(int num)
{
cout << "num: " << num << endl;
}
private:
int m_num;
};
int main()
{
Base b(97); // 'a' 对应的 acscii 值为97
Base b1('a'); // error
b.print();
b.print(97);
b.print('a'); // error
return 0;
}
第5行:禁用带 char类型参数的构造函数,防止隐式类型转换(char转int)
第6行:禁止使用带char类型的自定义函数,防止隐式类型转换(char转int)
第22行:对应的构造函数被禁用,因此无法使用该构造函数构造对象
第25行:对应的打印函数被禁用,因此无法给函数传递char类型参数
扩展:
- 与=default不同,=delete必须出现在函数第一次声明的时候
- 我们可以对任何函数指定=delete(除析构函数外),但是我们只能对编译器可以合成的默认构造函数或拷贝控制成员使用=default
如果一个类的析构函数被删除了:
- 编译器将不允许定义该类型的变量或创建该类的临时对象(因为该类不能被删除销毁了)
- 如果一个类中有一个成员,该成员为类类型,且该类类型删除了析构函数,则我们也不能定义这个类的变量或临时对象(因为该类的成员无法被删除,间接导致这个类也无法被删除销毁)
- 虽然我们不能定义这种类型的变量或成员,但是可以动态分配这种类型的对象,但是动态分配之后就不能释放这个对象了
struct NoDtor{
NoDtor() = default; //使用合成的默认构造函数
~NoDtor() = delete; //我们不能销毁NoDtor类型的对象
};
NoDtor nd; //错误,NoDtor的析构函数时删除的
NoDtor *p=new NoDtor(); //正确,但是我们不能delete p
delete p; //错误,NoDtor的析构函数是删除的