C++入门之String的模拟实现

目录

一、简洁的string

string默认构造函数的传统与现代写法

传统写法实现

(1)构造函数的实现

构造函数能这样写吗?

(2)析构函数

(3)拷贝构造函数

深浅拷贝问题

(4) 赋值构造函数=

现代写法实现

(1)拷贝构造函数

(2)赋值构造函数=

更加简洁的版本:

二、完整的一个简洁的string类

三、完整的string的模拟实现

传统写法

默认构造函数

第一种解决反法:

第二种解决方法:传缺省值

现代写法

string提供的swap与全局的swap的异同

string接口的实现

(1)size的实现

(2)operator[ ]的实现

(3)迭代器的实现

(4)增容reserve的实现

(5)rsize的实现

(6)尾插push_back的实现 

 (7)尾插字符串append的实现

(8)operator+=的实现

(9)find字符的实现

 (10)find字符串

(11)insert的实现 

(12)insert字符串的实现

(13)erase的实现

(14)大于,小于,等于,不等于,大于等于,小于等于的实现

(15)流提取的实现

(16)流插入的实现

(17)clear的实现

四、string模拟实现完整代码 

五、写时拷贝

引用计数的写时拷贝

std中的string


一、简洁的string

string默认构造函数的传统与现代写法

这部分内容借助一个简洁的string(没有size与capacity)进行说明。不考虑增删查改

传统写法实现

为了与库里面的string作区分,需要定义一个命名空间,我们就在这个命名空间中模拟实现string类

namespace pxl
{
	class string
	{
	public:


	private:
		char* _str;
	};

}
 

(1)构造函数的实现

string(char* str)
:_str(str) 
{

}

构造函数能这样写吗?

答案是不行的,我们平常时候string初始化是这样的 string s("hello")  ,这个hello是一个常量字符串,就算不是常量字符串他也是一个指针,这个hello是不能被修改的,如果要实现string的增删查改,是不能进行扩容的。所以要动态开辟一个和str一样大的空间,再将str的内容拷贝到_str上。

正确写法:

string(const char* str)
:_str(new char[strlen(str)+1])  
{
    strcpy(_str, str);
}

ps:strlen是不计算\0 的,所以要+1存 \0;

(2)析构函数

清理这块空间,并将这个指针置空

~string() 
{
	delete[] _str;
	_str = nullptr; 
}

(3)拷贝构造函数

深浅拷贝问题

浅拷贝: 也称值拷贝,编译器只是将对象中的值拷贝过来 。如果 对象中管理资源 ,最后就会 导致多个对象共 享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为 还有效,所以 当继续对资源进项操作时,就会发生发生了访问违规 。要解决浅拷贝问题, C++ 中引入了深拷贝。

深拷贝:给每个对象独立分配资源,保证多个对象之间不会因为资源共享而造成对此释放造成程序崩溃。如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

如果调用string类的默认拷贝构造函数时,会发生崩溃

报错原因:

s2去拷贝构造这个s1,我们知道默认拷贝构造完成的是浅拷贝,会把s1这块空间的每一个字节都拷贝到s2中。

通过调试可以看出确实拷贝了。

问题就在于调用析构函数的时候,s2先调用,将指针指向的空间释放了,这时s1中的_str指向的就是一块被释放掉的空间,那么s1中的_str就是一个野指针,在释放的时候就会报错,一块空间不能被释放两次 。

s2的本意并不是想和s1指向同一块空间,而是它也要一块空间,上面的值和s1一样。

所以我们就要自己实现深拷贝

以s2(s1)为例帮助大家理解,s1就是这个s,s2就是_str,给s2一块和s1一样大的空间,再把s1的内容拷贝给s2.

string(const string& s)
:_str(new char[strlen(s._str)+1])
{
   strcpy(_str, s._str); 
}

(4) 赋值构造函数=

默认生成的赋值= ,也存在一样的浅拷贝问题。

以s1=s3为例.

思路误区:

