【ONE·C++ || string类(二)】

在这里插入图片描述

总言

  STL:主要介绍string类的模拟实现,加深对string类各接口的熟悉度。(重点理解:迭代器、深拷贝
  

文章目录

  
  
  1)、总言
  对string类,由于库函数中本身有一个,为了避免冲突,这里我们的处理方式有两种:其一是更改类名称,其二是为我们自己模拟实现的类添加命名空间。
  本文章中选择了第二种处理方法:

namespace mystring//命名空间
{
	class string//我们模拟实现的类
	{
	public:

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

  通常情况下,_size_capacity不会出现负数的情况,因此我们采取size_t的类型。
  
  

1、对构造函数、析构、string::c_str

1.1、构造函数模拟实现1.0:string (const char* s);

  1)、模拟实现一:

namespace mystring
{
	class string
	{
	public:
		//string类的构造:
		string(const char* str)//传入参数为字符串时
			:_str(new char[strlen(str) + 1])//此处+1是多开了一个空间
			,_size(strlen(str))
			,_capacity(strlen(str))
		{
			
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

  注意事项:
  ①、此处初始化列表中,能否直接将str赋值给_str?
  回答:不能。_str(str),我们传入的str其类型是常量字符串const char*,而string类中的_str成员变量是char*
  解决方案:由于后续涉及string类的增删查改,我们最好采用动态开辟的方式。
  
  ②、new char[strlen(str) + 1]:我们之前学习string类的构造函数时有观察到,string类会为开辟出的对象保留一个位置以便放置'\0',因此此处我们在动态开辟空间时也要将其考虑进去。
  
  ③、_capacity(strlen(str)):但是实际计算大小时,记录的是有效字符的个数,故这个'\0'并没有记录进去。(这里要注意strlen和sizeof的区别
  
  
  问题: 思考上述模拟实现,有没有发现什么缺陷?
  回答:_str我们只开辟空间,没有将str中的数据拷贝过来,不能达成真正以字符串构造的目的。

		//string类的构造:
		string(const char* str)//传入参数为字符串时
			:_str(new char[strlen(str) + 1])//此处+1是多开了一个空间
			,_size(strlen(str))
			,_capacity(strlen(str))
		{
			strcpy(_str, str);
		}

  
  
  
  2)、问题说明及后续模拟实现引入

  问题引入: 上述模拟实现1.0不能满足所有条件,因为我们完全可以在初始化时传值:

	string s1();

  这样一个无参构造,需要如何解决其构造函数?
  
  方案: 此处对它的实现,①我们要么提供一个全缺省的构造,要么②在1.0的基础上提供一个无参构造
  
  
  
  
  

1.2、构造函数模拟实现2.0:无参构造函数(附析构函数、模拟实现c_str)

  根据上述解决方案,此处选择无参构造+带参的方式。带参在模拟实习1.0中讲解过,以下为无参构造实现过程的情况:
  

1.2.1、string( );

  问题:对无参的构造函数,是否还需要申请空间?为什么?
  回答: 需要。虽然我们说无参,但根据模拟实现一可知,实际需要一个空间放置不被统计'\0'
  实现如下:

namespace mystring
{
	class string
	{
	public:
		string()//假如是无参构造
			:_str(new char[1])
			, _size(0)
			,_capacity(0)
		{
			_str[0] = '\0';//我们初始化定义空对象,其内部也有一个'\0'
		}


	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

  
  注意事项:
   _str(new char[1]),此处我们使用new开辟空间时,尽管只申请一个空间,但我们仍然使用了方括号的形式,这是为了后续析构函数方便一次性释放。 这种new[]的统一写法,(对string(const char* str)有效、对string()也有效)。
  
   关于无参构造函数,初始化列表处_str(new char[1]),是否能让_str(nullptr)?

		string()//假如是无参构造
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{}

   回答:不能。此处原因不在于后续delete释放空指针。(因为delete、free会检查空指针,如果所释放的对象是空指针,那么它们不会做任何处理)。 此处真正错误的原因在于做一些调用返回时,会出错。比如下述情况:
   模拟实现c_str,根据之前所学,其会指向类中字符存储的指针。如果被_str被赋值为nullptr,那么后续调用s.c_str,通过指针打印时,会解引用空指针

	//string函数实现:c_str
	const char* c_str()const
	{
		return _str;
	}
		
	void test_string1()
	{
		string s1;
		cout << s1.c_str() << endl;
	}

  
  
  

1.2.2、c_string()

   相关解释见上述。

	//string函数实现:c_str
	const char* c_str()const
	{
		return _str;
	}

  
  
  

1.2.2、~string( );

   要注意,我们申请了空间,则需要在析构函数中将其销毁。

		//string类析构:
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

  
  
  
  

1.3、构造函数模拟实现3.0:全缺省的构造函数

   在之前学习模拟实现日期类中,我们也表明,写一个带参+无参,不如写一个全缺省方便。那么如果是含有全缺省的string类,该如何实现呢?怎么给值?
  

1.3.1、string(const char* str = “”);

   模拟实现3.1: 全缺省中,缺省参数是否能直接给nullptr?为什么?

		string(const char* str = nullptr)//缺省参数为空指针
			:_str(new char[strlen(str)+1])
			,_size(strlen(str))
			,_capacity(strlen(str))
		{
			strcpy(_str, str);
		}

   回答: 不能。因为当我们无参构造一个string类时,此时由于str=nullptr,那么接下来初始化列表中,函数strlen(str)计算字符串长度时,解引用空指针会导致崩溃。
  
  
   模拟实现3.2: 根据3.1,此处全缺省中,缺省参数该如何给?
   这里提供了"\0"的写法,问:是否可行?

		全缺省下的构造:
		string(const char* str = "\0")//一种写法
			:_str(new char[strlen(str)+1])
			,_size(strlen(str))
			,_capacity(strlen(str))
		{
			strcpy(_str, str);
		}

   回答: 可以,strlen统计'\0'前字符个数,故此处在无参构造情况下,strlen(str)计算结果为0,那么strlen(str)+1 =1,则实际只new出来一个空间。
   但这种写法实际上不严谨,str里实际存储为\0\0
  
  
   模拟实现3.3:
   采用空串"":实际默认隐含存储了一个'\0'

		全缺省下的构造:
		string(const char* str = "")//另一种写法"" ,常量字符串,以\0结尾。
			:_str(new char[strlen(str)+1])
			,_size(strlen(str))
			,_capacity(strlen(str))
		{
			strcpy(_str, str);
		}

  
  
  
  

1.3.2、优化处理:引入抛异常、对strlen

   问题说明: 针对上述小节中的构造函数,我们发现初始化列表中使用了好多次strlen(),要知道strlen使用的效率相对较低(找\0 O ( N ) O(N) O(N)),那么有没有什么优化的方式?

			:_str(new char[strlen(str)+1])
			,_size(strlen(str))
			,_capacity(strlen(str))

  
   优化version1.0:以下这样写能不能做到对strlen的优化?

	class string
	{
	public:
		string(const char* str = "")
			: _size(strlen(str))//先用一次strlen
			, _capacity(_size)//对_capacity我们使用_size初始化
			, _str(new char[_capacity+1])//此处同理
		{
			strcpy(_str, str);
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};

   回答: 这种写法是错误的。在类和对象·初始化列表中我们有提到,初始化列表的顺序与声明顺序一致,与初始化列表中定义的顺序无关。 若按照上述方法进行初始化,_capacity为随机值,那么_str在动态开辟空间时会崩溃。

在这里插入图片描述

  针对上述问题,一个提出的解决方案是:将类中成员变量的顺序调换。但这样子将后续的维护问题与成员变量的顺序关联上了,相对来说不是优解。
  
  此处需要引申一个话题:抛异常(详细介绍将在后续博文,此处先学着使用)。

int main()
{
	try {
		mystring::test_string1();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

  
  
  
  优化version2.0: 针对version1.0中存在的问题,一个解决方案是,混合使用或者干脆不用初始化列表,直接在函数体内初始化。这样做既能解决strlen带来的效率问题,又能解决初始化列表依赖关系。

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

			strcpy(_str, str);
		}

   PS:在类和对象(四)中,我们学习过有些成员变量必须在初始化列表中初始化,这里string类的三个成员都每次必要,故可用上述方法解决strlen效率问题。

  1、引用成员变量
  2const成员变量
  3、自定义类型成员(且该类没有默认构造函数时)

  
  
  
  
  
  

2、对 string::size、string::capacity、string::operator[ ]

2.1、string::size、string::capacity

  库中的函数示意图:
在这里插入图片描述

  
  
  模拟实现:

		//string函数实现:size()
		size_t size()const
		{
			return _size;
		}
		
		//string函数实现:capacity()
		size_t capacity()const
		{
			return _capacity;
		}

   细节说明: 由于不需要修改类中值,对this指针可使用const修饰。
  
  
  
  

2.2、string::operator[ ]

  库中的函数示意图:
在这里插入图片描述

  
  
  模拟实现: 根据库函数,这里为operator[]重载了两种模式:可读可写&只读

		//string函数实现:operator[]
		char& operator[](size_t pos)
		{
			assert(pos < _size);//防止下标越界访问
			return _str[pos];
		}
		const char& operator[](size_t pos)const
		{
			assert(pos < _size);
			return _str[pos];
		}

  
  
  演示结果如下:
在这里插入图片描述
  
  
  
  
  
  

3、迭代器(begin、end)、范围for

  库中的函数示意图:
在这里插入图片描述

3.1、普通迭代器、范围for

  1)、迭代器模拟实现:
   string类中的迭代器就是其原生指针。 需要注意的是,迭代器可能是指针,但不一定完全是指针。

		//string函数实现:迭代器
		typedef char* iterator;
		
		iterator begin()
		{
			return _str;
		}
		
		iterator end()
		{
			return _str + _size;
		}

  
  
  2)、范围for

   演示结果如下:实现了迭代器,就支持了范围for。

在这里插入图片描述

void test_string2()
	{
		//验证迭代器的实现:
		string s1("hello string!");
		cout << s1.c_str() << endl;

		string::iterator it=s1.begin();
		while (it < s1.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
		for (auto ch : s1)//持了迭代器,就支持了范围for
		{
			cout << ch << " ";
		}
		cout << endl;
	}

   反向迭代器相对于迭代器更为复杂一些,相关内容将在后续学习。
  
  
  

3.2、const迭代器

   相比之下,const迭代器的特点是只能读,不能写。

		typedef const char* const_iterator;

		const_iterator begin()const
		{
			return _str;
		}

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

  
  
  
  
  

4、拷贝构造和赋值

  总言: 如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
  库中示意图:

在这里插入图片描述
  

4.1、写法1.0

4.1.1、拷贝构造:version1.0

  1)、回顾浅拷贝
   相关章节:默认生成的拷贝构造函数,对内置类型按内存存储中的字节序完成拷贝(浅拷贝),对自定义类型是调用其拷贝构造函数完成拷贝的。
在这里插入图片描述
   注意:这里要理解s1、s2两次析构,由于_str指向的动态空间相同,当我们析构s1时,虽然将其置空,但只是针对s1._str而言,对于s2._str,其仍旧指向原先空间,但该空间权限已经被收回。
  
  
  
  2)、如何进行深拷贝的说明
   深拷贝即需要给对象分配自己单独的资源空间。
在这里插入图片描述
  演示代码如下:

		//深拷贝:
		string(const string& s)
			:_str(new char[s._capacity + 1])
			, _capacity(s._capacity)
			, _size(s._size)
		{
			strcpy(_str, s._str);
		}

  1、new char[s._capacity + 1]对于我们自己的_str使用new申请新的空间,空间大小同s中的有效数据_capacity,同时还要把隐藏的'\0'加上。
  2、除了要把_capacity_size的值匹配上,对于_str不要忘记将值拷贝过来。 由于string类中_str是字符指针,我们可以直接使用字符串拷贝函数strcpy
  
  演示结果如下: 如图所示,s1,s2指向的地址空间不同,这样delete时不会对同一块空间释放两次,且修改时不会造成连锁反应。

在这里插入图片描述
  
  
  
  

4.1.2、赋值运算符重载:version1.0

  1)、问题引入:如何进行赋值?
   接上述演示,假设现在我们有一个s3,要将其赋值给s1,该如何实现?

	void test_string3()
	{
		//验证拷贝构造
		string s1("hello world.");
		string s2(s1);

		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;


		//验证赋值
		string s3("1111111111 1111111111");
		s1 = s3;//如何实现?
	}

   问题:直接用默认生成的赋值运算符重载可以吗?
   回答:不行。
   原因:默认生成的赋值运算符重载属于浅拷贝,会让s1指向与s3相同的空间。这样一来存在两大问题:①s1更改指向,原先动态开辟的空间没有被释放,则 存在内存泄露的问题。②出了作用域调用析构函数时,同一块空间将释放两次
  
   那么,如何解决这个问题呢?我们可以仿照拷贝构造一样,单独开辟一份资源空间。(深拷贝)
  
  
  2)、模拟实现

