C++类拷贝控制 深拷贝 浅拷贝
普通类型对象之间的复制很简单,而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量,这篇文章将帮你理清c++类对象的拷贝方式
拷贝构造函数,拷贝赋值运算符
首先我们简单了解下默认的拷贝构造函数和拷贝赋值运算符
拷贝构造函数
第一个参数是自身类类型引用,其他参数都有默认值的构造函数就是拷贝构造函数
class sales_data { public: sales_data(); //默认构造函数 sales_data(const foo&); //默认拷贝构造函数 //... };
拷贝构造函数用来初始化非引用类类型参数,所以拷贝构造函数自己的参数必须是引用类型(如果不是引用:为了调用拷贝构造函数,必须拷贝它的实参,为了拷贝实参,又需要调用拷贝构造函数,无限循环)
合成拷贝构造函数(默认)
和默认构造函数一样,编译器会帮你定义一个默认拷贝构造函数(如果你没有手动定义的话),不同的是,如果你定义了其他构造函数,编译器还是会给你合成一个拷贝构造函数
举个例子:sales_data的合成拷贝构造函数等价于
class sales_data { public: sales_data(); sales_data(const sales_data&); private: std::string bookno; int units_sold = 0; double revenue = 0.0; }; sales_data::sales_data(const sales_data& origin) : bookno(origin.bookno), //使用string的拷贝构造函数 units_sold(origin.units_sold), //拷贝 revenue(origin.revenue) { //拷贝 //空函数体 }
直接初始化,拷贝初始化
通过以下几行代码不难理解
string dots(10,'.'); //直接初始化 string s(dots); //直接初始化 string s2 = dots; //拷贝初始化 string null_book = "9-999-9999-9" //拷贝初始化 string nines = strings(100,'9'); //拷贝初始化
使用直接初始化时,我们是在要求编译器使用普通的函数匹配,来选择与我们提供的参数最匹配的构造函数
使用拷贝初始化时,我们要求编译器将右侧运算符对象拷贝到正在创建的对象中(需要的话还进行类型转换
拷贝赋值运算符
赋值运算符本质也是函数,它由operator关键字后面接要定义的运算符的符号组成,赋值运算符就是一个名为operator=的函数,和其他函数一样,它也有一个返回类型和一个参数列表
参数表示运算符的运算对象,某些运算符(包括赋值运算符)必须定义为成员函数,如果一个运算符是成员函数,则其左侧运算对象就能绑定到隐式的上,对于一个二元运算符(例如赋值运算符),右侧运算对象就会作为显示参数传递
拷贝赋值运算符接受一个与其所在类相同类型的参数
class sales_data { public: sales_data& operator=(const sales_data&); };
为了与内置类型的赋值保持一直,赋值运算符通常返回一个指向其左侧运算对象的引用
合成拷贝赋值运算符(默认)
和拷贝构造函数一样,如果一个类未定义自己的拷贝赋值运算符,编译器会生成一个合成拷贝赋值运算符,类似拷贝构造函数,对于某些类,合成拷贝赋值运算符用来禁止该类型对象的赋值
拷贝赋值运算符会将右侧运算对象的每个非static
成员赋予左侧运算对象的对应成员,对于数组类型的成员,逐个赋值数组元素合成拷贝赋值运算符返回一个指向其左侧运算对象的引用
sales_data& sales_data::operator=(const sales_data& rhs) { bookno = rhs.bookno; units_sold = rhs.units_sold; revenue = rhs.revenue; return *this; }
浅拷贝
回头看看我们最初的sales_data类
class sales_data { public: sales_data(); sales_data(const sales_data&); private: std::string bookno; int units_sold = 0; double revenue = 0.0; };
以下这样的初始化看似没有什么问题
int main() { sales_data data1; sales_data data2 = data1; }
下面给出一个和sales_data不太一样的array类
class array { public: array() //构造函数 { m_icount = 5; m_parr = new int[m_icount]; } array(const array& rhs) //拷贝构造函数(相当于默认拷贝构造函数) { m_icount = rhs.m_icount; m_parr = rhs.m_parr; } private: int m_icount; int* m_parr; };
(这里的拷贝构造函数其实相当于编译器合成的默认拷贝构造函数)
我们用同样的方式初始化的时候:
int main() { array array1; array array2 = array1; }
默认拷贝构造函数可以完成对象的数据成员简单的复制,但是由于我们这里有一个指针类型的成员变量m_parr
,直接使用默认拷贝就会出现一个问题,两个对象的m_parr指针指向了同一块区域
当对象arr2通过对象arr1初始化,对象arr1已经申请了内存,那么对象arr2就会指向对象arr1所申请的内存,如果对象arr1释放掉内存,那么对象a中的指针就是野指针了,这就是浅拷贝
深拷贝
为了避免这样的内存泄露,拥有指针成员的对象进行拷贝的时候,需要自己定义拷贝构造函数,使拷贝后的对象指针成员拥有自己的内存地址
class array { public: array() { m_icount = 5; m_parr = new int[m_icount]; } array(const array& rhs) { m_icount = rhs.m_icount; m_parr = new int[m_icount]; for (int i = 0; i < m_icount; i++) { m_parr[i] = rhs.m_parr[i]; } } private: int m_icount; int* m_parr; };
对比一下
- 浅拷贝也叫位拷贝,拷贝的是地址
- 深拷贝也叫值拷贝,拷贝的是内容
深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