我们知道当调用默认拷贝构造函数时,一个对象对另一个对象初始化时,这时的赋值时逐成员赋值。这就是浅拷贝,当成员变量有指针时,浅拷贝就会在析构函数那里出现问题。例如下面的例子:
1 //test.h 2 #ifndef MYSTRING_H 3 #define MYSTRING_H 4 class MyString 5 { 6 char* m_str; 7 public: 8 MyString(char* str=""); 9 ~MyString(); 10 void Display(); 11 }; 12 #endif //MYSTRING_H 13 14 15 //test.cpp 16 #define _CRT_SECURE_NO_WARNINGS 17 #include "MyString.h" 18 #include<iostream> 19 #include<cstring> 20 using std::endl; 21 using std::cout; 22 23 MyString::MyString(char* str) 24 { 25 m_str = new char[strlen(str) + 1]; 26 memset(m_str, 0, strlen(str) + 1); 27 strcpy(m_str,str); 28 } 29 30 31 MyString::~MyString() 32 { 33 cout << "destructor" << endl; 34 delete[] m_str; 35 //m_str = 0; 36 37 } 38 39 void MyString::Display(){ 40 cout << m_str << endl; 41 } 42 43 //demo.cpp 44 #include"MyString.h" 45 #include<iostream> 46 using std::endl; 47 using std::cout; 48 49 int main(){ 50 MyString a("aaaaaa"); 51 MyString b = a;//调用默认拷贝构造函数,此时b对象,和a对象都是指向同一块内存空间,当析构函数调用时,那么同一块内存空间会被释放两次,从而产生运行时错误。 52 a.Display(); 53 54 return 0; 55 }
所以此时默认拷贝构造函数就出现问题,此时应该提供自己的拷贝构造函数,来实施深拷贝。
在类里面添加拷贝构造函数
void MyString::AllocMemAndCpy(char* other){//类里面的工具函数,负责分配内存和拷贝内存中的值 int len = strlen(other) + 1; m_str = new char[len]; memset(m_str,0,len); strcpy(m_str,other); } MyString::MyString(const MyString& other){//拷贝构造函数 AllocMemAndCpy(other.m_str); }
1 #include"MyString.h" 2 #include<iostream> 3 using std::endl; 4 using std::cout; 5 6 int main(){ 7 MyString a("aaaaaa"); 8 MyString b = a; 9 a.Display(); 10 b.Display(); 11 return 0; 12 }
此时运行结果就是正确的了,不会出现运行时错误了。
注意:当类中的成员变量有指针时,就要小心了。此时可能就需要拷贝构造函数了,还有就是类中有共享内存时,这是要在析构函数中计算还剩下多少对象,只有剩下最后一个对象被销毁时才能delete那块共享内存。
默认的赋值运算符也是浅拷贝,如果不添加自己的等号运算符,这个程序还是会出现运行时错误
1 #include"MyString.h" 2 #include<iostream> 3 using std::endl; 4 using std::cout; 5 6 int main(){ 7 MyString a("aaaaaa"); 8 MyString b = a; 9 a.Display(); 10 b.Display(); 11 12 MyString c; 13 c.Display(); 14 c = a;//程序在此处调用了默认的赋值运算符(=operator),默认的赋值运算符也是浅拷贝 15 //因此这时出现的运行时错误,也是在调用析构函数时,销毁两次同一块内存,为了使剩下成功运行,必须提供自己的等号运算符。 16 17 return 0; 18 }
上面的代码需要特别注意的是c=a;,这种赋值会造成被删除的两个指针指向同一块内存,在析构函数调用时,同一块内存会被析构函数释放两次。但是如果c=c;,这句代码就不会造成上述的错误,因为自己给自己赋值并没有使两个对象的指针同时指向一个块内存,就自然只会调用一次析构函数,从而释放一次对象。但是为了顾及中情况,应该添加如下的赋值运算符函数。
1 MyString& MyString::operator=(const MyString& other){ 2 if (this ==&other)//赋值对象与被赋值对象是同一个对象。即语句a=a; 3 return *this; 4 delete m_str;//因为赋值对象与被赋值对象不是同一个对象,因此 被赋值对象之前指向的内容应该被删除,即a=c;那么a之前指向的内存应该被释放。 5 AllocMemAndCpy(other.m_str); 6 return (*this); 7 }
禁止拷贝:
有时候有些对象时独一无二的,那么是独一无二的对象就禁止拷贝,禁止拷贝的办法就是把operator=() 与拷贝构造函数都声明成私有的,甚至是空函数都可以
1 #ifndef MYSTRING_H 2 #define MYSTRING_H 3 class MyString 4 { 5 char* m_str; 6 void AllocMemAndCpy(char* other); 7 MyString(const MyString& ms){} 8 MyString& operator=(const MyString& other){} 9 10 public: 11 MyString(char* str=""); 12 ~MyString(); 13 14 15 void Display(); 16 }; 17 #endif //MYSTRING_H
此时拷贝构造函数和operator=()都是声明在private中,并且都是空函数。此时Test a=c; b=c;这样的语句在编译时就会报错。
我们知道当我们不提供构造函数时,编译器会提供默认构造函数,当我们不提供拷贝构造函数时,编译器会提供默认拷贝构造函数,那么再创建一个空的类时,编译器到底提供什么呢?
1 class Empty {}; 2 Empty(); // 默认构造函数 3 Empty( const Empty& ); // 默认拷贝构造函数 4 ~Empty(); // 默认析构函数 5 Empty& operator=( const Empty& ); // 默认赋值运算符 6 Empty* operator&(); // 取址运算符 7 const Empty* operator&() const; // 取址运算符 const
上面的就是一个空类,我们在使用时要注意,有时候这些编译器默认提供的不适合,我们要自己写。