C++STL详解(二):string的模拟实现

文章目录

        • string的深浅拷贝问题
          • 浅拷贝
          • 深拷贝
          • 传统写法
        • string类各函数的接口
        • 默认成员函数
          • 构造函数
          • 拷贝构造函数
          • 赋值运算符重载
          • 析构函数
        • 与迭代器相关的函数
          • begin
          • end
        • 与容量和大小相关的函数
          • size
          • capacity
          • empty
          • reserve
          • resize
        • 修改字符串的函数
          • push_back
          • append
          • operator+=
          • swap
          • insert
          • erase
          • clear
          • c_str
        • 字符串访问与查找函数
          • find
          • operator[]
        • 关系运算符重载函数
          • <运算符重载
          • ==运算符重载
          • <=运算符重载
          • >运算符重载
          • >=运算符重载
          • !=运算符重载
        • <<和>>运算符的重载与getline函数
          • <<运算符重载
          • >>运算符重载
          • getline
        • string类的实现代码

string的深浅拷贝问题

在开始模拟实现string类之前,我们先来讲一下string的深浅拷贝

在前面学类和对象的时候我们知道对于像Date这种日期类使用浅拷贝(值拷贝)是没有问题的,但是对于像string这样的类是不能使用浅拷贝的,而是必须得使用深拷贝。那是为什么呢?下面我们就来好好说一说

浅拷贝:也称为值拷贝,编译器只是将对象中的值拷贝过来,拷贝出来的的目标对象的指针与源对象的指针指向的同一块内存空间的,此时他们会共享同一份资源。当一个对象增加或者删除数据的时候,另一个对象也会增加或者删除数据。并且更严重的问题是,当两个对象都销毁时,析构函数一共会被调用两次,并且析构的是同一块空间,从而导致该空间会被释放两次,从而导致编译器报错。

深拷贝:目标对象的指针与源对象的指针指向的空间是相互独立的,不会存在共享同一份资源的问题,从而也就不会有非法访问内存的问题了

下面我们再通过画图与代码来展示一下深拷贝与浅拷贝

浅拷贝

C++STL详解(二):string的模拟实现_第1张图片

       //析构函数
		~string()
		{
			delete[]_str;
			_str = nullptr;
		}
private:
		char* _str;

int main()
{
    string s1("hello");
    string s2(s1);
    return 0;
}

C++STL详解(二):string的模拟实现_第2张图片

深拷贝

C++STL详解(二):string的模拟实现_第3张图片

string的拷贝构造有这些问题,string的赋值运算符重载同样也存在着这些问题,因此赋值运算符重载也需要实现深拷贝

对于string的拷贝构造的深拷贝有传统写法与现代写法两种方式,赋值运算符重载也是如此,下面我们就来一起看一下。

传统写法
//传统写法
		//拷贝构造
		//s1=s3
		string(const string& s)
		{
            //申请一块与你一样大的空间
			_str = new char[strlen(s._str) + 1];
            //再将你空间里面的值给拷过来
			strcpy(_str, s._str);
		}
		//赋值运算符重载
		string& operator=(const string& s)
		{
			if (this != &s)
			{
			 //先释放自己的空间
			 delete[]_str;
			 //再开和你一样大的空间
			 _str = new char[strlen(s._str) + 1];
			 //再把你的值拷贝过来
			 strcpy(_str, s._str);
			}
			return *this;
		}

现代写法

