C语言中实现语义层面的copy-and-swap操作,具体示例和原理是怎样的?

2026-04-18 15:512阅读0评论SEO资源
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计4103个文字,预计阅读时间需要17分钟。

C语言中实现语义层面的copy-and-swap操作,具体示例和原理是怎样的?

目录 + 类对象的初始化 + 构造函数 + 构造函数重载 + 构造函数重载 + 构造函数重载 + 复制构造函数 + 拷贝构造函数 + 拷贝构造函数的调用时机 + 自定义拷贝构造函数 + 拷贝赋值,自定义operator=

目录
  • class对象的初始化
  • constructor 构造器
  • constructor overload 构造器重载
  • copy constructor 拷贝构造器
    • 拷贝构造器的调用时机
    • 自定义拷贝构造器
  • 拷贝赋值,copy assignment
    • 自定义 operator=
  • copy-and-swap 语义

    class对象的初始化

    我们有一个class Data, 里面有一个int m_d 变量,存储一个整数。

    class Data { int m_i; public: void print() { std::cout << m_i << std::endl; } };

    我们如果需要一个Data类的对象的话,可以这样写:

    void test() { Data d; d.print(); // 打印内部的变量 m_i }

    看到这里,应该能发现问题,虽然 d 变量已经实例化了,但是,我们好像没有在初始化的时候指定内部m_i到底是什么值。

    有没有一种可能性,我们并没有将 d 所引用的内存变成一个可以使用的状态。

    比如说,这里提一个业务需求,内部的m_i只能是奇数。

    而上述代码中的变量d所引用的内存中的m_i到底是什么数,是未知的,有可能你的编译器将m_i的初始值设置成了0,但这是于事无补的,因为我们的业务需求是:

    • m_i 必须是奇数

    所有用到d的地方,都会有这个假设,所以如果在初始化d的时候,没有保证这个m_i是奇数的话,那么后续的所有业务逻辑全部都会崩溃。

    说了这么多,实际上就是想道明一句话:

    • 想要使用一个类对象,先进行初始化,这个对象的内存变成一个合法的状态

    合法的状态大部分跟业务逻辑相关,比如上面的m_i必须是奇数

    constructor 构造器

    对象在实例化的时候,大抵有这么两步:

    • 分配内存:这里分栈和堆,又叫自动分配内存(函数栈自动张开)和手动(使用new操作符在堆上申请)
    • 填充内存

    分配好的内存,几乎都是混沌的,完全不知道里面存的数据是什么,所以需要第二步填充内存,使得这块内存变成合法的

    而 constructor 的最大职责就是这个。(打开文件,打开数据库,或者网络连接也能在这里面干)

    这意思就是,constructor 执行的时机一定是在内存已经准备好了的时候。

    拿上面的例子,我们这样来确保一个合法的m_i:

    class Data { int m_i; public: Data(int i): m_i{i} // 变量m_i初始化 {} }; void test() { Data d{3};// 这里确保了变量 m_i 为 3 }

    也许不想在初始化的非要想一个合法值传给m_i,我们可以搞一个默认constructor:

    class Data { int m_i; public: Data():m_i{1} {} }; void test() { Data d{}; // 这里不用填参数 }

    constructor overload 构造器重载

    constructor的形式有很多,但是它本质上就是一个函数,在初始化的时候会调用而已。

    只要是函数,那么就可以按照一般的函数的重载规则进行重载。

    上面的例子已经说明了这个用法

    Data() : m_i{1} // 不带参数 Data(int i) : m_i{i} // 带了一个int参数 i

    所以一个类该有什么样的constructor,由业务逻辑自己决定。

    copy constructor 拷贝构造器

    还是上面的Data的例子:

    void test { Data d1{5}; 调用 Data(int i) 进行初始化 Data d2{d1}; // 这个是啥????? }

    从写法上来看,我们可以猜测到,d2.m_i 应该拷贝自 d1.m_i, 所以最后的结果是 5。

    这没问题的,但是我们前面说了,初始化一定是调用了某个constructor,那么这里是调用的哪个constructor呢?

    答案是:

    Data(const Data& other);

    形如这样的参数是这样的constructor,还特意起了个名字:copy constructor, 也就是拷贝构造器

    这个函数接受一个参数,我们起了个名叫other,所以一看就明白了,这个other就是我们想要拷贝的对象。

    这个constructor,我们并没有手动提供,所以这是编译器自动给我们加上去的。

    你可能会问,编译器怎么知道这个函数内部应该怎样实现?

    对啊,编译器不知道,他对我们的业务逻辑以及合法性一无所知,所以,编译器只能提供一个比较基础的功能:

    • 逐个成员变量拷贝

    Data类里只有一个m_i, 所以这里编译器提供的这个constructor,就是做了大概这样的事情:

    class Data { int m_i; public: Data(const Data& other):m_i{other.m_i} {} };

    像m_i这种基础类型,就是直接拷贝了。那如果Data类内部有class类型的变量呢:

    class Foo { int m_i; }; class Data { Foo m_f; };

    从形式上看,编译器给我们提供的默认的拷贝构造器,应该是这样的:

    class Data { Foo m_f; public: Data(const Data& other):m_f{other.m_f} {} };

    虽然m_f不是基本类型的变量,但是形式上来看,和基本变量是一致的。

    有必要提一下:

    m_f{other.m_f}

    这句,实际上继续调用了Foo类的拷贝构造,所以到这里,那就是Foo类的事情了,与Data类无关了。

    总之:

    • 拷贝构造器,就是一个普通的构造器,接收一个参数const T &
    • 拷贝构造器,可以让我们新产生的对象去拷贝一个已有的老对象,进行初始化
    • 如果我们不提供一个拷贝构造器,那么编译器会给我们搞一个默认的,逐个成员拷贝的,拷贝构造器

    拷贝构造器的调用时机

    上面已经说过一种:

    Data d1{}; Data d2{d1} // 这里会调用拷贝构造器

    事实上,还有别的时候,拷贝构造器会被调用,那就是函数的传参,和返回值。

    class Data{}; // 内部省略 void foo(Data d) { // 一些逻辑 } void test() { Data d1{}; foo(d1); // 这一句调用了拷贝构造器 }

    函数传参的时候,如果是值类型参数,那么会调用拷贝构造器。

    再来看看函数返回值:

    class Data{}; // 内部省略 Data getData() { Data d1{}; return d1; // 这里也是调用拷贝构造器 } void test() { Data d{getData()}; // 这里依然调用了拷贝构造器 }

    从理论上来看,上面的 Data d{getData()} 这一句应该调用两次拷贝构造

    • 第一次是函数getData内部的一个局部d1,拷贝给了一个临时匿名变量
    • 第二次是这个临时匿名变量拷贝给了变量d

    但是如果你在拷贝构造器里加上打印,你会发现,没有任何东西会打印出来,也就是说,压根就没有调用到拷贝构造器。

    这不代表上面关于函数的说法是错的,这只是编译器的优化而已,因为来来回回的拷贝,实在是没有必要,所以在某些编译器认为可以的情况下,编译器就直接省了。这个不重要,就不具体往里面细说规则了。

    自定义拷贝构造器

    大部分时候,编译器生成的这个拷贝构造器就满足需求了。

    但是,如果我们的class包含了动态资源,比如说一个堆上动态的int数组, 默认的拷贝构造器就没那么好用了:

    class Data { int m_size; // 数组的元素个数 int* m_ptr; // 指向数组首元素的指针 public: Data(int size):m_size{size} { if (size > 0) { m_ptr = new int[size]{}; } } ~Data() { delete[] m_ptr; } };

    由于这个Data类,拥有一个动态的数组,所以我们提供了一个析构函数,省的这块内存不会被回收。

    然后,我们没有提供一个拷贝构造器,所以编译器就给我们添加了一个:

    class Data { // 忽略别的代码,现在只关注拷贝构造器 Data(const Data& other):m_size{other.m_size}, m_ptr{other.m_ptr} {} }; void test() { Data d1{10}; // 第一句 Data d2{d1}; // 第二句 }

    没什么悬念,就是按照成员,逐个拷贝,注意,连指针也是直接拷贝。

    所以上述test函数中,第二句执行了之后,整个内存应该是这样的:

    这有问题吗?

    有很大的问题,考虑一下test函数执行完毕前,是不是需要对这两个变量 d1,d2d1, d2d1,d2 进行析构。

    你会发现,两次析构,delete 的资源是一份!!!

    一份资源,被delete两次,这就是所谓double free问题。

    还有别的问题吗?

    有。考虑下面的代码:

    void foo(Data d) { // 一些逻辑 } void test() { Data d1{10}; foo(d1); // }

    上面代码里,foo执行完之前,会析构这个局部变量d!导致资源已经被delete!

    而外面d1和里面的d,指向的是同一份资源,也就是说,foo执行完之后,d1.m_ptr 成为了一个悬挂指针!

    没办法了,只能靠自己定义拷贝构造器,来解决上面的问题了:

    class Data { int m_size; // 动态数组的元素个数 int* m_ptr; // 指向数据的指针 public: Data(const Data& other){ if(other.m_ptr) { auto temp_ptr { new int[other.m_size]}; std::copy(other.m_ptr, other.m_ptr + other.m_size, temp_ptr); m_ptr = temp_ptr; m_size = other.m_size; } else { m_ptr = nullptr; } } };

    上面的拷贝构造器,才是真正的拷贝,这种拷贝一般称之为深拷贝

    进行深拷贝之后,新对象和老对象,各自都有一份资源,不会再有任何粘连了。

    拷贝赋值,copy assignment

    想要完成深拷贝,到现在只进行了一半。

    剩下的一般就是重载一个操作符,operator=,这是用来解决如下形式的拷贝:

    Data d1{10}; Data d2{2}; /// d2 = d1;

    这里,两个变量 d1,d2d1, d2d1,d2 都自己进行了初始化,在经过一堆代码逻辑之后,此时我们的需求是:

    • 清除 d2 的数据
    • 将 d1 完整的拷贝给 d2

    两个类对象之间用赋值操作符,其实是调用了一个成员函数:operator=

    对,这玩意虽然是操作符,但是操作符本质上也还是函数,这个函数的名字就是operator=

    还是一样的,如果我们不提供一个自定义的operator=, 那么编译器会给我们添加一个如下的:

    class Data { int m_size; int* m_ptr; public: Data(int size):m_size{size} // 普通构造器 { if (size > 0) { m_ptr = new int[size]{}; } } Data(const Data& other) // 拷贝构造器 { if(other.m_ptr) { auto temp_ptr { new int[other.m_size]}; std::copy(other.m_ptr, other.m_ptr + other.m_size, temp_ptr); m_ptr = temp_ptr; m_size = other.m_size; } else { m_ptr = nullptr; } } ~Data() // 析构 { delete[] m_ptr; } ///////// 编译器自动添加的 operator= Data& operator=(const Data& other) { m_size = other.m_size; m_ptr = other.m_ptr; return *this; } };

    看这个编译器自动添加的operator=, 是显而易见能发现问题的:

    • 自身的m_ptr指向的内存永远无法回收了

    自定义 operator=

    还是得靠自己来编写 operator=

    前方警告,终于要点题了,copy and swap 即将出现。

    先按照我们的思路来写一个:

    Data& operator=(const Data& other) { // 1. 首先清除本身的资源 delete[] m_ptr; // 2. 拷贝other的资源 m_size = other.m_size; if (other.m_ptr) { m_ptr = new int[m_size]; std::copy(other.m_ptr, other.m_ptr+m_size, m_ptr); } return *this; }

    如果按照上面的代码,来看下面的test函数,会发生什么问题:

    void test() { Data d1{10}; d1 = d1; // 自己赋值给自己 }

    我们在operator=里面看见,上来直接把整个资源删除了,GG!

    我们要加一个判断:

    C语言中实现语义层面的copy-and-swap操作,具体示例和原理是怎样的?

    Data& operator=(const Data& other) { if (this == &other) // 加了一个判断 { return *this; } // 1. 首先清除本身的资源 delete[] m_ptr; // 2. 拷贝other的资源 m_size = other.m_size; if (other.m_ptr) { m_ptr = new int[m_size]; // 这句有可能异常 std::copy(other.m_ptr, other.m_ptr+m_size, m_ptr); } return *this; }

    关于这里加不加判断,很多大师级人物也认为不该加:

    • 谁会写出这种 d1 = d1; 这种代码???加了判断,徒增烦恼而已。

    再来看上面注释那个, new 在申请新的内存的时候,可能会发生异常,此时出现了一个问题,在文章开头提及的:

    • 内存合法性

    m_size 已经拷贝过来了
    而真正的数据没有拷贝过来,导致这两个变量,不满足我们的业务合法性。

    所以再改改:

    Data& operator=(const Data& other) { // 1. 首先清除本身的资源 delete[] m_ptr; m_ptr = nullptr; // 2. 拷贝other的资源 auto temp_size {other.m_size}; if (other.m_ptr) { m_ptr = new int[temp_size]; std::copy(other.m_ptr, other.m_ptr+temp_size, m_ptr); m_size = temp_size; } return *this; }

    此时此刻,这个代码已经没啥大问题了,除了一样:

    • 代码重复了,我们发现在拷贝other的数据的时候,逻辑是和拷贝构造器是一模一样的

    c++里有一个原则:DRY: DonotRepeatYourself。

    别写重复的代码!

    所以接着往下,copy-and-swap正式出场:

    copy-and-swap 语义

    • 首先copy就是指拷贝构造器

    我们先来讲讲swap是个啥。

    就是说,我们需要写一个函数swap,如下:

    class Data { // 其余部分省略,将重点放在swap函数 friend void swap(Data &left, Data& right) { std::swap(left.m_size, right.m_size); std::swap(left.m_ptr, right.m_ptr); } };

    这个swap函数很简单,就是交换两个已有的Data对象的内部数据,仅此而已。

    现在,

    • copy有了
    • swap有了

    让我们写出最终极的operator=:

    Data& operator=(Data other) { swap(*this, other); return *this; }

    是不是惊呆了,就这么两句,就行了!

    仔细领略一下这个写法的高深之处:

    • 函数传参,用的值传参,而非引用,所以此时会调用拷贝构造器(copy)
    • 函数内部,交换了当前对象,和局部临时变量other的数据(swap)

    你可能会问,没有清除自身的资源啊???

    注意,other 是一个局部临时变量,这个函数结束之前,会进行析构,而析构的时候,other身上已经是被交换过的了,所以other被析构的时候,就是自身资源清除的时候。

    妙,妙,妙!!

    用如此短的代码实现了operator=, 实在是妙~

    以上就是C++语义copy and swap示例详解的详细内容,更多关于C++语义copy and swap的资料请关注自由互联其它相关文章!

    本文共计4103个文字,预计阅读时间需要17分钟。

    C语言中实现语义层面的copy-and-swap操作,具体示例和原理是怎样的?

    目录 + 类对象的初始化 + 构造函数 + 构造函数重载 + 构造函数重载 + 构造函数重载 + 复制构造函数 + 拷贝构造函数 + 拷贝构造函数的调用时机 + 自定义拷贝构造函数 + 拷贝赋值,自定义operator=

    目录
    • class对象的初始化
    • constructor 构造器
    • constructor overload 构造器重载
    • copy constructor 拷贝构造器
      • 拷贝构造器的调用时机
      • 自定义拷贝构造器
    • 拷贝赋值,copy assignment
      • 自定义 operator=
    • copy-and-swap 语义

      class对象的初始化

      我们有一个class Data, 里面有一个int m_d 变量,存储一个整数。

      class Data { int m_i; public: void print() { std::cout << m_i << std::endl; } };

      我们如果需要一个Data类的对象的话,可以这样写:

      void test() { Data d; d.print(); // 打印内部的变量 m_i }

      看到这里,应该能发现问题,虽然 d 变量已经实例化了,但是,我们好像没有在初始化的时候指定内部m_i到底是什么值。

      有没有一种可能性,我们并没有将 d 所引用的内存变成一个可以使用的状态。

      比如说,这里提一个业务需求,内部的m_i只能是奇数。

      而上述代码中的变量d所引用的内存中的m_i到底是什么数,是未知的,有可能你的编译器将m_i的初始值设置成了0,但这是于事无补的,因为我们的业务需求是:

      • m_i 必须是奇数

      所有用到d的地方,都会有这个假设,所以如果在初始化d的时候,没有保证这个m_i是奇数的话,那么后续的所有业务逻辑全部都会崩溃。

      说了这么多,实际上就是想道明一句话:

      • 想要使用一个类对象,先进行初始化,这个对象的内存变成一个合法的状态

      合法的状态大部分跟业务逻辑相关,比如上面的m_i必须是奇数

      constructor 构造器

      对象在实例化的时候,大抵有这么两步:

      • 分配内存:这里分栈和堆,又叫自动分配内存(函数栈自动张开)和手动(使用new操作符在堆上申请)
      • 填充内存

      分配好的内存,几乎都是混沌的,完全不知道里面存的数据是什么,所以需要第二步填充内存,使得这块内存变成合法的

      而 constructor 的最大职责就是这个。(打开文件,打开数据库,或者网络连接也能在这里面干)

      这意思就是,constructor 执行的时机一定是在内存已经准备好了的时候。

      拿上面的例子,我们这样来确保一个合法的m_i:

      class Data { int m_i; public: Data(int i): m_i{i} // 变量m_i初始化 {} }; void test() { Data d{3};// 这里确保了变量 m_i 为 3 }

      也许不想在初始化的非要想一个合法值传给m_i,我们可以搞一个默认constructor:

      class Data { int m_i; public: Data():m_i{1} {} }; void test() { Data d{}; // 这里不用填参数 }

      constructor overload 构造器重载

      constructor的形式有很多,但是它本质上就是一个函数,在初始化的时候会调用而已。

      只要是函数,那么就可以按照一般的函数的重载规则进行重载。

      上面的例子已经说明了这个用法

      Data() : m_i{1} // 不带参数 Data(int i) : m_i{i} // 带了一个int参数 i

      所以一个类该有什么样的constructor,由业务逻辑自己决定。

      copy constructor 拷贝构造器

      还是上面的Data的例子:

      void test { Data d1{5}; 调用 Data(int i) 进行初始化 Data d2{d1}; // 这个是啥????? }

      从写法上来看,我们可以猜测到,d2.m_i 应该拷贝自 d1.m_i, 所以最后的结果是 5。

      这没问题的,但是我们前面说了,初始化一定是调用了某个constructor,那么这里是调用的哪个constructor呢?

      答案是:

      Data(const Data& other);

      形如这样的参数是这样的constructor,还特意起了个名字:copy constructor, 也就是拷贝构造器

      这个函数接受一个参数,我们起了个名叫other,所以一看就明白了,这个other就是我们想要拷贝的对象。

      这个constructor,我们并没有手动提供,所以这是编译器自动给我们加上去的。

      你可能会问,编译器怎么知道这个函数内部应该怎样实现?

      对啊,编译器不知道,他对我们的业务逻辑以及合法性一无所知,所以,编译器只能提供一个比较基础的功能:

      • 逐个成员变量拷贝

      Data类里只有一个m_i, 所以这里编译器提供的这个constructor,就是做了大概这样的事情:

      class Data { int m_i; public: Data(const Data& other):m_i{other.m_i} {} };

      像m_i这种基础类型,就是直接拷贝了。那如果Data类内部有class类型的变量呢:

      class Foo { int m_i; }; class Data { Foo m_f; };

      从形式上看,编译器给我们提供的默认的拷贝构造器,应该是这样的:

      class Data { Foo m_f; public: Data(const Data& other):m_f{other.m_f} {} };

      虽然m_f不是基本类型的变量,但是形式上来看,和基本变量是一致的。

      有必要提一下:

      m_f{other.m_f}

      这句,实际上继续调用了Foo类的拷贝构造,所以到这里,那就是Foo类的事情了,与Data类无关了。

      总之:

      • 拷贝构造器,就是一个普通的构造器,接收一个参数const T &
      • 拷贝构造器,可以让我们新产生的对象去拷贝一个已有的老对象,进行初始化
      • 如果我们不提供一个拷贝构造器,那么编译器会给我们搞一个默认的,逐个成员拷贝的,拷贝构造器

      拷贝构造器的调用时机

      上面已经说过一种:

      Data d1{}; Data d2{d1} // 这里会调用拷贝构造器

      事实上,还有别的时候,拷贝构造器会被调用,那就是函数的传参,和返回值。

      class Data{}; // 内部省略 void foo(Data d) { // 一些逻辑 } void test() { Data d1{}; foo(d1); // 这一句调用了拷贝构造器 }

      函数传参的时候,如果是值类型参数,那么会调用拷贝构造器。

      再来看看函数返回值:

      class Data{}; // 内部省略 Data getData() { Data d1{}; return d1; // 这里也是调用拷贝构造器 } void test() { Data d{getData()}; // 这里依然调用了拷贝构造器 }

      从理论上来看,上面的 Data d{getData()} 这一句应该调用两次拷贝构造

      • 第一次是函数getData内部的一个局部d1,拷贝给了一个临时匿名变量
      • 第二次是这个临时匿名变量拷贝给了变量d

      但是如果你在拷贝构造器里加上打印,你会发现,没有任何东西会打印出来,也就是说,压根就没有调用到拷贝构造器。

      这不代表上面关于函数的说法是错的,这只是编译器的优化而已,因为来来回回的拷贝,实在是没有必要,所以在某些编译器认为可以的情况下,编译器就直接省了。这个不重要,就不具体往里面细说规则了。

      自定义拷贝构造器

      大部分时候,编译器生成的这个拷贝构造器就满足需求了。

      但是,如果我们的class包含了动态资源,比如说一个堆上动态的int数组, 默认的拷贝构造器就没那么好用了:

      class Data { int m_size; // 数组的元素个数 int* m_ptr; // 指向数组首元素的指针 public: Data(int size):m_size{size} { if (size > 0) { m_ptr = new int[size]{}; } } ~Data() { delete[] m_ptr; } };

      由于这个Data类,拥有一个动态的数组,所以我们提供了一个析构函数,省的这块内存不会被回收。

      然后,我们没有提供一个拷贝构造器,所以编译器就给我们添加了一个:

      class Data { // 忽略别的代码,现在只关注拷贝构造器 Data(const Data& other):m_size{other.m_size}, m_ptr{other.m_ptr} {} }; void test() { Data d1{10}; // 第一句 Data d2{d1}; // 第二句 }

      没什么悬念,就是按照成员,逐个拷贝,注意,连指针也是直接拷贝。

      所以上述test函数中,第二句执行了之后,整个内存应该是这样的:

      这有问题吗?

      有很大的问题,考虑一下test函数执行完毕前,是不是需要对这两个变量 d1,d2d1, d2d1,d2 进行析构。

      你会发现,两次析构,delete 的资源是一份!!!

      一份资源,被delete两次,这就是所谓double free问题。

      还有别的问题吗?

      有。考虑下面的代码:

      void foo(Data d) { // 一些逻辑 } void test() { Data d1{10}; foo(d1); // }

      上面代码里,foo执行完之前,会析构这个局部变量d!导致资源已经被delete!

      而外面d1和里面的d,指向的是同一份资源,也就是说,foo执行完之后,d1.m_ptr 成为了一个悬挂指针!

      没办法了,只能靠自己定义拷贝构造器,来解决上面的问题了:

      class Data { int m_size; // 动态数组的元素个数 int* m_ptr; // 指向数据的指针 public: Data(const Data& other){ if(other.m_ptr) { auto temp_ptr { new int[other.m_size]}; std::copy(other.m_ptr, other.m_ptr + other.m_size, temp_ptr); m_ptr = temp_ptr; m_size = other.m_size; } else { m_ptr = nullptr; } } };

      上面的拷贝构造器,才是真正的拷贝,这种拷贝一般称之为深拷贝

      进行深拷贝之后,新对象和老对象,各自都有一份资源,不会再有任何粘连了。

      拷贝赋值,copy assignment

      想要完成深拷贝,到现在只进行了一半。

      剩下的一般就是重载一个操作符,operator=,这是用来解决如下形式的拷贝:

      Data d1{10}; Data d2{2}; /// d2 = d1;

      这里,两个变量 d1,d2d1, d2d1,d2 都自己进行了初始化,在经过一堆代码逻辑之后,此时我们的需求是:

      • 清除 d2 的数据
      • 将 d1 完整的拷贝给 d2

      两个类对象之间用赋值操作符,其实是调用了一个成员函数:operator=

      对,这玩意虽然是操作符,但是操作符本质上也还是函数,这个函数的名字就是operator=

      还是一样的,如果我们不提供一个自定义的operator=, 那么编译器会给我们添加一个如下的:

      class Data { int m_size; int* m_ptr; public: Data(int size):m_size{size} // 普通构造器 { if (size > 0) { m_ptr = new int[size]{}; } } Data(const Data& other) // 拷贝构造器 { if(other.m_ptr) { auto temp_ptr { new int[other.m_size]}; std::copy(other.m_ptr, other.m_ptr + other.m_size, temp_ptr); m_ptr = temp_ptr; m_size = other.m_size; } else { m_ptr = nullptr; } } ~Data() // 析构 { delete[] m_ptr; } ///////// 编译器自动添加的 operator= Data& operator=(const Data& other) { m_size = other.m_size; m_ptr = other.m_ptr; return *this; } };

      看这个编译器自动添加的operator=, 是显而易见能发现问题的:

      • 自身的m_ptr指向的内存永远无法回收了

      自定义 operator=

      还是得靠自己来编写 operator=

      前方警告,终于要点题了,copy and swap 即将出现。

      先按照我们的思路来写一个:

      Data& operator=(const Data& other) { // 1. 首先清除本身的资源 delete[] m_ptr; // 2. 拷贝other的资源 m_size = other.m_size; if (other.m_ptr) { m_ptr = new int[m_size]; std::copy(other.m_ptr, other.m_ptr+m_size, m_ptr); } return *this; }

      如果按照上面的代码,来看下面的test函数,会发生什么问题:

      void test() { Data d1{10}; d1 = d1; // 自己赋值给自己 }

      我们在operator=里面看见,上来直接把整个资源删除了,GG!

      我们要加一个判断:

      C语言中实现语义层面的copy-and-swap操作,具体示例和原理是怎样的?

      Data& operator=(const Data& other) { if (this == &other) // 加了一个判断 { return *this; } // 1. 首先清除本身的资源 delete[] m_ptr; // 2. 拷贝other的资源 m_size = other.m_size; if (other.m_ptr) { m_ptr = new int[m_size]; // 这句有可能异常 std::copy(other.m_ptr, other.m_ptr+m_size, m_ptr); } return *this; }

      关于这里加不加判断,很多大师级人物也认为不该加:

      • 谁会写出这种 d1 = d1; 这种代码???加了判断,徒增烦恼而已。

      再来看上面注释那个, new 在申请新的内存的时候,可能会发生异常,此时出现了一个问题,在文章开头提及的:

      • 内存合法性

      m_size 已经拷贝过来了
      而真正的数据没有拷贝过来,导致这两个变量,不满足我们的业务合法性。

      所以再改改:

      Data& operator=(const Data& other) { // 1. 首先清除本身的资源 delete[] m_ptr; m_ptr = nullptr; // 2. 拷贝other的资源 auto temp_size {other.m_size}; if (other.m_ptr) { m_ptr = new int[temp_size]; std::copy(other.m_ptr, other.m_ptr+temp_size, m_ptr); m_size = temp_size; } return *this; }

      此时此刻,这个代码已经没啥大问题了,除了一样:

      • 代码重复了,我们发现在拷贝other的数据的时候,逻辑是和拷贝构造器是一模一样的

      c++里有一个原则:DRY: DonotRepeatYourself。

      别写重复的代码!

      所以接着往下,copy-and-swap正式出场:

      copy-and-swap 语义

      • 首先copy就是指拷贝构造器

      我们先来讲讲swap是个啥。

      就是说,我们需要写一个函数swap,如下:

      class Data { // 其余部分省略,将重点放在swap函数 friend void swap(Data &left, Data& right) { std::swap(left.m_size, right.m_size); std::swap(left.m_ptr, right.m_ptr); } };

      这个swap函数很简单,就是交换两个已有的Data对象的内部数据,仅此而已。

      现在,

      • copy有了
      • swap有了

      让我们写出最终极的operator=:

      Data& operator=(Data other) { swap(*this, other); return *this; }

      是不是惊呆了,就这么两句,就行了!

      仔细领略一下这个写法的高深之处:

      • 函数传参,用的值传参,而非引用,所以此时会调用拷贝构造器(copy)
      • 函数内部,交换了当前对象,和局部临时变量other的数据(swap)

      你可能会问,没有清除自身的资源啊???

      注意,other 是一个局部临时变量,这个函数结束之前,会进行析构,而析构的时候,other身上已经是被交换过的了,所以other被析构的时候,就是自身资源清除的时候。

      妙,妙,妙!!

      用如此短的代码实现了operator=, 实在是妙~

      以上就是C++语义copy and swap示例详解的详细内容,更多关于C++语义copy and swap的资料请关注自由互联其它相关文章!