		string& operator=(const string& s)
		{
			if (this != &s)
			{
				delete[] _str;//1、释放原有空间及资源
				_str = new char[s._capacity + 1];//2、开辟新空间
				strcpy(_str, s._str);
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		
		}

  细节理解:
  ①: s1 = s3;虽然我们可以在s1空间足够充裕时不释放而是直接交换内部数据,但我们会面临s1空间不足,或s1空间远大于s3的情况,因此不如统一路径先释放s1后开辟新空间
  ②: 如果直接使用_str = new char[s._capacity + 1];new失败了会将原数据释放,所以先使用一个tmp临时变量完成拷贝,确定new成功再说。(当然此处不这样也行,new失败了本来就要抛异常)。但一种相对比较提倡的写法如下:先开辟空间,再交换数据(若空间开辟失败就不用后续数据交换流程):

		string& operator=(const string& s)
		{
			if (this != &s)//4、判断是否为自己给自己赋值
			{
				char* tmp = new char[s._capacity + 1];//3、此处仍旧要+1.
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;//5
		}

  ③: new char[s._capacity + 1]多开辟一个空间是留给\0,因为_size_capacity不计预留的\0。
  ④: if (this != &s) ,这里考虑到的是自己赋值给自己的情况。如果不加if判断语句,那相当于先把s1释放了,再用s1赋值给s1。
  ⑤: return *this; this指针指向对象的地址,此处需要返回的是对象本身,故需要对this指针解引用。
  
  
  
  
  
  

4.2、写法2.0

   总体思路: 用形参中的类,临时构造一个tmps类,与待拷贝的*this类进行数据交换。
   即,借助已经写成的构造函数,使用数据交换swap来完成拷贝构造。
  

4.2.1、拷贝构造写法2.0

4.2.2.1、拷贝构造:version2.0

  1)、拷贝构造写法说明

		string(const string& s)//拷贝构造
			:_str(nullptr)
			,_capacity(0)
			,_size(0)
		{
			string tmps(s._str);
			swap(tmps._str, _str);
			swap(tmps._size, _size);
			swap(tmps._capacity, _capacity);
		}

   解释说明: 1、此处为*this初始化,是为了后续与tmp做成员交换。在没有初始化时,*this其类成员变量为随机值,与tmp交换后,tmp中,成员变量得到原先*this成员变量的随机值。由于tmp是临时变量,出了作用域要销毁,会调用对应的析构函数,会导致delete释放随机空间,崩溃。(delete释放nullptr不会崩溃)
在这里插入图片描述
  故而使用了初始化列表:

			:_str(nullptr)
			,_capacity(0)
			,_size(0)

  
  
  

4.2.2.2、拷贝构造:version2.1(string::swap实现)

  1)、string::swap模拟实现
   1、string类中也有一个成员函数swap,我们可以顺带实现它。

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

