当前课程知识点:基于Linux的C++ >  第十讲 操作符重载 >  10.6 赋值操作符重载(二) >  LinuxCPP1006

返回《基于Linux的C++》慕课在线视频课程列表

LinuxCPP1006在线视频

LinuxCPP1006

下一节:LinuxCPP1007

返回《基于Linux的C++》慕课在线视频列表

LinuxCPP1006课程教案、知识点、字幕

我们需要特别注意

当我们产生一个赋值的动作的时候

它事实上意味着

把一个对象拷贝给另外一个对象

当我们定义一个类的时候

我们事实上经常要为这个类

提供一个拷贝的构造函数

能够通过另外一个对象

来构造我们当前这个对象

这叫类的拷贝构造

赋值本身呢 因为涉及到将另外一个对象

也赋值给本对象

所以我们说赋值也是构造

理论上拷贝、赋值、析构

这三个实际上是三位一体的

拷贝动作和赋值动作和析构动作

一般应该同时出现

就是说 如果你定义了一个类

如果你提供了一个拷贝构造函数

提供了一个赋值构造函数

也就说我们的赋值操作符重载

你可以称之为赋值构造

那么理论上你应该同时提供析构函数

这三个往往是要么都出现

要么都不需要出现

缺省情况下边 如果你没有提供

编译器会给你自动生成拷贝构造函数

替你自动生成一个赋值操作符

自动替你生成一个析构函数

这些都是缺省的行为

缺省的行为其实就是什么也不做

就是把那个位序列简单地写过来

这个拷贝动作我们称它为浅拷贝

层次很浅 所以叫浅拷贝

如果这个对象上边没有指针成员

那么正常情况下边

缺省的拷贝构造和赋值构造

都能够完成我们的任务

如果你这个对象里边

它有一个指针成员

并且涉及到动态内存分配的问题

那么你就需要特别注意了

这个时候 你往往需要提供

自己的拷贝构造和赋值构造

如果你没有提供 那么你就要注意

这个时候它的拷贝构造也好

赋值构造也好

它是一个浅拷贝的机制

在很多时候你的程序

可能达不到你的要求

或者你的程序安全性会出大问题

所以这个时候

往往需要重载拷贝构造函数

和赋值构造函数

也就是我们的赋值操作符

必须做这个事

同时你必须提供相对应的析构函数

我们来看一个例子 我有一个类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

要完成这个销毁动作

然后才能重新地

对它进行创建连续的存储区

一个元素、一个元素地拷贝

这个是和拷贝构造函数的最大的不同

这是我们的深拷贝

基于Linux的C++课程列表:

第一讲 C/C++基本语法元素

-1.1 提纲

--LinuxCPP0101

-1.2 程序设计的基本概念

--LinuxCPP0102

-1.3 简单C/C++程序介绍

--LinuxCPP0103

-1.4 程序设计的基本流程

--LinuxCPP0104

-1.5 基本语法元素

--LinuxCPP0105

-1.6 程序设计风格

--LinuxCPP0106

-1.7 编程实践

--LinuxCPP0107

-第一讲 C/C++基本语法元素--编程实践提交入口

第二讲 程序控制结构

-2.1 提纲

--LinuxCPP0201

-2.2 结构化程序设计基础

--LinuxCPP0202

-2.3 布尔数据

--LinuxCPP0203

-2.4 分支结构

--LinuxCPP0204

-2.5 break语句

--LinuxCPP0205

-2.6 循环结构

--LinuxCPP0206

-2.7 编程实践

--LinuxCPP0207

-第二讲 程序控制结构--编程实践提交入口

第三讲 函数

-3.1 提纲

--LinuxCPP0301

-3.2 函数声明、调用与定义

--LinuxCPP0302

-3.3 函数调用栈框架

--LinuxCPP0303

-3.4 编程实践

--LinuxCPP0304

-第三讲 函数--编程实践提交入口

第四讲 算法

-4.1 提纲

--LinuxCPP0401

-4.2 算法概念与特征

--LinuxCPP0402

-4.3 算法描述

--LinuxCPP0403

-4.4 算法设计与实现

--LinuxCPP0404

-4.5 递归算法(一)

--LinuxCPP0405

-4.6 递归算法(二)

--LinuxCPP0406

-4.7 容错与计算复杂度

--LinuxCPP0407

-4.8 编程实践

--LinuxCPP0408

-第四讲 算法--编程实践提交入口

第五讲 程序组织与开发方法

-5.1 提纲

