首先,我们先写一个并不完美的类:
#include<iostream>
#include<cstring>
using namespace std;
class Mystring{
private:
char *p;
int len;
static int num;
friend ostream& operator<<(ostream& os, const Mystring& c);
public:
Mystring(const char *q);
Mystring(const Mystring& t);
~Mystring();
Mystring& operator=(const Mystring& t);
};
int Mystring::num=0;
Mystring::Mystring(const char *q){
len=strlen(q);
p=new char[len+1];
strcpy(p,q);
num++;
}
Mystring::~Mystring(){
delete []p;
p=nullptr;
}
ostream& operator<<(ostream& os, const Mystring& c){
os << c.p << " num: " << c.len << " all number: " << c.num;
return os;
}
int main(){
Mystring a("asdf");
Mystring b{a};
Mystring c=b;
Mystring d("ghjkl");
d=c;
cout << a << endl << b << endl << c << endl << d;
}
大家直接看代码,可能会有一点点费劲,这里讲解一下。我们这个类有一个char*p。这个地方很关键。因为我们在构造函数中,使用了new去动态创建了一个对象。这个对象中,拥有了这个动态创建的指针。
随后,我们使用了=号。用于一个对象为另一个对象赋值,拷贝。
问题随之而来,因为我们把指针也相等的赋值了过去。这意味着,两个对象的指针指向了同一段内存,而这一段内存是我们使用动态分配new主动分配的。当其中一个对象调用了析构函数,将自己销毁掉。这个对象里面指针所指向的内存也随之释放。但是,另一个对象的指针还指向了刚才被释放掉的内存。这个时候,如果继续对剩下的对象进行销毁,调用析构函数。就会对相同的内存连续释放(delete)了两次。从而导致程序崩溃。
C++提供下面这些默认函数(如果您没有提供):
- 默认构造函数。不接受参数也不执行任何操作。
- 默认析构函数,不执行任何操作。
- 拷贝(复制)构造函数。用对象初始化另一个新建对象,逐个复制非静态成员,复制的是成员的值(浅复制)。
- 拷贝赋值运算符。用对象赋值给另一个对象,逐个复制非静态成员,复制的是成员的值(浅复制)。
- 移动构造函数。C++11增加。
- 移动赋值运算符。C++11增加。
- 地址运算符。返回对象的地址。和我们想象一致,不再讨论。
使用默认拷贝构造函数和使用默认=赋值运算符,导致浅拷贝,在析构时会出现重复释放(delete)同一段内存,导致程序崩溃。
怎么解决这个问题呢?
1、自己定义拷贝构造函数和重载=号,实现深拷贝。
如下,我们自己实现了重载=和自定义拷贝构造函数。(这也是为什么,一旦类中出现了指针,就需要自己定义=和拷贝构造函数,因为一定会出现浅拷贝)只有自己实现了这些功能,才会实现深拷贝。
#include<iostream>
#include<cstring>
using namespace std;
class Mystring{
private:
char *p;
int len;
static int num;
friend ostream& operator<<(ostream& os, const Mystring& c);
public:
Mystring(const char *q);
Mystring(const Mystring& t);
~Mystring();
Mystring& operator=(const Mystring& t);
};
int Mystring::num=0;
Mystring::Mystring(const char *q){
len=strlen(q);
p=new char[len+1];
strcpy(p,q);
num++;
}
//重新写的拷贝构造函数
Mystring::Mystring(const Mystring& t){
p=new char[t.len+1];
len=t.len;
strcpy(p,t.p);
num++;
}
Mystring::~Mystring(){
delete []p;
p=nullptr;
}
//重新写的=号的重载
Mystring& Mystring::operator=(const Mystring& t){
delete[]p;
p=new char[t.len+1];
strcpy(p,t.p);
len=strlen(p);
return *this;
}
ostream& operator<<(ostream& os, const Mystring& c){
os << c.p << " num: " << c.len << " all number: " << c.num;
return os;
}
int main(){
Mystring a("asdf");
Mystring b{a};
Mystring c=b;
Mystring d("ghjkl");
d=c;
cout << a << endl << b << endl << c << endl << d;
}
浅拷贝:顾名思义,就是单纯的把值传了过去。但是因为涉及到了动态分配的内存,导致两个指针指向了同一个地方,最终释放的时候,会出现同一段内存被多次释放。最后崩溃掉。
而深拷贝:就是使用new重新再分配一段内存,分给拷贝的对象,不让两个指针指向同一段内存,只有他们各自指向了一段内存,才不会被多次释放。
重新写拷贝构造和重载=号,实际上就是多了一步new(分配内存)的步骤。代码如上,可以复制看看。
String s2(s1);
String s3 = s1;
string s4 = string(s1);
string* ps4 = new string(s1);
//1.调用拷贝构造函数
//2.调用拷贝构造函数
//3.调用拷贝构造函数
//4.调用拷贝构造函数
如上,是四种拷贝函数的用法。都是调用了拷贝构造函数。区别在于,有的是临时对象,有的是直接构造,有的是动态分配了一个,然后构造。
这里说一个重点,如果有动态分配的内存,那么new和delete在构造和析构中,一定要一一对应。
如果,一个类中,出现了指针并且涉及到了动态内存分布,我们就必须要重新写 析构函数 拷贝构造函数 拷贝赋值 (移动拷贝构造) (移动拷贝赋值)
如果对象中出现指针成员变量,那么必须实现构造函数、析构函数、拷贝构造函数和=重载函数,以达到深拷贝。
每日金句:
一切所谓不可能之事,其实都是尚未发生的事。
-----------黄泉