由深拷贝与浅拷贝引发的新方法引用计数、写时拷贝技术

一、理解深拷贝和浅拷贝:


先来看一个栗子,引出什么是浅拷贝。

#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;  
}  

s1给s2初始化时,会调用拷贝构造函数,因为没有编写,则会调用默认的拷贝构造函数,拷贝构造函数会按成员赋值,这样s2的指针会指向s1的指针指向的空间;
但析构的时候,会先释放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;  
}  

深拷贝就是在拷贝的时候,将指针指向的空间也一同拷贝,这样,析构的时候,自己释放自己指向的空间就可以了。

 

现在浅拷贝的问题已经解决了,但是不是一切都完美了呢?不要高兴太早。接着往下看。

 

二、理解深拷贝和浅拷贝各自的优缺点:

浅拷贝节省空间,相同的数据只保存一份,但因为多个指针指向同一个空间,会引发多次释放的问题;

深拷贝虽然每个指针会指向不同的空间,没有一个空间多次释放的问题,但可能保存的数据都是一样的,这样会导致空间的浪费。

 

有问题就要解决,于是前辈们又想出了一个办法-----引用计数。

三、使用引用计数解决浅拷贝实现中出现的问题:

#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;  
}  

运行上面的程序看着没有问题,可是,当我们再创建一个不同的对象时发现,不同的空间居然有相同的引用计数。(这是因为我们的计数用的是一个静态成员变量,static int use_count

,所有的对象都共用这一个静态成员变量来计数,这就导致了问题)。

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时,直接对进行更改

posted @ 2016-11-23 10:58  ren_zhg1992  阅读(187)  评论(0)    收藏  举报