设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间07 现在时间07 缓存数据 没有什么事情是简单轻松的,唯有不断努力,才能更加顺利。

没有什么事情是简单轻松的,唯有不断努力,才能更加顺利。

查看: 1242|回复: 0

C++语义copy and swap示例详解

[复制链接]

  离线 

TA的专栏

  • 打卡等级:热心大叔
  • 打卡总天数:221
  • 打卡月天数:0
  • 打卡总奖励:3221
  • 最近打卡:2025-04-16 21:39:29
等级头衔

等級:晓枫资讯-上等兵

在线时间
0 小时

积分成就
威望
0
贡献
411
主题
370
精华
0
金钱
4453
积分
834
注册时间
2023-1-9
最后登录
2025-5-31

发表于 2023-2-13 01:59:22 | 显示全部楼层 |阅读模式
class对象的初始化

我们有一个
  1. class Data
复制代码
, 里面有一个
  1. int m_d
复制代码
变量,存储一个整数。
  1. class Data
  2. {
  3.     int m_i;
  4.     public:
  5.     void print()
  6.     {
  7.         std::cout << m_i << std::endl;
  8.     }
  9. };
复制代码
我们如果需要一个Data类的对象的话,可以这样写:
  1. void test()
  2. {
  3.     Data d;
  4.     d.print(); // 打印内部的变量 m_i
  5. }
复制代码
看到这里,应该能发现问题,虽然 d 变量已经实例化了,但是,我们好像没有在初始化的时候指定内部
  1. m_i
复制代码
到底是什么值。
有没有一种可能性,我们并没有将 d 所引用的内存变成一个可以使用的状态。
比如说,这里提一个业务需求,内部的
  1. m_i
复制代码
只能是奇数。
而上述代码中的变量d所引用的内存中的
  1. m_i
复制代码
到底是什么数,是未知的,有可能你的编译器将m_i的初始值设置成了0,但这是于事无补的,因为我们的业务需求是:

  • m_i 必须是奇数
所有用到d的地方,都会有这个假设,所以如果在初始化d的时候,没有保证这个m_i是奇数的话,那么后续的所有业务逻辑全部都会崩溃。
说了这么多,实际上就是想道明一句话:

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

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


constructor 构造器

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

  • 分配内存:这里分栈和堆,又叫自动分配内存(函数栈自动张开)和手动(使用new操作符在堆上申请)
  • 填充内存
分配好的内存,几乎都是混沌的,完全不知道里面存的数据是什么,所以需要第二步
  1. 填充内存
复制代码
,使得这块内存变成
  1. 合法的
复制代码

而 constructor 的最大职责就是这个。(打开文件,打开数据库,或者网络连接也能在这里面干)
这意思就是,constructor 执行的时机一定是在内存已经准备好了的时候。
拿上面的例子,我们这样来确保一个合法的m_i:
  1. class Data
  2. {
  3.     int m_i;
  4.     public:
  5.     Data(int i): m_i{i} // 变量m_i初始化
  6.     {}
  7. };
  8. void test()
  9. {
  10.     Data d{3};// 这里确保了变量 m_i 为 3
  11. }
复制代码
也许不想在初始化的非要想一个合法值传给m_i,我们可以搞一个默认constructor:
  1. class Data
  2. {
  3.     int m_i;
  4.     public:
  5.     Data():m_i{1}
  6.     {}
  7. };
  8. void test()
  9. {
  10.     Data d{}; // 这里不用填参数
  11. }
复制代码
constructor overload 构造器重载

constructor的形式有很多,但是它本质上就是一个函数,在初始化的时候会调用而已。
只要是函数,那么就可以按照一般的函数的重载规则进行重载。
上面的例子已经说明了这个用法
  1.     Data() : m_i{1}        // 不带参数
  2.     Data(int i) : m_i{i}   // 带了一个int参数 i
复制代码
所以一个类该有什么样的constructor,由业务逻辑自己决定。

copy constructor 拷贝构造器

还是上面的Data的例子:
  1. void test
  2. {
  3.     Data d1{5};   调用 Data(int i) 进行初始化
  4.     Data d2{d1}; // 这个是啥?????
  5. }
复制代码
从写法上来看,我们可以猜测到,d2.m_i 应该拷贝自 d1.m_i, 所以最后的结果是 5。
这没问题的,但是我们前面说了,初始化一定是调用了某个constructor,那么这里是调用的哪个constructor呢?
答案是:
  1. Data(const Data& other);
