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. 基于【ESLint+JavaScript Standard Style】标准的VUE/JS/html风格指南

    小仙男前端代码风格规范指南v1.0 概述 本规范是适用于小仙男团队及前端团队所搭建的各种前端框架代码的通用风格规范指南: 使用时,请遵循指南细则进行代码风格约束,并在提交之前确保进行代码风格的修正操作 ...

  2. HTML、 input;、accept 属性-规定能够通过文件上传进行提交的文件类型

    定义和用法 文章地址: http://www.w3school.com.cn/tags/att_input_accept.asp accept 属性规定了可通过文件上传提交的服务器接受的文件类型. 注 ...

  3. [apue] 一图读懂 Unix 时间日期例程相互关系

    概览 开门见山先上图 界定一些术语,方便后面说明: GMT:格林威治平均时,太阳每天经过位于英国伦敦郊区的皇家格林威治天文台的时间为中午 12 点,1972 年之前使用的国际标准时间,因地球在它的椭圆 ...

  4. 指针和引用(pointer and reference),传值和传址

    pass by adress pass by reference和pass by pointer的共同点都在于传址,都是对于对象的地址的复制,而不会对对象进行产生副本的操作. pass by refe ...

  5. 3. 面向对象编程(OOP):

    面向对象编程的本质就是:以类的方式组织代码.以对象的组织(封装)数据 抽象:就是把不同的物品的共同点剥离出来,构成一个类.如每个人都有2条腿,我们可以把2条腿剥离出来 构成一个类 类与对象的关系 类: ...

  6. 第138篇:了解HTTP协议(TCP/IP协议,DNS域名解析,浏览器缓存)

    好家伙,发现自己的网络知识十分匮乏,赶紧补一下   这里先举个我生活中的例子 欸,作业不会写了,上网搜一下 用edge浏览器上bing必应搜一下(百度广告太多了,真不想用百度举例子)   假设这是我们 ...

  7. 解决Kibana(OpenSearch)某些字段无法搜索问题

    背景 最近在OpenSearch查看线上日志的时候,发现某个索引下有些字段无法直接在界面上筛选,搜索到也不高亮,非常的不方便,就像下面这样 字段左侧两个筛选按钮禁用了无法点击,提示 Unindexed ...

  8. vue 项目全局修改element-ui的样式/主题颜色

    一,安装sass 1 npm i sass sass-loader --save 二,安装element主题生成工具 // 全局安装npm i element-theme --save // 安装主体 ...

  9. 基于ChatGPT用AI实现自然对话

    1.概述 ChatGPT是当前自然语言处理领域的重要进展之一,通过预训练和微调的方式,ChatGPT可以生成高质量的文本,可应用于多种场景,如智能客服.聊天机器人.语音助手等.本文将详细介绍ChatG ...

  10. STL大全

    排序最速传说--sort 我们都学过一些排序的写法,比如冒泡排序,快速排序之类的东西,举个例子来说吧,这是快速排序的代码: #include<iostream> using namespa ...