   2、::swap此处使用了域作用限定符,前面为空白表示全局域,使用条件为我们将std在全局域展开。若不加该限定符,编译器会先在局部域中找,结果局部域中有swap,编译器会认为自己调用自己,但会因为局部域中的swap参数不匹配而报错。PS:此处也可以修改为std::swap
   3、这样一来在拷贝构造2.0的写法中我们可以直接使用类中的swap

	swap(tmps);//解释:this->swap(tmps);

  
  
  2)、将上述string::swap用于拷贝构造中
   说明: swap(tmps);拷贝构造中,此处先调用string类里的swap,string类中swap调用全局的swap。

		string函数实现:swap
		void swap(string& tmps)
		{
			::swap(tmps._str, _str);
			::swap(tmps._size, _size);
			::swap(tmps._capacity, _capacity);
		}
		
		拷贝构造:
		string(const string& s)
			:_str(nullptr)
			,_capacity(0)
			,_size(0)
		{
			string tmps(s._str);
			swap(tmps);//解释:this->swap(tmps);
		}

  
  
  
  
  

4.2.2.、赋值运算符重载2.0:

4.2.2.1、赋值运算符重载version2.0(含std::swap与string::swap说)

  1)、赋值运算符重载:
   根据拷贝构造2.0中的写法,同理可得赋值运算符重载:

