使用场景
在C++类中,我们在类的成员变量内定义了一个指针。这时我们需要去创建它的拷贝构造函数,否则编译器会为这个类创建默认的拷贝构造函数,而默认拷贝构造函数会导致浅拷贝问题;浅拷贝可能会会导致内存泄漏问题,也可能遇到双重析构问题
原理
比如我们有下面这个类
class myString {
public:
myClass(const char *cstr = 0) {
if (cstr)
{
m_data = new char[strlen(cstr) + 1];
strcpy(m_data, cstr);
}
}
~myClss();
private:
char *m_data;
};
如果我们使用这个类去创建一个实例,并用该实例给另一个实例赋值的话,就可能导致浅拷贝问题。
int main() {
myString c1;
myString c2(c1);//不行,浅拷贝
return 0;
}
这是因为,编译器的拷贝构造函数是将c1的数据原封不动地颁给c2,如果类里面没有指针,当然没有问题,但是当类里存在指针,编译器就会把c1的指针指向的地址原封不动地给c2,如图:
而且,当实例c1的生命结束时,我们释放指针,而另一个实例c2没有释放,这时去使用它会导致不可预期的错误。同时,当程序离开两个myString所在的作用域,程序会按顺序调用两次析构函数,后执行的析构将释放一个不存在的内存空间,这也是一个未定义的行为。
GDB调试验证
现在我们写好了一个C++代码如下
#include<cstring>
using namespace std;
class myString{
public:
myString(const char* c_str = 0){
if (c_str) {
m_data = new char[strlen(c_str) + 1];
strcpy(m_data, c_str);
} else {
m_data = new char[1];
*m_data = '\0';
}
}
~myString(){
delete[] m_data;
}
private:
char *m_data;
};
int main() {
const char* str = "HelloWorld!";
myString c1(str);
myString c2(c1);
return 0;
}
我们来进行GDB调试
先简单打几个断点:
然后把程序跑起来,打印str的值
然后把c1.m_data和c1.m_data的值固定输出,接着按n(单步执行,跳过函数)
可以看到c1和c2的m_data都成功赋值了HelloWorld!,但是它们都指向了同一个指针0x55555556aeb0,接下来我们按s继续调试(单步执行进入函数);
可以看到调用一次析构,析构的是this=0x7fffffffe368内存实例,而当函数出来时,两个实例的m_data指向的内存内容都没有了
我们继续往下看
我们看到this指针两次指向的时不同的地址,也就是析构函数是析构的不同实例。到此为止,我们成功用GDB验证了默认构造函数出现的问题。下面我们用深拷贝解决这个问题。
我们在有指针的类里面需要自定义拷贝构造函数:
myString::myString (const myString& x) {
m_data = new char[ strlen (x.m_data) + 1];
strcpy(m_data, x.m_data);
}
改完之后的我就不进行GDB调试了,总之,当这样更改完之后,我们的拷贝构造就变成了深拷贝,避免了双重析构的问题。
相关知识点
拷贝构造函数和默认的构造函数有一些区别,总的来说使用拷贝构造函数的初始化成为拷贝初始化,《C++ Primer》中介绍了几种除了上述所描述的情形以外会发生拷贝初始化的场景:
- 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员