目录
(C++11)使用=default来显式要求编译器生成合成的成员函数版本:
(c++11)=delete(可对任意成员函数使用此关键字)阻止拷贝:
移动构造函数和移动赋值运算符(c++11 noexcept ):
注意(一定要区分初始化和赋值之间的差别,构造函数基本是针对于初始化(直接初始化和拷贝初始化),而像=运算符用作赋值的,即两个已有的元素赋值)
C++11特性(explicit关键字)
C++内置类型具有自动转换的规则,而类也具有相对应的隐转换规则。其是针对于类中构造函数只接受一种实参类型的构造函数(为其制定类一套从实参类型向向类类型隐式转换的规则)。
例如sales_date类中,定义了构造函数sales_date(string x):s(x);(假设sales_data只有一个数据成员string 时)建立了string向sales_data转换的方式,
函数要求实参类型为sales_data类型时也同样适用。例如combine(sales_date d),同样可以将string类型作为实参类型传递。但注意C++只会自动执行一步的类型转换,而如果像上述一样以combine(“9999999”)来传递的话,会失败。因为进行了两次的自动类型转换(将“9999999”转换成string类型,又将string转换成sales_data类型,而c++只允许一次的类型转换,所以报错。所以我们可以显式的将其中一步类型显式转换,例如combine(string("9999999");(显式转换成string,隐式转换成sales_data),又或者是combine(sales_data("9999999"));(隐式转换成string,显式转换成sales_data),这样生成的sales_data类型是临时量,若函数结束了就会销毁。
注意用拷贝初始化时例如sales_data data = "9999999";是不允许的,其中“9999999”会自动隐式转换成string类型的,又会自动进行了隐式转换将string类型转换成sales_data类型,转换两次,故不合法。而sales_data data=string("9999999");是合法的,只进行一次隐式转换,将string转换成slaes_data数据类型。
而如果我们要抑制这种情况的话,就要使用c++11关键字explicit关键字加以抑制,阻止这种隐式类型的转换。例如:
class Sales_data{
public:
explicit Sales_data(string x):s(x){}
private:
string s;
}
这样就不会有隐式类型转换。并且在我们以explicit关键字定义构造函数时,就只能用作直接初始化形式的使用,不能用作拷贝初始化形式的使用。程序不会在自动转换中使用此构造函数。只能直接用Sales_data x(string("9999")) 来使用.
拷贝控制
类中有五种特殊的函数来控制类对象的拷贝,移动,赋值和销毁操作,包括:拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符,析构函数。还有构造函数来定义创建对象时做什么。
拷贝构造函数:
要求:拷贝构造函数的第一个参数一定要是本身类类型的的引用。(后面会解释,不然会无限循环)
class Sales_data{
Sales_data(const Sales_data &);
}
而拷贝构造函数基本上都是用在隐式转使用上的,故基本不用explicit加以阻止隐式隐式。
用法:Sales_data j = x(为sales_data类型实例);(注意只有在创建对象时既才是用的拷贝构造函数,如果是两个已经存在的类型相互赋值则是拷贝赋值运算符)
j=x(此时是用拷贝赋值运算符)!!!
合成拷贝构造函数:既为程序默认为我们创建的拷贝构造函数(无论是否自己创建了拷贝构造函数)Ps.对于某些类来说,合成拷贝构造函数会阻止我们拷贝该类类型的对象。)拷贝规则是:拷贝该类实例中各种类型,内置数据类型直接拷贝,类类型是用其自身的拷贝构造函数进行拷贝,数组类型对其每个元素进行拷贝(如果元素类型是类,依次调用其拷贝构造函数进行拷贝。)
直接初始化和拷贝初始化关系:
直接初始化相当于进行的是函数匹配,看哪个构造函数最符合参数要求,而拷贝初始化是将右侧对象拷贝(不一定是用拷贝构造函数,有时也是移动构造函数)到正在创建的对象中,如果需要还会进行类型转换。(像之前没被explicit限制的实参类型参数转换类类型的构造函数)。
拷贝初始化不仅发生在=时,还会发生在:
- 将一个对象作为实参传递给一个非引用类型的形参(拷贝构造函数第一个实参类型也是同理,如果是非引用类型,在传递时进行拷贝,而这时又需要拷贝构造函数进行拷贝,故会无限循环)
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号{}初始一个数组中都元素或一个聚合类的成员
拷贝赋值运算符(也具有合成拷贝赋值运算符):
运算符实际是一种函数,有operator后接要定义的运算符符号。(注意内置类型的运算符不能重载,主要是给复杂的数据类型提供像内置运算符一样的基本运算。)
而赋值运算符的参数表示运算符的运算对象,而要在类中重载运算符,要将其定义为成员函数,因为这样运算符左侧对象会被绑定到隐式的this指针上,(即调用其运算符本身的实例对象),而右侧运算符就为显式参数传递。
如:
class Foo{
public:
Foo& operator=(const Foo&);//赋值运算符
};
而内置数据类型一样,都会返回其本身,故返回值返回的是左侧对象的引用类型。而参数类型也是为const &类型的,因为基本运算右侧类型的数据不会发生改变,只是调用。
合成拷贝赋值运算符:
对于某些类,合成拷贝构造函数用来禁止该类型对象的赋值。除此目的之外,会将右侧对象每个非static进行拷贝。
合成拷贝赋值运算符等效于
Sales_data & Salews_Data::operator=(const Sales_Data & rhs)
{
bookNo = rhs.bookNo;
units_sold = rhs.units_sold;
revenue = rhs.revenue;
return *this;
}
析构函数
格式:
class s{
~s();
}
不接受参数,所以不能重载。无初始化列表,为隐式销毁。(注意当指向一个对象的引用或者是指针离开作用域时,析构函数不会执行)不会主动delete掉new创建的对象。
而成员的初始化是在函数体执行之前完成的,且按照在类中出现的顺序来进行初始化。而在一个析构函数中,首先会执行函数体,其次会销毁成员,按照初始化逆序来销毁。
合成析构函数:会自动为未创建析构函数的类添加合成析构函数。有时会被用来阻止该类型的对象被销毁。
析构函数体本身不直接销毁成员,是在析构函数体之后的析构阶段被销毁。
三五法则:
控制类的拷贝操作:拷贝构造函数,拷贝赋值运算符和析构函数(新标准下定义移动构造函数和移动赋值运算符)可以只定义其中的一个或两个操作,不需要全部定义。
需要析构函数时也需要拷贝和赋值操作。因为在需要自定义析构函数时,基本是处理new出来的成员变量,而此时如果直接使用合成的拷贝构造函数和拷贝赋值运算符,会造成访问无效内存。例如
sales_data
{
public:
sales()
{
s = new string;
}
string *s;
}
void x(sales_data a)
{
sales_data r = a;
}
而在此时r超出作用域时会调用析构函数,而此时拷贝构造函数是直接赋予new出来的指针,此时两个指针指向同一个new对象,而此时r析构函数会销毁掉new对象,而导致a接下来对于此成员变量的访问都是访问无效内存。
需要自定义拷贝构造函数时也需要拷贝赋值运算符,反之亦然,不意味着需要析构函数:
如果需要自己为每个类定义一个独一无二的数据,不仅要在拷贝构造函数中定义,还要定义拷贝赋值运算符,避免在赋值时将其独一无二的数据赋值过去。
(C++11)使用=default来显式要求编译器生成合成的成员函数版本:
class Sales_data{
public:
Sales_data()=default;
Sales_data(const Sales_data&) = default;
~Sales_data()=default;
Sales_data& operator=(const Sales_data &);
}
Sales_data& Sales_data::operator=(const Sales_data&)=default;
如果需要编译器为我们生成合成版本的成员函数,就可以用=default来定义,可以在声明或者在定义函数时使用,如果在类内定义就是内联函数,像构造函数,拷贝构造函数,析构函数,而如果在类外定义就是非内联函数,如拷贝赋值运算符。
只能default用于具有合成版本的成员函数,即为构造函数,拷贝控制成员
(c++11)=delete(可对任意成员函数使用此关键字)阻止拷贝:
有些类不需要定义拷贝构造函数,或拷贝赋值运算符,例如iostream类阻止了拷贝,避免多个对象写入或读取相同的io缓冲。但我们不能不定义拷贝控制类成员,因为编译器会为我们编译合成版本。
在新版本中,可以将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝,其定义为:我们虽然声明了他,但不能以任何方式使用他。
struct N{
N()=default;
N(const N&) = delete;
N &operator= (const N&) = delete;
~N()=default;
}
而与default有差别,=delete只能在第一次声明时就进行=delete,因为default是生成合成版本的函数,只有在类实例化时才会调用,而delete是告诉编译器这个函数不能被在任何地方被使用。
虽然可以对任何成员函数使用,但是析构成员不能是删除的成员。如果定义了删除的析构函数,就无法销毁此类的对象。所以对于析构函数被删除的类,不允许定义该类型的变量或是临时对象。又或是如果一个类中有成员变量的析构函数被删除,那么这个类也不能定义该类的变量或临时对象。但是可以动态分配这种类型的对象,只是不能释放这些对象。
而在没有=delete前,阻止拷贝的方法是将拷贝构造函数和拷贝赋值运算符声明为private(但是不能将析构函数定义为private结果同上)这样普通的用户就不能访问他们,但其友元和成员函数还是可以访问他们,为了阻止其拷贝,可以在private中只是声明不定义他们。这是合法的,但是企图访问一个未定义的成员将导致一个链接错误,可以这样来阻止任何拷贝该类型对象的企图,因为会在编译阶段标记为错误。成员函数和友元函数中的拷贝操作会导致链接错误。
但是在新标准下还是优先使用=delete来阻止拷贝。
综上会为这些合成的成员函数定义为删除的函数:
如果类的某个成员的析构函数是删除的或不可访问的,那么其合成析构函数是删除的,同时其合成构造函数也是删除的
如果类的某个成员的拷贝构造函数是删除的或不可访问的
如果类中某个成员的拷贝赋值运算符是可删除或是不可访问的,或是类中有const的或引用成员(引用成员是因为在赋值时只是对其绑定的对象赋值,而不是改变他绑定的对象),则类的合成拷贝赋值运算符被定义为删除的
当不可能拷贝,赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。
拷贝控制和资源管理:
此类对象拷贝的意义通常由两种:
一是行为像值一样的类:
会拷贝给对方一个副本,而副本和原对象是完全独立的,互不影响。而其中的指针成员也只是拷贝其所绑定的对象的值,副本的改变不会影响到原来指针中的值。对于一般的数据成员,都是直接进行拷贝就行的,主要还是针对于指针的类型,而在拷贝构造函数中就是要用new 来接受目标对象指针中的值,而不是单纯的两个指针之间赋值。
class H{
public:
H(const string &s=string()):str(new string(s)),i(0){}
H(const H& p):str(new string(*p.str)),i(p.i){}
H& operator= (coonst H&);
~H(){delete str;}
private:
string* str;
int i;
};
H& H::operator=(const H& rhs)
{
auto newp = new string(*rhs.str);
delete str;
str = newp;
i = rhs.i;
return *this;
}
在此类中,构造函数是接受一个string类型的数据,将其作为new string的初始值来初始化string指针str。而拷贝构造函数也是将拷贝的对象指向的值作为初始值new新的string* 来接受数据,同时要delete掉原来的str值。而在拷贝赋值运算符中,首先一定要先将拷贝对象的值保存下来,在删除原来的str值,将其赋值。因为要确保任何情况下的接受H的对象都不出问题,包括其本身为拷贝对象接受时,如果直接delete旧的str的值,那么在拷贝新的(其实是其本身已经被删除的值)出现空悬指针的请况。一定要注意。
而一类是像指针一样行为的类:
这种类型下拷贝相当于共享数据,共同管理数据,只有在最后一个指向这个数据的对象被销毁时数据才会销毁。而这就需要定义一个计数器,来计数每个关联到同一个数据的类对象的数量,其类型不能定义为其中的类型数据成员,因为一是其其他拷贝对象对于其他同样被拷贝的对象信息更新不及时,二是如果是采用static的方式,会导致一个类只能同时使用一个相同的数据,而不能进行对不同数据的共同管理。此时就需要借助new来创建一个int类型的计数器,每个类成员可以绑定的不同的数据上,同时也会绑定到开始初始数据时的计数器指针上。因此要对类的拷贝控制成员进行处理,使其只在绑定的计数器为0时才销毁数据。
class hs{
public:
hs(const string &s = string()):str(new string(s)),i(0),use(new int){}
hs(const hs&h):str(h.str),i(h.i).use(p.use){++*use;}
hs& operator=(const hs&h)
{
++*h.use;//也是为了确保在复制对象是自身时的情况
if(--*use==0)
{ delete str;
delete use;
}
str= h.str;
i=h.i;
use=h.use;
return *this;
}
~hs()
{ if(--*use==0)
{
delete str;
delete use;
}
}
private:
string *str;
int i;
int *use;
}
交换操作:
一般在排序时会使用swap交换元素,而这个swap都是调用std::swap函数来执行的,但如果类不定义自己的swap函数,会默认使用std::swap函数,这样可能造成不必要的拷贝,例如直接将整个类对象进行拷贝赋值交换,而不是将其中指针类型交换地址,内置类型交换值的形式。
class l{
public:
friend void swap(l& l1,l&l2);
private:
string *str;
int i;
};
inline void l::swap(l&l1,l&l2)
{ using std::swap;//之后会将为什么using的声明没有隐藏掉l版本中的swap声明
swap(l1.str,l2.str);//这边最好不要写std::swap函数,因为在不加限定时,如果存在类型特定的swap 版本,会优先匹配
swap(l1.i,l2.i);
}
如果加上了swap的定义,那么在拷贝赋值运算符中就要使用他们,可以用作将左侧运算对象与右侧运算对象的副本进行交换
H& H::operator=(H r)//注意没加&
{
swap(*this,r);
return *this;
}
因为是类似赋值形式的类(而类似指针行为的类本身std的函数就是交换二者的地址,计数器不会改变,只是相互交换,不需要自定义swap函数),不是将其两者的地址交换,而是将其副本拷贝。即使拷贝函数被销毁了,其左侧也不会影响,也处理的自赋值的情况。
C++11特性(移动构造函数,右值引用):
右值和左值:
当一个对象被用作右值时,用的是对象的值,类似于赋值语句的右边,只是他值的拷贝,而当对象是左值时,用的是他的身份(在内存中的位置)在需要右值的地方可以用左值替代(而右值引用不行),当一个左值被当作右值使用时,实际使用的是他的内容。类似的场景:赋值运算符需要一个非常量左值作为其左侧对象,得到的结果也都是左值。 取地址符作用于左值,返回一个指向该对象的指针,这是右值。 解引用,下标运算符求值结果都是左值 内置类型和迭代器的前置递增递减运算符作用于左值对象,前置结果也是左值而后置是返回右值!!!!。对于decltype(c++11)返回的结果也不同,左值返回的是引用,就是一个正常的类型。(详情。。)
正常的左值引用时不能绑定在一些即将销毁或者是临时的变量上的(但是const的引用可以绑定到右值上),字面值常量,要求转换的表达式(i*42,i*42.1),函数返回是右值的表达式。而这种右值类型(字面值常量,函数返回是非引用类型的函数,算术,关系,位以及后置递增递减运算符生成右值)的就可以用右值引用来绑定,但不能绑定左值,注意:变量都是左值,相当于只有运算对象没有运算符的表达式,故右值引用对象也是左值,
正常引用是&,而右值引用是&&,int && z = i*42;(乘法结果是右值),绑定到将要销毁的,临时的对象上,即所引用的对象即将销毁,或该对象没有其他用户,意味着使用右值引用的代码可以自由接管所引用的对象的资源(窃取)。
标准库的move函数(头文件:utility):
右值引用虽然不能绑定在左值上,可以显示将一个左值转换成右值引用类型(将在16章泛型和模板中解释原理),用的是标准库的move函数。用法:
#include <utility>
int &&rr1 = 42;//字面值是右值
int &&rr2 = rr1;//错误,因为表达式rr1是左值。
int &&rr3 = std::move(rr1);
一旦显示转换后,这个左值只能用来销毁或者赋新值,而不能继续使用原来移后源的值。而使用std::move不用using声明,将在18章讲解。避免名名字冲突。
移动构造函数和移动赋值运算符(c++11 noexcept ):
而我们在进行一些拷贝工作时,例如将存储空间扩大,需要将之前存储空间内的数据在一一拷贝过去,然后销毁(符合右值的特征),十分麻烦,而借助移动构造函数和移动赋值运算符可以接管原来数据内存的所有权。
和拷贝构造函数一样,其第一个参数要是同类型的引用(右值引用),如果要有其他的参数要有默认参数。
StrVec(StrVec &&s)noexcept//移动操作不应抛出任何异常
:elements(s.elements),first(s.first),cap(s.cap)//成员初始化接管s中的资源
{
s.elements=s.first=s.cap=nullptr;
}
而移动构造函数时不不会申请新的内存空间,接管原来数据中内存的资源。所以是通常不会抛出异常的,但如果我们不主动声明,告知标准库不会抛出异常,不然可能会认为我们移动这些类对象会抛出异常,而为了处理这种可能性而做一些额外的工作。故使用C++11 关键字noexcept可以承诺一个函数不抛出异常,用法出现在参数列表和初始化列表开始的冒号之间。(而且必须在类头文件的声明和定义上都要指定noexcept.)。
而我们需要知道的是虽然移动构造函数不抛出异常,但是也是允许抛出异常的。标准库能对异常发生时自身的行为提供保障。像vector保证,如果我们调用push_back发生异常vector自身不会发生改变。而在vector执行push_back时可能会进行内存的重新分配,将元素从旧空间移动到新空间中去,而移动一个对象的值通常会改变它的值,像用移动构造函数移动了部分元素时如果抛出异常,就会有问题。旧空间中的移动源元素已经被改变了,而新空间还未构造的元素尚不存在。这样vector就不能保证在抛出异常时自己不变的要求。而如果使用的时拷贝构造函数,在拷贝过程发生异常,旧元素保持不变,而可以直接将新分配的空间释放并返回。vector原有的元素仍然存在不变。所以为了避免有这种问题,除非vector知道元素类型的移动构造函数不会抛出异常,否则在重新分配内存的过程中,就必须使用拷贝构造函数而不是移动构造函数。如果希望在vector重新分配内存这类情况下对我们自定义类型的对象进行移动而不是拷贝,就必须显式的告诉标准库我们的移动构造函数可以安全使用。通过noexcept来实现。
移动赋值运算符与移动构造函数类似,
S& S::operator=(S &&s)noexcept
{
//先检测自赋值
if(this!=&s)
{
free();
ele=s.ele;
first = s.first;
cap = s.cap;
s.ele=s.first=s.cap=nullptr;
}
return *this;
}
检查自赋值情况,虽然移动赋值运算符需要右侧运算对象的一个右值,但是有可能是由move调用返回的结果,与其他任何赋值运算符一样,关键点是我们不能使用右侧运算对象的资源之前就释放左侧运算对象的资源(可能是相同的资源)。
移后源对象必须是可析构的:
在执行移动的过程中,必须确保移后源对象进入可析构的状态,可通过将移后源对象置为nullptr来实现,置为析构安全的状态。还要确保对象仍然是有效的,即可以安全的赋予新值或者是安全的使用而不依赖移后源对象的数据。例如当我们从一个string或容器对象移动数据时,我们知道移后源对象仍然保持有效。可对其执行诸如empty或者size的操作。但是不知道会得到什么结果。
合成的移动操作:
与拷贝操作不同,编译器不会为某些类合成移动操作,特别是已经定义了自己的拷贝构造函数和拷贝赋值运算符或者析构函数。只有当一个类没有定义(任何自己版本(即只有合成版本的)的拷贝控制成员,且每个非static数据成员可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。编译器可以移动内置类型的成员。如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员。
struct X
{
int i;//内置类型可以移动
string s;//string定义了自己的移动操作
};
struct hasX{
X men;//X 有合成的移动操作
};
X x,x2=std::move(x);//使用合成的移动构造函数
hasX hx ,hx2=std::move(hx);//同上
而与拷贝操作不同,移动操作不会隐式的定义为删除的函数,只有当我们显示要求编译器生成=default移动操作时,且不能移动所有元素时,会为移动操作定义为删除的函数.
编译器将合成的移动构造函数(移动赋值运算符)定义为删除的规则:
1.有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值运算符类似
2.如果有类成员的移动构造函数或者移动赋值运算符被定义为删除或者不可访问,则移动构造函数被定义为删除。
3.如果析构函数被定义为删除或者不可访问的,则类的移动构造函数被定义为删除的
4.如果有类成员是const或者是引用,则移动赋值运算符被定义为删除的。(不包括移动构造函数)
struct h{
h()=default;
h(h&&) = default;
Y men;//Y为一个类,定义了自己的拷贝构造函数但未定义自己的移动构造函数
};//此时将有一个删除的移动构造函数
h hy,hy2= std::move(hy);//错误,移动构造函数时删除的
定义一个移动构造函数或移动赋值运算符的类也必须定义自己的拷贝操作,否则,这些成员默认被定义为删除的。
如果一个类有拷贝构造函数和移动构造函数,会用普通的函数匹配规则确定用哪一个构造函数。如果一个类没有移动构造函数,只有拷贝构造函数,即使是试图用move,编译器也不会合成移动构造函数,可以将F&&转换成const F&完成拷贝构造。区分移动和拷贝的重载函数通常有一个版本接受一个const&,一个接受T&&。
而当赋值运算符可以实现拷贝运算符和移动赋值运算符两种功能(但此时不能定义移动赋值运算符,因为二者的参数匹配优先相同:一个是直接&&匹配,一个是用移动构造函数传参,存在二义性)。
class H{
.....
H& operator= (H r)
{
swap(*this,r);
return *this;
}
}
//hp和hp2都是H的对象
hp = hp2;
hp = std::move(hp2);
因为参数类型非引用类型的,所以会根据实参来对应构造参数,根据实参的类型选择拷贝构造函数或者移动构造函数。
移动迭代器(c++ 11):
在支持拷贝的一些标准库中,可能没有移动版本的拷贝,像uninitialized_copy就只能拷贝,但我们可以用c++11新标准移动迭代器适配器,通过该变一个迭代器的解引用运算符来适配此迭代器,一般一个迭代器解引用返回指向一个元素的左值,而移动迭代器返回解引用返回是右值。通过标准库make_move_iterator函数将一个普通迭代器转换成一个移动迭代器。可以直接适配在要求普通迭代器上。
void S::reallocate()
{
auto last = uninitialized_copy(make_move_iterator(begin()),make_move_iterator(end()),first);
....
}
要小心使用移动操作,只有确保不会对移后源对象没有其他用户,移动操作时安全的情况下使用。
右值和左值引用成员函数(c++11):
通常情况下,没有办法阻止对右值的赋值,string s1="111",s2="www";auto n=(s1+s2).find('a);find函数是在S1+S2的时候调用的,此时是右值,s1+s2="wow";
都是向右值赋值。但我们可以在我们自己设计的类中添加不让右值赋值,即强制左侧对象(this)是左值。用到新标准的特性,在函数定义和声明的参数列表后放置一个引用限定符。
class Foo{
Foo& operator=(const Foo&)&;//只能向修改的左值赋值
//...
};
Foo& Foo::operator=(const Foo& rhs)&
{
//....
return *this
}
引用限定符可以是&或者&&,分别指出this可以指出一个左值或右值。类似const限定符,只能用于非static成员函数,而且必须出现在函数的声明和定义中。可以同时用const限定符和引用限定符,但引用限定符必须跟随在const限定符之后。Foo someMen()const &;
可以用const和引用限定符来重载函数,const有加和不加两个版本,而引用限定符则是要么全部重载函数都加上引用限定符,或者所有都不加。如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须使用限定符。