//现代写法
		//拷贝构造
		//这个就有像:我想吃东西但是我又不想自己做,我就点个外卖让骑手给我送过来,然后我家里还有一点垃圾
		//早上出门忘记丢了,但是我又不想出门丢垃圾,我就让骑手帮我丢垃圾,如果不丢垃圾就给你差评
		string(const string& s)
		{
			//首先得将_str置为空指针
			//否则temp与_str交换了之后,出了函数就会调析构函数
			//此时就会非法访问内存
			_str = nullptr;
			string temp(s._str);
			swap(_str, temp._str);
		}

		//赋值运算符重载
		//s1 = s3
		string& operator=(string s)
		{
			swap(_str, s._str);
			return *this;
		}
        //第二种写法
        string& operator=(const string& s)
		{
			//防止出现这种情况
			//s1 = s1
			if (this != &s)
			{
				string temp(s);
				swap(_str, temp._str);
			}
			return *this;

讲完了深浅拷贝问题,接下来我们就来模拟实现一下string类吧

string类各函数的接口

namespace mlf
{
	//模拟实现string类
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;

		//默认成员函数
		string(const char* str = "");         //构造函数
		string(const string& s);              //拷贝构造函数
		string& operator=(const string& s);   //赋值运算符重载函数
		~string();                            //析构函数

		//迭代器相关函数
		iterator begin();
		iterator end();
		const_iterator begin()const;
		const_iterator end()const;

		//容量和大小相关函数
		size_t size()const;
		size_t capacity();
		void reserve(size_t n);
		void resize(size_t n, char ch = '\0');
		bool empty()const;

		//修改字符串相关函数
		void push_back(char ch);
		void append(const char* str);
		string& operator+=(char ch);
		string& operator+=(const char* str);
		string& insert(size_t pos, char ch);
		string& insert(size_t pos, const char* str);
		string& erase(size_t pos, size_t len = npos);
		void clear();
		void swap(string& s);
		char* c_str()const;

		//访问字符串相关函数
		char& operator[](size_t i);
		const char& operator[](size_t i)const;
		size_t find(char ch, size_t pos = 0)const;
		size_t find(const char* str, size_t pos = 0)const;

	private:
		char* _str;       
		size_t _size;     //记录字符串当前的有效长度
		size_t _capacity; //记录字符串当前的容量
		static const size_t npos; //静态成员变量
	};
    //静态成员变量要在类外面初始化
	const size_t string::npos = -1;
    
        //关系运算符重载函数
		bool operator>(const string& s1,const string& s2)const;
		bool operator>=(const string& s1,const string& s2)const;
		bool operator<(const string& s1,const string& s2)const;
		bool operator<=(const string& s1,const string& s2)const;
		bool operator==(const string& s1,const string& s2)const;
		bool operator!=(const string& s1,const string& s2)const;

	//<<和>>运算符重载函数
	istream& operator>>(istream& in, string& s);
	ostream& operator<<(ostream& out, const string& s);
	istream& getline(istream& in, string& s);
}

注意:为了防止与标准库里面的string类产生命名冲突,模拟实现的时候需要放在自己的命名空间当中,这个时候命名空间就发挥它的作用。

默认成员函数

构造函数

需要注意的是:这里的缺省值不能够给nullptr,因为如果是nullptr的话strlen(nullptr)就会对nullptr解引用,程序就会崩溃。因此我们的缺省值给的是空字符串,我们将这里的_capacity与_size给的是一样的值,因此开空间的时候需要多开一共给’\0’,当然你也可以选择不像我们这样把capacity与size给一样的值,具体实现根据自己的情况来。开完了与str字符串一样的空间之后我们再将str的C字符串拷贝到已经开好的空间里面。

//构造函数
		string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			//为存储字符串开空间,多开的一个给'\0'
			_str = new char[_capacity + 1];
			//将C字符串拷贝到已经开好的空间
			strcpy(_str, str);
		}
拷贝构造函数

注意:这里的str需要给一个初始值,一般给它置成nullptr。如果不给的话就会出问题,为什么呢?因为如果str没有给初始值的话,它就是个随机值,指向的就是一块随机的空间,当与temp对象交换之后,出了作用域temp调用它的析构函数,此时释放的就是一块随机的空间,从而导致程序发生错误。

        //拷贝构造
		//s1(s3)
		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			string temp(s._str);
			/*swap(_str, temp._str);
			swap(_size, temp._size);
			swap(_capaticy, temp._capaticy);*/
			swap(temp);
		}
赋值运算符重载
        //赋值运算符重载
		//s1 = s3
		string& operator=(string s)
		{
			/*swap(_str, s._str);
			swap(_size, s._size);
			swap(_capaticy, s._capaticy);*/
			swap(s);//交换两个对象的str,size,capacity

			return *this;//支持连续赋值
		}

