区分什么是初始化和赋值。
对于一个类来讲,如果用一个已经存在的对象去构造一个新对象,这个过程就是初始化的过程。如果用一个已经存在的对象去给另一个已经存在的对象赋值,这就是赋值的过程。
在初始化和赋值的过程中,假设类涉及到堆内存,如果采用编译器系统默认给定的拷贝构造函数和赋值运算符的重载函数进行对象之间的初始化过程和赋值过程,轻则发生浅拷贝,重则内存泄漏,这样的程序都是有问题的。下面分析为什么在使用默认的拷贝构造函数时会出现浅拷贝问题。
class Cstring{
private:
char* _data;
public:
Cstring(const char* str){
if(str!=NULL){
_data = new char[strlen(str)+1];
strcpy(_data,str);
}
else{
//不处理为NULL的原因是,降低类中其他函数的逻辑复杂度,统一处理
//不需要判断_data是否NULL,分开处理
_data = new char[1];
*_data = '\0';
}
}
~Cstring(){
delete[] _data;
}
};
测试程序 :
int main(){
Cstring str("hello");
Cstring str1 = str;
return 0;
}
因为调用默认的拷贝构造函数,只是简单的内存值拷贝,这样会使得对象str1中的_data和对象str中的_data指向同一块在堆上的内存空间,str1相对于str后构造,所以str1会先进行析构,释放掉了这块堆内存。使得str中的_data变为野指针,此str对象进行析构。将会释放野指针指向heap上的一块堆内存,这是非常不安全的。
解决方案:进行深拷贝,每个对象有自己的堆内存
Cstring::Cstring(const Cstring& src){
this->_data = new char[strlen(src._data)+1];
strcpy(_data,src.data);
}
如上图,这样就实现了在拷贝构造函数中深拷贝。
下面再讨论一下使用默认赋值运算符重载的拷贝构造函数在使用时出现的内存泄漏以及浅拷贝问题。
测试程序:
int main(){
Cstring str("hello");
Cstrint str1("world");
str = str1;//可以理解为str.=(str1)这样的调用方式
}
编译器系统默认的赋值运算符的重载函数是简单的赋值。
解决方案:自己实现赋值运算符重载函数,两个目的:1.避免出现内存泄漏的问题。2.进行深拷贝
Cstring& Cstring(const Cstring& src){
//防止发生在自赋值
if(this == &src){
return *this;
}
delete[] this->_data;//释放当前对象在堆上的空间,避免出现内存泄漏
_data = new char[strlen(src._data)+1];//申请空间,避免出现浅拷贝
strcpy(_data,src._data);//进行内存拷贝
return *this;
}
//经过上述的处理,内存泄漏和浅拷贝的问题都得以解决
总结:在实现类的构造函数中,往往会涉及到堆内存的开辟。如果对拷贝构造函数和赋值运算符的重载函数不进行重写,会发生浅拷贝以及内存泄漏等问题导致程序出错。