• C++对象拷贝


    前言:本教程使用到的工具是vs2010;能用VC++6就用VC++6,因为vs2010生成的汇编代码可能有点乱;此外,文章中涉及的汇编,我都会予以解释,所以放心观看。

    目录

    一、什么是对象拷贝?

    二、C++对象拷贝

            栈区拷贝

            堆区拷贝

            父类的值是否会被拷贝

            默认拷贝构造---浅拷贝存在的问题

            深拷贝

    赋值运算符实现深拷贝

    总结


    一、什么是对象拷贝?

            首先看下面这张图:

            假设我们现在有一个对象数组,里边有三个对象,O1、O2和O3;但是随着业务逻辑的提升,这三个对象已经无法满足我们的使用了;那么我们肯定要再添加几个对象进去,但是我们又不想重新创建O1、O2、O3;那么怎么办呢?

            首先在C语言中,我们肯定是用一些拷贝函数之类的,通过赋值拷贝将一个对象的内容拷贝给另一个对象进行使用;这个过程就叫对象拷贝

            当然C++中也有类似拷贝对象内容的方式,并且C++的编译器支持我们用更简单的方式进行内存复制,接下来就让我们看看C++中如何实现对象拷贝;

    二、C++对象拷贝

            栈区拷贝

            首先测试代码如下:

    1. #include
    2. #include
    3. class CObject
    4. {
    5. private:
    6. int x;
    7. int y;
    8. public:
    9. CObject() {}
    10. CObject(int x,int y)
    11. {
    12. this->x = x;
    13. this->y = y;
    14. }
    15. };
    16. int main()
    17. {
    18. return 0;
    19. }

            我们定义了一个类,这个类有两个成员x和y,然后有两个构造函数;

            那么C++中拷贝构造函数是什么样的呢?如下:

            我们先验证一下,是否真正地实现了拷贝:

            没问题;但是问题来了,我们没有写任何拷贝构造函数,那么上面的拷贝又是如何实现的呢?

            这就是C++比较人性化的一个特点,其实我们写好了任意一个类(类型)的时候,它默认都已经生成了一个拷贝构造函数,帮我们实现了最简单的内存复制; 

            那么这个拷贝构造函数是如何实现的呢?

            断点打在拷贝构造的地方,编译、调试、ALT+8转到反汇编:

            其实C++默认提供的拷贝构造函数是很简单的,就是把第一个对象的值从内存地址取出来放到第二个对象里; 

            我们上面的拷贝方式是在栈区进行对象拷贝,下面我们看如何在对象拷贝构造对象;

            堆区拷贝

            我们知道,我们平常在堆区创建一个对象,基本new一个无参构造函数,或者有参构造函数:

            那么如果我们new一个拷贝构造函数,不就是在堆区进行拷贝构造了嘛,如下:

            没有问题,那么我们看看堆区的拷贝构造函数如何实现的,一样转到反汇编:

            我们拿下来一步一步分析:

    010113EA  push        8                                          -- 给new函数传入8,意为申请8字节内存
    010113EC  call        operator new (1011181h)      -- 调用new,之前的文章讲过new底层
    010113F1  add         esp,4                                    -- 外平栈
    010113F4  mov         dword ptr [ebp-0E4h],eax    -- 将eax(new返回的地址)放到ebp-E4这个地址里
    010113FA  cmp         dword ptr [ebp-0E4h],0        -- 将0与new的返回地址进行比较
    01011401  je          main+62h (1011422h)             -- 如果返回地址等于0(申请堆区内存为空)就跳转到1011422h
    01011403  mov         eax,dword ptr [ebp-0E4h]     -- 如果不为空,将返回地址(堆区地址)放到eax里
    01011409  mov         ecx,dword ptr [c1]                 -- 将c1的首地址(第一个值)取出,放到ecx里
    0101140C  mov         dword ptr [eax],ecx              -- 将ecx里的值放到eax这个地址里,也就是将c1的第一个成员放到堆区申请的地址中
    0101140E  mov         edx,dword ptr [ebp-8]           -- 将ebp-8(c1的第二个成员地址)里的值放到edx里,ebp-8指的就是c1的第二个成员,这里vs2010编译器生成的汇编有点乱
    01011411  mov         dword ptr [eax+4],edx           -- 将c1的第二个成员的值,放到申请到的堆区首地址+4的位置上
    01011414  mov         eax,dword ptr [ebp-0E4h]     -- 将ebp-0E4中的堆区地址再次赋值给eax,反正我是没有明白这一步的意义是什么,如果是怕别人修改了eax的值,导致后面返回给c2指针的是一个别人想要的地址的话,可以解释的通;当然大家如果有不同的看法,或者我讲错了,请评论告诉我,这里我迷瞪了老半天了
    0101141A  mov         dword ptr [ebp-0ECh],eax    --  将eax中存放的堆区地址存放到ebp-EC
    01011420  jmp         main+6Ch (101142Ch)          --  如果堆区地址不为空,跳过下一步
    01011422  mov         dword ptr [ebp-0ECh],0        --  申请的堆区地址为空的话,将ebp-EC地址中的值置空,因为下面要将EC中的值赋值给对象指针c2,如果申请地址为空,那就赋值空
    0101142C  mov         ecx,dword ptr [ebp-0ECh]    -- 无论此时EC地址中的值是一个已经拷贝了c1值的堆区地址,还是一个空地址,都要赋值给ecx
    01011432  mov         dword ptr [c2],ecx                 -- 将ecx赋值给对象指针c2

             我们可以看到如果是在堆区拷贝的话,是直接把c1的值放到堆区申请的地址里,然后再将这个已经赋好值的堆区地址赋值给指针c2;

            父类的值是否会被拷贝

            我们现在知道,C++提供的默认拷贝构造,可以将一个类的对象的值全部拷贝给另一个对象,那么问题来了,如果第一个对象有父类,构造的时候构造了一个父类,那么第二个对象能否继承第一个对象父类的值呢?

            代码如下:

    1. #include
    2. #include
    3. class CBase
    4. {
    5. private:
    6. int x;
    7. int y;
    8. public:
    9. CBase(){}
    10. CBase(int x,int y)
    11. {
    12. this->x = x;
    13. this->y = y;
    14. }
    15. };
    16. class CTeach:public CBase
    17. {
    18. private:
    19. int z;
    20. public:
    21. CTeach(){}
    22. CTeach(int x,int y,int z):CBase(x,y)
    23. {
    24. this->z = z;
    25. }
    26. };
    27. int main()
    28. {
    29. CTeach ct(10,20,30);
    30. CTeach ct1(ct);
    31. return 0;
    32. }

            断点打到return 0;

            调试:

            可以看到父类的也被拷贝过来了;

            如果是堆区拷贝呢?如下:

            一样,也被拷贝了过来;

            我们现在知道了,通过拷贝构造也是可以将源对象父类的值拷贝过来的; 

            上面这么一说,听起来默认拷贝构造很完美啊,但是真的是这样吗?如果是这样,那本篇文章也该结束了;

            下面说说默认拷贝构造的不足之处;

            默认拷贝构造---浅拷贝存在的问题

            我们先来看如下代码:

    1. #include
    2. #include
    3. #include
    4. class CString
    5. {
    6. private:
    7. int m_length;
    8. char* m_str;
    9. public:
    10. CString() {}
    11. CString(const char* str)
    12. {
    13. m_length = strlen(str)+1; // m_length大小为传入的str长度+1
    14. m_str = new char[m_length]; // 申请m_length长度的堆区空间
    15. memset(m_str,0,m_length); // 初始化申请到的堆区空间
    16. strcpy(m_str,str); // 将传入的str拷贝到该堆区空间中
    17. }
    18. ~CString()
    19. {
    20. delete[] m_str;
    21. }
    22. };
    23. int main()
    24. {
    25. CString cs01("C语言");
    26. CString cs02(cs01);
    27. return 0; // 断点打在这里
    28. }

            我们调试一下:

            查看监视窗口:

            我们可以看到,使用默认拷贝构造的时候,cs02将cs01的简简单单的拷贝了过来;

            为什么这样说呢?

            首先,cs01的m_str成员申请到了一个堆区的地址,所以此时m_str中存放的是一个地址,只是这个地址中的值是"C语言";

            当cs02使用默认拷贝构造的时候,将cs01的m_str这个指针的值直接拷贝了过来,我们知道cs01的m_str的值就是cs01申请到的堆区地址,如果cs02的m_str和cs01的m_str指针的值一样的话,这就意味着cs02和cs01两个对象的指针成员指向了同一块地址;

            这会造成什么样的后果呢?

            我们想一下,如果对象cs01的生命周期结束了,是不是会调用析构函数?并且我们析构函数里边写的是释放m_str申请的内存;

            如果cs01对象释放了m_str的内存,那么指向相同地址cs02::m_str的地址也会被释放,但是cs02生命周期并没有结束啊,如果cs02再次操作m_str这个指针成员,会访问到一个不属于自己的地址,导致野指针问题的出现;

            上面这种将值简简单单的拷贝过来的拷贝构造,被称为浅拷贝(值拷贝);

            那么如果遇到这种情况我们应该如何解决呢?

            解决方法就是自己写一个深拷贝构造函数

            深拷贝

            直接看深拷贝实现的代码,然后一步一步分析,如下:

            这就是重写的深拷贝构造函数,实现深拷贝的原理主要是重新申请一份堆区地址,将原来对象堆区地址中的值拷贝到新的堆区地址中 ;

            下面让我们测试一下效果如何:

            没有问题,两个对象指向了不同的地址,不用担心一个对象的指针被释放了会影响到另一个对象了;

            当然深拷贝还有一种写法就是传入指针,如下:

            测试一下效果:

            没有问题; 

            另外提一下,如果你自己添加了拷贝构造函数,那么编译器不会再为你提供默认的拷贝构造函数,所有的事情你都必须在你自己写的拷贝函数里做好;

            这句话怎么理解呢?

            因为有时候可能有些人会想,既然我添加拷贝构造函数只是为了弥补浅拷贝的不足,只是为了实现深拷贝,那么我就给指针重新申请一个地址不就行了吗?对于不是指针的成员(例如我们上面写的m_length)我就不管了,让编译器自己提供的默认拷贝构造函数去复制,我只关心指针成员不就行了;

            这种想法不可取,因为我们上面说了,如果你自己写了拷贝构造,无论你怎么写,编译器都不会再提供拷贝构造了,所以你不能只在乎指针成员,所有成员你都要自己手动复制,因为编译器不会再帮你了;

            这也就有了一句话:如果不需要深拷贝,不要自己添加拷贝构造函数!

    赋值运算符实现深拷贝

            我们上面学了默认的拷贝构造是浅拷贝,但是我们自己可以实现深拷贝;

            其实在C++中,也可以直接使用赋值运算符进行拷贝构造,和编译器提供的默认构造方式一样都是浅拷贝,如下:

    ​​​​​​​

     

            那么我们怎样实现深拷贝呢?和上面的差不多,只不过一个是重载构造函数,一个是重载算术运算符:

            测试一下,如下:

     

            查看一下值:

     

            实现了深拷贝;

            至于为什么构造时候的 = 和我们重载的 = 不一样,请看大佬的这篇文章《C++深拷贝赋值运算符》 

            需要注意的是:

            1、如果重载了赋值运算符,那么我们要和自己写深拷贝构造函数一样,必须对所有的属性都要处理;

            2、如果不需要用到深拷贝,没必要重载 = ;

     

    总结

            1、当我们构造对象的时候传入的参数是一个对象的话,那么编译器会为我们提供一个默认的拷贝构造函数,但是这个默认的拷贝构造是浅拷贝,只能帮我们把一个对象的值复制给另一个对象;

            2、我们可以自己实现深拷贝构造函数,解决浅拷贝复制指针的问题;

            3、如果不需要深拷贝,不要自己添加拷贝构造函数!

            4、如果你自己添加了拷贝构造函数,那么编译器不会再为你提供默认的拷贝构造函数,所有的事情你都必须在你自己写的拷贝函数里做好;

            5、如果重载了赋值运算符,那么我们要和自己写深拷贝构造函数一样,必须对所有的属性都要处理;

            6、如果不需要用到深拷贝,没必要重载赋值运算符 ;

    结语:如果有讲的不好的地方或者听不懂的地方,都欢迎在评论区留言或者私信,感谢大家的观看!

  • 相关阅读:
    java-php-python-航空订票管理系统计算机毕业设计
    SPA项目开发之首页导航+左侧菜单
    OpenCV实现多角度多尺度模板匹配(基于形状)
    Redis中的Lua脚本(五)
    【JavaWeb】会话 cookie
    十、2023.10.4.计算机网络(one).10
    kafka分布式安装部署
    PC_访存过程@内存地址翻译过程@具有快表TLB和cache的多级存储系统
    使用Java实现一个简单的贪吃蛇小游戏
    力扣200岛屿数量解法3种
  • 原文地址:https://blog.csdn.net/qq_52572621/article/details/127913478