String 类的实现(2)引用计数与写时拷贝

1.引用计数 

  我们知道在C++中动态开辟空间时是用字符new和delete的。其中使用new test[N]方式开辟空间时实际上是开辟了(N*sizeof(test)+4)字节的空间。如图示其中保存N的值主要用于析构函数中析构对象的次数delete[] p时先取N(*((int*)p-1))。我们参照这种机制在实现String类的时候提供一个计数,将指向new开辟的空间的指针个数保存下来,当计数不小于或不等于0时不进行析构对象,也不释放空间。直到计数为0时释放空间。

String的所有赋值、拷贝构造操作,计数器都会 +1 ; string 对象析构时,如果计数器为 0 则释放内存空间,否则计数器 -1 。实现代码如下

 1 //引用计数方法
 2 int my_strlen(const char *p)
 3 {
 4     int count = 0;
 5     assert(p);
 6     while (*p != '')
 7     {
 8         p++;
 9         count++;
10     }
11     return count;
12 }
13 char* my_strcopy(char* dest, const char* str)
14 {
15     assert(dest != NULL);
16     assert(str != NULL);
17     char* ret = dest;
18     while (*dest++ = *str++)
19     {
20         ;
21     }
22     return ret;
23 }
24 class String
25 {
26 public:
27     String(const char *pStr = "")
28     {
29         if (pStr == NULL)
30         {
31             _pStr = new char[1];
32             *_pStr = '';
33         }
34         else
35         {
36             _pStr = new char[strlen(pStr) + 1];
37             my_strcopy(_pStr, pStr);
38         }
39         _pCount = new int(1);
40     }
41     String(const String& s)
42         :_pStr(s._pStr)
43         ,_pCount(s._pCount)
44     {
45         _pStr++;
46         *(_pCount)++;
47     }
48 
49     ~String()
50     {
51         if (_pStr && (0 == --(*_pCount)))
52         {
53             delete[] _pStr;
54             _pStr = NULL;
55             delete[] _pCount;
56             _pCount;
57         }
58     }
59 
60     String& operator=(const String& s)
61     {
62         if (this != &s)
63         {
64             if (_pStr && (0 == --(*_pCount)))
65             {
66                 delete[] _pStr;
67                 delete[] _pCount;
68             }
69             _pStr = s._pStr;
70             _pCount = s._pCount;
71             --(*_pCount);
72         }
73         return *this;
74     }
75 
76 private:
77     char *_pStr;
78     int *_pCount;
79 };
82 int main()
83 {
84     String s1;
85     String s2 = "1234";
86     String s3(s2);
88     String s4;
89     s4 = s2;
90 }

 引用计数定义成类普通成员变量和静态成员变量(被static修饰)的优劣问题

    当类成员是静态时,它不属于类的任何一个对象,存在于任何一个对象之外,不由类的构造函数初始化,而对象的创建需要调用构造函数,所以它无法计数到正在使用同一块空间的对象的个数;对象中不包含任何与静态数据成员有关的数据,而我们的计数_Count就与对象绑定在一起;普通成员不可以是不完全类型;非静态成员不能作为默认实参,它的值本身属于对象的一部分。

2.写时拷贝

 由于释放内存空间,开辟内存空间时花费时间,因此,在我们在不需要写,只是读的时候就可以不用新开辟内存空间,就用浅拷贝的方式创建对象,当我们需要写的时候才去新开辟内存空间。这种方法就是写时拷贝。这也是一种解决由于浅拷贝使多个对象共用一块内存地址,调用析构函数时导致一块内存被多次释放,导致程序奔溃的问题。这种方法同样需要用到引用计数:使用int *保存引用计数;采用所申请的4个字节空间。

 1 #include<iostream>  
 2 #include<stdlib.h>  
 3 using namespace std;
 4 class String
 5 {
 6 public:
 7     String(const char *pStr = "")
 8     {
 9         if (pStr == NULL)
10         {
11             _pStr = new char[1 + 4];
12             *((int*)pStr) = 1;
13             _pStr = (char*)(((int*)_pStr) + 1);
14             *_pStr = '';
15         }
16         else
17         {
18             _pStr = new char[my_strlen(pStr) + 1 + 4];
19             my_strcopy(_pStr, pStr);
20             *((int*)_pStr - 1) = 1;
21         }
22     }
23 
24     String(const String& s)
25         :_pStr(s._pStr)
26     {
27         ++GetCount();
28     }
29 
30     ~String()
31     {
32         Release();
33     }
34 
35     String& operator=(const String& s)
36     {
37         if (this != &s)
38         {
39             Release();
40             _pStr = s._pStr;
41             --(GetCount());
42         }
43         return *this;
44     }
45 
46     char& operator[](size_t index)//写时拷贝
47     {
48         if (GetCount() > 1)      //当引用次数大于1时新开辟内存空间
49         {
50             char* pTem = new char[my_strlen(_pStr) + 1 + 4];
51             my_strcopy(pTem + 4, _pStr);
52             --GetCount();       //原来得空间引用计数器减1
53             _pStr = pTem + 4;
54             GetCount() = 1;
55         }
56         return _pStr[index];
57     }
58     const char& operator[](size_t index)const
59     {
60         return _pStr[index]; 
61     }
62    friend ostream& operator<<(ostream& output, const String& s)
63    {
64         output << s._pStr;
65         return output;
66    }
67 private:
68     int& GetCount()
69     {
70         return *((int*)_pStr - 1);
71     }
72     void Release()
73     {
74         if (_pStr && (0 == --GetCount()))
75         {
76             _pStr = (char*)((int*)_pStr - 1);
77             delete _pStr;
78         }
79     }
80 
81     char *_pStr;
82 };
83 
84 int main()
85 {
86     String s1;
87     String s2 = "1234";
88     String s3(s2);
89     s2[0] = '5';
90     String s4;
91     s3 = s4;
92 } 

   写时拷贝能减少不必要的内存操作,提高程序性能,但同时也是一把双刃剑,如果没按 stl 约定使用 String ,可能会导致极其严重的 bug ,而且通常是很隐蔽的,因为一般不会把注意力放到一个赋值语句。修改 String 数据时,先判断计数器是否为 1(为 1 代表没有其他对象共享内存空间),为 1 则可以直接使用内存空间(如上例中的 s2 ),否则触发写时拷贝,计数 -1 ,拷贝一份数据出来修改,并且新的内存计数器置 1 ; string 对象析构时,如果计数器为 1 则释放内存空间,否则计数也要 -1 。

写时拷贝存在的线程安全问题

   线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。String类写时拷贝可能存在的问题详见:http://blog.csdn.net/haoel/article/details/24077

原文地址:https://www.cnblogs.com/33debug/p/6661774.html