这里向大家介绍C++库函数中的一个文件<string.h> 。为了简化程序员的工作量并提高代码的安全性和健壮性 C++标准库找专门提供了<string>文件用来提供对字符串进行多种操作的库函数接口,这些接口配合C++中<iostream>文件中的strlen strstr strcpy .......系列函数共同为字符串的处理服务。
本文中模拟的string类没有加迭代器 为了简化抽象出 <string>文件中string类的功能、内部实现机制和设计思想 。
拷贝构造函数 operator=()中的浅拷贝与深拷贝String::String(const String& s)//浅拷贝:_str(s._str){}
测试用例
void Test2(){String s1("hello");String s2(s1);}
这里的s2 中的字符指针直接指向了s1中字符指针指向的字符串 。 对象s1析构之后 s2中指针指向的字符串已被释放 再调用s2的析构函数导致同一空间被析构两次程序崩溃。编译器只是将指针的内容拷贝过来,导致多个对象共用一块内存空间,当其中任意对象将这块空间释放之后,另外一些对象并不知道这块空间已经还给了操作系统,以为还有效,所以再对这块空间进行操作时,造成了违规访问。
为解决浅拷贝问题只有进行深拷贝 即为新对象重新开辟字符串空间让新对象字符指针指向自己的字符串空间 该空间字符串内荣是源字符串的拷贝String(char* str = ""):_str(new char[strlen(str) + 1]){strcpy(_str, str);}
String & operator = ( const String & s)//写法1{if ( this != &s){delete [] _str;_str = new char [ strlen (s._str) + 1];strcpy (_str, s._str);}}
String & operator= ( const String & s)//写法2{if ( this != &s){char * tmp = new char [ strlen (s._str) + 1];strcpy (tmp, s._str);delete [] _str;_str = tmp;}return * this ; //为了支持链式访问}
void Test2(){String s1( "hello" );String s2(s1);Srring s3;s3 = s2;}拷贝的对象s2中_str的值(字符串的地址)和s1对象中的_str的值不同,重新开辟了空间(即就是深拷贝)。
一般情况下,上面的两种写法都可以,但是相对而言,第二种更优一点。对于第一种,先释放了旧空间,但是如果下面用new开辟新空间时有可能失败——>抛异常,而这时你是将s2赋值给s3,不仅没有赋值成功(空间开辟失败),而且也破坏了原有的s3对象。对于第二种,先开辟新空间,将新空间的地址赋给一个临时变量,就算这时空间开辟失败,也不会影响原本s3对象。综上:第二种方法更优一点。最后的返回值是为了支持链式访问。例如:s3 = s2 = s1;上面所写的拷贝构造函数和赋值运算符重载函数属于传统写法,下面我们一起来看看它们的现代写法:String ( const String & s):_str( NULL ){String tmp(s._str);swap (_str, tmp._str);}String & operator = ( const String & s){if ( this != &s){String tmp(s._str);swap (_str, tmp._str);}return * this ;}
下面两幅图介绍一下现代写法拷贝构造函数和赋值运算符重载的思想:
从上面的深拷贝我们可以看出,相比浅拷贝,深拷贝的效率明显较低,因为每拷贝一个对象就需要开辟空间和释放空间,再有就是赋值运算符重载也是一样的需要重新开辟空间并释放空间。假如有这样的一种情况,拷贝和赋值得到的对象只用于”读”,而不用于”写”,那么是不是就不需要重新开辟空间了呢?
string的引用计数指针版本:在对象中搞一个指向多个对象公共引用计数的指针 用以记录该字符串空间被几个对象的字符指针共同管理namespace COW1 //写时拷贝 引用计数指针{class String{public :String ( const char * str = "" ):_refCountPtr( new int (1)){ //引用计数指针版本的String构造函数初始化标识字符串字符数的_size//字符串空间大小的_capacity//字符串_str//引用计数指针_refCountPtr指向的空间 并初始化为一(同一个字符串最初只有一个对象管理)_size = strlen (str);_capacity = _size;_str = new char [_capacity + 1];strcpy (_str, str);}String ( const String & s) //引用计数方式的String类的拷贝构造函数:_refCountPtr(s._refCountPtr),_size(s._size),_capacity(s._capacity),_str(s._str){ //每次做拷贝的时候只是让新对象的字符指针和引用计数指针指向原对象空间//并++两对象引用计数指针指向的共同引用计数 实际上内存中的字符串只有一份(*_refCountPtr)++; //这样做是为了节省空间 所谓“写时拷贝 不拷就赚”}String & operator = ( const String & s){if (_str != s._str){Ralese ();_refCountPtr = s._refCountPtr;_str = s._str;(*_refCountPtr)++;}return * this ;}void Ralese (){if (--(*_refCountPtr) == 0){cout << "delete[]" << _str << endl ;delete [] _str;delete _refCountPtr;}}void PushBack ( const char & ch){//修改字符串的时候 才真正拷贝一份新的在使用角度上已经有的新字符串//在该字符串上做修改CopyOnWrite ();if (_size == _capacity){}_str[_size++] = ch;_str[_size] = '\0' ;}void CopyOnWrite (){if (*_refCountPtr > 1){ //写时拷贝发生的条件 即同一个在内存中的字符串被多个对象//的字符指针所引用char * newSize = new char [_capacity];strcpy (newSize,_str);*(_refCountPtr)--;_str = newSize;_refCountPtr = new int (1); //本函数负责修改公共引用计数 拷贝原对象所有内容}}char & operator[] ( size_t pos){CopyOnWrite (); //非const类型的operator[]函数因为要修改_str[X]中的内容return _str[pos]; //必须为要修改的对象的字符串重新开辟空间 否则在应用层面上将形成//改变了多个字符串的现象 因为在底层他们各自的char*指向同一字符串}char & operator[] ( size_t pos) const{return _str[pos];}~String (){Ralese ();}const char * c_str (){return _str;}private :char * _str;int * _refCountPtr;size_t _size;size_t _capacity;};// "有时候读的时候也要拷贝"void TestString1 (){/*String s1("hello world");String s2(s1);s1 = s2;String s3("dadadada");s3 = s1;String s4("dadadsqwqdq");s1 = s4;*/String s1( "hello world" );String s2(s1);cout << s1[0] << endl ;cout << s1. c_str () << endl ;cout << s2. c_str () << endl ;}}以下是图示思路:
使用指针可以完成引用计数的浅拷贝,但是因为每构造一个对象,都需要开辟两块空间,这样容易造成内存碎片。由new[]可以联想到类似模型—->只开辟一块空间(多开四个字节),把引用计数放在字符串首地址的前四个字节上。这样不但解决了内存碎片问题,而且也可以程序的运行效率。
string的引用计数在对象空间前4字节版本:
namespace COW2 //写时拷贝 在对象空间前4个字节存放引用计数{class String //在构造时初始化引用计数 涉及到拷贝时修改引用计数{public :String ( const char * str = "" ):_size( strlen (str)), _capacity(_size){_str = new char [_capacity + 5];strcpy (_str + 4, str);_str += 4;GetCountRef ()++;}int & GetCountRef (){return *(( int *)(_str - 4));}String ( const String & s):_size(s._size),_capacity(s._capacity),_str(s._str){GetCountRef ()++;}String & operator = ( const String & s){if (_str != s._str){Ralese ();_size = s._size;_capacity = s._capacity;_str = s._str;GetCountRef ()++;}}void Ralese (){if (-- GetCountRef () == 0){delete [] (_str - 4);}}void CopyOnwrite (){if ( GetCountRef () > 1){char * newSize = new char [_capacity + 5];strcpy (newSize + 4, _str);GetCountRef ()--;_str = newSize + 4;*(( int *)(_str - 4)) = 1;}}char & operator[] ( size_t pos){CopyOnwrite ();return _str[pos];}char operator[] ( size_t pos) const{return _str[pos];}~String (){Ralese ();}const char * c_str (){return _str;}private :char * _str;size_t _size;size_t _capacity;};}关于string类中涉及的增删查改工作:namespace DC //实现String类的增删查改{class String{public :String ( const char * str = "" ){_size = strlen (str);_capacity = _size;_str = new char (_capacity + 1);strcpy (_str,str);}String ( const String & s){String tmp(s._str); //拷贝构造时先创造一个临时对象 让临时对象和当前Swap (tmp); //对象内容交换 再因为临时对象出函数作用域要被释放 所以释放对象//留下拷贝后的对象。}void Swap ( String & tmp){swap (_str, tmp._str);swap (_size, tmp._size);swap (_capacity, tmp._capacity);}String & operator = ( String & s){Swap (s);return * this ;}size_t Size (){return _size;}size_t Capacity (){return _capacity;}char * c_str (){return _str;}void PushBack ( char ch){if (_size == _capacity){Expand (_capacity * 2);}_str[_size++] = ch;_str[_size] = '\0' ;}void PushBack ( const char * str){size_t len = strlen (str);if (_size + len > _capacity){Expand (_size + len);}strcpy (_str + len, str);}void Popback (){assert (_size);--_size;}void Insert ( char ch, int pos){if (_size > _capacity){Expand (_capacity * 2);}int end = _size;while (end >= pos){_str[end+ 1] = _str[end];--end;}_str[pos] = ch;++_size;}//插入方法同顺序表void Insert ( const char * str, int pos){if (_size + strlen (str) > _capacity){Expand (_size + strlen (str));}int end = _size;while (end >= ( int )pos){_str[end + strlen (str)] = _str[end];--end;}while (*str){_str[pos++] = *str++;}_size += strlen (str);}void Erase ( size_t pos, size_t count){if (pos + count > _size - 1){_str[pos] = '\0' ;_size = pos; //pos 到 count超过整个字符串 删去pos之后字符}else {strcpy (_str + pos, _str + pos + count); //将 pos 至 count 段的字符覆盖掉_size -= count;}}void Expand ( size_t n){if (n > _capacity){_str = ( char *) realloc (_str, n + 1);assert (_str);_capacity = n;}}int Find ( const char ch){for ( size_t i = 0; i < _size; ++i){if (_str[i] == ch){return i;}else {return -1;}}}int Find ( const char * str) const{assert (str);const char * srcStr = _str;const char * subStr = str;size_t srcIndex = 0;size_t subIndex = 0;size_t sublen = strlen (str);while (srcIndex < _size - sublen - 1){size_t matchIndex = srcIndex; //这里是在一个疑似的第一个字符索引while (_str[matchIndex] == str[subIndex]){ //该循环在疑似字串中一个个比对matchIndex++; //字符直到比到要找的字串结尾2subIndex++;if (subIndex == sublen){return srcIndex;}}subIndex = 0; //字串的索引重新置零 从下一个子串的头开始找srcIndex++; //找以下一个字符为首的字符串是不是字串str}return -1;}~String (){if (_str){delete [] _str;_str = NULL ;_capacity = _size = 0;}}bool operator< ( const String & s) const{size_t i = 0;for (; i < _size && i < s._size; ++i){if (_str[i] < s._str[i]){return true ;}else if (_str[i] > s._str[i]){return false ;}}if (i == _size && i < s._size) //?{return true ;}else{return false ;}}inline bool operator<= ( const String & s) const{return * this < s || * this == s;}inline bool operator> ( const String & s) const{return !(* this <= s);}bool operator>= ( const String & s) const ;bool operator== ( const String & s) const{size_t i = 0;for (; i < _size && i < s._size; ++i){if (_str[i] != s._str[i]){return false ;}}if (i == _size && i == s._size){return true ;}else{return false ;}}private :char * _str;size_t _size;size_t _capacity;};}