需要注意的是:上面的这种写法无法避免自己给自己赋值,但是一般来说是不会出现自己给自己赋值这种操作的,但是为了避免发生自己给自己赋值的情况,你可以这样写:

        string& operator=(const string& s)
		{
			//防止出现这种情况
			//s1 = s1
			if (this != &s)
			{
				string temp(s);//用s对象拷贝构造temp
				swap(temp);//交换这两个对象
			}
			return *this;//支持连续赋值

注:swap成员函数的模拟实现在后面

析构函数
        //析构函数
		~string()
		{
			delete[]_str;//释放str指向的空间
			_str = nullptr;//将str置空,防止它成为野指针
			_size = 0;//有效个数置成0
			_capacity = 0;//空间大小置成0
		}

我们来说一下在实现接口函数的小细节:

  • 对于只读的接口函数我们只实现加const的版本
  • 对于只写的接口函数我们只实现不加const的版本
  • 对于可读可写的接口函数,我们需要实现两个版本,一个加const的版本,一个不加const的版本。

因此具体要不要加const要看接口的功能性

与迭代器相关的函数

我们string类里面的迭代器底层其实是用指针实现的,但是需要注意的是并不是所有的迭代器都是指针。由于迭代器去访问字符串的时候既可以读也可以修改字符串的内容,那么我们就需要实现两种版本的迭代器,一种只支持读字符串,一种支持既可以读字符串也可以修改字符串。

        typedef char* iterator;
		typedef const char* const_iterator;
		
begin

begin成员函数的作用就是返回字符串中第一个字符的地址

        //可读可写
		iterator begin()
		{
			return _str;//返回字符串中第一个字符的地址
		}
        
        //只读
		const_iterator begin()const
		{
			return _str;//返回字符串中第一个字符的地址
		}
end

end成员函数的作用就是返回字符串中最后一个字符的后一个字符的地址(’\0’的地址),注意:’\0’不是有效字符,它是标识字符用来标识字符串结束的字符。

        //可读可写
        iterator end()
		{
			return _str + _size;//返回字符串中最后一个字符的后一个字符的地址('\0'的地址)
		}

        //只读
        const_iterator end()const
		{
			return _str + _size;//返回字符串中最后一个字符的后一个字符的地址('\0'的地址)
		}

与容量和大小相关的函数

我们模拟实现的string类对象的成员变量权限都是私有的,我们不能够在类外面对其直接访问,那么如果我们想访问size与capacity的话,我们可以自己实现两个成员函数,通过调用成员函数来获取string对象的大小和容量。

size

size成员函数用于获取当前字符串的有效长度(不包括’\0’,’\0’不是有效字符)

        size_t size()const
		{
			return _size;//返回当前字符串的有效长度
		}
capacity

capacity成员函数用于获取当前字符串的容量大小

        size_t capacity()const
		{
			return _capacity;//返回当前字符串的容量大小
		}
empty

empty成员函数用来判断当前字符串是否为空

        //判断当前字符串是否为空
		bool empty()const
		{
			return strcmp(_str, "") == 0;
		}
reserve

实现reserve函数的时候我们首先得清楚它的规则:

1.当n大于当前对象的空间容量时,就需要把空间扩大到n或者大于n

2.当n小于当前对象的空间容量时,空间容量不发生变化。

		// 开空间,扩展capacity
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				//开n+1个空间,还有一个是给'\0'的
				char* temp = new char[n + 1];
				//将原数据拷贝到temp里面
				//把源字符串中的所有数据都拷过去
				strncpy(temp, _str,_size+1);
				//释放_str
				delete[]_str;
				//再让_str指向temp
				_str = temp;
				//然后更新capacity
				_capacity = n;
			}
		}

注意:我们这里需要使用strncpy去将源字符串的数据拷贝到temp里面,而不是使用strcpy,这是为了防止源字符串中含有有效字符’\0’而为拷贝过去,到了下次再插入字符或者字符串的时候字符串中会出现随机值的情况(因为有效字符’\0’丢了),如果源字符串中含有多个’\0’的话strcpy拷贝完第一个’\0’它就结束拷贝了,因此我们不能使用strcpy。

