拷贝控制操作:拷贝构造函数 拷贝赋值运算符 移动构造函数 移动赋值运算符 析构函数
如果一个类没有定义所有这些拷贝控制成员,编译器会自动为它定义缺失的操作。
一、拷贝构造函数
定义:一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值(拷贝构造函数第一个参数必须是引用类型,而且一般是const引用)
原因:拷贝构造函数被用来初始化非引用类型参数,如果其参数不是引用类型,为了调用拷贝构造函数,我们必须拷贝它的实参,无限循环。
使用拷贝初始化的场合:
1.将一个对象作为实参传递给一个非引用类型的形参(对象以值传递的方式传入函数参数)
2.从一个返回类型为非引用类型的函数返回一个对象(对象以指传递的方式从函数返回)
3.对象需要另外一个对象初始化
如果两个对象已经存在,就会使用拷贝赋值运算符,如果是使用一个对象去创建(注意是创建)另一个不存在的对象,则会使用拷贝构造函数。
拷贝运算(对象在声明的同时马上进行初始化操作):class1 A("af"); class1 B=A; 赋值运算:class1 A("af"); class1 B;B=A;
合成的拷贝构造函数会将其参数成员逐个拷贝到正在创建的对象中,编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中(浅拷贝)。(因为static成员属于类,不属于对象)
class A{ public: A(){ sn++; } ~A(){ sn--; } static int GetValue(){ return sn; } private: static int sn; }; int A::sn = 0; int main(void) { A a1;//调用构造函数,sn++ cout << "1:" << A::GetValue() << endl; A a2(a1);//调用合成拷贝构造函数,对sn无操作,因为sn不属于对象 cout << "2:" << A::GetValue() << endl; system("pause"); return 0; }
输出均为1,注意析构的时候sn会减两次,变为-1
二、拷贝赋值运算符
如果赋值操作符可以作为全局函数重载的话,可能会出现表达错误的语句
如
int operator=(int a, integer b);
这样重载之后,语句
2 = a; 表述也是正确的,但是却是明显的语法错误
为了避免此类错误,需要将赋值操作符重载为成员函数
--------------------------------------------------
首先要知道,如果类中没有重载赋值操作符时,类会自动生成一个默认的赋值操作符。例如,有两个同类对象A和B,当你没有将赋值操作符重载,而进行 A=B 的操作时,编译器会自动调用赋值操作将B的数据成员拷贝到A中。
而如果你重载了一个全局的赋值操作符,那么编译器不知道是否还需要再自己合成一个赋值操作符,从而引发歧义。
重载运算符的参数表示运算符的运算对象,某些运算符,包括赋值运算符,必须定义为成员函数,如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数。
拷贝赋值运算符接受一个与其所在类相同类型的参数(引用):
class foo { public: foo& operator=(const foo&); //... };
为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用(return *this),目的是进行链式赋值(a=b=c).
class A { public: A(){} A(int id, char *t_name) { _id = id; name = new char[strlen(t_name) + 1]; strcpy(name, t_name); } //重载copy构造函数,深拷贝 A::A(A &a) { _id = a._id; name = new char[strlen(a.name) + 1]; if (name != NULL) strcpy(name, a.name); } //如果直接返回对象,而不是引用,则会调用拷贝构造函数 A& operator =(A& a) { if (this == &a)// 问:什么需要判断这个条件?(不是必须,只是优化而已)。答案:提示:考虑a=a这样的操作。 return *this; //首先释放自己的内存,避免内存泄漏 if (name != NULL) delete name; this->_id = a._id; int len = strlen(a.name); name = new char[len + 1]; strcpy(name, a.name); return *this; } ~A() { cout << "~destructor" << endl; delete name; } int _id; char *name; };
三、析构函数
由于析构函数不接受参数,因此它不能被重载,对一个给定类,只会有唯一一个析构函数。
析构函数完成的工作:在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化,在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。
四、三五法则
1.需要析构函数的类也需要拷贝和赋值操作
合成析构函数不会delete一个指针数据成员,故在构造函数中分配动态内存后需要定义一个析构函数来释放内存。合成的拷贝和赋值操作只是简单的拷贝指针成员,这样可能会出现多个对象拥有同一个指针,当它们销毁时同一个指针会被delete多次,出现未定义错误。
2.需要拷贝操作的类也需要赋值操作,反之亦然。
五、阻止拷贝
1.定义删除的函数
在新标准下,我们可以通过将拷贝构造函数和拷贝赋值运算符定义为delete function来阻止拷贝。delete function是指我们虽然声明了它们,但不能以任何方式使用它们。
=delete必须出现在函数第一次声明的时候,因为编译器需要知道一个函数是删除的,以便禁止试图使用它的操作。
注意:我们不能删除析构函数,对于一个删除了析构函数的类型,编译器将不允许定义该类型的变量或创建该类型的临时对象,但可以动态分配这种类型的对象,不能释放这些对象。
struct NoDtor { NoDtor() = default;//使用合成默认构造函数 ~NoDtor() = delete; }; NoDtor nd;//错误 NoDtor *p = new NoDtor();//动态分配,准确 delete p;//错误,编译阶段就出错
2.private拷贝控制
在新标准发布之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为private的来阻止拷贝,为了阻止友元和成员函数进行拷贝,我们将这些拷贝控制成员声明为private的,但是不定义它们。(正常情况下,友元可以访问private成员)
声明但不定义一个成员函数是合法的,试图访问一个未定义的成员将导致一个链接错误。试图拷贝对象的用户代码将在编译阶段被标记为错误(因为不能访问private成员),成员函数或友元函数中的拷贝操作将会导致链接时错误。