复制代码
形如这样的参数是这样的constructor,还特意起了个名字:copy constructor, 也就是
  1. 拷贝构造器
复制代码

这个函数接受一个参数,我们起了个名叫
  1. other
复制代码
,所以一看就明白了,这个
  1. other
复制代码
就是我们想要拷贝的对象。
这个constructor,我们并没有手动提供,所以这是编译器自动给我们加上去的。
你可能会问,编译器怎么知道这个函数内部应该怎样实现?
对啊,编译器不知道,他对我们的业务逻辑以及
  1. 合法性
复制代码
一无所知,所以,编译器只能提供一个比较基础的功能:

  • 逐个成员变量拷贝
Data类里只有一个m_i, 所以这里编译器提供的这个constructor,就是做了大概这样的事情:
  1. class Data
  2. {
  3.     int m_i;
  4.     public:
  5.     Data(const Data& other):m_i{other.m_i}
  6.     {}
  7. };
复制代码
像m_i这种基础类型,就是直接拷贝了。那如果Data类内部有class类型的变量呢:
  1. class Foo
  2. {
  3.     int m_i;
  4. };
  5. class Data
  6. {
  7.     Foo m_f;
  8. };
复制代码
从形式上看,编译器给我们提供的默认的拷贝构造器,应该是这样的:
  1. class Data
  2. {
  3.     Foo m_f;
  4.     public:
  5.     Data(const Data& other):m_f{other.m_f}
  6.     {}
  7. };
复制代码
虽然m_f不是基本类型的变量,但是形式上来看,和基本变量是一致的。
有必要提一下:
  1. m_f{other.m_f}
复制代码
这句,实际上继续调用了
  1. Foo类的拷贝构造
复制代码
,所以到这里,那就是
  1. Foo类
复制代码
的事情了,与
  1. Data类
复制代码
无关了。
总之:

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

拷贝构造器的调用时机

上面已经说过一种:
  1. Data d1{};
  2. Data d2{d1} // 这里会调用拷贝构造器
复制代码
事实上,还有别的时候,拷贝构造器会被调用,那就是函数的传参,和返回值。
  1. class Data{}; // 内部省略
  2. void foo(Data d)
  3. {
  4.     // 一些逻辑
  5. }
  6. void test()
  7. {
  8.     Data d1{};
  9.     foo(d1); // 这一句调用了拷贝构造器
  10. }
复制代码
函数传参的时候,如果是值类型参数,那么会调用拷贝构造器。
再来看看函数返回值:
  1. class Data{}; // 内部省略
  2. Data getData()
  3. {
  4.     Data d1{};
  5.     return d1; // 这里也是调用拷贝构造器
  6. }
  7. void test()
  8. {
  9.     Data d{getData()}; // 这里依然调用了拷贝构造器
  10. }
复制代码
从理论上来看,上面的
  1. Data d{getData()}
复制代码
这一句应该调用两次拷贝构造

  • 第一次是函数getData内部的一个局部d1,拷贝给了一个临时匿名变量
  • 第二次是这个临时匿名变量拷贝给了变量d
但是如果你在拷贝构造器里加上打印,你会发现,没有任何东西会打印出来,也就是说,压根就没有调用到拷贝构造器。
这不代表上面关于函数的说法是错的,这只是编译器的优化而已,因为来来回回的拷贝,实在是没有必要,所以在某些编译器认为可以的情况下,编译器就直接省了。这个不重要,就不具体往里面细说规则了。

自定义拷贝构造器

大部分时候,编译器生成的这个拷贝构造器就满足需求了。
但是,如果我们的class包含了动态资源,比如说一个堆上动态的int数组, 默认的拷贝构造器就没那么好用了:
  1. class Data
  2. {
  3.     int m_size; // 数组的元素个数
  4.     int* m_ptr; // 指向数组首元素的指针
  5.     public:
  6.     Data(int size):m_size{size}
  7.     {
  8.         if (size > 0)
  9.         {
  10.             m_ptr = new int[size]{};
  11.         }
  12.     }
  13.     ~Data()
  14.     {
  15.         delete[] m_ptr;
  16.     }
  17. };