		string& operator=(const string& s)
		{
			if (this!=&s)
			{
				//string tmps(s._str);//可以调用构造
				string tmps(s);//也可以调用拷贝构造
				swap(tmps);
			}
			return *this;
		}

   说明:
   1、string tmps(s._str);string tmps(s);:此处两种写法都可以,前者使用的是构造函数,后者使用的是拷贝构造(使用后者的前提是我们自己实现了拷贝构造)

   2、此处做得很绝的一点是,出了作用域tmp销毁调用析构,就顺带把s1原先交换前指向的动态空间给释放掉了,不需要我们在此处手动写delete(析构函数中已经写了)。
  
  

  2)、swap函数说明:

   1、关于是否可以写::swap(*this,tmps),即,使用库里的全局swap
   回答: 不可以,库里的全局swap实际是个模板,假如我们在赋值运算符重载中使用它,全局库里的swap又在其内部使用了赋值运算符重载a=b,那么此处会造成死循环。( std::swap()相关链接

在这里插入图片描述
  

   与之相反,调用string类里swap,根据我们的实现,此处只是简单的内置类型的交换(没有涉及自定义类的赋值运算符重载)。此外,使用全局库中的swap,则连同地址也会一并被换掉(相对而言做到操作更多更复杂)。

		//string函数实现:swap
		void swap(string& tmps)
		{
			::swap(tmps._str, _str);
			::swap(tmps._size, _size);
			::swap(tmps._capacity, _capacity);
		}
	void test_string4()
	{
		string s1("hello world");
		string s2("XXXXXXX");

		s1.swap(s2);//调用string类里swap:直接交换内部成员变量
		swap(s1, s2);//调用全局库中的swap:会去掉拷贝构造。
	}

  
  
  

4.2.2.2、赋值运算符重载:使用传值传参进一步简化

   折回上述问题,关于赋值运算符重载:若直接使用传值传参,则可省掉了tmp,相对更为精髓。(但与库中函数形参不匹配)

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

  
  
  
  
  
  

5、增删查改

5.1、string::reserve、string::push_back、string::operator+=(char c)

5.1.1、string::reserve

  1)、库函数中声明回顾:
