这里我们用类String 来介绍这两个函数:
拷贝构造函数是一种特殊构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用拷贝构造函数。为啥形参必须是对该类型的引用呢?试想一下,假如形参是该类的一个实例,由于是传值参数,我们把形参复制到实参会调用拷贝构造函数,如果允许拷贝构造函数传值,就会在拷贝构造函数内调用拷贝构造函数,从而形成无休止的递归调用导致栈溢出。
1 | string(const string &s); |
赋值函数,也是赋值操作符重载,因为赋值必须作为类成员,那么它的第一个操作数隐式绑定到 this 指针,也就是 this 绑定到指向左操作数的指针。因此,赋值操作符接受单个形参,且该形参是同一类类型的对象。右操作数一般作为const 引用传递。
1 | string& operator=(const string &s); |
拷贝构造函数和赋值函数并非每个对象都会使用,另外如果不主动编写的话,编译器将以“位拷贝”的方式自动生成缺省的函数。在类的设计当中,“位拷贝”是应当防止的。倘若类中含有指针变量,那么这两个缺省的函数就会发生错误。这就涉及到深复制和浅复制的问题了。
拷贝有两种:深拷贝,浅拷贝
当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。
但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。
深拷贝与浅拷贝的区别就在于\深拷贝会在堆内存中另外申请空间来储存数据**,从而也就解决了指针悬挂的问题。指向不同的内存空间,但内容是一样的
简而言之,\当数据成员中有指针时,必须要用深拷贝**。
1 | class A{ |
这里再总结一下深复制和浅复制的具体区别:
- 当拷贝对象状态中包含其他对象的引用时,如果需要复制的是引用对象指向的内容,而不是引用内存地址,则是深复制,否则是浅复制。
- 浅复制就是成员数据之间的赋值,当值拷贝时,两个对象就有共同的资源。而深拷贝是先将资源复制一份,是对象拥有不同的资源(内存区域),但资源内容(内存里面的数据)是相同的。
- 与浅复制不同,深复制在处理引用时,如果改变新对象内容将不会影响到原对象内容
- 与深复制不同,浅复制资源后释放资源时可能会产生资源归属不清楚的情况(含指针时,释放一方的资源,其实另一方的资源也随之释放了),从而导致程序运行出错
深复制和浅复制还有个区别就是执行的时候,浅复制是直接复制内存地址的,而深复制需要重新开辟同样大小的内存区域,然后复制整个资源。
好,有了前面的铺垫,下面开始讲讲拷贝构造函数和赋值函数,其实前面第一部分也已经介绍了许多
这里以string 类为例来进行说明
1 | class String |
类String 拷贝构造函数与普通构造函数的区别是:在函数入口处无需与 NULL 进行比较,这是因为“引用”不可能是NULL,而“指针”可以为NULL。(这是引用与指针的一个重要区别)。然后需要注意的就是深复制了。
相比而言,对于类String 的赋值函数则要复杂的多:
1、首先需要执行检查自赋值
这是防止自复制以及间接复制,如 b = a; c = b; a = c;之类,如果不进行自检的话,那么后面的 delete 将会进行自杀操作,后面随之的拷贝操作也会出错,所以这是关键的一步。还需要注意的是,自检是检查地址,而不是内容,内存地址是唯一的。必须是 if(this == &rhs)
2、释放原有的内存资源
必须要用 delete 释放掉原有的内存资源,如果此时不释放,该变量指向的内存地址将不再是原有内存地址,也就无法进行内存释放,造成内存泄露。
3、分配新的内存资源,并复制资源
这样变量指向的内存地址变了,但是里面的资源是一样的
4、返回本对象的引用
这样的目的是为了实现像 a = b = c; 这样的链式表达,注意返回的是 *this 。
但仔细一想,上面的程序没有考虑到异常安全性,我们在分配内存之前用delete 释放了原有实例的内存,如果后面new 出现内存不足抛出异常,那么之前delete 的 m_data 将是一个空指针,这样很容易引起程序崩溃,所以我们可以调换下顺序,即先 new 一个实例内存,成功后再用 delete 释放原有内存空间,最后用 m_data 赋值为new后的指针。
接下来说说拷贝构造函数和赋值函数之间的区别。
拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建是调用的,而赋值函数只能在已经存在了的对象调用。看下面代码:
1 | String a("hello"); |
上面说明出现“=”的地方未必调用的都是赋值函数(算术符重载函数),也有可能拷贝构造函数,那么什么时候是调用拷贝构造函数,什么时候是调用赋值函数你? 判断的标准其实很简单:如果临时变量是第一次出现,那么调用的只能是拷贝构造函数,反之如果变量已经存在,那么调用的就是赋值函数。