一、理解深拷贝和浅拷贝:
#include <iostream> using namespace std; class String { public: String(const char *str = "") { if(str == NULL) { data = new char[1]; data[0] = '\0'; } else { data = new char[strlen(str)+1]; strcpy(data,str); } } ~String() { delete []data; data = NULL; } private: char *data; }; int main() { String s1("hello"); String s2 = s1; String s3; s3 = s1; return 0; }
但析构的时候,会先释放s2指向的空间,但当析构s1指向的空间时,因为s2和s1是指向相同空间的,s2已经将空间释放,s1就没有空间可以释放,所以s1的析构就导致了程序的非法访问,造成程序的崩溃。这种现象就叫做浅拷贝,即只拷贝指针。
//重写拷贝构造函数: String(const String &s) //深拷贝 { data = new char [strlen(s.data)+1]; strcpy(data,s.data); } //重写赋值语句: String& operator=(const String &s) //深赋值 { if(this != &s) { delete []data; data = new char[strlen(s.data)+1]; strcpy(data,s.data) } return *this; }
深拷贝就是在拷贝的时候,将指针指向的空间也一同拷贝,这样,析构的时候,自己释放自己指向的空间就可以了。
二、理解深拷贝和浅拷贝各自的优缺点:
浅拷贝节省空间,相同的数据只保存一份,但因为多个指针指向同一个空间,会引发多次释放的问题;
深拷贝虽然每个指针会指向不同的空间,没有一个空间多次释放的问题,但可能保存的数据都是一样的,这样会导致空间的浪费。
三、使用引用计数解决浅拷贝实现中出现的问题:
所以只要能够解决浅拷贝中的同一个空间多次的释放的问题,当然是最好的!
这就引出了引用计数的方法:
当一个空间被一个指针指向时,计数为1,当每多一个指针指向时,计数加 1.
当析构时,释放一个指针对象,空间不释放,计数减 1,当计数为 0 时,释放空间
#include <iostream> using namespace std; class String { public: String(const char *str = "") { if(str == NULL) { data = new char[1]; data[0] = '\0'; } else { data = new char[strlen(str)+1]; strcpy(data,str); } ++use_count; } //重写拷贝构造函数: String(const String &s) //浅拷贝,引用计数加 1 { data = s.data; ++use_count; } //重写赋值语句: String& operator=(const String &s) //浅赋值,引用计数加 1 { if(this != &s) { data = s.data; ++use_count; } return *this; } ~String() //析构,引用计数减 1 { if(--use_count == 0) //当引用计数为 0 时,释放空间 { delete []data; data = NULL; } } private: char *data; static int use_count; }; int String::use_count = 0; int main() { String s1("hello"); String s2 = s1; return 0; }
运行上面的程序看着没有问题,可是,当我们再创建一个不同的对象时发现,不同的空间居然有相同的引用计数
String s3("world");
s3没有拷贝s1和s2,而是一个新的空间的指针对象,但我们发现还是相同的引用计数加 1,所以这样写的引用计数程序是有问题的。
注意:每个空间应该具有自己的引用计数,而不能所有空间共享一个引用计数。
四、解决引用计数中的写时拷贝技术实现
//引用计数器类 class String_rep { public: String_rep(const char *str):use_count(0) { if(str == NULL) { data = new char[1]; data[0] = '\0'; } else { data = new char[strlen(str)+1]; strcpy(data,str); } } String_rep(const String_rep &rep):use_count(0) { data = new char[strlen(rep.data)+1]; strcpy(data,rep.data); } String_rep& operatro=(const String_rep &rep) { if(this != &rep) { delete []data; data = new char[strlen(rep.data)+1]; strcpy(data,rep.data); } return *this; } ~String_rep() { delete []data; data = NULL; } public: void increment() { ++use_count; } void decrement() { if(--use_count == 0) { delete this; //调动自身的析构函数 } } private: char *data; int use_count; }; class String { public: String(const char *str = "") { rep = new String_rep(str); rep->increment(); } String(const String &s) { rep = s.rep; rep->increment(); } ~String() { rep->decrement(); } private: Stirng_rep *rep; }; int main() { String s1("hello"); String s2 = s1; String s3("world"); return 0; }
一个String对象中只维护一个 指向String_rep类的rep指针:
s1 String_rep
[rep] ----- > data -------->[ h e l l o \0]
| use_count
| /
s2 /
[rep]_____/
创建s1对象,调用构造函数,指向一个String_rep对象,引用计数加 1
s1给s2初始化,调用拷贝构造函数,进行浅拷贝,s1和s2指向相同的String_rep对象,引用计数加 1,该对象的指针指向同一个空间
s3 String_rep
[rep]-------> data -------->[ w o r l d \0]
use_count
创建s3对象,调用构造函数,指向一个新的String_rep对象,引用计数加 1
赋值语句:
s3 = s2:
String& operator=(const String &s) //赋值函数的编写要小心,只进行浅拷贝会发生内存泄漏 { if(this != &s) { rep = s.rep; rep->increment(); } return *this; }
赋值函数的编写要小心,只进行浅拷贝会发生内存泄漏,因为s3对象的rep指针原本指向的是String_rep对象,及String_rep
对象指针指向的空间,如果单纯将s2对象的rep值赋值给s3对象的rep值,则s3对象的rep指针指向的空间内存都会泄漏;
重写赋值语句:
String& operator=(const String &s) { if(this != &s) { rep->cecrement(); //delete rep = s.rep; //new rep->increment(); //strcpy } return *this; }
将s3对象rep指针原先指向String_rep的引用计数减 1,再将s3的rep指针赋值为s2的rep指针,该String_rep对象的引用计数 加1
以上的浅拷贝的引用计数方式,解决了相同数据多份空间而造成浪费的问题,但如果我们更改任何一个空间的内容时,所有的拷贝都会发生更改,这是错误的,应该只更改自己的,不应该影响别的对象。
这就提出了写时拷贝技术,即只是拷贝时共享相同的空间,但当自己需要修改数据时,应该将数据拷贝出来,
然后改变自己的指向,即进行深拷贝。
//当需要修改时,在String类中的修改函数:
s2.to_upper();
void to_upper() { if(rep->use_count > 1) { String_rep *new_rep = new String_rep(rep->data); //1. rep->decrement(); //2. rep = new_rep; //3. rep->increment(); } char *ch = rep->data; //4. while(*ch != '\0') { *ch -= 32; ++ch; } }
当s2对象的rep指针指向的String_rep引用计数大于1时,修改时
1.用原来String_rep对象指针指向的数据创建一个新的String_rep对象;
2.将s2对象的rep指针指向的String_rep引用计数减 1;
3.将s2对象的rep指针指向新的String_rep对象,并将引用计数加 1
4.对s2对象的rep指针指向的新的String_rep对象指针指向的数据进行更改。
当s2对象的rep指针指向的String_rep引用计数等于1时,直接对进行更改