在这里插入图片描述
   1、如果 n 大于当前字符串容量,则该函数会导致容器将其容量增加到 n 个字符(或更大)。
   2、此函数对字符串长度没有影响,并且无法更改其内容。
  

  2)、模拟实现

		void reserve(size_t n)
		{
			if (n>_capacity)
			{
				//开空间:
				char* tmp = new char[n + 1];//n个有效空间,一个\0空间
				//拷贝值:
				strcpy(tmp, _str);
				//释放旧空间:
				delete[]_str;
				//重新给定指向:
				_str = tmp;
				_capacity = n;//注意别忘记,另reserve里只是对_capacity做了修改,不会变动_size。
			}

		}

  
  
  
  

5.1.2、string::push_back

在这里插入图片描述

   注意事项:
   1、插入数据前要进行扩容检查
   2、尾插,是在_size下标处,故结束后不要忘记放置'\0'

		void push_back(char ch)
		{
			//检查扩容:
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
				//此处三目运算符是为了预防构造时参数为空的情况。
			}
			//尾插
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';//注意此处别忘了放置\0

		}

  
  
  
  

5.1.3、string::operator+=(char ch)

在这里插入图片描述
   如上述,operator+=在类中有三个函数重载,这里实现的是+=一个字符的情况,可借助push_back完成。

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

  
  演示结果如下:
在这里插入图片描述
  
  
  
  

5.2、string::append、string::operator+=(const char* str)

   库函数中声明回顾:
在这里插入图片描述
  
  

5.2.1、string::append (const char* s)

   思路分析:
   ①首先要计算出插入的字符串的长度
   ②对比插入前后扩容问题 ,若容量空间不够,则需要扩容,但这里不能直接使用二倍扩容,这种方法不一定满足容量空间(比如原先空间为8,二倍扩容为16,如果插入字符串str+_size值大于16,容量空间不够)。
   ③在容量空间足够的情况下, 将需要的字符串拷贝/追加过 来,④并完成对字符串string长度的更新工作