1.0 直接将s3赋值给s1,听起来没问题,但是前提得s1的空间足够,不够的话就得扩容,这里的new又不支持realloc,所以就又得释放空间,开一个新空间。

2.0 假如s1有一万个空间,s3只有10个数据,如果将s3赋值给s1,就会造成空间的浪费。

所以大拷小,有可能空间不够要扩容。小拷大,则会造成空间的浪费。按照这种思路就会多了很多无谓的判断。只有两个空间均这种思路才有用。

正确思路:

直接将s1的空间释放掉,再开一个和s3一样的空间,再把s3的数据拷贝给s1。

(这里的s就代表着s3,_str代表s1.)

string& operator=(const string& s)
{
	if (this != &s)
    {
	 delete[] _str;
	 _str = new char[strlen(s._str) + 1];
	 strcpy(_str, s._str);
	}

	return *this;
}

但这里还存在着个小细节,我们知道delete是不可能失败的,但是new是可能失败的(借钱会失败,但是还钱不会失败),若这里开空间失败了,它会把s1的空间给释放了,给s1造成了影响,所以要进行优化.

思路:我们借助一个tmp,让tmp深拷贝一个s3,然后在让s1指向这个空间,这样的话就算开空间失败了,也不会对s1造成影响。

		string& operator=(const string& s)
		{
		
			if (this != &s)
			{
				char* tmp = new char[strlen(s._str) + 1];
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;
			}

			return *this;
		}

 ps:这里进行判断是为了防止自己给自己赋值问题的发生(s1=s1),避免效率降低。

现代写法实现

(1)拷贝构造函数

以s2(s1)为例: 首先利用构造函数构造出一个字符串内容为s1._str的对象tmp,之后再把s2与tmp交换,这样s2就拷贝构造了s1.

		string(const string& s)
			:_str(nullptr) 
		{
			string tmp(s._str);
			swap(_str, tmp._str);
		}

ps:_str(nullptr) 这个初始化必须写上,因为调用拷贝构造时,s2指向的是个随机的地址(s2并没有构造出来,所以s2不是new出来的),和tmp交换以后,tmp就成了随机值,但当tmp出了作用域以后,就会调用析构函数,delete只会销毁new出来的,它对这个随机地址不能进行释放,所以对于这个随机的地址就会报错。但是delete nullptr是没有任何问题,因为编译器自动进行了检查 。

通过3次调用证明:s2确实是个随机值 

更严谨一些,就对析构函数进行个条件判断。

~string() 
{
	if (_str)
	{
		delete[] _str;
		_str = nullptr;
	}
			 
}

(2)赋值构造函数=

以s1=s3为例,首先利用tmp深拷贝一个s3,然后tmp与s1交换,tmp出作用域的时候又会将s1的空间给释放,s1则是一举两得,既得到了新空间,又释放了旧空间。

string& operator=(const string& s)
{
	if (this != &s)
	{
		string tmp(s);
		swap(_str, tmp._str);
	}
	return *this;		
}

更加简洁的版本:

s1=s3; 直接利用传值传参的时候进行了拷贝构造,s充当了tmp就是s3的拷贝,再把s1和s交换,形参也是一个参数,出了作用域也会调用析构函数,把s1换过来的空间销毁。

这种写法的缺陷就是没办法判断传入的参数是不是它本身,因为形参与实参的地址是不一样的s就不是s3了,但是这里不判断也没有大的问题,只不过是s的形参和s的实参换了一下(地址交换了以下,其余啥都一样)没有任何影响。

string& operator=(string s)
{
	swap(_str, s._str);
	return *this;
}

总结:现代写法的本质就是进行复用。

二、完整的一个简洁的string类

namespace pxl
{
	class string
	{
	public:
		string(const char* str=" ")
			:_str(new char[strlen(str)+1])  //构造函数不能这样写,因为_str存在栈上,无法实现增容等问题
			//,所以要动态开辟一个和str一样大的空间,再将str的内容拷贝到_str上
		{
			strcpy(_str, str);
		}