C++STL详解(二):string的模拟实现_第4张图片

resize

同样的实现resize函数的时候我们也得弄清楚它的规则:

1.当n小于size时,就将字符串的有效长度(size)变成n

2.当n大于size但是小于capacity的时候,我们需要在当前字符串的后面添加字符ch,如果ch未给出,则默认就是’\0’,直到size与n相等为止,再将’\0’赋给字符串下标为n的位置,然后更新size的值即可。如果n是大于capacity的话我们就需要先增容,然后再去执行后面的操作。

		// 开空间+初始化,扩展capacity 并且初始化空间。size也要动
		void resize(size_t n,char ch = '\0')
		{
			//如果n小于_size
			if (n < _size)
			{
				//那么有效个数就是n
				_size = n;
				_str[_size] = '\0';
			}
			//n大于_size
			else
			{
				//如果n大于capacity
				//我们就增容
				if (n>_capacity)
				{
					reserve(n);
				}
					begin指向'\0'的位置
					//char* start = _str + _size;
					//char* end = _str + n;
					//while (start != end)
					//{
					//	*start++ = ch;
					//	++_size;
					//}
				for (size_t i = _size; i < n; i++)
				{
					_str[i] = ch;
				}
					_str[n] = '\0';
					_size = n;
			}
		}

修改字符串的函数

push_back

push_back函数的作用就是在字符串中尾插一个字符,但是插入的时候我们需要先判断是否需要增容,如果需要增容就调用reserve函数扩容,再将字符ch放到size下标的位置上,注意我们需要手动的将size+1下标的位置的字符设置成’\0’,否则打印字符串的时候会非法访问内存,因为尾插的字符后面不一定是’\0’。

        void push_back(char ch)
		{
			//判断是否需要增容
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
			_str[_size] = ch;
			_str[_size + 1] = '\0';
			++_size;
		}

我们push_back函数还可以通过复用insert函数来实现

//尾插字符
void push_back(char ch)
{
    insert(_size,ch);
}
append

append函数的作用是在字符串的后面尾插一个字符串,同样的我们也需要先判断尾插前的size与当前字符串的长度是否已经超过了capacity,超过了我们就需要调用reserve函数增容。空间足够了之后我们再调用strcpy函数将要插入的字符串拷贝到当前字符串的尾部,这里有一个小细节:_str+size的这个下标的位置正好就是当前字符串’\0’的位置,因为待插入字符串的后面自己带有’\0’,所以我们不需要在拷贝完成后再去后面设置’\0’了。

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

同样的我们这里的append函数也可以复用insert函数来实现

//尾插字符串
void push_back(const char* str)
{
    insert(_size,str);
}
operator+=

实现+=运算符的重载是为了实现字符串与字符、字符串与字符串之间能够直接使用+=运算符进行尾插。

+=运算符实现字符串与字符之间的尾插我们可以复用push_back函数来完成

        //s1+='x'
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;//支持连续+=字符
		}

+=运算符实现字符串与字符串之间的尾插我们可以复用append函数来实现

        //s1+="xxxxx"
		string& operator+=(const char* str)
		{
			append(str);
			return *this;//支持连续+=字符串
		}
swap

swap函数用于交换两个对象的数据,我们这里实现swap函数可以直接调用库里面的swap模板函数即可,我们如果像调用库里面的swap模板函数的话,需要在swap前面加上"::"(作用域限定符),告诉编译器优先去全局范围找swap函数,如果不加上作用域限定符编译器就会认为你调用的就是你正在实现的swap函数(就近原则)