void append(const char* str)
		{
			//append仍旧需要扩容检查,只是其是在尾部追加字符串而非字符
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				//此处我们不能直接仿照push_back中的写法进行二倍扩容等。
				//所以我们干脆使用reserve重定义存储空间:
				reserve(_size + len);//至少需要_size+len的空间
			}
			//尾插字符串:
			strcpy(_str+_size, str);
			//修改属性:
			_size += len;
			//此处_capacity我们在使用reserve时做了处理。
		}

   此处strcpy(_str + _size, str);若换为strcat(_str, str)可以吗?回答是可以,但是strcat追加需要找\0,相率相对较低。

  演示结果如下:

在这里插入图片描述

   既然实现了上述append功能,我们自然而然可以利用它们实现以下接口:
  
  
  

5.2.2、operator+=(const char* str)

   由于push_back只是尾插一个字符,要实现operator+=字符串,此处借助了上述的append函数。

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

  
  

5.2.3、append(const string& s)

   同理,append中给定的是类对象本身,那么我们只需要调用append函数,将传入参数修改其成员变量_str即可。

		void append(const string& s)
		{
			append(s._str);
		}

  
  

5.2.4、append(size_t n, char ch)

		void append(size_t n, char ch)
		{
			reserve(_size + n);//提前开辟充裕空间
			for (int i = 0; i < n; i++)
			{
				push_back(ch);
			}
		}

  
  
  
  

5.3、string:: insert(实现insert,可实现push_back、append)

  库函数中声明回顾: 这里主要模拟实现下列这两个。
在这里插入图片描述
  

  

5.3.1、string::insert(size_t pos, char ch)

  1)、模拟实现insert 1.0

		string& insert(size_t pos, char ch)
		{
			//首先要断言一下插入的位置:pos需要合法
			//此处涉及的一个问题是边界问题:_size位置处能否插入?事实上只要我们处理好最后的收尾工作即可。
			assert(pos <= _size);

			//扩容检查:
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

			//数据插入:向后挪动数据,从后往前遍历
			size_t end = _size;//这里默认把_size处的'\0'也算入了
			while (end >= pos)
			{
				_str[end + 1] = _str[end];
				--end;
			}
			_str[pos] = ch;

			//收尾处理:
			++_size;
			_str[_size] = '\0';//实际_size挪动时把'\0'也一并挪动了,这里从严谨角度再赋值一次。(当然,若不算入另有写法)
			return *this;
		}

  演示结果如下:
在这里插入图片描述

  
  2)、模拟实现insert 2.0
   针对1.0版本,是否存在什么问题?

			//数据插入:向后挪动数据,从后往前遍历
			size_t end = _size;
			while (end >= pos)
			{
				_str[end + 1] = _str[end];
				--end;
			}
			_str[pos] = ch;

   看看上述代码,如果pos位置在下标为0的位置处,那么头插会存在问题。若end是int类型数据,那么end=-1时退出循环。这不是正确的吗?但问题是上述代码中 end值是size_t无符号整型-1对应的无符号数是一个很大的值,因此才说此处会陷入死循环中。

   所以有如下几种解决方案:
  1、如下图,将end的类型改为int类型,是否可行?(否)

在这里插入图片描述
  
   2、既如此,那么把pos的类型也一并修改,总该可以了吧? (否)
   回答:这种做法行得通,但是事实上我们观察库函数里的pos,其提供的都是size_t类型。而且这里还会有一个问题:pos本意指代下标,使用int类型的pos,万一传入的是负数呢?
   PS:这里也解释了为什么assert(pos <= _size);不检查<0的情况,因为pos为无符号整型。

在这里插入图片描述
  
   3、综上所述,这里我们建议将end初始值置成_size+1,如下述演示:

		/insert插入:版本2.0
		string& insert(size_t pos, char ch)
		{
			//首先要断言一下插入的位置:pos需要合法
			//此处涉及的一个问题是边界问题:_size位置处能否插入?事实上只要我们处理好最后的收尾工作即可。
			assert(pos <= _size);

			//扩容检查:
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

			//数据插入:
			size_t end = _size+1;
			while (end >pos) //理解:end-1 >= pos → end>=pos+1 → end > pos
			{
				_str[end] = _str[end-1];//步骤一:向后挪动数据,从后往前遍历
				--end;
			}
			_str[pos] = ch;//步骤二:插入数据ch

			//收尾处理:
			++_size;
			_str[_size] = '\0';
			return *this;
		}

  演示结果如下:

