拷贝构造函数其实就是对象的拷贝,在程序设计中,对象的拷贝其实很常见。
(1)拷贝构造函数的两种实现方式
【方式一】
Copy c1(10), c2;
c2 = c1; // 对象的默认拷贝方式
采用直接赋值的方式,Copy 是一个类,它定义了两个对象:c1 和 c2,将 c1 直接赋值给 c2。
【方式二】
Copy(const Copy& copy)
{
this->a = copy.a;
}
Copy c1(10);
Copy c2(c1); // 拷贝构造函数
自定义一个 构造函数,参数时对象的引用,将已经建立实例的对象作为实参传递构造方法中,从而实现对象的拷贝。
(2)浅拷贝和深拷贝
浅拷贝 只是对指针的拷贝,浅拷贝后两个指针指向同一个内存空间;
深拷贝 不仅对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针;
当对一个已知对象进行拷贝时,编译系统会自动调用一种构造函数——拷贝构造函数,如果用户未定义拷贝构造函数,则会调用默认拷贝构造函数。
当拷贝一个基类指针到派生类时,如果调用系统默认的拷贝构造函数,这时只是对指针进行拷贝,两个指针指向同一个地址,这就会导致指针被分配了一次内存,但内存被释放了两次(两次调用析构函数),造成程序崩溃。所以在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。
到底是 浅拷贝 还是 深拷贝? 这个话题的讨论是针对于类中存在指针成员的前提之下的,所以我们
先看下如下演示代码:
class Copy
{
public:
Copy()
{
cout << "执行了无参构造函数" << endl;
}
Copy(int a)
{
cout << "执行了有参构造函数" << endl;
this->a = a;
}
Copy(const Copy& copy)
{
cout << "执行拷贝构造函数" << endl;
this->a = copy.a;
}
~Copy()
{
cout << "执行了析构函数" << endl;
}
int getA()
{
return a;
}
void setA(int a)
{
this->a = a;
}
int* getP()
{
return p;
}
void setP(int* p)
{
this->p = p;
}
private:
int a;
int* p;
};
void print(Copy& copy) // 打印数组
{
cout << copy.getA() << endl;
int* p = copy.getP();
for (size_t i = 0; i < 10; i++) // 打印数组的值
{
cout << *p << " ";
p++;
}
cout << endl;
}
Copy 有一个成员变量是一个指针p,对p的拷贝到底是 浅拷贝 还是 深拷贝 是根据多个Copy对象中的指针 p 指向的内存是 共享 还是 独立 来判断,如果多个Copy对象中的指针 p 指向的内存是同一块内存,那么是 浅拷贝,不是它们的内存都是独立存在的,那么是 深拷贝。
现在在栈空间中创建c1和c2两个对象,并且c1初始化:
int* name = new int[10]; // 在堆内存开辟空间
for (size_t i = 0; i < 10; i++) // 数组的初始化
{
name[i] = i;
}
Copy c1(10), c2; // 定义变量 c1 和 c2
c1.setP(name); // c1的成员变量p指向name
c2使用直接赋值的方式对c2进行初始化(将c1拷贝到c2):
c2 = c1; // 拷贝
c1初始化后,a = 10,p指向name数组,数组的值为:{0,1,2,3,4,5,6,7,8,9};
c2是通过赋值( c2 = c1)的方式初始化的,开始打印c1和c2的值:
cout << "打印 c1:" << endl;
print(c1); // 打印c1数组
cout << "打印 c2:" << endl;
print(c2); // 打印c2数组
打印结果是:
打印 c1:
10
0 1 2 3 4 5 6 7 8 9
打印 c2:
10
0 1 2 3 4 5 6 7 8 9
c1和c2的a的值都是10,a是非指针成员,非指针成员不用管它,因为非指针成员无法区分到底是 浅拷贝 还是 深拷贝。
c1和c2的成员变量p指向的数组的值是一致的,但是此时依然无法确定到底是 浅拷贝 还是 深拷贝。
为了区分 浅拷贝 还是 深拷贝,我们的策略是对c2的成员p指向的数组重新赋值,再次打印c1和c2指针p的数组的值。
下面,对c2的p指向的数组重新赋值:
for (size_t i = 0; i < 10; i++) // 数组的初始化
{
name[i] = i * 2;
}
c1.setP(name);
输出代码如下:
c1.setA(20);
cout << "打印 c1:" << endl;
print(c1); // 打印c1数组
cout << "打印 c2:" << endl;
print(c2); // 打印c2数组
输出的结果为:
打印 c1:
20
0 2 4 6 8 10 12 14 16 18
打印 c2:
10
0 2 4 6 8 10 12 14 16 18
c1和c2的a的值分别是20、10,a是非指针成员,非指针成员不用管它,因为这只能说明c1和c2是独立内存,c1和c2在拷贝之前就已经开辟了空间,我们研究的对象是指针p是 浅拷贝 还是 深拷贝。
c1和c2的成员变量p指向的数组的值是一致的,当c2的指针p指向数组的值改变时,c1的指针p指向的数组的值也发生了改变,此时已经可以给出结论:c1和c2的指针p所指向的是同一块内存空间,像这种只复制指针,没有复制指针所指向的空间和空间中的数据,就叫做 浅拷贝;
所以,另一个结论就是:对象直接赋值的方式(默认拷贝方式)叫做浅拷贝。
如果,我们给指针p所指向的数组重新分配内存空间,那么就做到了 完全拷贝,这种方式叫做 深拷贝;
演示代码如下:
Copy(const Copy& copy)
{
cout << "执行拷贝构造函数" << endl;
this->a = copy.a;
// this->p = new int[10]; // 将p指向重新分配的内存空间
this->p = (int*)malloc(sizeof(int) * 10); // 将p指向重新分配的内存空间
memcpy(this->p, copy.p, 10 * sizeof(int)); // 将 copy.p 所指向的内存区域复制 10 * sizeof(int)个字节到 this->p 中
}
写一个构造函数,形式参数是本身对象的引用。
将指针 p 指向新的内存空间,开辟新的内存空间常用方式有两种:
【第一种】
C++ 新增的方式
this->p = new int[10]; // 将p指向重新分配的内存空间
使用 new 关键字开辟内存。
【第二种】
C 的方式
this->p = (int*)malloc(sizeof(int) * 10); // 将p指向重新分配的内存空间
使用 malloc 或者 calloc 函数开辟内存。
最后,使用 memcpy 函数将一个内存空间的数据拷贝到另一个内存空间中;
我们替换掉直接赋值的拷贝代码:
Copy c2(c1); // 拷贝
执行的结果是:
打印 c1:
0 2 4 6 8 10 12 14 16 18
打印 c2:
0 1 2 3 4 5 6 7 8 9
c1和c2的指针p所指向的内存空间不一致,所以这种方式叫做 深拷贝*。
注意,我们并没有说 Copy c2(c1) 的赋值方式是 深拷贝*,如果将构造函数的代码改成直接赋值指针,而不重新开辟内存:
Copy(const Copy& copy)
{
cout << "执行拷贝构造函数" << endl;
this->a = copy.a;
this->p = copy.p;
}
那么,c1和c2的成员p所指向的内存空间是同一个,这个方式依然叫做 浅拷贝。
所以,判断是哪种拷贝方式的核心思想是:两个指针指向的内存空间是否是同一个
。
(3)析构函数释放指针的注意点
为了防止内存泄漏,我们需要在析构函数中释放指针:
~Copy()
{
delete p;
}
我们知道,c1和c2对象创建时,都执行了构造函数,相应的,他们的析构函数要 执行两次,即 delete p 会 执行两次。
如果是 浅拷贝,那么c1的指针p和c2的指针p指向同一块内存区域,第一此执行 delete p 后,p所指向的内存空间已经释放,当第二次执行 delete p 后,由于p指向的内存空间已经释放,所以程序会 直接崩溃。
如果是 深拷贝,c1的p和c2的p指向不同的内存空间,执行两次析构函数分别释放两块内存空间,所以程序不会崩溃。
所以得出结论:如果类中含有指针,那么它的拷贝不能用直接赋值的方式,直接重写构造方法,给指针重新分配内存空间
。
[本章完...]
网友评论