C++ 惯用法之 Copy-Swap 拷贝交换

这是“C++ 惯用法”合集的第 3 篇,前面 2 篇分别介绍了 RAII 和 PIMPL 两种惯用法:

正式介绍 Copy-Swap 之前,先看下《剑指 Offer》里的第️题:

如下为类型 CMyString 的声明,请为该类型添加赋值运算符函数。

class CMyString {
public:
CMyString(char* pData = nullptr);
CMyString(const CMyString& str);
~CMyString(); private:
char* m_pData;
};

作为面试题,这道题目虽然基础,但考察点颇多,有区分度:

  • 返回值类型应为引用类型,否则将无法支持形如 s3 = s2 = s1 的连续赋值
  • 形参类型应为 const 引用类型
  • 无资源泄露,正确释放赋值运算符左侧的对象的资源
  • 自赋值安全,能够正确处理 s1 = s1 的语句
  • 考虑异常安全

解法 1

CMyString& operator=(const CMyString& str)
{
if(this == &str)
return *this; delete[] m_pData;
m_pData = nullptr;
m_pData = new char[strlen(str.m_pData) + 1];
strcpy(m_pData, str.m_pData);
return *this;
}

上面代码有些细节需要注意:

  • 删除数组使用 delete[] 运算符
  • strlen 的结果不含字符串末尾的结束符 \0
  • strcpy 会拷贝结束符 \0

解法 1 满足考察点中除异常安全外的所有要求:new 的时候可能由于内存不足抛异常,但此时赋值运算符左侧的的对象已被释放,m_pData 为空指针,导致左侧对象处于无效状态。

解决方案:只要先 new 分配空间,再 delete 释放原来的空间即可。这样可以保证即使 new 失败抛异常,赋值运算符左侧对象也尚未修改,仍处于有效状态。

解法 2

《剑指 Offer》中给出了更好的解法:先创建赋值运算符右侧对象的一个临时副本,然后交换赋值运算符左侧对象和该临时副本的 m_pData,当临时对象 strTemp 离开作用域时,自动调用其析构函数,释放 m_pData 指向的资源(即赋值运算符左侧对象原来的内存):

CMyString& operator=(const CMyStirng& str)
{
if(this != &str)
{
CMyString strTemp(str);
char* pTemp = m_pData;
m_pData = strTemp.m_pData;
strTemp.m_pData = pTemp;
}
return *this;
}

解法 2 巧妙地利用了类原本的拷贝构造、析构函数自动进行资源管理,同时又不涉及底层的 new[]/delete[] 操作,可读性更强,也不容易出错。

解法 2 是 Copy-Swap 的雏形。C++ 中管理资源类通常会定义自己的 swap 函数,与其他拷贝控制成员(拷贝/移动构造、拷贝/移动赋值运算符、析构)不同,swap 不是必须,但却是重要的优化手段,以下是使用 Copy-Swap 惯用法的解法:

解法 3

class CMyString {
friend void Swap(CMyString& lhs, CMyString& rhs) noexcept
{
// 对 CMyString 的成员逐一交换
std::swap(lhs.m_pData, rhs.m_pData);
}
// ...
}; CMyString(CMyString&& str) : CMyString()
{
Swap(*this, str);
} CMyString& operator=(CMyStirng str)
{
Swap(*this, str);
return *this;
}

这里有几点需要注意:

  • 拷贝赋值运算符的形参类型不再是 const 引用,因为 Copy-Swap 需要先对赋值运算符右侧对象进行拷贝,这里直接使用值传递。这样一来,也使得 Copy-Swap 天然地异常安全、自赋值安全。

    • 异常安全:进入函数 operator=() 之前,先进行拷贝
    • 自赋值安全:形参是一个新创建的临时对象,永远不可能是对象自身
  • 不需要额外实现移动赋值运算符:如果赋值运算符右侧是一个右值,则自动调用 CMyString 的移动构造来构造形参

这还没完...

标准库 std::swap 及 ADL

C++ 标准库也提供了 swap 函数,理论上需要一次拷贝,两次赋值:

void swap(CMyString& lhs, CMyString& rhs)
{
CMyString tmp(lhs);
lhs = rhs;
rhs = tmp;
}

其中 CMyString tmp(lhs) 会调用 CMyString 的拷贝构造进行深拷贝,效率上不如 CMyString 类自己实现的直接交换指针的效率高。

在进行 swap(v1, v2) 的调用时,如果类实现了自己的 swap 版本,其匹配程度优于标准库的版本。如果类没有定义自己的 swap,则使用标准库的 swap。这种查找匹配方式被称为 ADL(Argument-Dependent Lookup)。

注意不能使用 std::swap 形式,因为这样会强制使用标准库的 swap。正确的做法是提前使用 using std::swap 声明,而后续所有的 swap 都应该是不加限制的(这一点刚好和 std::move 相反):

void swap(Bar& lhs, Bar& rhs)
{
using std::swap;
swap(lhs.m1, rhs.m1);
swap(lhs.m2, rhs.m2);
swap(lhs.m3, rhs.m3);
}

最终的结果

class CMyString {
friend void swap(CMyString& lhs, CMyString& rhs) noexcept
{
// 对 CMyString 的成员逐一交换
using std::swap;
swap(lhs.m_pData, rhs.m_pData);
}
// ...
}; CMyString(CMyString&& str) : CMyString()
{
swap(*this, str);
} CMyString& operator=(CMyStirng str)
{
swap(*this, str);
return *this;
}

