前言:本教程使用到的工具是vs2010;能用VC++6就用VC++6,因为vs2010生成的汇编代码可能有点乱;此外,文章中涉及的汇编,我都会予以解释,所以放心观看。
目录
一、什么是对象拷贝?
首先看下面这张图:
假设我们现在有一个对象数组,里边有三个对象,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、如果不需要用到深拷贝,没必要重载赋值运算符 ;
结语:如果有讲的不好的地方或者听不懂的地方,都欢迎在评论区留言或者私信,感谢大家的观看!