C++对象拷贝

28 篇文章 1 订阅

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

目录

一、什么是对象拷贝?

二、C++对象拷贝

        栈区拷贝

        堆区拷贝

        父类的值是否会被拷贝

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

        深拷贝

赋值运算符实现深拷贝

总结


一、什么是对象拷贝?

        首先看下面这张图:

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

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

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

二、C++对象拷贝

        栈区拷贝

        首先测试代码如下:

#include <stdio.h>
#include <Windows.h>

class CObject
{
private:
	int x;
	int y;
public:
	CObject() {}
	CObject(int x,int y)
	{
		this->x = x;
		this->y = y;
	}
};

int main()
{

	return 0;
}

        我们定义了一个类,这个类有两个成员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++提供的默认拷贝构造,可以将一个类的对象的值全部拷贝给另一个对象,那么问题来了,如果第一个对象有父类,构造的时候构造了一个父类,那么第二个对象能否继承第一个对象父类的值呢?

        代码如下:

#include <stdio.h>
#include <Windows.h>

class CBase
{
private:
	int x;
	int y;
public:
	CBase(){}
	CBase(int x,int y)
	{
		this->x = x;
		this->y = y;
	}
};

class CTeach:public CBase
{
private:
	int z;
public:
	CTeach(){}
	CTeach(int x,int y,int z):CBase(x,y)
	{
		this->z = z;
	}
};

int main()
{
	CTeach ct(10,20,30);

	CTeach ct1(ct);

	return 0;
}

        断点打到return 0;

        调试:

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

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

        一样,也被拷贝了过来;

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

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

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

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

        我们先来看如下代码:

#include <stdio.h>
#include <Windows.h>
#include <stdlib.h>

class CString
{
private:
	int   m_length;
	char* m_str;
public:
	CString() {}
	CString(const char* str)
	{
		m_length = strlen(str)+1;    // m_length大小为传入的str长度+1 
		m_str = new char[m_length];  // 申请m_length长度的堆区空间
		memset(m_str,0,m_length);    // 初始化申请到的堆区空间
		strcpy(m_str,str);           // 将传入的str拷贝到该堆区空间中
	}
    ~CString()
	{
		delete[] m_str;
	}
};

int main()
{
	CString cs01("C语言");
	CString cs02(cs01);
	return 0;                         // 断点打在这里
}

        我们调试一下:

        查看监视窗口:

        我们可以看到,使用默认拷贝构造的时候,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、如果不需要用到深拷贝,没必要重载赋值运算符 ;

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

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值