C++ 惯用法之 Copy-Swap 拷贝交换的更多相关文章

  1. RAII惯用法详解

    [1]什么是RAII惯用法? RAII是Resource Acquisition Is Initialization的缩写,意为“资源获取即初始化”. 它是C++之父Bjarne Stroustrup ...

  2. 使用copy来拷贝对象

    拷贝对象 您通过将 copy 消息发送给对象,以制作对象的副本. NSArray *myArray = [yourArray copy]; 要拷贝,接收对象的类必须遵守 NSCopying 协议.如果 ...

  3. Linux命令 swap:内存交换空间

    swap 内存交换空间的概念 swap使用上的限制

  4. [转]编译防火墙——C++的Pimpl惯用法解析

    impl(pointer to implementation, 指向实现的指针)是一种常用的,用来对“类的接口与实现”进行解耦的方法.这个技巧可以避免在头文件中暴露私有细节(见下图1),因此是促进AP ...

  5. copy深浅拷贝

    我们在很多方法里都看到copy()方法,这是对变量的复制,赋值,下面来看一下实例: 复制方法调用的是copy模块中的方法: import copy copy.copy()         #前拷贝 c ...

  6. WPF - 绑定及惯用法(一)

    写在前面:这仍然是一些没有经过严格审阅的文字.虽然我的确执行了初稿.复稿以及审阅等一系列用以保证文章质量的方法,但是仍然担心其中是否有错误.希望您能帮助指出,以在下一次我在版本更新时进行修正.所有的错 ...

  7. 编译防火墙——C++的Pimpl惯用法解析

    http://blog.csdn.net/lihao21/article/details/47610309 Pimpl(pointer to implementation, 指向实现的指针)是一种常用 ...

  8. Python [拷贝copy / 深度拷贝deepcopy] | 可视化理解

    Python 是一门面向对象的语言, 在Python一切皆对象. 每一个对象都有由以下三个属性组成: ------------------------------------------------- ...

  9. 包,logging日志模块,copy深浅拷贝

    一 包 package 包就是一个包含了 __init__.py文件的文件夹 包是模块的一种表现形式,包即模块 首次导入包: 先创建一个执行文件的名称空间 1.创建包下面的__init__.py文件的 ...

  10. C/C++:copy control (拷贝控制)

    前言:当定义一个类的时候,我们显示或者隐式地指定在此类型的对象拷贝,移动,赋值,销毁时做些什么,一个类通过定义五种特殊的成员函数来控制这些操作,包括拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值 ...

随机推荐

  1. redhat6安装10g rac过程中的报错

    问题描述:redhat 6 来安装oracle10.2.0.1的集群,坑太多了,不建议这样安装,即使安装成功,在升级过程中也会有各种报错.redhat5安装还比较顺利,6就一路坑 1.缺少依赖 lib ...

  2. Jupyter Notebook(或vscode插件) 一个cell有多个输出

    方法一 在文件的开头加上如下代码,该方法仅对当前文件有效 from IPython.core.interativeshell import InteractiveShell InteractiveSh ...

  3. Pwn系列之Protostar靶场 Stack3题解

    (gdb) disass main Dump of assembler code for function main: 0x08048438 <main+0>: push ebp 0x08 ...

  4. docker常用命令之帮助启动类命令和镜像命令

    docker安装之后,启动时会报如下错误: Job for docker.service failed because start of the service was attempted too o ...

  5. ChatGPT 中文指令指南,教会你如何使用chatgpt实现中文你想要的答案

    ChatGPT 中文指令指南,教会你如何使用chatgpt实现中文你想要的答案 1.学习英语--替代词典 App 场景 例子 Prompts 解释中文英文意思,并解释单词的词根词缀.可以替代词典. 告 ...

  6. 程序员IT行业,外行眼里高收入人群,内行人里的卷王

    程序员 一词,在我眼里其实是贬义词.因为我的其他不是这行的亲朋友好友,你和他们说,你是一名程序员· 他们 第一刻板影响就是,秃头,肥胖,宅男,油腻,不修边幅 反正给人一种不干净,不好形象,,,,不知道 ...

  7. 2020-09-06:Docker的命名空间有哪些?

    福哥答案2020-09-06: 福哥口诀法:命进I网挂U用 1.进程命名空间.CLONE_NEWPID.进程编号. 2.IPC 命名空间.CLONE_NEWPIPC.信号量.消息队列何共享内存. 3. ...

  8. Jenkins - 构建时运行Selenium打不开浏览器解决方法

    Jenkins-构建时运行Selenium打不开浏览器解决方法 前言 为了让Jenkins执行的Web自动化测试任务,能顺利调出浏览器页面,我们需要以命令行的方式启动Jenkins并执行脚本. 注:通 ...

  9. PHP反序列化常用魔术方法

    PHP反序列化 php序列化(serialize):是将变量转换为可保存或传输的字符串的过程 php反序列化(unserialize):就是在适当的时候把这个字符串再转化成原来的变量使用 PHP反序列 ...

  10. UCOS II 源码分析一

    再进行ucos操作系统源码分析前,先对ucos源码文件结构说个简单说明,只有掌握了源码文件结构才能在接下来的源码分析中逐渐感受到会当凌绝顶, 一览众山小,最后的感受就是RTOS也不是很神秘!下面以正点 ...