zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    CUDA实例练习(十二):矩阵相乘
    CUDA实例练习(十一):零拷贝内存
    CUDA实例练习(十):多个cuda流
    CUDA实例练习(九):页锁定主机内存
    CUDA实例练习(八):原子操作(直方图)
    算法实例(三):快速排序
    算法实例(二):冒泡排序
    算法实例(一):简单桶排序
    CUDA实例练习(七):点积运算
    CUDA实例练习(六):矢量求和
  • 原文地址:https://www.cnblogs.com/33debug/p/6661774.html
Copyright © 2011-2022 走看看