在这里插入图片描述
  
  
  
  

5.3.2、string:: insert(size_t pos, const char* str)

  假如是插入字符串呢?代码如下:

		string& insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			size_t len = strlen(str);
			if (len + _size > _capacity)
			{
				reserve(len + _size);
			}

			size_t end = _size + len;
			while (end >= pos + len)//等价于 end > pos + len -1;
			{
				_str[end] = _str[end - len];
				--end;
			}
			strncpy(_str + pos, str,len);

			_size += len;
			_str[_size ] = '\0';
			return *this;
		}

  演示结果如下:
在这里插入图片描述

   同理可修改其它:

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

  
  
  
  

5.4、string::erase

  1)、库函数中声明回顾:
在这里插入图片描述
  
  2)、模拟实现:
  演示代码如下:

		void erase(size_t pos, size_t len = npos)
		{
			assert(pos <= _size);
			//不需要挪动数据的情况:即全部删完
			if (len == npos || len + pos > _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				//需要挪动数据的情况:
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}

		}

  演示结果如下:
在这里插入图片描述
  
  
  
  

5.5、流插入和流提取

   不是所有的流插入、流提取都需要设置为友元。 我们在日期类中使用了友元是因为涉及访问私有成员。而string类中我们完全可以使用下标来实现这一功能。

5.5.1、基本实现

  演示代码如下:

	ostream& operator<<(ostream& cou, const string& s)
	{
		for (size_t i = 0; i < s.size(); ++i)
		{
			cou << s[i];
		}
		return cou;
	}
	istream& operator>>(istream& ci, string& s)
	{
		char ch;
		ch = ci.get();//注意这里要使用该函数的原因
		while (ch != ' ' && ch != '\n')
		{
			s += ch;
			ch = ci.get();
		}
		return ci;
	}

  
  

5.5.2、优化处理(含string::clear())

   上述流提取中,若输入字符串很长,不断+=会频繁扩容,效率很低,因此我们可以优化一下 :

	istream& operator>>(istream& ci, string& s)
	{
		char ch;
		ch = ci.get();
		//用于优化的数组:
		const int N = 32;//常量数组
		char buff[N];

		size_t i = 0;

		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;//先将插入字符放入数组中
			if (i == N - 1)//若字符放满,则一次性挪动到string类中
			{
				buff[N] = '\0';//留一个位置给'\0'
				s += buff;
				i = 0;//注意需要将i置回0以便下一轮使用
			}
			ch = ci.get();
		}
		//这是为最后一轮作处理:若ch读到\n或'',则跳出while循环,那么残余部分没有被放入string类中
		buff[i] = '\0';
		s += buff;
		return ci;
	}

  

   同理,对于流提取,仍旧有一些细节:
在这里插入图片描述

  如上图所示:假如类原先就有字符,那么cin后在标准库中会被覆盖,针对这一问题,我们可在类中实现一个clear函数:

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

在这里插入图片描述
  
  再此基础上我们来实现流提取:

	istream& operator>>(istream& ci, string& s)
	{
		s.clear();

		char ch;
		ch = ci.get();
		//用于优化的数组:
		const int N = 32;//常量数组
		char buff[N];

		size_t i = 0;

		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;//先将插入字符放入数组中
			if (i == N - 1)//若字符放满,则一次性挪动到string类中
			{
				buff[N] = '\0';//留一个位置给'\0'
				s += buff;
				i = 0;//注意需要将i置回0以便下一轮使用
			}
			ch = ci.get();
		}
		//这是为最后一轮作处理:若ch读到\n或'',则跳出while循环,那么残余部分没有被放入string类中
		buff[i] = '\0';
		s += buff;
		return ci;
	}

  
  
  
  
  

5.6、其它接口:比较、substr、resize、find

5.6.1、stirng::substr