		//传统写法
		/*string(const string& s)
			:_str(new char[strlen(s._str)+1])
		{
			strcpy(_str, s._str); 
		}*/

		//s3=s1
		//string& operator=(const string& s)
		//{
			/*if (this != &s)
			{
				delete[] _str;
				_str = new char[strlen(s._str) + 1];
				strcpy(_str, s._str);
			}*/

		//	if (this != &s)
		//	{
		//		char* tmp = new char[strlen(s._str) + 1];
		//		strcpy(tmp, s._str);
		//		delete[] _str;
		//		_str = tmp;
		//	}

		//	return *this;
		//}

		~string() 
		{
			if (_str)
			{
				delete[] _str;
				_str = nullptr;
			}
			 
		}

        //现代写法 本质上就是复用
		string(const string& s)
			//:_str(nullptr) //必须写上,因为s2指向的是个随机的地址,和tmp交换以后,tmp就成了随机值,但当
			//tmp出了作用域以后,就会调用析构函数,delete只会销毁new出来的,所以对于这个随机的地址就会报错,
			//但是delete nullptr是没有任何问题的。编译器自动进行了检查
		{
			string tmp(s._str);
			swap(_str, tmp._str);
		}
		//s1 = s3;
		/*string& operator=(const string& s)
		{
			if (this != &s)
			{
				string tmp(s);
				swap(_str, tmp._str);
			}
			return *this;
		}*/

		string& operator=(string s)
		{
		    swap(_str, s._str);
			return *this;
		}

	private:
		char* _str;
	};

}

三、完整的string的模拟实现

传统写法

_capacity表示的是有效字符的个数,不算\0,但是初始_str空间的时候要+1,存\0,这里将开空间放到了里面是因为,strlen的时间复杂度是O(N),尽量少去调用,提高效率。

namespace pxl
{
	class string
	{
	public:
		string(const char* str)
			:_size(strlen(str))
			,_capacity(_size)
		{
			_str = new char[_capacity+1];
			strcpy(_str, str);
		}

		string(const string& s)
			:_size(s._size)
			,_capacity(s._capacity)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, s._str);
		}
		 //s1=s3
		string& operator=(string s)
		{
			if (this != &s)
			{
				char* tmp = new char[strlen(s._str) + 1];
				strcpy(tmp,s._str);
				delete _str;
				_str = tmp;
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}

		~string() 
		{
			if (_str)
			{
				delete[] _str;
				_str = nullptr;
			}
			 
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; //有效字符的空间数,不算\0.
	};

}

默认构造函数

这时候还缺少一个默认构造函数

string()
	:_str(nullptr) //不能这样写,这样会存在问题
{
}

但是这个默认构造函数不能这样写,这样写会存在问题。通过模拟实现的c_str,就可以检测出这个问题。

const char* c_str()
{
	return _str;
}

输出s2的时候崩溃了,因为这是C形式的字符串遇到\0,才截止,但是对于s2返回的是空指针,这是不行的。

第一种解决反法:

        string()
			:_str(new char[1])
			,_size(0)
			,_capacity(0)
		{
			_str[0] = '\0';
		}

第二种解决方法:传缺省值

注意:string(const char* str = nullptr) 这样传缺省值也是不行的,程序也是会崩溃的,缺省值不能给空,因为strlen,是不会检查空的,而是对这个字符串检查\0,遇到\0就停止,相当于直接对字符串解引用,所以如果缺省值是空,就会有空指针问题.所以要给个空字符串,因为常量字符串默认就有\0 。

正确写法:

string(const char* str = "")
			:_size(strlen(str))
			,_capacity(_size)
		{
			_str = new char[_capacity+1];
			strcpy(_str, str);
		}

空字符串,strlen以后是0,_capacity也是0,_str开一个空间存\0,strcpy将\0,拷贝给_str;(直接将缺省值给成 \ 0 这样也是可行的,但不推荐,因为常量字符串默认就会有个\0,这样虽然也可以,但是显得你太业余了)。