C++STL详解(二):string的模拟实现_第5张图片

        //s1.swap(s2)
		void swap(string& s)
		{
			//调用alogrithm中的swap库函数
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
insert

insert函数我们也需要实现两个版本,一个版本用来插入字符,一个版本用来插入字符串

实现insert函数插入字符时,我们需要先判断pos位置是否合法,若不合法则报错,然后再判断是否需要增容,空间不够就调用reserve函数来增容。插入字符的过程就通过指针的方式来实现,将pos位置的字符以及pos后面的字符都向后挪动一个位置,将pos这个位置给空出来,然后再将要插入的字符放到pos位置即可。

C++STL详解(二):string的模拟实现_第6张图片

        //插入字符
		string& insert(size_t pos, char ch)
		{
			//pos位置必须合法
			assert(pos >= 0 && pos <= _size);
			//判断是否需要增容
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
			//end指向'\0'的位置
			char* end = _str + _size;
			while (end >= _str + pos)
			{
				*(end + 1) = *end;
				--end;
			}
			//通过挪动数据将pos位置空出来了之后,将ch放到pos位置上
			_str[pos] = ch;
			_size++;

			return *this;
		}

实现insert函数插入字符串,我们也需要判断pos位置是否合法,若不合法则报错,再来判断要插入字符串的长度+当前字符串的有效长度是否大于当前字符串的空间,如果大于我们就调用reserve函数扩容。与上面类似插入字符串的过程就通过指针的方式来实现,将pos位置的字符以及pos后面的字符都向后挪动len个位置,给待插入字符留出足够位置,再插入该字符即可。

这里同样需要注意的是:我们不能够使用strcpy函数来拷贝,因为strcpy会将要插入的字符串中的’\0’也给一起拷过来,这样是会出问题的,因此在这里我们也要使用strncpy函数。

C++STL详解(二):string的模拟实现_第7张图片

        //插入字符串
		string& insert(size_t pos, const char* str)
		{
			//pos位置必须合法
			assert(pos >= 0 && pos <= _size);
			size_t len = strlen(str);
			//如果当前有效数据个数和要插入的字符串长度大于空间容量就增容
			if (len + _size > _capacity)
			{
				reserve(len + _size);
			}
			//挪动数据
			char* end = _str + _size;
			while (end >= _str + pos)
			{
				*(end + len) = *end;
				--end;
			}
			//从pos位置开始的len个长度位置已经挪出来了
			//注意这里不能够用strcpy,因为strcpy会把str字符串里面的'\0'也给拷过来
			//所以我们要用strncpy
			strncpy(_str + pos, str, len);
			//更新_size
			_size += len;

			return *this;
		}
erase

erase函数的作用是删除字符串中任意位置开始的n个字符。删除字符前我们也需要判断pos位置的合法性,进行删除操作的时候分两种情况。

1.剩余的字符长度小于要删的长度

这种情况比较简单我们只需要将pos位置放上’\0’,然后更新我们的size即可

C++STL详解(二):string的模拟实现_第8张图片

2.剩余字符的长度大于要删的长度

这种情况其实也不难,我们需要找到要删除的最后一个字符的下一个位置,然后通过调用strcpy函数,将该位置以及其后面位置的字符覆盖到前面需要删除的字符位置上即可,并且这个时候我们不需要手动的在字符串后面加’\0’,因此字符串末尾本身就有’\0’.

C++STL详解(二):string的模拟实现_第9张图片

string& erase(size_t pos = 0, size_t len = npos)
		{
			//pos位置必须合法
			assert(pos < _size);
			//计算从pos位置到_size位置剩余字符的长度
			size_t lenleft = _size - pos;
			//1.剩余的字符长度小于要删的长度(后面全部删完)
			if (len>lenleft)
			{
				_str[pos] = '\0';
				_size -= len;
			}
			//2.剩余的字符长度大于要删的长度
			else
			{
				//end是要删除的最后一个字符的下一个位置
				char* end = _str + pos + len;
				//利用strcpy函数将后面字符,放到pos位置上,通过覆盖我们就删除了要删除的字符串
				strcpy(_str + pos, end);
			}

			return *this;
		}
clear

实现clear函数我们只需要让字符串的有效长度置成0,再让字符串size下标的位置设置成’\0’即可

        //清空字符串
		void clear()
		{
			_size = 0;
			_str[_size] = '\0';
		}
c_str

c_str函数用于获取对象C类型的字符串,实现的时候直接返回对象的成员变量_str即可

        //返回C类型的字符串
        char* c_str()const
		{
			return _str;
		}

字符串访问与查找函数

find

我们这里的find函数可以实现两个版本,一个版本用来查找一个字符,一个版本用来查找一个字符串,找到了就返回第一次找到的下标,没找到则返回npos(整型的最大值),我们这里的find成员函数是通过调用库函数strstr来实现的。

        //查找一个字符
		size_t find(char ch, size_t pos = 0)const
		{
            assert(pos<_size);//检测pos位置的合法性
			for (size_t i = pos; i < _size; i++)
			{
				//找到了就返回下标
				if (_str[i] == ch)
				{
					return i;
				}
			}
			//没找到返回npos
			return npos;
		}


       //查找一个字符串
		size_t find(const char* str, size_t pos = 0)const
		{
            //检测pos位置的合法性
            assert(pos<_size);
			const char* ret = strstr(_str+pos, str);
			//找到了就返回下标
			if (ret)
			{
				//当前位置减去起始位置就是它的下标
				return ret - _str;
			}
			//没找到就返回npos
			else
			{
				return npos;
			}
		}
operator[]

实现[]运算符的重载是为了让string类对象可以像C字符串一样,可以通过[]+下标的方式去访问字符串中对应位置的字符。在C字符串中通过[]+下标的方式我们既可以去遍历访问当前字符串的某个字符,也可以去修改字符串中的内容。因此这里的operator[]我们也可以实现两个版本,一个只读的版本,一个可读可写的版本。

        //只读
		const char& operator[](size_t pos)const
		{
            //检测pos位置的合法性
			assert(pos < _size);
			return _str[pos];
		}
        //可读可写
		char& operator[](size_t pos)
		{
            //检测pos位置的合法性
			assert(pos < _size);
			return _str[pos];
		}

关系运算符重载函数

关系运算符有>、>=、<、<=、、!=这六个,但是对于C++中任意一个类的关系运算符重载,我们均只需要重载两个,剩下的几个通过复用前面两个重载好的运算符来实现即可。我们这里通过调用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);
	}