--LinuxCPP0501

-5.2 库与接口

--LinuxCPP0502

-5.3 随机数库(一)

--LinuxCPP0503

-5.4 随机数库(二)

--LinuxCPP0504

-5.5 作用域与生存期

--LinuxCPP0505

-5.6 典型软件开发流程(一)

--LinuxCPP0506

-5.7 典型软件开发流程(二)

--LinuxCPP0507

-5.8 编程实践

--LinuxCPP0508

-第五讲 程序组织与开发方法--编程实践提交入口

第六讲 复合数据类型

-6.1 提纲

--LinuxCPP0601

-6.2 字符

--LinuxCPP0602

-6.3 数组(一)

--LinuxCPP0603

-6.4 数组(二)

--LinuxCPP0604

-6.5 结构体

--LinuxCPP0605

-6.6 编程实践

--LinuxCPP0606

-第六讲 复合数据类型--编程实践提交入口

第七讲 指针与引用

-7.1 提纲

--LinuxCPP0701

-7.2 指针基本概念

--LinuxCPP0702

-7.3 指针与函数

--LinuxCPP0703

-7.4 指针与复合数据类型(一)

--LinuxCPP0704

-7.5 指针与复合数据类型(二)

--LinuxCPP0705

-7.6 字符串

--LinuxCPP0706

-7.7 动态存储管理(一)

--LinuxCPP0707

-7.8 动态存储管理(二)

--LinuxCPP0708

-7.9 引用

--LinuxCPP0709

-7.10 编程实践

--LinuxCPP0710

-第七讲 指针与引用--编程实践提交入口

第八讲 链表与程序抽象

-8.1 提纲

--LinuxCPP0801

-8.2 数据抽象(一)

--LinuxCPP0802

-8.3 数据抽象(二)

--LinuxCPP0803

-8.4 链表(一)

--LinuxCPP0804

-8.5 链表(二)

--LinuxCPP0805

-8.6 链表(三)

--LinuxCPP0806

-8.7 链表(四)

--LinuxCPP0807

-8.8 函数指针(一)

--LinuxCPP0808

-8.9 函数指针(二)

--LinuxCPP0809

-8.10 抽象链表(一)

--LinuxCPP0810

-8.11 抽象链表(二)

--LinuxCPP0811

-8.12 编程实践

--LinuxCPP0812

-第八讲 链表与程序抽象--编程实践提交入口

第九讲 类与对象

-9.1 提纲

--LinuxCPP0901

-9.2 程序抽象与面向对象

--LinuxCPP0902

-9.3 类类型

--LinuxCPP0903

-9.4 对象(一)

--LinuxCPP0904

-9.5 对象(二)

--LinuxCPP0905

-9.6 类与对象的成员(一)

--LinuxCPP0906

-9.7 类与对象的成员(二)

--LinuxCPP0907

-9.8 类与对象的成员(三)

--LinuxCPP0908

-9.9 继承(一)

--LinuxCPP0909

-9.10 继承(二)

--LinuxCPP0910

-9.11 继承(三)

--LinuxCPP0911

-9.12 多态(一)

--LinuxCPP0912

-9.13 多态(二)

--LinuxCPP0913

-9.14 编程实践

--LinuxCPP0914

-第九讲 类与对象--编程实践提交入口

第十讲 操作符重载

-10.1 提纲

--LinuxCPP1001

-10.2 四则运算符重载(一)

--LinuxCPP1002

-10.3 四则运算符重载(二)

--LinuxCPP1003

-10.4 关系与下标操作符重载

--LinuxCPP1004

-10.5 赋值操作符重载(一)

--LinuxCPP1005

-10.6 赋值操作符重载(二)

--LinuxCPP1006

-10.7 赋值操作符重载(三)

--LinuxCPP1007

-10.8 赋值操作符重载(四)

--LinuxCPP1008

-10.9 赋值操作符重载(五)

--LinuxCPP1009

-10.10 流操作符重载(一)

--LinuxCPP1010

-10.11 流操作符重载(二)

--LinuxCPP1011

-10.12 流操作符重载(三)

--LinuxCPP1012

-10.13 操作符重载总结

--LinuxCPP1013

-10.14 编程实践

--LinuxCPP1014

-第十讲 操作符重载--编程实践提交入口

第十一讲 泛型编程

-11.1 提纲

--LinuxCPP1101

-11.2 泛型编程概览

--LinuxCPP1102

-11.3 异常处理机制(一)

--LinuxCPP1103