现代写法

		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			string tmp(s._str);
			swap(_str, tmp._str);
			swap(_size, tmp._size);
			swap(_capacity, tmp._capacity);
		}

		string& operator=(string s)
		{
			swap(_str, s._str);
			swap(_size, s._size);
			swap(_capacity, s._capacity);
			return *this;
		}

为了简便也可以将交换整合成一个函数(看个人喜好)

		void swap(string& s) //自己定义的swap
		{
			std::swap(_str, s._str); //调用库里面的swap
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			string tmp(s._str);
			swap(tmp);
		}

		string& operator=(string s)
		{
			swap(s);
			return *this;
		}

string提供的swap与全局的swap的异同

string的swap和全局的swap都可以实现交换,但是string的效率会更高一些因为它仅仅是对成员变量进行交换即可,原理类似于我们自己实现的。而全局的swap,调用的时候会进行三次string的深拷贝。所以推荐使用自带的swap;

 

string接口的实现

(1)size的实现

直接返回_size即可。

size_t size() const
{
	return _size;
}

(2)operator[ ]的实现

普通版本:可读可写

char& operator[](size_t pos)
{
    assert(pos < _size); //处理越界
    return _str[pos];
}

const版本:只能读​​​​​​​,不能写

const char& operator[](size_t pos) const  //重载出const版本
{
	assert(pos < _size);
	return _str[pos];
}

借助这两个就可以进行遍历字符串 

(3)迭代器的实现

 string的迭代器本质就是一个指针

		typedef char* iterator;
        typedef const char* const_iterator;        

        //普通版本的迭代器 可读可写
		iterator begin()
		{
			return _str; //返回起始位置
		}

		iterator end()
		{
			return _str + _size; //返回最后一个字符位置的下一个位置
		}
        
        //const版本的迭代器 只能读
		const_iterator begin() const
		{
			return _str;
		}

		const_iterator end()  const
		{
			return _str + _size;
		}

迭代器针对普通对象的遍历与修改:

当我们实现了迭代器以后会发现一个神奇的东西,范围for居然也可以使用了。

所以范围for的本质就是迭代器,支持迭代器自然而然的也就支持了范围for.        

铁证:如果你将begin改成Begin,那么范围for就失效了。

(4)增容reserve的实现

       void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete _str;
				_str = tmp;
				_capacity = n;
			}
		}

注意:这里_str和tmp虽然都指向了一块空间,但他们并不会调用析构函数,因为他们两个都是内置类型而不是自定义类型。 

(5)rsize的实现

		void resize(size_t n, char ch = '\0')
		{
			if (n <= _size)
			{
				_size = n;
				_str[_size] = '\0';
				
			}
			else
			{
				if (n > _capacity)
				{
					reserve(n);
				}
				size_t num = n - _size;
				for (size_t i = 0; i < num; i++)
				{
					_str[_size] = ch;
					_size++;
				}
				_size = n;
				_str[_size] = '\0';
			}

		
		}

 同样这块的for循坏可以memset代替,使代码更简洁。

		void resize(size_t n, char ch = '\0')
		{
			if (n <= _size)
			{
				_size = n;
				_str[_size] = '\0';
				
			}
			else
			{
				if (n > _capacity)
				{
					reserve(n);
				}
				memset(_str + _size, ch, n - _size);
				_size = n;
				_str[_size] = '\0';
			}
		}

(6)尾插push_back的实现 

先检查容量,在进行尾插。

		void push_back(char ch)
		{
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
			_str[_size] = ch;
			_size++;
			_str[_size] = '\0'; 
		}

 

 (7)尾插字符串append的实现

		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			for (size_t i = 0; i <= len; i++)
			{
				_str[_size] = str[i];
				_size++;
			}
			_size--;
		}

_szie--:是因为最后赋 \0 的时候_size 会多加一,所以要把它减一。

如果嫌麻烦可以直接用strcop函数

		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			strcpy(_str + _size, str);
			_size += len;
		}

(8)operator+=的实现

复用push_back与append即可

		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}

(9)find字符的实现