<<和>>运算符的重载与getline函数

这里将>>与<<运算符重载成全局函数,因为如果重载成成员函数的话,左操作数默认就是是this指向的调用函数的对象。但我们平时敲代码的时候一般cout与cin才是左操作数,所以为了和平时保持一致,我们将<<与>>运算符重载成全局函数。

<<运算符重载

我们这里可以采用范围for来遍历字符串并输出,最后再返回out支持连续输出

    //输出
	ostream& operator<<(ostream& out, const string& s)
	{
		//使用范围for遍历字符串并输出
		for (auto e : s)
		{
			out << e;
		}
		//支持连续输出
		return out;
	}
>>运算符重载

我们实现>>运算符重载的时候,我们需要先清空一下字符串,如果没有先清空字符的话,假如我们这个字符串开始就有数据的话,这个时候你没有清空字符串,然后直接输入,那么输入的字符串就会插入到原来字符串的后面,这样就达不到我们输入的效果了所以我们需要先清空一下字符串。

注意:我们这里不能够用cin或者scanf去读字符,因为使用cin或者scanf去读的时候,当你读取结束你输入了空格或者换行符的时候它会忽略掉它,因此就读取不了了。因此我们需要使用in.get()去读取字符,它不会忽略空格或者换行符,这里的in.get()与我们C语言的getchar的作用差不多,当我们读取到的字符是’ ‘或者’\n’的时候就停止读取。

//输入
	istream& operator>>(istream& in, string& s)
	{
		//先清空一下字符串
		s.clear();
		char ch;
		//读取一个字符
		ch = in.get();
		//当读取到的字符不是空格或者换行符时继续读取
		while (ch != ' '&&ch != '\n')
		{
			//读取到的字符尾插到字符串后面
			s += ch;
			ch = in.get();
		}
		//支持连续输入
		return in;
	}
getline

