我们需要特别注意
当我们产生一个赋值的动作的时候
它事实上意味着
把一个对象拷贝给另外一个对象
当我们定义一个类的时候
我们事实上经常要为这个类
提供一个拷贝的构造函数
能够通过另外一个对象
来构造我们当前这个对象
这叫类的拷贝构造
赋值本身呢 因为涉及到将另外一个对象
也赋值给本对象
所以我们说赋值也是构造
理论上拷贝、赋值、析构
这三个实际上是三位一体的
拷贝动作和赋值动作和析构动作
一般应该同时出现
就是说 如果你定义了一个类
如果你提供了一个拷贝构造函数
提供了一个赋值构造函数
也就说我们的赋值操作符重载
你可以称之为赋值构造
那么理论上你应该同时提供析构函数
这三个往往是要么都出现
要么都不需要出现
缺省情况下边 如果你没有提供
编译器会给你自动生成拷贝构造函数
替你自动生成一个赋值操作符
自动替你生成一个析构函数
这些都是缺省的行为
缺省的行为其实就是什么也不做
就是把那个位序列简单地写过来
这个拷贝动作我们称它为浅拷贝
层次很浅 所以叫浅拷贝
如果这个对象上边没有指针成员
那么正常情况下边
缺省的拷贝构造和赋值构造
都能够完成我们的任务
如果你这个对象里边
它有一个指针成员
并且涉及到动态内存分配的问题
那么你就需要特别注意了
这个时候 你往往需要提供
自己的拷贝构造和赋值构造
如果你没有提供 那么你就要注意
这个时候它的拷贝构造也好
赋值构造也好
它是一个浅拷贝的机制
在很多时候你的程序
可能达不到你的要求
或者你的程序安全性会出大问题
所以这个时候
往往需要重载拷贝构造函数
和赋值构造函数
也就是我们的赋值操作符
必须做这个事
同时你必须提供相对应的析构函数
我们来看一个例子 我有一个类A
class A我里面定义了两个字段
一个字段是int_n 一个字段是int*_p
什么意思呢 它其实是一个动态数组
_p将会指向那个数组的0号元
也就是它里面存的是那个数组的基地址
它会分配一段连续的存储区
存储一系列的整数
_n就表示这个数组的元素个数
明确了吧 就这两个字段
都是private的：_n、_p
class A数据成员就这么定义
因为涉及到指针
并且涉及到_p所指向的目标存储区的
动态存储分配的问题
所以当我们在定义这个类的时候
必须定义自己的拷贝构造函数
必须重载赋值操作符
必须提供析构函数
一个无参数的缺省构造 简单
_n(0),_p(NULL)
0、NULL 传进去 没有元素
指针——空指针 简单吧
好 这是缺省构造 单参数构造
如果你只传n值 也就是元素的个数
那么它就把n值给_n
然后呢 它就new出来这一段存储空间
包含n个元素的存储空间
然后把这个存储空间的基地址传给_p
也就是给你创建一个
能够包含n个元素的一个整数数组之后
把这个数组的基地址传给_p
这个数组被你构造出来了
但是里边什么元素都没有
就这个意思 明确吧
单参数的版本 前面写了explicit
表示这是一个单参数的构造函数
你必须显式地使用这个构造函数
构造这个动态数组
比如讲把这个数组赋值为n
把一个整数赋值给它
你不可以做这个事情
因为它不能够直接赋值嘛 对吧
一个是class A的一个对象
右边是一个整数
是不可以直接赋值的嘛
所以那个时候编译器就会自动地
尝试着把那个整数n
转换成class A的一个对象 然后再赋值
这个转换就涉及到把单参数的n
转换成class A的一个对象
就涉及到要调用class A的
单参数的构造函数A::A( int n )
才能完成转换
我写了一个explicit
就告诉你 除非我自己写a(n)
否则你不要主动把n转换成a(n)
不要做这个事
explicit这个关键字就这个意思
看看我们提供的两个拷贝构造函数
和我们的赋值操作符：A( const A & that )
初始化列表里面写_n(that._n), _p(that._p)
A & operator=( const A & that )
_n=that._n, _p=that._p; return *this;
我们就这么写 这就是我们标准的实现
这个实现策略就是浅拷贝
就是说 这俩函数如果你不写
编译器就会给你自动生成
生成的就是位序列
一个接着一个地拷贝过来
它给你做的事就这么多
其它所有的额外工作它一点都不做
而这个事就叫浅拷贝
你不写 编译器给你做的事就这样
所以你写了也一样
浅拷贝 有的时候它就会导致问题
我们看后边它的实现
这是下标操作符的重载
定义对象a 构造（4）
4个元素 b对象缺省构造
一个元素没有
for循环 我把那个a数组里面
数据给它写进去
把那4个元素都写进去
1、2、3、4给存进去 然后我输出
看看它在对象赋值之前
它的数据都是什么 对吧
看a类数据 然后我把a赋值给b
这样一个赋值将会调用
我们重载的那个赋值操作符
把a的数据一个接着一个地赋值过去
能做到这一点吗 做不到
它只能把a的数据成员
一个接着一个地赋值给b
形成这样一个的赋值动作
然后你看b的数据对象
你也可以输出 看它的结果
显示倒一点的问题都没有
然后我们return 程序就结束了
然后一编译 这程序一点问题都没有
一运行 数据显示一点问题都没有
然后程序一结束 系统就崩溃了
它就在整个程序结束前 系统崩溃
这就是我们这个程序最终所导致的结果
这里面就涉及到浅拷贝中
最重要的一个问题
一旦你的数据成员中含有一个指针
当你采用一个浅拷贝的时候
就需要特别注意
我们来看这个图
一开始 我有一个对象a
在_n那个字段里面写着整数4
_p呢 将会指向一个连续分配的存储区
这个存储区呢 包含4个整数
里面当然是1、2、3、4
被我们赋值进去的
这个就是原始对象的全部的存储空间
它的布局就这个样子
你要特别注意的就是
因为这个对象a
它事实上是main函数里面
定义的一个局部变量
所以它是在栈上进行分配的
而_p（就是a的那个成员）
它所指向的那个存储区域
是我们new出来的
_p所指向的存储空间是在堆上分配的
它不在栈上 注意我们这个存储布局
它是按照这个模式
当我完成一个
把a赋值给b这个动作的时候
b本身当然也是在栈上分配的
那么a的_n就给b的_n 它就给4
a的_p就给b的_p
_p也会保存动态分配的
那片存储区的基地址
所以b的_p这个指针
也将指向那片存储区
现在你注意 这里面就意味着
我们有两个对象的两个字段
它们的两个_p字段
都指向一个分配的存储区
这个存储区是被我们动态分配的
注意这个概念
两个指针 指向同一个目标数据对象
当main函数结束的时候
局部变量将要被销毁
它将尝试着去销毁a这个对象
OK 没有问题
在析构函数中会调用delete[]
销毁这段目标存储区域
然后把它设为NULL
这片存储区也释放 做完了这事
然后它尝试着销毁 b对象
它发现这个指针不是NULL
将会尝试着去delete[]它
销毁它所指向的目标存储区
完蛋了 这是一个空悬指针
两个指针指向同一个目标数据对象
当你在a对象上面
已经销毁这个目标数据对象之后
那么b 它的_p指针
所指向的这个目标数据对象
还需要销毁吗 不需要
这个指针已经空悬了 特别注意这一点
所以当你尝试着在b对象上面
去进行析构的时候 系统崩溃
delete[] 不成功嘛 系统就崩溃了
这个就是浅拷贝的语义
给我们导致的问题
那这个问题该怎么解决呢
这个时候我们就需要使用深拷贝
事实上我们提供深拷贝
不仅仅是为了解决这个空悬指针的问题
而是在很多时候
当我们构造一个额外对象的时候
我希望赋值的操作
我事实上是想产生一个
原始对象完整副本的一个动作
原始对象是a对象
有_n、_p，有这个数组
当我想产生一个副本的时候
我不仅希望这个b对象中的数据
和a对象的数据完全一模一样
事实上我们还要完成_p
所指向的那个目标数组的一个副本
我也要把它做一份出来
缺省的拷贝行为
它只能拷贝_n、_p这两个字段的值
它不能够拷贝某一个特定的字段
如果它是个指针的话
它所指向的那个目标数据区的那个副本
那个副本是做不出来的
所以它是浅拷贝嘛
它层次只到这一层 它没到这一层
我们现在要实现自己的深拷贝的话
那么就需要自己在实现拷贝构造函数
和我们的赋值操作符的时候
当这个数据字段是指针的时候
我们要重新地构造一个
它指针所指向的目标数据对象的一个副本
然后让这两个_p指向不同的地方
这个就叫深拷贝
所以我们实现这段代码的时候
就不一样了
A( const A & that )
A & operator=( const A & that )
这就是我们的深拷贝你看
函数原型倒跟我们刚才的是一模一样
但是实现肯定是不一样了 拷贝构造
_n传进来 但是_p
我们不传 我们new int[_n]
重新构造一片相同大小的存储区
然后一个元素、一个元素地拷贝过来
_n是拷贝的 _p是不拷贝的
是重新构造一片存储区
然后一个元素、一个元素地拷贝
这样才能完成深拷贝的这个动作
operator= 重载的时候也一样
_n给_n，_p呢 不直接拷贝
我们必须构造一个存储区
一个元素、一个元素地拷贝
然后return*this
这是operator=的实现
注意了 赋值操作符的实现
和拷贝构造函数的实现
不仅仅体现在我们这个return这一方的变化
因为拷贝构造函数是没有返回值的
赋值操作符的重载
它实际上是有一个返回值的
不仅仅体现在这一点
它还包括了一个最重要的一点
当我进行赋值操作的时候
事实上意味着我们的本对象已经存在了
那么它的_p就有可能
指向一个有意义的数组
而这个数组在我们赋值操作之后
它就没有意义了
我们必须构造一个跟that那个对象
一模一样的那个数组才成 对吧
所以在真正的替this->_p的字段
去做这个new int[_n]之前
你必须销毁_p所指向的那个目标数组：
if(_p) delete[]_p
要完成这个销毁动作
然后才能重新地
对它进行创建连续的存储区
一个元素、一个元素地拷贝
这个是和拷贝构造函数的最大的不同
这是我们的深拷贝