找到了返回该字符的下标,没找到返回npos(整形的最大值)

		size_t find(char ch)
		{
			for (size_t i = 0; i < _size; i++)
			{
				if (ch == _str[i])
				{
					return i;
				}
			}
			return npos;
		}

 

 (10)find字符串

		size_t find(const char* s, size_t pos = 0)
		{
			const char* ptr = strstr(_str + pos, s); //从pos位置开始找s
			if (ptr == nullptr)
			{
				return npos;
			}
			else
			{
				return ptr - _str; //返回找到位置的下标
			}
		}

(11)insert的实现 

 

		string& insert(size_t pos, char ch)
		{
			assert(pos <= _size);
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : 2 * _capacity);
			}
			size_t end = _size;
			while (end >= pos)
			{
				_str[end + 1] = _str[end];
				end--;
			}
			_str[pos] = ch;
			_size += 1;

			return *this;
		}

 这段代码逻辑上是没有问题的,但是头插的时候就会发生问题原因在于,end是size_t类型的头插时最后一步,end变成-1,但是又因为它是无符号型的,end就会变成一个非常大的数字,发生越界,这个过程程序也会崩溃。如果将end改成int也会越界因为pos是size_t类型的,他俩比较的时候会发生类型的提升,往类型大的提升。

解决方法:

  • 1.0  int end; (int)pos 但是这种方法非常不好,违背了接口的一致性
  • 2.0  采用两个指针
  • 3.0  采用将 end=_size+1;  _str[end]=_str[end-1]; 避免end变为负数
		string& insert(size_t pos, char ch)
		{
			assert(pos <= _size);
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : 2 * _capacity);
			}
			size_t end = _size + 1;
			while (end > pos)
			{
				_str[end] = _str[end - 1];
				end--;
			}
			_str[pos] = ch;
			_size += 1;

			return *this;
		}

图解方法三:

 完整代码:

		string& insert(size_t pos, char ch)
		{
			assert(pos <= _size);
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : 2 * _capacity);
			}
			size_t end = _size + 1;
			while (end > pos)
			{
				_str[end] = _str[end - 1];
				end--;
			}
			_str[pos] = ch;
			_size += 1;

			return *this;
		}

(12)insert字符串的实现

思路图:

代码实现: 

		string& insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			size_t end = _size + len;
			while (end >= pos+len)
			{
				_str[end] = _str[end - len];
				end--;
			}
			
			for (size_t i = 0; i < len; i++)
			{
				_str[pos] = str[i];
				pos++;
			}
			_size += len;

			return* this;
		}

 当然为了简便可以使用strncpy

		string& insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			size_t end = _size + len;
			while (end >= pos+len)
			{
				_str[end] = _str[end - len];
				end--;
			}
			strncpy(_str+pos, str, len);
			_size += len;

			return* this;
		}

但是要注意尽可能的少用因为头插和中间插的时间复杂度太高了。

与此同时insert实现了puh_back和append就可以实现复用了

		void push_back(char ch)
		{
			insert(_size, ch);
		}
        void append(const char* str)
		{
			insert(_size, str);
		}

(13)erase的实现

思路:

1.0 删除后剩余的字符大于等于_szie(将pos后面的字符都删了)直接将pos位置置成\0即可

2.0删除后剩余的字符小于_szie(从pos位置开始删,删了以后还有剩余)

代码实现:

		string& erase(size_t pos = 0, size_t len = npos)
		{
			if (len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				size_t prev = pos;
				size_t end = _size;
				while (prev < end)
				{
					_str[prev] = _str[prev + len];
					prev++;
				}
				_size -= len;
			}
			return *this;
		}

