C++中Copy-Swap实现拷贝交换

 更新时间:2023年07月09日 15:40:23   作者:Zijian/TENG  
本文主要介绍了C++中Copy-Swap实现拷贝交换,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

正式介绍 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实现拷贝交换的文章就介绍到这了,更多相关C++ Copy-Swap拷贝交换内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:

相关文章

  • C语言入门篇--函数及数组用法

    C语言入门篇--函数及数组用法

    本篇文章是c语言基础篇,主要为大家介绍了C语言的函数与数组,每个函数本质上都实现一个最小的功能,而main函数只负责调用函数,实现代码的核心逻辑,提高代码的可维护性
    2021-08-08
  • C++字符串反转的几种方法

    C++字符串反转的几种方法

    通过不同的方法,实现对所输入字符串的反转,具有一定的参考价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-06-06
  • C++ Boost Optional示例超详细讲解

    C++ Boost Optional示例超详细讲解

    Boost是为C++语言标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称
    2022-11-11
  • C++详解如何通过模板实现元素的反序

    C++详解如何通过模板实现元素的反序

    这篇文章主要介绍了C++中模板(Template)实现元素的反序,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-06-06
  • C++设计模式编程中proxy代理模式的使用实例

    C++设计模式编程中proxy代理模式的使用实例

    这篇文章主要介绍了C++设计模式编程中proxy代理模式的使用实例解析,代理模式可以被归类为结构型的设计模式,代理模式主张为对象提供一种代理以控制对这个对象的访问,需要的朋友可以参考下
    2016-03-03
  • C++List容器常用函数接口刨析

    C++List容器常用函数接口刨析

    最近我学习了C++中的STL库中的list容器,对于常用容器,我们不仅要会使用其常用的函数接口,我们还有明白这些接口在其底层是如何实现的。所以特意整理出来一篇博客供我们学习
    2022-08-08
  • OpenCV实现轮廓的发现

    OpenCV实现轮廓的发现

    这篇文章主要为大家详细介绍了OpenCV如何实现轮廓的发现,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-05-05
  • C++中继承与组合的区别详细解析

    C++中继承与组合的区别详细解析

    C++的“继承”特性可以提高程序的可复用性。正因为“继承”太有用、太容易用,才要防止乱用“继承”
    2013-09-09
  • C++另辟蹊径计算1到n的和

    C++另辟蹊径计算1到n的和

    从1加到100,高斯的故事,我们学过。今天,我们写一个程序来试试。首先,用笨方法。一个数一个数的加,我们一般人就是这样干的吗。在计算机程序里面,怎么办呢?1我们把求和的功能写成一个可以针对不同的N运用的,C++里面叫函数
    2023-02-02
  • VC实现批量删除指定文件的方法

    VC实现批量删除指定文件的方法

    这篇文章主要介绍了VC实现批量删除指定文件的方法,是一个比较普遍且实用的功能,需要的朋友可以参考下
    2014-07-07

最新评论