getline函数的作用是读取一行含有空格的字符串,它的实现和>>运算符的实现差不多,只不过它是当读取到的字符是’\n’的时候就停止读取。

//读取一行含有空格的字符串
	istream& getline(istream& in, string& s)
	{
		//先清空一下字符串
		s.clear();
		char ch;
		//读取一个字符
		ch = in.get();
		//当读取到的字符不是换行符时继续读取
		while (ch != '\n')
		{
			//读取到的字符尾插到字符串后面
			s += ch;
			ch = in.get();
		}
		//连续读取
		return in;
	}

string类的实现代码

namespace mlf
{
	// 管理字符串的数组,可以增删查改
	//	// 字符串数组的结尾有\0
	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(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			//为存储字符串开空间,多开的一个给'\0'
			_str = new char[_capacity + 1];
			//将C字符串拷贝到已经开好的空间
			strcpy(_str, str);
		}
		//析构函数
		~string()
		{
			delete[]_str;
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}

		//s1.swap(s2)
		void swap(string& s)
		{
			//调用alogrithm中的swap库函数
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		//拷贝构造
		//s1(s3)
		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			string temp(s._str);
			/*swap(_str, temp._str);
			swap(_size, temp._size);
			swap(_capaticy, temp._capaticy);*/
			swap(temp);
		}

		//赋值运算符重载
		//s1 = s3
		string& operator=(string s)
		{
			/*swap(_str, s._str);
			swap(_size, s._size);
			swap(_capaticy, s._capaticy);*/
			swap(s);

			return *this;
		}

		//只读
		const char& operator[](size_t pos)const
		{
			assert(pos < _size);
			return _str[pos];
		}

		//可读可写
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		// 开空间+初始化,扩展capacity 并且初始化空间。size也要动
		void resize(size_t n,char ch = '\0')
		{
			//如果n小于_size
			if (n < _size)
			{
				//那么有效个数就是n
				_size = n;
				_str[_size] = '\0';
			}
			//n大于_size
			else
			{
				//如果n大于capacity
				//我们就增容
				if (n>_capacity)
				{
					reserve(n);
				}
					begin指向'\0'的位置
					//char* start = _str + _size;
					//char* end = _str + n;
					//while (start != end)
					//{
					//	*start++ = ch;
					//	++_size;
					//}
				for (size_t i = _size; i < n; i++)
				{
					_str[i] = ch;
				}
					_str[n] = '\0';
					_size = n;
			}
		}