在这里插入图片描述

		string substr(size_t pos, size_t len = npos)const
		{
			assert(pos < _size);
			size_t reallen = len;//是为了记录真实取得的字符长度
			if (len == npos || len + pos > _size)
			{
				reallen = _size - pos;//真实长度为[pos,_size)
			}
			string tmp;
			for (size_t i = 0; i < reallen; i++)
			{
				tmp += _str[pos+i];
			}
			return tmp;
		}

  
  
  

5.6.2、string::find

在这里插入图片描述

		size_t find(char ch, size_t pos = 0)const
		{
			assert(pos < _size);

			for (size_t i = pos; i < _size; i++)
			{
				if (_str[i] == ch)
					return i;//若找到对应字符,则返回下标
			}
			return npos;//若找不到,则返回npos
		}

		size_t find(const char* sub, size_t pos = 0)const
		{
			const char*p =strstr(_str + pos, sub);//此处为暴力匹配,其它匹配方法:kmp/bm
			if (p == nullptr)
			{
				return npos;
			}
			else {
				return p - _str;
			}
		}

  
  
  

5.6.3、string::resize

在这里插入图片描述

		void resize(size_t n, char ch = '\0')
		{
			if (n > _size)
			{
				//开辟空间,插入数据
				reserve(n);
				for (size_t i = _size; i < n; ++i)
				{
					_str[i] = ch;
				}
				_str[n] = '\0';
				_size = n;
			}
			else
			{
				//删除数据
				_str[n] = '\0';
				_size = n;
			}
		}

  
  

5.6.4、operator比较运算符重载

		bool operator >(const string& s)const
		{
			return strcmp(_str, s._str) > 0;
		}

		bool operator == (const string & s)const
		{
			return strcmp(_str, s._str) == 0;
		}

		bool operator >=(const string& s)const
		{
			return (*this == s) || (*this > s);
		}


		bool operator <(const string& s)const
		{
			return !(*this >= s);
		}

		bool operator <=(const string& s)const
		{
			return !(*this > s);
		}

		bool operator !=(const string& s)const
		{
			return !(*this == s);
		}

  
  
  
  

6、其它相关知识

6.1、

1、vs下string类内存大小设置
2、其它string类实现方案
3、针对浅拷贝而设置的方案:引用计数和写时拷贝。
   浅拷贝存在的两问题:
      ①、析构两次/多次
      ②、一个对象修改影响另一个对象
   针对一:引用计数,最后一个析构对象释放空间
   针对二:写时拷贝,只要在真正需要深拷贝时才开辟新空间,若实际中没有相关场景需求则不写。
  
  
  

6.2、整体要实现的框架汇总

   模拟实现string类(此处列举声明):

namespace mystring
{

    class string
    {

        friend ostream& operator<<(ostream& _cout, const mystring::string& s);

        friend istream& operator>>(istream& _cin, mystring::string& s);

    public:

        typedef char* iterator;

    public:

        / 默认成员函数

        string(const char* str = "");

        string(const string& s);

        string& operator=(const string& s);

        ~string();
        /



        /// iterator
        iterator begin();

        iterator end();
        /



        /// modify
        void push_back(char c);

        string& operator+=(char c);

        void append(const char* str);

        string& operator+=(const char* str);

        void clear();

        void swap(string& s);

        const char* c_str()const;
        /



        /// capacity
        size_t size()const;

        size_t capacity()const;

        bool empty()const;

        void resize(size_t n, char c = '\0');

        void reserve(size_t n);
        /




        /// access
        char& operator[](size_t index);

        const char& operator[](size_t index)const;
        /



        /// relational operators
        bool operator<(const string& s);

        bool operator<=(const string& s);

        bool operator>(const string& s);

        bool operator>=(const string& s);

        bool operator==(const string& s);

        bool operator!=(const string& s);
        /



        // 返回c在string中第一次出现的位置
        size_t find(char c, size_t pos = 0) const;

        // 返回子串s在string中第一次出现的位置
        size_t find(const char* s, size_t pos = 0) const;

        // 在pos位置上插入字符c/字符串str,并返回该字符的位置
        string& insert(size_t pos, char c);
        string& insert(size_t pos, const char* str);



        // 删除pos位置上的元素,并返回该元素的下一个位置
        string& erase(size_t pos, size_t len);

    private:
        char* _str;
        size_t _capacity;
        size_t _size;
    };

}

  
  
  
  
  
  

Fin、共勉。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值