简便写法: 

 

		string& erase(size_t pos = 0, size_t len = npos)
		{
			if (len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
			return *this;
		}

(14)大于,小于,等于,不等于,大于等于,小于等于的实现

字符串比较规则:比较的时候,从字符串左边开始,一次比较每个字符,直接出现差异、或者其中一个串结束为止。

  • 比如ABC与ACDE比较,第一个字符相同,继续比较第二个字符,由于第二个字符是后面一个串大,所以不再继续比较,结果就是后面个串大。
  • 再如ABC与ABC123比较,比较三个字符后第一个串结束,所以就是后面一个串大。

所以,长度不能直接决定大小,字符串的大小是由左边开始最前面的字符决定的。

小于​​​​​​​的实现

	bool operator<(const string& s1, const string& s2)
	{ 
		size_t i1 = 0, i2 = 0;
		while (i1 < s1.size()&& i2 < s2.size())
		{
			if (s1[i1] < s2[i2])
			{
				return true;
			}
			else if (s1[i1] > s2[i2])
			{	
				return false;
			}
			else
			{
				i1++;
				i2++;
			}
		}
		return i2 < s2.size() ? true : false;

	}

 

当然也可以通过调用c_str,利用strcmp来直接进行比较

	bool operator<(const string& s1, const string& s2)
	{ 
		return strcmp(s1.c_str(), s2.c_str()) < 0;
	}

其余的实现,进行复用 

	bool operator==(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) == 0;
	}
	bool operator<=(const string& s1, const string& s2)
	{
		return s1 < s2 || s1 == s2;
	}
	bool operator>(const string& s1, const string& s2)
	{
		return !(s1 <= s2);
	}
	bool operator>=(const string& s1, const string& s2)
	{
		return !(s1 < s2);
	}
	bool operator!=(const string& s1, const string& s2)
	{
		return !(s1 == s2);
	}

(15)流提取的实现

这两种任意一种都可以

	ostream& operator<<(ostream& out, const string& s)
	{
        //现代写法
		for (auto e : s )
		{
			out << e;
		}
		return out;
	}
	ostream& operator<<(ostream& out, const string& s)
	{
		//传统写法
		for (size_t i = 0; i < s.size(); i++)
		{
			out << s[i];
		}

		return out;
	}

 但是这样写是不行的

因为在有些场景下这种输出和范围for输出是不一样的,c_str是遇到\0就截止了,而范围for是都遍历一遍才结束。

eg:这种场景下,把\0当成有效字符。c_str就不能实现我们的目的。

(16)流插入的实现

	istream& operator>>(istream& in, string& s)
	{
		char ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			s += ch;
			ch = in.get();
		}
		return in;
	}

这里不用写\0,是因为我们的构造函数就有一个\0; 

这段代码目前存在一个小问题

官方的cin会把原来s1的内容都覆盖掉,而我们自己实现的却是直接加到s1后面去

 

(17)clear的实现

        void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

所以这里我们先进行clear一下,代码就没问题了。 

	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			s += ch;
			ch = in.get();
		}

		return in;
	}

四、string模拟实现完整代码 

namespace pxl
{
	//增删查改
	class string
	{
	public:

		typedef char* iterator; //在这的迭代器就是一个指针
		typedef const char* const_iterator;

		iterator begin()
		{
			return _str; //第一个字符的地址
		}

		iterator end()
		{
			return _str + _size; //最后一个数据的下一个位置
		}

		const_iterator begin() const
		{
			return _str;
		}

		const_iterator end()  const
		{
			return _str + _size;
		}


		//string()
		//	:_str(nullptr) //不能这样写,这样会存在问题
		//{}

		/*string()
			:_str(new char[1])
			,_size(0)
			,_capacity(0)
		{
			_str[0] = '\0';
		}*/

		//\0这样也是可行的,但不推荐
		string(const char* str = " ")//缺省值也不能给空,因为strlen,是不会检查空的,而是对这个字符串遇到\0,就停止,
			//相当于直接对字符串解引用,所以如果缺省值是空,就会有空指针问题.所以给个空字符串,因为常量字符串默认就有\0
			:_size(strlen(str))
			, _capacity(_size)
		{
			_str = new char[_capacity + 1];//多出来的1给\0 准备
			strcpy(_str, str);
		}

		//传统写法
		/*string(const string& s)
			:_size(s._size)
			, _capacity(s._capacity)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, s._str);
		}*/

		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}


