在目标函数后面声明“=default”表示该函数为默认函数。默认函数让我们得以明确指示编译器按“默认”的方式生成目标函数,前提是一个函数可由编译器自动产生,如:默认构造函数、析构函数、拷贝构造函数、移动构造函数、复制赋值操作符和移动赋值操作符等。
使用默认函数的原因有如下几点:
1.改变函数访问限制。按默认方式,编译器只会产生公有“public”函数,若想声明为保护或私有类型则必须手动实现。
2.充当说明注释。若编译器生成的函数即可满足所需,则声明为默认使得函数的意图清晰明了。
3.令编译器生成默认函数。一般来说,仅当用户没有声明自定义构造函数时,编译器才会生成默认构造函数,针对这种情况,添加“=default”可明确指示编译器生成默认函数。
4.令析构函数成为虚函数,托付给编译器生成。
5.强制拷贝构造函数只接收特定形式参数。如:使之不接受const引用,而改为接收源对象的非const引用。
6.编译器产生的函数具备某些特殊性质,可以利用默认生成函数的特殊性质。
以下是上述应用的例子:
class Y
{
private:
Y() = default; //改变访问级别为public
public:
Y(Y&) = default; //接收非const引用
Y& operator=(const Y&) = default; //充当注解
protected:
virtual ~Y() = default; //改变访问级别为public,并加入虚函数性质
};
默认函数的特点1:
上面提到,编译器产生的函数具备特殊性质,与自定义函数最大的差异是编译器有可能生成平实函数,当类作为一个整体满足一定要求(详见C++官方文件ISO/IEC 14882:2011),相关成员函数才会成为平实函数,其特点如下:
平实函数(trival function),实际意义是默认构造函数和析构函数不执行任何操作;复制、赋值和移动操作仅仅涉及最简单、直接按位进行内存的复制/转移操作,没有其它任何行为;若对象所含默认函数全是平实函数,则可按照POD(Plain Old Data)方式进行处理。
1.如果某对象的拷贝构造函数、拷贝赋值操作符和析构函数都是平实函数,那么它就可以通过memcpy()或memmove()复制。
2.constexpr函数所用的字面值型别(literal type)必须具备平实的构造函数、拷贝构造函数和析构函数。
3.若要允许一个类被联合体(union)包含,若联合体已经具备自定义构造和析构函数,则这个类必须具有平实的构造函数,拷贝构造函数,复制操作符和析构函数。
4. 若某个类充当了类模版std::atomic<>的模版参数,则他应该带有平实的拷贝赋值操作符,才能提供该类型的原子操作。
默认函数的特点2:
如果用户没有为某个类提供构造函数,那么它便得以充当聚合体,其初始化过程可依照聚合体表达式完成:
聚合体(aggregate),是C++11引入的概念,通常可以是数组、联合体、结构体或类(不得含有虚函数或自定义构造函数,也不得继承父类的构造函数)。
struct aggregrate { aggregrate() = default; aggregrate(const aggregrate&) = default; int a; double b; }; aggregrate x = { 42, 3.141 };//x.a = 42; x.b=3.141
默认函数的特点3:
假定某个类的默认构造函数由编译器自动生成,其每个数据成员与全部基类同样如此,且成员都属于内建型别。那么在初始化这个类的实例时,在该实例没有静态生存期的情况下,是否显式调用默认构造函数,会影响数据成员的初始化:不显式调用则数据成员的值未确定,读取会引起未定义行为;显式调用则数据成员初始化为0。
静态生存期(static storage duration),指某些对象随整个程序开始而获得存储空间,到程序结束空间才被回收,这些对象包括静态局部变量、静态数据成员、全局变量等。
struct X
{
int a;
};
X x1; //x1.a尚未确定
X x2 = X(); //x2.a==0成立
若实例存在静态生存期,则默认构造函数的类的数据成员会被初始化为0。
原子类型正是应用了这一性质,将自身的构造函数显式声明为“默认”。除去以下情形,原子类型的初始值是未定义:具有静态生存期;显式调用默认构造函数,进行零值初始化;明确设定了初始值。各种原子类型均具备一个构造函数,单独接受一个参数作为初始值,它们都被声明为constexpr函数,以准许发生静态初始化。