		// 开空间,扩展capacity
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				//开n+1个空间,还有一个是给'\0'的
				char* temp = new char[n + 1];
				//将原数据拷贝到temp里面
				//把最后一个'\0'也拷过去
				strncpy(temp, _str,_size+1);
				//释放_str
				delete[]_str;
				//再让_str指向temp
				_str = temp;
				//然后更新capacity
				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			//判断是否需要增容
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
			_str[_size] = ch;
			_str[_size + 1] = '\0';
			++_size;
		}

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

		//s1+='x'
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		//s1+="xxxxx"
		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}

		//插入字符
		string& insert(size_t pos, char ch)
		{
			//pos位置必须合法
			assert(pos >= 0 && pos <= _size);
			//判断是否需要增容
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
			//end指向'\0'的位置
			char* end = _str + _size;
			while (end >= _str + pos)
			{
				*(end + 1) = *end;
				--end;
			}
			//通过挪动数据将pos位置空出来了之后,将ch放到pos位置上
			_str[pos] = ch;
			_size++;

			return *this;
		}

		//插入字符串
		string& insert(size_t pos, const char* str)
		{
			//pos位置必须合法
			assert(pos >= 0 && pos <= _size);
			size_t len = strlen(str);
			//如果当前有效数据个数和要插入的字符串长度大于空间容量就增容
			if (len + _size > _capacity)
			{
				reserve(len + _size);
			}
			//挪动数据
			char* end = _str + _size;
			while (end >= _str + pos)
			{
				*(end + len) = *end;
				--end;
			}
			//从pos位置开始的len个长度位置已经挪出来了
			//注意这里不能够用strcpy,因为strcpy会把str字符串里面的'\0'也给拷过来
			//所以我们要用strncpy
			strncpy(_str + pos, str, len);
			//更新_size
			_size += len;

			return *this;
		}

		string& erase(size_t pos = 0, size_t len = npos)
		{
			//pos位置必须合法
			assert(pos < _size);
			//计算从pos位置到_size位置剩余字符的长度
			size_t lenleft = _size - pos;
			//1.剩余的字符长度小于要删的长度(后面全部删完)
			if (len>lenleft)
			{
				_str[pos] = '\0';
				_size -= len;
			}
			//2.剩余的字符长度大于要删的长度
			else
			{
				//end是要删除的最后一个字符的下一个位置
				char* end = _str + pos + len;
				//利用strcpy函数将后面字符,放到pos位置上,通过覆盖我们就删除了要删除的字符串
				strcpy(_str + pos, end);
			}

			return *this;
		}

		//查找一个字符
		size_t find(char ch, size_t pos = 0)const
		{
            assert(pos<_size);//检测pos位置的合法性
			for (size_t i = pos; i < _size; i++)
			{
				//找到了就返回下标
				if (_str[i] == ch)
				{
					return i;
				}
			}
			//没找到返回npos
			return npos;
		}

		//查找一个字符串
		size_t find(const char* str, size_t pos = 0)const
		{
            assert(pos<_size);//检测pos位置的合法性
			const char* ret = strstr(_str+pos, str);
			//找到了就返回下标
			if (ret)
			{
				//当前位置减去起始位置就是它的下标
				return ret - _str;
			}
			//没找到就返回npos
			else
			{
				return npos;
			}
		}

		//清空字符串
		void clear()
		{
			_size = 0;
			_str[_size] = '\0';
		}

		//判空
		bool empty()const
		{
			return strcmp(_str, "") == 0;
		}

		char* c_str()const
		{
			return _str;
		}

		size_t size()const
		{
			return _size;//返回当前字符串的有效长度
		}
        
         size_t capacity()const
		{
			return _capacity;//返回当前字符串的容量大小
		}
		private:
			char* _str;
			size_t _size;
			size_t _capacity;

			static const size_t npos;
	};

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

	 bool operator==(const string& s1, const string& s2)const
	{
		return strcmp(s1.c_str(), s2.c_str()) == 0;
	}
    //实现了上面的<和==之后,其他的我们都可以通过复用<与==来实现
	 bool operator<=(const string& s1, const string& s2)const
	{
		return s1 < s2 || s1 == s2;
	}

	 bool operator>(const string& s1, const string& s2)const
	{
		return !(s1 <= s2);
	}

	bool operator>=(const string& s1, const string& s2)const
	{
		return !(s1 < s2);
	}

	bool operator!=(const string& s1, const string& s2)const
	{
		return !(s1 == s2);
	}

	//输出
	ostream& operator<<(ostream& out, const string& s)
	{
		//使用范围for遍历字符串并输出
		for (auto e : s)
		{
			out << e;
		}
		//连续输出
		return out;
	}
	//输入
	istream& operator>>(istream& in, string& s)
	{
		//先清空一下字符串
		s.clear();
		char ch;
		//读取一个字符
		ch = in.get();
		//当读取到的字符不是空格或者换行符时继续读取
		while (ch != ' '&&ch != '\n')
		{
			//读取到的字符尾插到字符串后面
			s += ch;
			ch = in.get();
		}
		//连续输入
		return in;
	}
	//读取一行含有空格的字符串
	istream& getline(istream& in, string& s)
	{
		//先清空一下字符串
		s.clear();
		char ch;
		//读取一个字符
		ch = in.get();
		//当读取到的字符换行符时继续读取
		while (ch != '\n')
		{
			//读取到的字符尾插到字符串后面
			s += ch;
			ch = in.get();
		}
		//连续读取
		return in;
	}

	const size_t string::npos = -1;

以上就是模拟实现string类的全部内容了,如果觉得文章对你有帮助的话可以给作者三连一波,感谢你的支持。

你可能感兴趣的:(C++,c++,开发语言,后端)