博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
String 类的实现(2)引用计数与写时拷贝
阅读量:5942 次
发布时间:2019-06-19

本文共 4417 字,大约阅读时间需要 14 分钟。

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 != '\0') 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 String25 {26 public:27     String(const char *pStr = "")28     {29         if (pStr == NULL)30         {31             _pStr = new char[1];32             *_pStr = '\0';33         }34         else35         {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
2 #include
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 = '\0'; 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

你可能感兴趣的文章
oracle exp/imp命令详解
查看>>
开发安全的 API 所需要核对的清单
查看>>
Mycat源码中的单例模式
查看>>
WPF Dispatcher介绍
查看>>
fiddler展示serverIP方法
查看>>
C语言中的随意跳转
查看>>
WPF中如何将ListViewItem双击事件绑定到Command
查看>>
《聚散两依依》
查看>>
小tips:你不知道的 npm init
查看>>
Mac笔记本中是用Idea开发工具在Java项目中调用python脚本遇到的环境变量问题解决...
查看>>
Jmeter也能IP欺骗!
查看>>
Rust 阴阳谜题,及纯基于代码的分析与化简
查看>>
ASP.NET Core的身份认证框架IdentityServer4(4)- 支持的规范
查看>>
(原創) array可以使用reference方式傳進function嗎? (C/C++)
查看>>
170多个Ionic Framework学习资源(转载)
查看>>
Azure:不能把同一个certificate同时用于Azure Management和RDP
查看>>
Directx11教程(15) D3D11管线(4)
查看>>
Microsoft Excel软件打开文件出现文件的格式与文件扩展名指定格式不一致?
查看>>
ios ble 参考
查看>>
linux中注册系统服务—service命令的原理通俗
查看>>