复制代码
由于这个Data类,拥有一个动态的数组,所以我们提供了一个析构函数,省的这块内存不会被回收。
然后,我们没有提供一个拷贝构造器,所以编译器就给我们添加了一个:
  1. class Data
  2. {
  3.     // 忽略别的代码,现在只关注拷贝构造器
  4.     Data(const Data& other):m_size{other.m_size}, m_ptr{other.m_ptr}
  5.     {}
  6. };
  7. void test()
  8. {
  9.     Data d1{10}; // 第一句
  10.     Data d2{d1}; // 第二句
  11. }
复制代码
没什么悬念,就是按照成员,逐个拷贝,注意,连指针也是直接拷贝。
所以上述test函数中,第二句执行了之后,整个内存应该是这样的:
030034qbbjip6ccbbudot1.webp

这有问题吗?
有很大的问题,考虑一下test函数执行完毕前,是不是需要对这两个变量 d1,d2d1, d2d1,d2 进行析构。
你会发现,两次析构,delete 的资源是一份!!!
一份资源,被delete两次,这就是所谓
  1. double free
复制代码
问题。
还有别的问题吗?
有。考虑下面的代码:
  1. void foo(Data d)
  2. {
  3.     // 一些逻辑
  4. }
  5. void test()
  6. {
  7.     Data d1{10};
  8.     foo(d1);
  9.     //
  10. }
复制代码
上面代码里,foo执行完之前,会析构这个局部变量d!导致资源已经被delete!
而外面d1和里面的d,指向的是同一份资源,也就是说,foo执行完之后,d1.m_ptr 成为了一个
  1. 悬挂指针
复制代码
!
没办法了,只能靠自己定义拷贝构造器,来解决上面的问题了:
  1. class Data
  2. {
  3.     int m_size; // 动态数组的元素个数
  4.     int* m_ptr; // 指向数据的指针
  5.     public:
  6.     Data(const Data& other){
  7.         if(other.m_ptr)
  8.         {
  9.             auto temp_ptr { new int[other.m_size]};
  10.             std::copy(other.m_ptr, other.m_ptr + other.m_size, temp_ptr);
  11.             m_ptr = temp_ptr;
  12.             m_size = other.m_size;
  13.         }
  14.         else
  15.         {
  16.             m_ptr = nullptr;
  17.         }
  18.     }
  19. };
复制代码
上面的拷贝构造器,才是真正的拷贝,这种拷贝一般称之为
  1. 深拷贝
复制代码

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

拷贝赋值,copy assignment

想要完成
  1. 深拷贝
复制代码
,到现在只进行了一半。
剩下的一般就是重载一个操作符,
  1. operator=
复制代码
,这是用来解决如下形式的拷贝:
  1. Data d1{10};
  2. Data d2{2};
  3. ///
  4. d2 = d1;
复制代码
这里,两个变量 d1,d2d1, d2d1,d2 都自己进行了初始化,在经过一堆代码逻辑之后,此时我们的需求是:

  • 清除 d2 的数据
  • 将 d1 完整的拷贝给 d2
两个类对象之间用赋值操作符,其实是调用了一个成员函数:
  1. operator=
复制代码

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

还是一样的,如果我们不提供一个自定义的
  1. operator=
复制代码
, 那么编译器会给我们添加一个如下的:
  1. class Data
  2. {
  3.     int m_size;
  4.     int* m_ptr;
  5.     public:
  6.     Data(int size):m_size{size} // 普通构造器
  7.     {
  8.         if (size > 0)
  9.         {
  10.             m_ptr = new int[size]{};
  11.         }
  12.     }
  13.     Data(const Data& other) // 拷贝构造器
  14.     {
  15.         if(other.m_ptr)
  16.         {
  17.             auto temp_ptr { new int[other.m_size]};
  18.             std::copy(other.m_ptr, other.m_ptr + other.m_size, temp_ptr);
  19.             m_ptr = temp_ptr;
  20.             m_size = other.m_size;
  21.         }
  22.         else
  23.         {
  24.             m_ptr = nullptr;
  25.         }
  26.     }
  27.     ~Data()               // 析构
  28.     {
  29.         delete[] m_ptr;
  30.     }
  31.     ///////// 编译器自动添加的 operator=
  32.     Data& operator=(const Data& other)
  33.     {
  34.         m_size = other.m_size;
  35.         m_ptr = other.m_ptr;
  36.         return *this;
  37.     }
  38. };
复制代码
看这个编译器自动添加的
  1. operator=
复制代码
, 是显而易见能发现问题的:

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

自定义 operator=

还是得靠自己来编写
  1. operator=
复制代码

前方警告,终于要点题了,
  1. copy and swap
