c++中的构造(包括移动),赋值(包括移动),析构详解
这五种操作:构造(包括移动),赋值(包括移动),析构其实就是定义了对一个对象进行构造,赋值,析构时的行为。理解这些行为并不复杂,复杂的是理解在继承下这些行为的表现。需要注意的是他们并不会被继承(传统意义上的继承)。
拷贝构造函数
形式:
class Foo{
public:
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数
};
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。引用是必须的(否则会出现无限循环),const是通常的。不应是explicit(传参,返回都是隐式)。
如果我们没有为一个类定义一个拷贝构造函数,编译器会为我们定义一个。合成版本会逐个按位拷贝非static成员,如果是类类型,机会调用类的拷贝构造函数。
拷贝赋值运算符
形式:
class Foo(){
public:
Foo& operator=(const Foo& ); //赋值云运算符
};
这本质上是运算符重载,重载运算符本质上是函数,其名字由operator关键字后接表示要定义的运算符的符号。因此,赋值运算符就是一个名为operator=的函数。类似于其他任何函数,运算符函数也有一个返回类型和一个参数列表。
重载运算符的参数表示运算符的运算对象。赋值运算符,必须定义为成员函数。如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数。右侧参数作为参数传递。为了与内置类型赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用。
与处理拷贝构造函数一样,如果一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝运算符。就是将右侧运算对象的每个非static成员赋值于左侧运算对象的对应成员,这一功作主要是通过成员类型的拷贝赋值运算符来完成的。
析构函数
形式:
class Foo{
public:
~Foo(); //析构函数
};
由于析构函数不接受参数,因此它不能被重载。对于一个给定的类,只会有唯一一个析构函数。
如同构造函数有一个初始化部分和一个函数体,析构函数也有一个函数体和一个析构部分。在一个构造函数中,成员初始化是在函数体执行之前完成的,且按照他们在类中出现的顺序进行初始化。在一个析构函数中,首先执行函数体,然后销毁成员。成员按照初始化顺序的逆序销毁。
在对象最后一次使用之后,析构函数的函数体可执行类设计者希望执行的任何收尾工作。通常析构函数释放对象在生存期分配的所有资源。
在一个析构函数中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的。成员销毁时发生什么完全依赖于成员的类型。销毁类类型的成员需要执行成员自己的析构函数。内置类型没有析构函数,因此销毁内置类型成员什么也不需要做。
隐式销毁一个内置指针类型的成员不会delete它所指向的对象。
与普通指针不同,智能指针是类类型,所以具有析构函数。因此,与普通指针不同,智能指针成员在析构阶段会被自动销毁。
何时会调用析构函数?
无论何时一个对象被销毁,就会自动调用其析构函数。
变量在离开作用域时被销毁。
当一个对象被销毁时,其成员被销毁。
容器被销毁时,其元素被销毁
对于动态分配的对象,当其指向它的指针应用delete运算符时被销毁。
对于临时对象,当创建它的完整表达式结束时被销毁。
析构函数自动运行,我们的程序可以按需求分配资源,无需担心何时释放这些资源。
当一个类未定义自己的析构函数时,编译器会为它定义一个合成的析构函数。合成的析构函数体为空。如下:
class Sales_data{
public:
~Sales_data(){}
};
在析构函数执行完毕后,成员会被自动销毁。特别的,string的析构函数会被调用,它将释放bookNo成员所用的内存。
认识到析构函数体本身并不直接销毁成员是非常重要的。成员是在析构函数体之后隐含的析构阶段中被销毁的。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。
移动构造函数
形式:
class StrVec{
public:
StrVec(StrVec&& s) noexcept:elements(s.elements),first_free(s.first_free),cap(s.cap)
{
s.elements=s.first_free=s.cap=nullptr;
}
};
类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用,不同于构造函数的是,这个引用参数在移动构造函数中是一个右值引用。与拷贝构造函数一样,任何额外的参数都必须有默认实参。
除了完成资源移动,移动构造函数还必须确保移动后源对象处于这样一种状态,销毁它是无害的.特别是,一旦资源完成移动,源对象必须不在指向被移动的资源,这些资源的所有权已经归属新创建的对象。
由于移动操作“窃取”资源,它并不分配资源。因此,移动操作不会抛出任何异常。用noexcept告知标准库我们的移动构造函数不会抛出异常,因此标准库减少了未处理抛出异常这种可能性而做的额外的工作。在一个构造函数中,noexcept出现在参数列表和初始化列表开始的冒号之间。声明与定义都必须指定noexcept。不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept。
移动赋值运算符
形式:
class StrVec{
public:
StrVec& operator=(StrVec&& rhs)noexcept{
if(this!=(&rhs)){
free() //释放已有元素
elements=rhs.elements; //从rhs接管资源
first_free=rhs.first_free;
cap=rhs.cap;
rhs.elements=rhs.first_free=rhs.cap=nullptr;
}
return *this;
}
};
我们检查this指针与rhs的地址是否相同。如果相同,右侧和左侧运算对象指向相同的对象,我们不需要做任何事情。否则,我们释放左侧运算对象所使用的内存,并接管给定对象的内存。与移动构造函数一样,我们将rhs中的指针职位nullptr。
我们费尽心机的检查自赋值情况可能有些奇怪。毕竟,移动赋值运算符需要右侧运算对象的一右值。我们进行检查的原因是次此右值可能是move调用的返回结果。与其他任何赋值运算符一样,关键点是我们不能在使用右侧运算对象的资源之前就释放左侧运算对象的资源(可能是相同资源)。
从一个对象移动数据并不会销毁此对象,但有时在移动操作完成后,源对象会被销毁。因此 ,当我们编写一个移动操作时,必须确保移动后源对象进入一个可析构状态。我们的StrVec的移动操作满足这一要求,就是通过将以后源对象的指针成员设为nullptr实现的。
除了将移动后源对象置为析构安全的状态后,移动操作还必须保证对象仍然是有效的。也就是说可以安全的为其赋予新值,或可以安全的使用而不依赖于当前的值。另一方面,移动操作后对源对象留下的值没有任何要求。因此我们的程序不应依赖于移后源对象中的值。
如果一个类定义了自己的拷贝构造函数,拷贝赋值运算符或析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。只有当一个类没有定义任何版本的拷贝控制成员,且类的每个非static成员都可移动时,编译器才会为它合成移动构造函数和移动赋值运算符。编译器可以移动内置类型的成员,如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员。
需要析构函数的类也需要拷贝和赋值操作
需要拷贝操作的类也需要赋值操作,反之亦然
使用=default
将拷贝控制成员定义为=default,来现实的要求编译器生成合成的版本。
class Foo{
public:
Foo()=default;
Foo(const Foo& )=default;
Foo& operator=(const Foo& );
~Foo()=default;
};
Foo& Foo::operator=(const Foo& )=default;
当我们在类内用=default修饰成员的声明时,合成的函数隐式的声明为内联的。如果我们不希望合成的构造函数是内联的,应该只对成员的类外定义使用=default.就像对拷贝赋值运算符所做的那样。
我们只能对具有合成版本的成员函数使用=default(即,默认构造函数或拷贝控制成员)。
阻止拷贝
为了阻止拷贝,看起来可能应该不定义拷贝控制成员,但是,这种策略是无效的,如果我们的类为定义这些操作,编译器会为他们合成新的版本。
我们可以将拷贝构造函数和赋值运算符定义为删除的函数来阻止拷贝,删除的函数看起来是这样一种函数:我们虽然声明了他们,但我们不能使用他们。在函数的参数列表后面加上=delete来指出我们希望它定义为删除的。
class NoCopy{
public:
NoCopy()=default; //使用合成的默认构造函数
NoCopy(const NoCopy& )=delete; //阻止拷贝
NoCopy& operator=(const NoCopy& )=delete; //阻止拷贝
~NoCopy()=delete; //使用合成的默认构造函数
};
与=default不同。=delete必须出现在函数第一次声明的时候。我们可以对任何函数指定=delete(我们只能对编译器可以合成的默认构造函数或拷贝控制成员使用=default).析构函数不能是删除的成员。如果析构函数被删除,就无法销毁此类型的对象了,对于一个删除了析构函数的类型,编译器将不允许定义该类型的变量或创建该类型的临时对象,没有定义创建就可以。对于析构函数已经删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针。
在继承中基类的析构函数是虚函数,派生类继承的是虚属性,而不是析构函数。