		//现代写法
		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			string tmp(s._str);
			/*swap(_str, tmp._str);
			swap(_size, tmp._size);
			swap(_capacity, tmp._capacity);*/

			this->swap(tmp);
		}



		//s3=s1
		//传统写法
		/*string& operator=(const string& s)
		{
			if (this != &s)
			{
				char* tmp = new char[s._capacity + 1];
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;
				_size = s._size;
				_capacity = s._capacity;
			}

			return *this;
		}*/

		//现代写法
		string& operator=(string s)
		{
			/*	swap(_str, s._str);
				swap(_size, s._size);
				swap(_capacity, s._capacity);*/

			this->swap(s);
			return *this;
		}




		~string()
		{
			delete[] _str;
			_str == nullptr;
			_size = _capacity = 0;
		}


		const char* c_str() const
		{
			return _str;
		}

		size_t size() const
		{
			return _size;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size); //处理越界
			return _str[pos];
		}

		const char& operator[](size_t pos) const  //重载出const版本
		{
			assert(pos < _size);
			return _str[pos];
		}


		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];  //tmp是内置类型不会去调用析构函数
				strcpy(tmp, _str);
				delete[] _str;

				_str = tmp;
				_capacity = n;
			}
		}

		void resize(size_t n, char ch = '\0')
		{
			if (n < _capacity) //空间足够
			{
				_size = n;
				_str[n] = '\0';
			}
			else
			{
				if (n > _capacity)
				{
					reserve(n);
				}
				/*for (int i = _size; i < n; i++)
				{
					_str[_size] = ch;
					_size++;
				}
				_str[_size] = '\0';*/
				memset(_str + _size, ch, n - _size);
				_size = n;
				_str[_size] = '\0';
			}
		}


		void push_back(char ch)
		{

	/*		if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';*/

			insert(_size, ch);
		}
		void append(const char* str)
		{
			//size_t len = strlen(str);
			//if (_size + len > _capacity)
			//{
			//	reserve(_size + len);
			//}
			//strcpy(_str + _size, str);
			//_size += len;

			///*	for (int i = 0; i <len; i++)
			//	{
			//		_str[_size] = str[i];
			//		_size++;
			//	}
			//	_str[_size] = '\0';*/


			insert(_size, str);

		}
		string& operator+=(const char* str)
		{
			//this->append(str);
			append(str);
			return *this;
		}

		string& operator+=(char ch)
		{
			//this->push_back(ch);
			push_back(ch);
			return *this;
		}

		size_t find(char ch)
		{
			for (int i = 0; i < _size; i++)
			{
				if (ch == _str[i])
				{
					return i;
				}
			}
			return npos;
		}

		size_t find(const char* s, size_t pos = 0)
		{
			const char* ptr = strstr(_str + pos, s);
			if (ptr == nullptr)
			{
				return pos;
			}
			else
			{
				return ptr - _str;
			}
		}

		string& insert(size_t pos,char ch)
		{
			assert(pos <= _size);
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
		    //size_t end = _size;
			//while (end >= pos) //这样写会因为end变成-1,越界end是无符号的
			//{
			//	_str[end + 1] = _str[end];
			//	--end;
			//}
			//将end改成int也会越界因为pos是size_t类型的,他俩比较的时候会发生类型的提升,往类型大的提升
			//方法:int end; (int)pos 但是这种方法非常不好,违背了接口的一致性

			size_t end = _size+1;
			while (end > pos)
			{
				_str[end] = _str[end - 1];
				--end;
			}

			_str[pos] = ch;
			++_size;

			return *this;
		}

		string& insert(size_t pos, const char* s)
		{
			assert(pos <= _size);
			size_t len = strlen(s);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			size_t end = _size + len;
			while (end > pos+len)
			{
				_str[end] = _str[end - len];
				end--;
			}
			strncpy(_str + pos, s, len);
			_size += len;
			return *this;
		}


		string& erase(size_t pos = 0, size_t len = npos)
		{
			assert(pos < _size);
			if (len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
			return *this;
		}

		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}



	private:
		char* _str;
		size_t _size;
		size_t _capacity; //能存储有效字符的个数不包括\0
		static const size_t npos;
	};
	const size_t string::npos = -1;


	bool operator<(const string& s1, const string& s2)
	{
		//size_t i1 = 0, i2 = 0;
		//while (i1 < s1.size(), i2 < s2.size())
		//{
		//	if (s1[i1] < s2[i2])
		//	{
		//		return true;
		//	}
		//	else if (s1[i1] > s2[i2])
		//	{
		//		return false;
		//	}
		//	else
		//	{
		//		i1++;
		//		i2++;
		//	}
		//}
		//return i2 < s2.size() ? true : false;

		return strcmp(s1.c_str(), s2.c_str()) < 0;
		
	}

	bool operator==(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) == 0;
	}
	bool operator<=(const string& s1, const string& s2)
	{
		return s1 < s2 || s1 == s2;
	}
	bool operator>(const string& s1, const string& s2)
	{
		return !(s1 <= s2);
	}
	bool operator>=(const string& s1, const string& s2)
	{
		return s1 > s2 || s1 == s2;
	}
	
	bool operator!=(const string& s1, const string& s2)
	{
		return !(s1 == s2);
	}


	ostream& operator<<(ostream& out, const string& s) //并不是友元,是否用友元要看是否去访问它的私有
	{
		for (auto ch : s)
		{
			out << ch;
		}

		/*for (int i = 0; i < s.size(); i++)
		{
			out << s[i];
		}*/

		//out << s.c_str(); //不能这样写因为遇到\0就结束了
		return out;
	}

	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			s += ch;
			ch = in.get();
		}

		return in;
	}

	void test0()
	{
		string s1("aaa");
		cin >> s1;
		cout << s1;
	}

	void test()
	{
		string s1("hello");
		string s2;
		cout << s1.c_str() <<endl;
		cout << s2.c_str() << endl; //这个会崩溃,因为这是C形式的字符串遇到\0,才截止
		//但是对于s2返回的是空指针,这是不行的

		for (size_t i = 0; i < s1.size(); i++)
		{
			cout << s1[i]<< " ";
		}
	}
	void test2()
	{
		string s1("hello world");
		string::iterator it = s1.begin();
		while (it != s1.end())
		{
			*it += 1;
			++it;
		}
		it = s1.begin();
		while (it != s1.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;

		//范围for被编译以后就替换成了迭代器,所以支持了迭代器就支持了范围for
		for (auto e : s1)
		{
			cout << e << " ";
		}

	}

	void test3()
	{
		string s1("hello world");
		s1.push_back('h');
		s1.append("hhhhhhhhhh");
		s1 += 'c';
		s1 += "yz 123";
	}

	void test4()
	{
		string s1("hello");
		s1.resize(30,'x');
		s1.insert(0, "kok");
	}

}

五、写时拷贝

浅拷贝的问题:

1.0 析构两次

2.0其中一个对象修改会影响另外一个

深拷贝会解决这个问题,但是效率低

引用计数的写时拷贝

s1指向这块空间,计数为1,s2也指向这块空间计数为2,析构的时候看这个计数,计数大于2就不进行析构而是将计数减减,他会在要进行更改某个对象的时候在进行深拷贝在insert/+=/erase等函数中,先查看引用计数,如果引用计数不是1,要先进行深拷贝,再去修改。

这个技术就是在赌用户不去修改对象只是进行引用计数,不进行深拷贝,效率就高了,如果用户去写效率和深拷贝就是一样的。

缺陷:引用计数存在线程安全的问题,需要加锁,在多线程环境下要付出代价。

在动态库,静态库中有些场景会存在一些问题

std中的string

class string
{
private:
	char _Buf[16]; //字符长度小于16,就存在这个数组中
	char* _Ptr;    //字符长度大于16,就会在堆上去申请
	size_t _mySize;
	size_t _myRes;

};

std中的string会有一个buffer,如果存储的字符长度小于16就会存到栈上,大于16就会存储到堆上

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值