复制代码
即将出现。
先按照我们的思路来写一个:
  1. Data& operator=(const Data& other)
  2. {
  3.     // 1. 首先清除本身的资源
  4.     delete[] m_ptr;
  5.     // 2. 拷贝other的资源
  6.     m_size = other.m_size;
  7.     if (other.m_ptr)
  8.     {
  9.         m_ptr = new int[m_size];
  10.         std::copy(other.m_ptr, other.m_ptr+m_size, m_ptr);
  11.     }
  12.     return *this;
  13. }
复制代码
如果按照上面的代码,来看下面的test函数,会发生什么问题:
  1. void test()
  2. {
  3.     Data d1{10};
  4.     d1 = d1; // 自己赋值给自己
  5. }
复制代码
我们在
  1. operator=
复制代码
里面看见,上来直接把整个资源删除了,GG!
我们要加一个判断:
  1. Data& operator=(const Data& other)
  2. {
  3.     if (this == &other) // 加了一个判断
  4.     {
  5.         return *this;
  6.     }
  7.     // 1. 首先清除本身的资源
  8.     delete[] m_ptr;
  9.     // 2. 拷贝other的资源
  10.     m_size = other.m_size;
  11.     if (other.m_ptr)
  12.     {
  13.         m_ptr = new int[m_size]; // 这句有可能异常
  14.         std::copy(other.m_ptr, other.m_ptr+m_size, m_ptr);
  15.     }
  16.     return *this;
  17. }
复制代码
关于这里加不加判断,很多大师级人物也认为不该加:

  • 谁会写出这种
    1. d1 = d1;
    复制代码
    这种代码???加了判断,徒增烦恼而已。
再来看上面注释那个, new 在申请新的内存的时候,可能会发生异常,此时出现了一个问题,在文章开头提及的:

  • 内存合法性
m_size 已经拷贝过来了
而真正的数据没有拷贝过来,导致这两个变量,不满足我们的业务合法性。
所以再改改:
  1. Data& operator=(const Data& other)
  2. {
  3.     // 1. 首先清除本身的资源
  4.     delete[] m_ptr;
  5.     m_ptr = nullptr;
  6.     // 2. 拷贝other的资源
  7.     auto temp_size {other.m_size};
  8.     if (other.m_ptr)
  9.     {
  10.         m_ptr = new int[temp_size];
  11.         std::copy(other.m_ptr, other.m_ptr+temp_size, m_ptr);
  12.         m_size = temp_size;
  13.     }
  14.     return *this;
  15. }
复制代码
此时此刻,这个代码已经没啥大问题了,除了一样:

  • 代码重复了,我们发现在拷贝other的数据的时候,逻辑是和拷贝构造器是一模一样的
c++里有一个原则:
  1. DRY
复制代码
: Do not Repeat Yourself。
别写重复的代码!
所以接着往下,copy-and-swap正式出场:

copy-and-swap 语义


  • 首先copy就是指拷贝构造器
我们先来讲讲swap是个啥。
就是说,我们需要写一个函数swap,如下:
  1. class Data
  2. {
  3.     // 其余部分省略,将重点放在swap函数
  4.     friend void swap(Data &left, Data& right)
  5.     {
  6.         std::swap(left.m_size, right.m_size);
  7.         std::swap(left.m_ptr, right.m_ptr);
  8.     }
  9. };
复制代码
这个swap函数很简单,就是交换两个已有的Data对象的内部数据,仅此而已。
现在,

  • copy有了
  • swap有了
让我们写出最终极的
  1. operator=
复制代码
:
  1. Data& operator=(Data other)
  2. {
  3.     swap(*this, other);
  4.     return *this;
  5. }
复制代码
是不是惊呆了,就这么两句,就行了!
仔细领略一下这个写法的高深之处:

  • 函数传参,用的值传参,而非引用,所以此时会调用拷贝构造器(copy)
  • 函数内部,交换了当前对象,和局部临时变量other的数据(swap)
你可能会问,没有清除自身的资源啊???
注意,other 是一个局部临时变量,这个函数结束之前,会进行析构,而析构的时候,other身上已经是被交换过的了,所以other被析构的时候,就是自身资源清除的时候。
妙,妙,妙!!
用如此短的代码实现了
  1. operator=
复制代码
, 实在是妙~
以上就是C++语义copy and swap示例详解的详细内容,更多关于C++语义copy and swap的资料请关注晓枫资讯其它相关文章!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表