当前课程知识点:基于Linux的C++ > 第十讲 操作符重载 > 10.8 赋值操作符重载(四) > LinuxCPP1008
我们来看移动赋值与移动构造的
这样一个实现
为了实现这个移动语义
我们这个拷贝构造函数
和重载的赋值操作符
和原先的那个深拷贝动作就不一样了
拷贝构造函数是 A( A && that )
这是一个右值引用
双 “&” 记号表示右值引用
而不是左值引用了
A & operator=( A && that )
这是一个右值引用的赋值操作符重载
返回值呢 仍然是这个对象的左值引用
不是右值引用 注意就在这里
看看我们怎么实现它
第一个拷贝构造的实现
that 是我们传进来的那个对象
那个参数的前面写的是 A &&
为了保证在函数内部它能够修改 that 所引用的那个目标数据对象
为了保证在函数内部它能够修改 that 所引用的那个目标数据对象
虽然你是以右值引用的形式传递进来的
但是在函数内部
它实际上仍然被当做左值来对待
所以在这个地方写的这个 that
它实际上是个左值 它不是右值
传的是右值引用没错
但是在内部是把它当做左值来看的
所以这个地方是个左值
我们的赋值操作符 A & A::operator=( A && that )
内部的代码实现
_n = that._n, _p = that._p, that._n = 0
that._p = nullptr(null pointer,空指针)
return *this
这就是我们的移动赋值语义
我要把 that 的数据成员
一个一个地拷贝到我们的 *this 里
然后让 _p 指针做一次所有权的移交
*this 的 _p 字段将会指向那个目标数组
而 that 的 _p 字段将会变成空指针
不再指向那个目标数组
形成这个所有权的移交
所以你看到在这个赋值的操作符里边
目标引用的那个数据对象 that
它的内容将会被修改
你不把它当成左值 它能修改吗
它修改不了的 对吧
所以非常重要的一个地方
这就是完成我们的移动语义
这里面有一个地方需要特别解释
就是 null pointer 空指针 nullptr
在 C++11 里边
给我们预定义的一个新的关键字
它表示空指针类型
它的类型是 nullptr_t 这种类型的一个对象
它的类型是 nullptr_t 这种类型的一个对象
已经帮我们定义好的
你就直接用就行了
这个空指针类型 nullptr
可以隐式转换成任意指针类型和 bool 类型
但是你不能把它转换成整数类型
特别注意这一条
定义它的目的是为了取代 NULL 这个宏
你不编译 C++11 的代码
那么实际上你就不需要
也不能使用这个关键字 nullptr
就直接使用老式的 NULL 也是可以的
如果你不使用这段代码
你使用我这条注释掉的这段代码
*this = that
我告诉你 它是不能工作的
它不会调用我们这个重载的赋值操作符
因为在函数的内部
that 是左值而不是右值
that 是左值
那就意味着你可以传左值
你可以传左值的引用
但是你不能传右值的引用
你不能把 that 传给 A && that
这不可以 这是一个右值引用
那是左值 不能传给它的
参数类型是不匹配的 编译器会报错
除非你实现了一个左值引用版本
否则的话 编译器是通不过的
可是如果你实现了左值引用
那不就是深拷贝了嘛
它就不是我们的移动拷贝了嘛
对吧 它就不一样了
所以这一条语句是不可以的
和我们刚才讲的拷贝构造
或者是一个赋值操作符重载一样
你可以在这里面测试 that
那个目标对象和 *this 是不是相等的
如果是相等 你可以不赋值
还是我前面那个说法
就是这个测试很多时候
并不能够给我们提升程序效率
事实上反而会降低我们的程序效率
所以测不测试看你的程序是不是需要它
这是我们的移动赋值与移动构造
它具体的实现代码很短
但是可解释的地方其实是很多的
因为毕竟是 C++11 中新引入的知识
我们特别需要说明
如果这样一个对象
有的时候我需要深拷贝
有的时候我需要移动拷贝
有的时候我需要深拷贝赋值
有的时候我需要移动赋值
那怎么办呢
就是说这两个语义我都需要
那么当你在实现这个类的时候
你就必须为它重载
两个版本的构造函数和赋值操作符
两个你都得提供
我们现在习惯于按照这样的一个方式
同时提供它的拷贝语义和移动语义
一个 A( const A & that )
一个 A( A && that )
第一个当然是左值引用
对 const A 的左值引用
第二个就是对 A 的右值引用
这是两个版本
第一个版本 左值引用版本完成我们的深拷贝语义
第一个版本 左值引用版本完成我们的深拷贝语义
第二个 右值版本完成我们的移动语义
第二个 右值版本完成我们的移动语义
这两者是不一样的
因为我们拷贝是不需要修改
目标引用的那个内容的
所以传 const A & 就 OK 了
不需要 A &
因为我们右值引用是需要修改那个目标数据对象的值的
因为我们右值引用是需要修改那个目标数据对象的值的
所以事实上不应该传递 const A &&
所以提供两个版本
赋值操作符也一样
A & operator=( const A & that )
这是第一个 深拷贝版本
第二个 A & operator=( A && that )
这是一个移动赋值版本
两个版本你都要写
所以我们这里面要写几个函数 4 个
2 个构造函数 2 个重载的 operator=
那我们看我们怎么用它
我们现在来看这段代码
我首先构造一个对象 a
4 个元素 然后把它每个元素值分别初始化成 1、2、3、4
4 个元素 然后把它每个元素值分别初始化成 1、2、3、4
4 个整数 我们用这个数组来构造对象 a
然后我使用拷贝构造来构造对象 b
传的那个参数是刚才的那个对象 a
所以它调用的是拷贝构造
它会把 a 这个型解释成对那个对象 a 的一个引用
它会把 a 这个型解释成对那个对象 a 的一个引用
用 const 表达它是一个不会在构造函数内部
用 const 表达它是一个不会在构造函数内部
修改目标数据对象的值的这样一个引用
所以它相对于引用的版本将是 const A &
这是一个拷贝构造
如果是你这么写:b = a
那么它会调用普通的赋值操作符
就是我们刚才重载的那个普通赋值
深拷贝的那个版本去完成它的拷贝动作
这是 b = a 要做的事
如果你想调用它的移动赋值或移动构造那个版本
如果你想调用它的移动赋值或移动构造那个版本
那么就需要这么写
A c( static_cast〈 A && 〉( a ) )
你必须按照这个方式来写
必须把 a 静态转型成对它的右值引用
必须把 a 静态转型成对它的右值引用
然后用右值引用去构造那个对象 c
这个时候它才能调用那个右值引用版本去进行移动构造
这个时候它才能调用那个右值引用版本去进行移动构造
如果你不做静态转型
就是我前面讲的
它就会调用它的深拷贝版本
因为它认为那个东西是个左值引用
它不是右值引用
所以它就会调用它的左值引用版本
而不调用它的右值引用版本
特别注意这一条 赋值也一样
你必须写 c = static_cast〈 A && 〉( a )
你必须按照这个方式赋值
它才能够调用它的移动赋值版本
移动语义和深拷贝语义
如果混合在一起编程的时候
两者都需要同时提供
那么在调用的时候就需要特别小心
你传递的那个引用
到底是一个左值引用还是一个右值引用
将会决定它到底调用的是个深拷贝版本
还是一个移动版本
特别注意这一条
-1.1 提纲
-1.2 程序设计的基本概念
-1.3 简单C/C++程序介绍
-1.4 程序设计的基本流程
-1.5 基本语法元素
-1.6 程序设计风格
-1.7 编程实践
-第一讲 C/C++基本语法元素--编程实践提交入口
-2.1 提纲
-2.2 结构化程序设计基础
-2.3 布尔数据
-2.4 分支结构
-2.5 break语句
-2.6 循环结构
-2.7 编程实践
-第二讲 程序控制结构--编程实践提交入口
-3.1 提纲
-3.2 函数声明、调用与定义
-3.3 函数调用栈框架
-3.4 编程实践
-第三讲 函数--编程实践提交入口
-4.1 提纲
-4.2 算法概念与特征
-4.3 算法描述
-4.4 算法设计与实现
-4.5 递归算法(一)
-4.6 递归算法(二)
-4.7 容错与计算复杂度
-4.8 编程实践
-第四讲 算法--编程实践提交入口
-5.1 提纲
-5.2 库与接口
-5.3 随机数库(一)
-5.4 随机数库(二)
-5.5 作用域与生存期
-5.6 典型软件开发流程(一)
-5.7 典型软件开发流程(二)
-5.8 编程实践
-第五讲 程序组织与开发方法--编程实践提交入口
-6.1 提纲
-6.2 字符
-6.3 数组(一)
-6.4 数组(二)
-6.5 结构体
-6.6 编程实践
-第六讲 复合数据类型--编程实践提交入口
-7.1 提纲
-7.2 指针基本概念
-7.3 指针与函数
-7.4 指针与复合数据类型(一)
-7.5 指针与复合数据类型(二)
-7.6 字符串
-7.7 动态存储管理(一)
-7.8 动态存储管理(二)
-7.9 引用
-7.10 编程实践
-第七讲 指针与引用--编程实践提交入口
-8.1 提纲
-8.2 数据抽象(一)
-8.3 数据抽象(二)
-8.4 链表(一)
-8.5 链表(二)
-8.6 链表(三)
-8.7 链表(四)
-8.8 函数指针(一)
-8.9 函数指针(二)
-8.10 抽象链表(一)
-8.11 抽象链表(二)
-8.12 编程实践
-第八讲 链表与程序抽象--编程实践提交入口
-9.1 提纲
-9.2 程序抽象与面向对象
-9.3 类类型
-9.4 对象(一)
-9.5 对象(二)
-9.6 类与对象的成员(一)
-9.7 类与对象的成员(二)
-9.8 类与对象的成员(三)
-9.9 继承(一)
-9.10 继承(二)
-9.11 继承(三)
-9.12 多态(一)
-9.13 多态(二)
-9.14 编程实践
-第九讲 类与对象--编程实践提交入口
-10.1 提纲
-10.2 四则运算符重载(一)
-10.3 四则运算符重载(二)
-10.4 关系与下标操作符重载
-10.5 赋值操作符重载(一)
-10.6 赋值操作符重载(二)
-10.7 赋值操作符重载(三)
-10.8 赋值操作符重载(四)
-10.9 赋值操作符重载(五)
-10.10 流操作符重载(一)
-10.11 流操作符重载(二)
-10.12 流操作符重载(三)
-10.13 操作符重载总结
-10.14 编程实践
-第十讲 操作符重载--编程实践提交入口
-11.1 提纲
-11.2 泛型编程概览
-11.3 异常处理机制(一)
-11.4 异常处理机制(二)
-11.5 运行期型式信息(一)
-11.6 运行期型式信息(二)
-11.7 模板与型式参数化
-11.8 题外话:术语翻译
-11.9 泛型编程实践(一)
-11.10 泛型编程实践(二)
-11.11 泛型编程实践(三)
-11.12 泛型编程实践(四)
-11.13 泛型编程实践(五)
-11.14 泛型编程实践(六)
-11.15 泛型编程实践(七)
-11.16 泛型编程实践(八)
-11.17 泛型编程实践(九)
-11.18 泛型编程实践(十)
-11.19 编程实践
-第十一讲 泛型编程--编程实践提交入口
-12.1 提纲
-12.2 程序执行环境(一)
-12.3 程序执行环境(二)
-12.4 程序执行环境(三)
-12.5 程序执行环境(四)
-12.6 输入输出(一)
-12.7 输入输出(二)
-12.8 文件系统
-12.9 设备
-12.10 库(一)
-12.11 库(二)
-12.12 makefile文件(一)
-12.13 makefile文件(二)
-12.14 makefile文件(三)
-12.15 编程实践
-第十二讲 Linux系统编程基础--编程实践提交入口
-13.01 提纲
-13.02 进程基本概念
-13.03 信号
-13.04 进程管理(一)
-13.05 进程管理(二)
-13.06 进程管理(三)
-13.07 进程间通信(一)
-13.08 进程间通信(二)
-13.09 进程间通信(三)
-13.10 进程间通信(四)
-13.11 进程池
-13.12 编程实践
-第十三讲 进程编程--编程实践提交入口
-14.1 提纲
-14.2 线程基本概念
-14.3 线程管理(一)
-14.4 线程管理(二)
-14.5 线程管理(三)
-14.6 线程管理(四)
-14.7 线程同步机制(一)
-14.8 线程同步机制(二)
-14.9 C++11线程库(一)
-14.10 C++11线程库(二)
-14.11 C++11线程库(三)
-14.12 C++11线程库(四)
-14.13 C++11线程库(五)
-14.14 编程实践
-第十四讲 线程编程--编程实践提交入口
-15.1 提纲
-15.2 Internet网络协议
-15.3 套接字(一)
-15.4 套接字(二)
-15.5 编程实践
-第十五讲 网络编程--编程实践提交入口