-11.4 异常处理机制(二)

--LinuxCPP1104

-11.5 运行期型式信息(一)

--LinuxCPP1105

-11.6 运行期型式信息(二)

--LinuxCPP1106

-11.7 模板与型式参数化

--LinuxCPP1107

-11.8 题外话:术语翻译

--LinuxCPP1108

-11.9 泛型编程实践(一)

--LinuxCPP1109

-11.10 泛型编程实践(二)

--LinuxCPP1110

-11.11 泛型编程实践(三)

--LinuxCPP1111

-11.12 泛型编程实践(四)

--LinuxCPP1112

-11.13 泛型编程实践(五)

--LinuxCPP1113

-11.14 泛型编程实践(六)

--LinuxCPP1114

-11.15 泛型编程实践(七)

--LinuxCPP1115

-11.16 泛型编程实践(八)

--LinuxCPP1116

-11.17 泛型编程实践(九)

--LinuxCPP1117

-11.18 泛型编程实践(十)

--LinuxCPP1118

-11.19 编程实践

--LinuxCPP1119

-第十一讲 泛型编程--编程实践提交入口

第十二讲 Linux系统编程基础

-12.1 提纲

--LinuxCPP1201

-12.2 程序执行环境(一)

--LinuxCPP1202

-12.3 程序执行环境(二)

--LinuxCPP1203

-12.4 程序执行环境(三)

--LinuxCPP1204

-12.5 程序执行环境(四)

--LinuxCPP1205

-12.6 输入输出(一)

--LinuxCPP1206

-12.7 输入输出(二)

--LinuxCPP1207

-12.8 文件系统

--LinuxCPP1208

-12.9 设备

--LinuxCPP1209

-12.10 库(一)

--LinuxCPP1210

-12.11 库(二)

--LinuxCPP1211

-12.12 makefile文件(一)

--LinuxCPP1212

-12.13 makefile文件(二)

--LinuxCPP1213

-12.14 makefile文件(三)

--LinuxCPP1214

-12.15 编程实践

--LinuxCPP1215

-第十二讲 Linux系统编程基础--编程实践提交入口

第十三讲 进程编程

-13.01 提纲

--LinuxCPP1301

-13.02 进程基本概念

--LinuxCPP1302

-13.03 信号

--LinuxCPP1303

-13.04 进程管理(一)

--LinuxCPP1304

-13.05 进程管理(二)

--LinuxCPP1305

-13.06 进程管理(三)

--LinuxCPP1306

-13.07 进程间通信(一)

--LinuxCPP1307

-13.08 进程间通信(二)

--LinuxCPP1308

-13.09 进程间通信(三)

--LinuxCPP1309

-13.10 进程间通信(四)

--LinuxCPP1310

-13.11 进程池

--LinuxCPP1311

-13.12 编程实践

--LinuxCPP1312

-第十三讲 进程编程--编程实践提交入口

第十四讲 线程编程

-14.1 提纲

--LinuxCPP1401

-14.2 线程基本概念

--LinuxCPP1402

-14.3 线程管理(一)

--LinuxCPP1403

-14.4 线程管理(二)

--LinuxCPP1404

-14.5 线程管理(三)

--LinuxCPP1405

-14.6 线程管理(四)

--LinuxCPP1406

-14.7 线程同步机制(一)

--LinuxCPP1407

-14.8 线程同步机制(二)

--LinuxCPP1408

-14.9 C++11线程库(一)

--LinuxCPP1409

-14.10 C++11线程库(二)

--LinuxCPP1410

-14.11 C++11线程库(三)

--LinuxCPP1411

-14.12 C++11线程库(四)

--LinuxCPP1412

-14.13 C++11线程库(五)

--LinuxCPP1413

-14.14 编程实践

--LinuxCPP1414

-第十四讲 线程编程--编程实践提交入口

第十五讲 网络编程

-15.1 提纲

--LinuxCPP1501

-15.2 Internet网络协议

--LinuxCPP1502

-15.3 套接字(一)

--LinuxCPP1503

-15.4 套接字(二)

--LinuxCPP1504

-15.5 编程实践

--LinuxCPP1505

-第十五讲 网络编程--编程实践提交入口

课程文档

-课程PDF文件

LinuxCPP1006笔记与讨论

也许你还感兴趣的课程:

© 柠檬大学-慕课导航 课程版权归原始院校所有,
本网站仅通过互联网进行慕课课程索引,不提供在线课程学习和视频,请同学们点击报名到课程提供网站进行学习。