Default constructor
默认构造函数是在声明类的对象但未使用任何参数初始化时调用的构造函数。如果类定义没有构造函数,则编译器会假定该类具有隐式定义的默认构造函数。
因此,在声明这样的类之后:
编译器假定Example具有默认构造函数。
因此,可以通过简单地声明它们而无需任何参数来构造此类的对象:
但是,只要一个类的某个构造函数接受了显式声明的任意数量的参数,编译器便不再提供隐式默认构造函数,并且不再允许该类的新对象不带参数的声明。
例如,以下类:
在这里,我们声明了一个带有int类型参数的构造函数。
因此,以下对象声明将是正确的:
但是以下内容:
是无效的,因为该类已使用带有一个参数的显式构造函数声明,并替换了不带任何参数的隐式默认构造函数。
因此,如果需要在没有参数的情况下构造此类的对象,则还应在该类中声明适当的默认构造函数。
例如:
在这里,Example3具有一个默认的构造函数(即没有参数的构造函数)定义为一个空块:
这样就可以在不带参数的情况下构造Example3类的对象(例如在此示例中声明了foo)。
通常,为没有其他构造函数的所有类隐式定义这样的默认构造函数,因此不需要显式定义。
但是在这种情况下,Example3具有另一个构造函数:
当在类中显式声明任何构造函数时,不会自动提供任何隐式默认构造函数。
Destructor
析构函数实现与构造函数相反的功能:当类的生存期结束时,它们负责类所需的必要清理。我们在前几章中定义的类没有分配任何资源,因此实际上并不需要任何清理。
但是,现在让我们想象一下,上一个示例中的类分配了动态内存来存储其作为数据成员的字符串。
在这种情况下,在对象寿命结束时自动释放一个负责释放此内存的功能将非常有用。
为此,我们使用析构函数。
析构函数是一个成员函数,与默认构造函数非常相似:它不带任何参数,也不返回任何值,甚至不返回void。
它还使用类名作为自己的名称,但前面加上波浪号(〜):
在构造上,Example4为字符串分配存储空间。析构函数随后释放的存储。
对象的析构函数在其生命周期结束时被调用;
在foo和bar的情况下,这发生在main函数的末尾。
Copy constructor
当对象传递其自身类型的命名对象作为参数时,将调用其副本构造函数以构造副本。复制构造函数是一个构造函数,其第一个参数是对该类本身的类型引用(可能是const限定),并且可以用此类型的单个参数调用。
例如,对于MyClass类,副本构造函数可能具有以下签名:
如果一个类没有定义自定义副本,也没有移动构造器(或赋值),则提供一个隐式副本构造器。
此副本构造函数仅执行其自身成员的副本。
例如,对于一个类,例如:
隐式的拷贝构造函数是自动定义的。为此功能假定的定义执行浅复制,大致等效于:
此默认副本构造函数可能适合许多类的需求。但是浅拷贝只复制自身的类成员,而这可能不是我们对上面定义的类Example4之类的期望,因为它包含处理其存储的指针。对于该类,执行浅表复制意味着将复制指针值,而不是内容本身。
这意味着两个对象(副本和原始对象)将共享一个字符串对象(它们都将指向同一对象),并且在某个时刻(销毁时)两个对象都将尝试删除相同的内存块
,可能导致程序在运行时崩溃。
这可以通过定义以下执行深层复制的自定义复制构造函数来解决:
此副本构造函数执行的深拷贝为新字符串分配存储空间,该字符串已初始化为包含原始对象的copy。
这样,两个对象(副本和原始对象)都具有存储在不同位置的内容的不同copy。
Copy assignment
初始化对象时,不仅可以在构造时复制对象:还可以在任何赋值操作中复制对象。
看到不同:
注意,baz在构造时使用等号初始化,但这不是赋值操作!
(尽管看起来像一个):**对象的声明不是赋值操作,它只是调用单参数构造函数的另一种语法。**对foo的赋值是赋值操作。
这里没有声明任何对象,但是正在对现有对象执行操作; foo。
复制赋值运算符是operator =的重载,该运算符将类本身的值或引用作为参数。
返回值通常是对* this的引用(尽管这不是必需的)。
例如,对于类MyClass,副本分配可能具有以下签名:
复制赋值运算符也是一个特殊的函数,如果一个类没有定义自定义复制或移动赋值(也没有移动构造函数),则隐式定义它。
但是同样,隐式版本执行浅复制,该复制适用于许多类,但不适用于具有指向其处理其存储的对象的指针的类,如Example5中的情况。
在这种情况下,不仅该类具有两次删除指向的对象的风险,而且分配还会通过不删除分配之前对象所指向的对象而造成内存泄漏。
这些问题可以通过删除前一个对象并执行深层复制的副本分配来解决:
甚至更好的是,由于其字符串成员不是常量,因此可以重新利用相同的字符串对象:
Move constructor and assignment
与复制类似,移动也使用对象的值将值设置为另一个对象。
但是,与复制不同,内容实际上是从一个对象(源)转移到另一个对象(目标)的:源丢失了该内容,该内容由目标接管。
仅当值的源是未命名的对象时,才会发生这种移动。
未命名的对象是本质上是临时的,因此甚至都没有命名的对象。
未命名对象的典型示例是函数或类型转换的返回值。
使用诸如此类的临时对象的值来初始化另一个对象或分配其值,实际上并不需要复制:该对象永远不会用于其他任何用途,因此可以将其值移动到目标位置。
这些情况触发move构造函数和move分配:
当使用未命名的临时对象在构造上初始化对象时,将调用move构造函数。同样,当为一个对象分配一个未命名的临时值时,将调用移动分配:
fn返回的值和MyClass构造的值都是未命名的临时变量。
在这些情况下,无需进行复制,因为未命名的对象的生存期非常短,并且在进行更有效的操作时可以被另一个对象获取。move构造函数和move赋值是对类本身采用rvalue类型引用的参数的成员:通过在类型后面加上两个“&”符号来指定右值引用。作为参数,右值引用匹配此类型的临时变量的参数。
移动的概念对于管理其使用的存储的对象最为有用,例如使用new和Delete分配存储的对象。
在此类对象中,复制和移动实际上是不同的操作:
-从A复制到B意味着将新的内存分配给B,然后将A的全部内容复制到为B分配的新内存中。
-从A移到B意味着已经分配给A的内存将转移到B而无需分配任何新存储。
它只涉及复制指针。
编译器已经优化了许多情况,这些情况在返回值优化中被正式要求移动构造调用。
最值得注意的是,当函数返回的值用于初始化对象时。
在这些情况下,move构造函数实际上可能永远不会被调用。
请注意,即使右值引用可用于任何函数参数的类型,但对于除move构造函数之外的其他用法很少有用。
右值引用很棘手,不必要的使用可能是很难跟踪的错误来源。
Implicit members
六个特殊成员函数是在某些情况下在类上隐式声明的成员:
请注意,在相同的情况下,为什么不是所有特殊成员函数都隐式定义的。
这主要是由于与C结构和早期C ++版本的向后兼容性,实际上,其中有些不赞成使用。
幸运的是,每个类都可以显式选择这些成员中的哪些成员具有其默认定义,或者分别通过使用关键字default和delete删除这些成员。
语法是以下之一:
例如:
在这里,Rectangle可以使用两个int参数构造,也可以默认构造(不带参数)。
但是,不能从另一个Rectangle对象复制构造它,因为该功能已被删除。
因此,假设最后一个示例的对象,以下语句将无效:
但是,可以通过将其副本构造函数定义为:
这基本上等于:
请注意,关键字default并未定义等于默认构造函数的成员函数(即,默认构造函数表示没有参数的构造函数),而是等于如果未删除则隐式定义的构造函数。
通常,为了将来的兼容性,鼓励显式定义一个复制/移动构造函数或一个复制/移动分配但不同时定义两者的类,对未明确定义的其他特殊成员函数指定delete或default。