C++模拟实现string类

news2024/9/20 22:31:51

目录

  • 前言:什么是string类?
    • string类的模拟实现
  • 一、四个默认成员函数
    • 1.1 构造函数
    • 1.2 拷贝构造函数
    • 1.3 赋值重载函数
    • 1.4 析构函数
  • 二、迭代器
  • 三、c_str()函数
  • 四、size和capacity函数
  • 五、reserve函数
  • 六、resize函数
  • 七、push_back函数
  • 七、append函数
  • 八、find函数(查找字符或者字符串)
  • 九、substr函数
  • 十、clear函数
  • 十一、insert(字符、字符串)函数
  • 十二、erase函数
  • 十三、运算符重载函数
    • 13.1 operator[ ]
    • 13.2 operator+=一个字符
    • 13.3 operator+=一个字符串
    • 13.4 operator比较大小的运算符
  • 十四、重载“流插入--cout”,“流提取--cin”运算符
  • 十五、代码汇总

前言:什么是string类?

在C语言中,字符串是以’\0’结尾的字符的集合,为了操作方便,C标准库中已经提供了一些str系列的库函 数,但是这些库函数与字符串是分离的,不符合面向对象编程的思想,而且底层空间需要用户自己管理,很可能会造成越界访问。
C++中对于string的定义为:typedef basic_string string; 也就是说C++中的string类是一个泛型类,由模板实例化出来的一个标准类,string本质上不是一个标准数据类型。
至于我们为什么不直接用string标准数据类型而用类是因为一个叫做编码的东西,我们每个国家的语言不同 比如说英语使用26个英文字母基本就能表述所有的单词 但是对于中文的字符呢?就要用其他编码方式啊(比如说utf-8 、utf-16、utf-32)

string类的模拟实现

一、四个默认成员函数

对于任何一个类,在C++98下都必须要有六个默认成员函数:构造函数,析构函数,拷贝构造函数,赋值重载函数,普通对象取地址函数,const对象取地址函数,C++11又增加了两个默认成员函数,分别是移动构造函数和移动赋值重载函数。其中取地址成员函数一般都不需要自己实现的,C++11下增加的两个成员函数会在后面C++11的内容中详述,这里主要实现前四个默认成员函数。

1.1 构造函数

构造函数是完成初始化工作的,一般都会用一个字符串构造一个string对象的,在给这个string对象开辟了空间之后还需要给它初始化,完成初始化工作的函数就是构造函数。

		//构造函数
		//构造string对象可以是有参数的,也可以是无参的,所以参数可以给缺省值,
		//无参构造时,构造出来的string对象应该是一个空对象,所以缺省值用空串(注意:空串不是空格)
		//根据string类的成员函数,需要初始化_size,_capacity,_str,_size表示
		//string对象有效字符的数目,应该等于str的长度,_capacity有效字符容量可以初始化和_size一样的值
		//同时,需要给_str开辟_capacity+1大小的空间,因为_capacity是有效字符的容量,而string的
		//最后一个位置要放一个'\0'作为结束标志(兼容C语言,C语言字符串是以'\0'为结尾的),所以需要
		//多开一个空间,开辟好空间后需要把str字符串的内容都拷贝到string对象管理的_str中,这样就完成
		//了构造函数的初始化工作了(注意,要把'\0'也拷贝到_str上去)
		string(const char* str = "" )
			:_size(strlen(str))
			, _capacity(_size)
			, _str(new char[_capacity+1])
		{
			memcpy(_str, str, _size + 1);
		}

1.2 拷贝构造函数

拷贝构造函数一定要用深拷贝,即需要手动开辟一块与原来对象一样大小的空间,然后再赋值给新的对象,如果用浅拷贝,即没有开辟新的空间存放新的对象的值,即新旧两个对象的_str指针都指向了同一块空间,那么在析构的时候就会出现一块空间被释放两次的情况,造成程序崩溃。

		拷贝构造(传统写法)
		string(const string& s)
		{
			//开辟一块与s一样大的空间,要比_capacity多一个,因为
			//_capacity是有效容量,后面要预留一个位置放'\0'
			char* tmp = new char[s._capacity + 1];
			if (tmp == nullptr)
			{
				perror("string(const string& s) fail");
				exit(-1);
			}
			//把原对象的值拷贝到新对象对应的地址上,s._size是s的有效字符的长度
			//需要+1把'\0'也拷贝到tmp上去
			memcpy(tmp, s._str,s._size+1);
			//更新新对象的_size和_capacity
			_size = s._size;
			_capacity = s._capacity;
			//最后把tmp交回给_str管理
			_str = tmp;
			
		}


		//拷贝构造(现代写法)
		//现代写法有一个很重要的细节就是新对象的_str必须要初始化为nullptr,
		//因为后面的Swap函数会交换两个对象的成员函数,如果_str不初始化为nullptr,
		//也就是随机值,那么交换后_str给了tmp的_str,因为tmp是临时对象,出来作用域
		//会被析构的,即会对_str进行析构,而_str指向的是随机空间,所以会非法析构,造成
		//程序崩溃,_size和_capacity是栈上的数据不初始化也没有关系,_str是动态申请的
		//一定要置为nullptr
		string(const string& s)
			:_size(0)
			,_capacity(0)
			,_str(nullptr)
		{
			//用s对象的c_str()调用构造函数,得到的tmp就是我们想要的
			//对象,再交换tmp和*this对象的成员变量交换即可
			string tmp(s._str);
			Swap(tmp);
		}

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


1.3 赋值重载函数

赋值重载函数跟拷贝构造函数一样,也存在浅拷贝对同一块空间释放两次的情况,并且浅拷贝还存在内存泄露的问题,所以也需要深拷贝来解决问题。

		赋值(传统写法)
		string& operator=(const string& s)
		{
			//可以先判断一下是否是给自己赋值
			if (this != &s)
			{
				//先开一块和s对象一样大的空间,预留一个'\0'的位置
				char* tmp = new char[s._capacity + 1];
				if (tmp)
				{
					//把s对象的字符串内容都拷贝到临时空间tmp中
					memcpy(tmp, s._str,s._size+1);
					//更新_size和_capacity
					_size = s._size;
					_capacity = s._capacity;
					//释放原来对象的_str指向的空间
					delete[] _str;
					//_str指向新空间
					_str = tmp;
				}
			}
			return *this;
		}

		//赋值(现代写法)
		//现代写法是直接利用参数传参时会调用拷贝构造函数构造一个临时的对象
		//刚好这个对象的值就是新的对象想要的,所以直接令这个临时对象和新对象
		//交换成员变量就可以了,并且因为s是临时对象,所以赋值函数结束时会
		//调用析构函数释放内存,即把旧对象所指向的内容释放掉了,无需手动释放
		string& operator=(string s)
		{
			Swap(s);
			return *this;
		}

1.4 析构函数

析构函数是释放动态开辟的内存的。

		//析构函数
		//只需把动态申请的内存释放了即可
		~string()
		{
			//先判断是否为nullptr吧,是nullptr就没必要释放了
			if (_str)
			{
				//释放空间
				delete[] _str;
				//置空
				_str = nullptr;
				_size = _capacity = 0;
			}
		}

二、迭代器

对于string类,迭代器就是一个指针

		typedef char* iterator;
		typedef const char* const_iterator;

		//begin就是第一个字符的地址,即_str
		iterator begin()
		{
			return _str;
		}
		
		//const版本
		const_iterator begin() const
		{
			return _str;
		}

		//end就是指向最后一个有效元素的下一个位置
		iterator end()
		{
			return _str + _size;
		}

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

三、c_str()函数

		//c_str()函数的返回值就是string对象中C类型的字符串指针
		const char* c_str() const
		{
			return _str;
		}

四、size和capacity函数

		const size_t size() const
		{
			return _size;
		}

		const size_t capacity() const
		{
			return _capacity;
		}

五、reserve函数

reserve函数是扩容函数,调整空间大小用的。参数为目标空间的大小。

		void reserve(size_t n)
		{
			//如果n>_capacity,需要扩容;如果小于,一般都不会缩容的
			if (n > _capacity)
			{
				//扩容需要开辟一块目标大小的空间,然后把原来的数据拷贝到新空间里
				char* tmp = new char[n + 1];
				if (tmp)
				{
					//把原来数据拷贝到新空间中
					memcpy(tmp, _str,_size+1);
					//释放旧的空间
					delete[] _str;
					//把新的空间交给_str指针管理
					_str = tmp;
					//更新_capacity为新的空间大小
					_capacity = n;
				}
			}
		}

六、resize函数

resize函数跟reserve函数的功能类似,也是调整空间的大小,但是resize支持删除数据,并且支持把空间初始化为特定值。
在这里插入图片描述

		void resize(size_t n, char ch = '\0')
		{
			//场景一
			if (n <= _size)
			{
				_str[n] = '\0';
				_size = n;
			}
			else
			{
				//如果需要扩容,则是场景三;否则,则是场景二
				//场景三只不过是在场景二的前面多加了一步扩容的工作
				if (n > _capacity)
				{
					//扩容
					reserve(n);
				}
				//从_size开始放ch,直到n-1位置
				for (int i = _size; i < n; i++)
				{
					_str[i] = ch;
				}
				//更新_size为新的大小
				_size = n;
				//最后一定要放上一个'\0'作为字符串的结束标志
				_str[_size] = '\0';
			}
		}

七、push_back函数

push_back是尾插函数,即在string对象尾插字符。

		void push_back(const char ch)
		{
			//先检查容量是否足够,不够则扩容
			if (_size == _capacity)
			{
				//一般是两倍扩容
				reserve(_capacity == 0 ? 4 : 2 * _capacity);
			}
			//因为_size是最后一个有效字符的下一个位置,所以尾插应该插入到_str[_size]位置
			_str[_size] = ch;
			_size++;
			//后面记得要跟上'\0',不要漏了
			_str[_size] = '\0';
		}

七、append函数

append的作用是尾插一个字符串。

		void append(const char* str)
		{
			size_t len = strlen(str);
			//先判断所剩空间是否>=插入的字符串的长度
			//如果小于,则需要先扩容
			if (_size + len > _capacity)
			{
				//扩大容量到原来字符串+插入字符串的长度
				reserve(_size + len);
			}
			//把所需尾插的字符串尾插到原字符串的后面
			strcpy(_str + _size, str);
			//更新_size
			_size += len;
		}

八、find函数(查找字符或者字符串)

		//从pos位置开始查找某个字符
		size_t find(char ch, size_t pos = 0)
		{
			//先判断pos位置是否合法
			assert(pos < _size);
			//遍历_str字符串比对就行了,找到了就返回下标
			for (int i = pos; i < _size; i++)
			{
				if (_str[i] == ch)
				{
					return i;
				}
			}
			//找不到返回npos,npos是整形的最大值
			return npos;
		}

		//从pos位置开始查找字符串
		size_t find(const char* str, size_t pos = 0) const
		{
			assert(pos < _size);
			//判断str是否为_str的子串
			char* ptr = strstr(_str + pos, str);
			if (ptr)
			{
				//如果str是_str的子串,因为strstr找到后返回的是子串的第一个
				//字符在源串的地址,所以减去源串的起始地址就是对应字符串在源串中的下标
				return ptr - _str;
			}
			else
			{
				//找不到返回一个整形的最大值,表示找不到
				return npos;
			}
		}

九、substr函数

substr函数是在string对象中的某个位置开始,取len长度的字符串构建一个新的string对象的函数。

		string substr(size_t pos = 0, size_t len = npos)
		{
			assert(pos < _size);
			size_t n = len;
			//如果n==npos即没传len参数时,默认取pos位置后面的全部字符,
			//如果n大于从pos位置往后的所有字符的长度,也是取pos后面的全部字符
			if (n == npos || pos + n > _size)
			{
				//_size-pos就是从pos位置开始后面全部字符的长度
				n = _size - pos;
			}
			//创建一个临时string对象
			string tmp;
			//开n个空间即可,因为有n个有效字符
			tmp.reserve(n);
			//取出从pos位置开始,后面的全部字符尾插到tmp中即可
			for (int i = pos; i < pos + n; i++)
			{
				tmp += _str[i];
			}
			return tmp;
		}

十、clear函数

clear函数是清空string对象的内容。

		void clear()
		{
			//只需要在_str[0]位置放一个'\0'即可完成clear工作
			_str[0] = '\0';
			//清空之后需要把_size置为0
			_size = 0;
		}

十一、insert(字符、字符串)函数

insert(字符)函数是从pos位置插入n个字符ch。


		void insert(size_t pos,size_t n,const char ch)
		{
			//判断pos的合法性,pos==_size的时候是尾插
			assert(pos <= _size);
			//如果原来字符串的长度+插入字符数目后超出容量,那么就扩容
			if (_size + n > _capacity)
			{
				reserve(_size + n);
			}
			
			//把从pos位置开始往后的所有数据都往后移动n位,腾出n个空位置
			//以便插入n个ch字符
			size_t end = _size;
			//要注意防止pos=0时死循环问题,end永远>=0,因为end和pos的类型都`在这里插入代码片`是size_t,
			//当pos等于0时,end>=pos循环继续,也就是说end<0时才会停止,但是end不可能<0,
			//因为当end减为-1的时候,类型是size_t,所以会被看作是无符号数,-1对应的无符号数
			//是整形的最大值,会导致死循环,所以需要外加一个条件判断end是否减为npos了,如果是
			//则循环停止,否则继续
			while (end >= pos && end != npos)
			{
				//挪动数据
				_str[end + n] = _str[end];
				end--;
			}
			//从pos位置开始把n个ch字符插入进去
			for (int i = 0; i < n; i++)
			{
				_str[i + pos] = ch;
			}
			//更新_size
			_size += n;


		}

insert(字符串)函数是从pos位置插入一个字符串。

		void insert(size_t pos, const char* str)
		{
			//判断pos位置的合法性
			assert(pos <= _size);
			int len = strlen(str);
			//判断剩余空间大小是否足够插入字符串str,不够则扩容
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			//要注意防止pos=0时死循环问题,end永远>=0,因为end和pos的类型是都size_t,
			//当pos等于0时,end>=pos循环继续,也就是说end<0时才会停止,但是end不可能<0,
			//因为当end减为-1的时候,类型是size_t,所以会被看作是无符号数,-1对应的无符号数
			//是整形的最大值,会导致死循环,所以需要外加一个条件判断end是否减为npos了,如果是
			//则循环停止,否则继续
			size_t end = _size;
			while (end >= pos && end != npos)
			{
				//挪动数据,腾出len个位置的空间,插入字符串str
				_str[end + len] = _str[end];
				end--;
			}
			//把字符串str拷贝到从_str+pos位置开始的这段位置上
			memcpy(_str + pos, str, sizeof(char) * len);
			//更新_size的值
			_size += len;
		}

十二、erase函数

erase函数的作用是从pos位置开始删除n个字符。

		void erase(size_t pos, size_t n = npos)
		{
			//pos==_size不是有效字符,不能删除
			assert(pos < _size);
			//如果n无参或者n的值大于从pos位置到结尾的长度,那就把从pos位置开始的
			//所有字符都删掉,即_str[pos]='\0'即可,_size是最后一个有效元素下标+1,
			//所以新的长度就是pos
			if (n >= _size - pos || n==npos )
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				//从后往前挪动数据,从pos位置开始往后覆盖即可
				size_t end = pos + n;
				while (end <= _size)
				{
					_str[pos++] = _str[end++];
				}
				//更新_size
				_size -= n;
			}
		}

十三、运算符重载函数

13.1 operator[ ]

		//C语言中[]能直接访问字符串中的某个位置的元素,并且能做修改,所以
		//返回的该字符的引用
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

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

13.2 operator+=一个字符

operator+=一个字符等于是尾插一个字符的意思,所以可以复用push_back函数,但是+=是有返回值的,返回值是+=后的对象。

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

13.3 operator+=一个字符串

对象+=字符串是尾插一个字符串的意思,所以可以服用append,但是+=是有返回值的,返回值是+=后的对象。

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

13.4 operator比较大小的运算符


		bool operator<(const string& s) const
		{
			int i1 = 0;
			int i2 = 0;
			//本质上是比较两个字符串的大小
			while (i1 < _size && i2 < s._size)
			{
				//遇到小于,返回true
				if (_str[i1] < s[i2])
				{
					return true;
				}
				//遇到大于,返回false
				else if (_str[i1] > s[i2])
				{
					return false;
				}
				//遇到等于,继续往后比较
				else
				{
					i1++;
					i2++;
				}
			}
			if (i1 == _size && i2 < s._size)
			{
				return true;
			}
			else
			{
				return false;
			}
		}
		bool operator==(const string& s) const
		{
			//本质上是比较两个字符串是否相等
			int i1 = 0;
			int i2 = 0;
			while (i1 < _size && i2 < s._size)
			{
				if (_str[i1] != s[i2])
				{
					return false;
				}
				else
				{
					i1++;
					i2++;
				}
			}
			if (i1 == _size && i2 == s._size)
			{
				return true;
			}
			else
			{
				return false;
			}
		}
		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);
		}

十四、重载“流插入–cout”,“流提取–cin”运算符

	ostream& operator<<(ostream& out, const string& s)
	{
		//for (int i = 0; i < s.size(); i++)
		//{
		//	out << s[i];
		//}

		//把s对象的_str字符串的内容逐一打印出来即可
		//这里不建议直接打印c_str(),因为c_str()是按照C语言
		//的规则,即'\0'为结束标志的,但是string类的是按照_size
		//来确定对象的字符数目的,所以在"abc\0def"这样特殊的string类,
		//打印时如果直接打印c_str(),只能打印出abc,而根据string类的要求
		//应该以_size为大小打印,所以会打印出abcdef,所以这里还是要区分一下,
		//不用c_str()的形式打印
		for (const auto& e : s)
		{
			out << e;
		}

		//流插入需要返回一个ostream对象,作为连续流插入的前提,可以是引用,因为out出了这个函数不销毁
		return out;
	}

	istream& operator>>(istream& in, string& s)
	{
		//调用流提取函数时需要先清空对象本身的内容
		s.clear();

		//为了减少频繁地扩容,可以创建一个缓冲区(字符数组)先存数据,待缓冲区满了之后
		//再把缓冲区(字符数组)的内容尾插到s对象中
		char buff[128] = { 0 };

		//读取第一个字符
		char ch = in.get();

		//先去掉前导空格或者换行
		while (ch == ' ' || ch == '\n')
		{
			ch = in.get();
		}

		//按照流提取,遇到空格或者换行就停止读取,否则继续
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			//读到的字符存放在缓冲区buff中
			buff[i++] = ch;

			//当缓冲区填满后,尾插到s中
			if (i == 127)
			{
				//因为字符串需要以'\0'结尾,所以先在最后一个位置放'\0'再尾插
				buff[i] = '\0';
				s += buff;
				//重新回到缓冲区的0下标开始放字符
				i = 0;
			}
			//从stdin中一个字符一个字符地读取上来
			ch = in.get();
		}

		//如果i!=0,证明缓冲区里还有字符,需要添加'\0'成为字符串再尾插到s对象中
		if (i != 0)
		{
			buff[i] = '\0';
			s += buff;
		}

		//流提取需要返回istream,作为连续流提取的前提,可以返回引用,因为in对象
		// 出了这个函数后不销毁
		return in;
	}

十五、代码汇总

string.h

#pragma once

#include <cstring>
#include <stdlib.h>
#include <iostream>
#include <assert.h>
using namespace std;

namespace kb
{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;

		//begin就是第一个字符的地址,即_str
		iterator begin()
		{
			return _str;
		}
		
		//const版本
		const_iterator begin() const
		{
			return _str;
		}

		//end就是指向最后一个有效元素的下一个位置
		iterator end()
		{
			return _str + _size;
		}

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

		//构造函数
		//构造string对象可以是有参数的,也可以是无参的,所以参数可以给缺省值,
		//无参构造时,构造出来的string对象应该是一个空对象,所以缺省值用空串(注意:空串不是空格)
		//根据string类的成员函数,需要初始化_size,_capacity,_str,_size表示
		//string对象有效字符的数目,应该等于str的长度,_capacity有效字符容量可以初始化和_size一样的值
		//同时,需要给_str开辟_capacity+1大小的空间,因为_capacity是有效字符的容量,而string的
		//最后一个位置要放一个'\0'作为结束标志(兼容C语言,C语言字符串是以'\0'为结尾的),所以需要
		//多开一个空间,开辟好空间后需要把str字符串的内容都拷贝到string对象管理的_str中,这样就完成
		//了构造函数的初始化工作了(注意,要把'\0'也拷贝到_str上去)
		string(const char* str = "" )
			:_size(strlen(str))
			, _capacity(_size)
			, _str(new char[_capacity+1])
		{
			memcpy(_str, str, _size + 1);
		}

		//拷贝构造(传统写法)
		//string(const string& s)
		//{
		//	//开辟一块与s一样大的空间,要比_capacity多一个,因为
		//	//_capacity是有效容量,后面要预留一个位置放'\0'
		//	char* tmp = new char[s._capacity + 1];
		//	if (tmp == nullptr)
		//	{
		//		perror("string(const string& s) fail");
		//		exit(-1);
		//	}
		//	//把原对象的值拷贝到新对象对应的地址上,s._size是s的有效字符的长度
		//	//需要+1把'\0'也拷贝到tmp上去
		//	memcpy(tmp, s._str,s._size+1);
		//	//更新新对象的_size和_capacity
		//	_size = s._size;
		//	_capacity = s._capacity;
		//	//最后把tmp交回给_str管理
		//	_str = tmp;
		//	
		//}

		//赋值(传统写法)
		//string& operator=(const string& s)
		//{
		//	//可以先判断一下是否是给自己赋值
		//	if (this != &s)
		//	{
		//		//先开一块和s对象一样大的空间,预留一个'\0'的位置
		//		char* tmp = new char[s._capacity + 1];
		//		if (tmp)
		//		{
		//			//把s对象的字符串内容都拷贝到临时空间tmp中
		//			memcpy(tmp, s._str,s._size+1);
		//			//更新_size和_capacity
		//			_size = s._size;
		//			_capacity = s._capacity;
		//			//释放原来对象的_str指向的空间
		//			delete[] _str;
		//			//_str指向新空间
		//			_str = tmp;
		//		}
		//	}
		//	return *this;
		//}

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

		//拷贝构造(现代写法)
		//现代写法有一个很重要的细节就是新对象的_str必须要初始化为nullptr,
		//因为后面的Swap函数会交换两个对象的成员函数,如果_str不初始化为nullptr,
		//也就是随机值,那么交换后_str给了tmp的_str,因为tmp是临时对象,出来作用域
		//会被析构的,即会对_str进行析构,而_str指向的是随机空间,所以会非法析构,造成
		//程序崩溃,_size和_capacity是栈上的数据不初始化也没有关系,_str是动态申请的
		//一定要置为nullptr
		string(const string& s)
			:_size(0)
			,_capacity(0)
			,_str(nullptr)
		{
			//用s对象的c_str()调用构造函数,得到的tmp就是我们想要的
			//对象,再交换tmp和*this对象的成员变量交换即可
			string tmp(s._str);
			Swap(tmp);
		}

		//赋值(现代写法)
		//现代写法是直接利用参数传参时会调用拷贝构造函数构造一个临时的对象
		//刚好这个对象的值就是新的对象想要的,所以直接令这个临时对象和新对象
		//交换成员变量就可以了,并且因为s是临时对象,所以赋值函数结束时会
		//调用析构函数释放内存,即把旧对象所指向的内容释放掉了,无需手动释放
		string& operator=(string s)
		{
			Swap(s);
			return *this;
		}

		//c_str()函数的返回值就是string对象中C类型的字符串指针
		const char* c_str() const
		{
			return _str;
		}

		const size_t size() const
		{
			return _size;
		}

		const size_t capacity() const
		{
			return _capacity;
		}

		//C语言中[]能直接访问字符串中的某个位置的元素,并且能做修改,所以
		//返回的该字符的引用
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

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

		void reserve(size_t n)
		{
			//如果n>_capacity,需要扩容;如果小于,一般都不会缩容的
			if (n > _capacity)
			{
				//扩容需要开辟一块目标大小的空间,然后把原来的数据拷贝到新空间里
				char* tmp = new char[n + 1];
				if (tmp)
				{
					//把原来数据拷贝到新空间中
					memcpy(tmp, _str,_size+1);
					//释放旧的空间
					delete[] _str;
					//把新的空间交给_str指针管理
					_str = tmp;
					//更新_capacity为新的空间大小
					_capacity = n;
				}
			}
		}

		void resize(size_t n, char ch = '\0')
		{
			//场景一
			if (n <= _size)
			{
				_str[n] = '\0';
				_size = n;
			}
			else
			{
				//如果需要扩容,则是场景三;否则,则是场景二
				//场景三只不过是在场景二的前面多加了一步扩容的工作
				if (n > _capacity)
				{
					//扩容
					reserve(n);
				}
				//从_size开始放ch,直到n-1位置
				for (int i = _size; i < n; i++)
				{
					_str[i] = ch;
				}
				//更新_size为新的大小
				_size = n;
				//最后一定要放上一个'\0'作为字符串的结束标志
				_str[_size] = '\0';
			}
		}

		void push_back(const char ch)
		{
			//先检查容量是否足够,不够则扩容
			if (_size == _capacity)
			{
				//一般是两倍扩容
				reserve(_capacity == 0 ? 4 : 2 * _capacity);
			}
			//因为_size是最后一个有效字符的下一个位置,所以尾插应该插入到_str[_size]位置
			_str[_size] = ch;
			_size++;
			//后面记得要跟上'\0',不要漏了
			_str[_size] = '\0';
		}

		void append(const char* str)
		{
			size_t len = strlen(str);
			//先判断所剩空间是否>=插入的字符串的长度
			//如果小于,则需要先扩容
			if (_size + len > _capacity)
			{
				//扩大容量到原来字符串+插入字符串的长度
				reserve(_size + len);
			}
			//把所需尾插的字符串尾插到原字符串的后面
			strcpy(_str + _size, str);
			//更新_size
			_size += len;
		}

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

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

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

		//size_t find(const char* str, size_t pos = 0) const
		//{
		//	const char* tmp = strstr(_str + pos, str);
		//	if (tmp)
		//	{
		//		return tmp - _str;
		//	}
		//	return npos;
		//}

		void insert(size_t pos,size_t n,const char ch)
		{
			//判断pos的合法性,pos==_size的时候是尾插
			assert(pos <= _size);
			//如果原来字符串的长度+插入字符数目后超出容量,那么就扩容
			if (_size + n > _capacity)
			{
				reserve(_size + n);
			}
			
			//把从pos位置开始往后的所有数据都往后移动n位,腾出n个空位置
			//以便插入n个ch字符
			size_t end = _size;
			//要注意防止pos=0时死循环问题,end永远>=0,因为end和pos的类型是size_t,
			//当pos等于0时,end>=pos循环继续,也就是说end<0时才会停止,但是end不可能<0,
			//因为当end减为-1的时候,类型是size_t,所以会被看作是无符号数,-1对应的无符号数
			//是整形的最大值,会导致死循环,所以需要外加一个条件判断end是否减为npos了,如果是
			//则循环停止,否则继续
			while (end >= pos && end != npos)
			{
				//挪动数据
				_str[end + n] = _str[end];
				end--;
			}
			//从pos位置开始把n个ch字符插入进去
			for (int i = 0; i < n; i++)
			{
				_str[i + pos] = ch;
			}
			//更新_size
			_size += n;


		}

		void insert(size_t pos, const char* str)
		{
			//判断pos位置的合法性
			assert(pos <= _size);
			int len = strlen(str);
			//判断剩余空间大小是否足够插入字符串str,不够则扩容
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			//要注意防止pos=0时死循环问题,end永远>=0,因为end和pos的类型是都size_t,
			//当pos等于0时,end>=pos循环继续,也就是说end<0时才会停止,但是end不可能<0,
			//因为当end减为-1的时候,类型是size_t,所以会被看作是无符号数,-1对应的无符号数
			//是整形的最大值,会导致死循环,所以需要外加一个条件判断end是否减为npos了,如果是
			//则循环停止,否则继续
			size_t end = _size;
			while (end >= pos && end != npos)
			{
				//挪动数据,腾出len个位置的空间,插入字符串str
				_str[end + len] = _str[end];
				end--;
			}
			//把字符串str拷贝到从_str+pos位置开始的这段位置上
			memcpy(_str + pos, str, sizeof(char) * len);
			//更新_size的值
			_size += len;
		}

		void erase(size_t pos, size_t n = npos)
		{
			//pos==_size不是有效字符,不能删除
			assert(pos < _size);
			//如果n无参或者n的值大于从pos位置到结尾的长度,那就把从pos位置开始的
			//所有字符都删掉,即_str[pos]='\0'即可,_size是最后一个有效元素下标+1,
			//所以新的长度就是pos
			if (n >= _size - pos || n==npos )
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				//从后往前挪动数据,从pos位置开始往后覆盖即可
				size_t end = pos + n;
				while (end <= _size)
				{
					_str[pos++] = _str[end++];
				}
				//更新_size
				_size -= n;
			}
		}

		//从pos位置开始查找某个字符
		size_t find(char ch, size_t pos = 0)
		{
			//先判断pos位置是否合法
			assert(pos < _size);
			//遍历_str字符串比对就行了,找到了就返回下标
			for (int i = pos; i < _size; i++)
			{
				if (_str[i] == ch)
				{
					return i;
				}
			}
			//找不到返回npos,npos是整形的最大值
			return npos;
		}

		//从pos位置开始查找字符串
		size_t find(const char* str, size_t pos = 0) const
		{
			assert(pos < _size);
			//判断str是否为_str的子串
			char* ptr = strstr(_str + pos, str);
			if (ptr)
			{
				//如果str是_str的子串,因为strstr找到后返回的是子串的第一个
				//字符在源串的地址,所以减去源串的起始地址就是对应字符串在源串中的下标
				return ptr - _str;
			}
			else
			{
				//找不到返回一个整形的最大值,表示找不到
				return npos;
			}
		}

		string substr(size_t pos = 0, size_t len = npos)
		{
			assert(pos < _size);
			size_t n = len;
			//如果n==npos即没传len参数时,默认取pos位置后面的全部字符,
			//如果n大于从pos位置往后的所有字符的长度,也是取pos后面的全部字符
			if (n == npos || pos + n > _size)
			{
				//_size-pos就是从pos位置开始后面全部字符的长度
				n = _size - pos;
			}
			//创建一个临时string对象
			string tmp;
			//开n个空间即可,因为有n个有效字符
			tmp.reserve(n);
			//取出从pos位置开始,后面的全部字符尾插到tmp中即可
			for (int i = pos; i < pos + n; i++)
			{
				tmp += _str[i];
			}
			return tmp;
		}

		void clear()
		{
			//只需要在_str[0]位置放一个'\0'即可完成clear工作
			_str[0] = '\0';
			//清空之后需要把_size置为0
			_size = 0;
		}

		bool operator<(const string& s) const
		{
			int i1 = 0;
			int i2 = 0;
			//本质上是比较两个字符串的大小
			while (i1 < _size && i2 < s._size)
			{
				//遇到小于,返回true
				if (_str[i1] < s[i2])
				{
					return true;
				}
				//遇到大于,返回false
				else if (_str[i1] > s[i2])
				{
					return false;
				}
				//遇到等于,继续往后比较
				else
				{
					i1++;
					i2++;
				}
			}
			if (i1 == _size && i2 < s._size)
			{
				return true;
			}
			else
			{
				return false;
			}
		}
		bool operator==(const string& s) const
		{
			//本质上是比较两个字符串是否相等
			int i1 = 0;
			int i2 = 0;
			while (i1 < _size && i2 < s._size)
			{
				if (_str[i1] != s[i2])
				{
					return false;
				}
				else
				{
					i1++;
					i2++;
				}
			}
			if (i1 == _size && i2 == s._size)
			{
				return true;
			}
			else
			{
				return false;
			}
		}
		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);
		}

		//析构函数
		//只需把动态申请的内存释放了即可
		~string()
		{
			//先判断是否为nullptr吧,是nullptr就没必要释放了
			if (_str)
			{
				//释放空间
				delete[] _str;
				//置空
				_str = nullptr;
				_size = _capacity = 0;
			}
		}
	private:
		size_t _size;
		size_t _capacity;
		char* _str;
	public:
		static const size_t npos;
	};
	const size_t string::npos = -1;

	ostream& operator<<(ostream& out, const string& s)
	{
		//for (int i = 0; i < s.size(); i++)
		//{
		//	out << s[i];
		//}

		//把s对象的_str字符串的内容逐一打印出来即可
		//这里不建议直接打印c_str(),因为c_str()是按照C语言
		//的规则,即'\0'为结束标志的,但是string类的是按照_size
		//来确定对象的字符数目的,所以在"abc\0def"这样特殊的string类,
		//打印时如果直接打印c_str(),只能打印出abc,而根据string类的要求
		//应该以_size为大小打印,所以会打印出abcdef,所以这里还是要区分一下,
		//不用c_str()的形式打印
		for (const auto& e : s)
		{
			out << e;
		}

		//流插入需要返回一个ostream对象,作为连续流插入的前提,可以是引用,因为out出了这个函数不销毁
		return out;
	}

	istream& operator>>(istream& in, string& s)
	{
		//调用流提取函数时需要先清空对象本身的内容
		s.clear();

		//为了减少频繁地扩容,可以创建一个缓冲区(字符数组)先存数据,待缓冲区满了之后
		//再把缓冲区(字符数组)的内容尾插到s对象中
		char buff[128] = { 0 };

		//读取第一个字符
		char ch = in.get();

		//先去掉前导空格或者换行
		while (ch == ' ' || ch == '\n')
		{
			ch = in.get();
		}

		//按照流提取,遇到空格或者换行就停止读取,否则继续
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			//读到的字符存放在缓冲区buff中
			buff[i++] = ch;

			//当缓冲区填满后,尾插到s中
			if (i == 127)
			{
				//因为字符串需要以'\0'结尾,所以先在最后一个位置放'\0'再尾插
				buff[i] = '\0';
				s += buff;
				//重新回到缓冲区的0下标开始放字符
				i = 0;
			}
			//从stdin中一个字符一个字符地读取上来
			ch = in.get();
		}

		//如果i!=0,证明缓冲区里还有字符,需要添加'\0'成为字符串再尾插到s对象中
		if (i != 0)
		{
			buff[i] = '\0';
			s += buff;
		}

		//流提取需要返回istream,作为连续流提取的前提,可以返回引用,因为in对象
		// 出了这个函数后不销毁
		return in;
	}

	void test1(void)
	{
		string s1("hello world");
		cin >> s1;
		//s1 += ' ';
		//s1 += 'k';
		//s1 += 'o';
		//s1 += "be";

		cout << s1 << endl;

		s1.insert(3,"######");
		s1.erase(3, 30);
		//s1.push_back('k');
		//s1.push_back('o');

		//s1.append("be");

		string s2(s1);

		s1.reserve(20);

		s1.resize(13, '#');

		string s3;
		s3 = s2;
	}

	void test2(void)
	{
		//string s1("bb");
		//string s2("aaa");
		//cout << (s1 < s2) << endl;

		string s1("hello");
		string s2("hello");
		cout << (s1 < s2) << endl;
		cout << (s1 > s2) << endl;
		cout << (s1 == s2) << endl << endl;


		string s3("hello");
		string s4("helloxxx");
		cout << (s3 < s4) << endl;
		cout << (s3 > s4) << endl;
		cout << (s3 == s4) << endl << endl;


		string s5("helloxxx");
		string s6("hello");
		cout << (s5 < s6) << endl;
		cout << (s5 > s6) << endl;
		cout << (s5 == s6) << endl << endl;



	}
}

以上就是string类的模拟实现啦!内容属实有点多哦,你学会了吗?希望大家下去也能自己好好地写一下代码,实现以下这个string类,如果您能自己实现出来,那么您对string类的理解一定会达到一个新的高度的。好啦,今天的分享就到这里,如果这篇文章对你有所帮助的话,那么点点小心心,点点关注呗,您的小心心和关注就是对我莫大的支持和鼓励哦,后期还会持续更新C++相关知识哦,我们下期见啦!!!!!!!!!!!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/729598.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C#基础学习_方法的重载

C#基础学习_方法的重载 在类中定义方法的一般方式(不使用重载时): public int Add(int a, int b){return a + b;}

mysql解决varchar字段内容不区分大小写问题

测试内容: 解决方案: 方案一:将查询条件用binary()括起来 select * from sys_menu where binary permission_str SYSTEM 方案二:将排序规则改为utf8_xxx_cs(推荐) utf8_xxx_ci不区分大小写&#xff0c;ci为case insensitive的缩写&#xff0c;即大小写不敏感。 utf8_xxx_c…

Python学习笔记(十九)————json相关

目录 &#xff08;1&#xff09;什么是json &#xff08;2&#xff09; json有什么用 &#xff08;3&#xff09; json格式数据转化 &#xff08;4&#xff09;Python数据和Json数据的相互转化 &#xff08;1&#xff09;什么是json JSON 是一种轻量级的数据交互格式。可以按照…

linux下nmap的使用

linux下nmap的使用 文章目录 centos下nmap的使用安装使用1234567 centos下nmap的使用 安装 yum install nmap使用 1 nmap your-ip&#xff08;域名&#xff09;开放了22、80、111、443端口 2 nmap -p 端口 IP(域名)&#xff0c;判断ip是否开放指定端口3 默认脚本扫描&am…

JavaWeb 速通HTML

目录 一、拾枝杂谈 1.网页组成 : 1 结构 2 表现 3 行为 2.HTML入门 : 1 基本介绍 2.基本结构 : 3.HTML标签 : 1 基本说明 2 注意事项 二、常用标签汇总及演示 1.font标签 : 1 定义 2 演示 2.字符实体 : 1 定义 2 演示 3.标题标签 : 1 定义 2 演示 4. 超链接标签 : 1…

清华青年AI自强作业hw6:基于ResNet实现IMAGENET分类任务

清华青年AI自强作业hw6&#xff1a;基于ResNet实现IMAGENET分类任务 简述作业实现相关链接 一起学AI系列博客&#xff1a;目录索引 简述 hw6作业为基于ResNet模型&#xff0c;并利用VGG标准模块和GoogleNet中的inception模块对IMAGENET数据集进行20类分类。模型输入图像尺寸为2…

特征选择算法 | Matlab实现基于互信息特征选择算法的回归数据特征选择 MI

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 特征选择算法 | Matlab实现基于互信息特征选择算法的回归数据特征选择 MI 部分源码 %--------------------

OpenCV红色图像上用绿色画了两条线

// 红色图像上 画上蓝色的线 // 在图中画一条直线 函数原型 // 函数作用 // 给定一个图像img,连接点pt1和pt2的坐标,在图中画一条直线,color表明线的颜色cv.line() // 其中需要注意的是,点坐标(x, y)中,x代表图片的列,y代表图片的行 // CvPoint pt1, //直线起点 // CvPoi…

Android - AIDL

一、概念 二、使用 2.1 Server 端 2.1.1 创建 .aidl 文件 Module右键→New→AIDL→AIDL File&#xff08;如果不可选&#xff0c;在build.gradle的buildFeatures节点添加aidl true&#xff09;。 在创建的接口中定义抽象方法。 interface IDemoAidlInterface {//自己的业…

MySQL库表的简单操作

1.创建数据Market&#xff0c;在数据库中创建表customers&#xff0c;表结构如图 (1)创建数据库Market create database Market&#xff1b; (2)创建数据表customers&#xff0c;在c_num字段添加主键约束&#xff0c;c_birth字段上添加非空约束 create table customers&#…

IDEA 导入多模块项目并设置运行

1、导入多模块项目 导入的步骤点击左上角File->New->Module from Existing Sources&#xff0c;最后点击选择项目的pom文件即可 2、设置模块 如果pom.xml文件是灰色并且有横杠&#xff0c;则需要这样设置&#xff1a; 把√去掉。 项目结构如图所示&#xff1a; 本项…

Linux下如何使用git?如何搭建自己的库?

前言&#xff1a; 本文主要介绍如何在 Linux 中使用 git 、gitee 账号注册、如何新建远程仓库与本地仓库 以及 git 的常用指令。 一.安装git 切换至 root 用户身份&#xff0c;输入指令&#xff1a; yum install -y git使用 sudo 指令&#xff1b; sudo yum install -y git二…

《Redis 核心技术与实战》课程学习笔记(四)

Redis 持久化机制 AOF日志&#xff1a;宕机了&#xff0c;Redis 如何避免数据丢失&#xff1f; 一旦服务器宕机&#xff0c;内存中的数据将全部丢失。目前&#xff0c;Redis 的持久化主要有两大机制&#xff0c;即 AOF 日志和 RDB 快照。 AOF 日志是如何实现的&#xff1f; …

【架构设计】架构知识体系

文章目录 1、什么是架构和架构本质2、架构分类2.1、业务架构&#xff08;俯视架构&#xff09;2.2、应用架构&#xff08;剖面架构&#xff0c;也叫逻辑架构图&#xff09;2.2.1、职责划分: 明确应用&#xff08;各个逻辑模块或者子系统&#xff09;边界2.2.2、职责之间的协作&…

[TPAMI 2022] 神经网络元学习:综述

Meta-Learning in Neural Networks: A Survey | IEEE Journals & Magazine | IEEE Xplore Meta-Learning in Neural Networks: A Survey 摘要&#xff1a; 近年来&#xff0c;元学习&#xff08;或“学会学习”&#xff09;领域的兴趣急剧上升。与使用固定学习算法从头开…

TensorFlow: mode.save()报错 non-trackable object: (None, None)

问题描述 环境&#xff1a;tensorflow2.2.0 执行model.save(), 报错 ...non-trackable object: (None, None)... 解决办法 搞了半天没有找到好的办法&#xff0c;只能通过升级搞定 pip uninstall tensorflow pip install tensorflow2.6.0 pip install --upgrade keras2.6…

机器学习算法总结

机器学习算法 参考博客&#xff1a;https://blog.csdn.net/lemonbit/article/details/125775595#:~:text干货| 机器学习模型与算法最全分类汇总&#xff01; 1 线性回归、逻辑回归、Lasso回归、Ridge回归、线性判别分析 2 K近邻、决策树、感知机、神经网络、支持向量机 3,AdaBo…

用for循环打印图形

目录 一、打印矩形 二、打印直角三角形 1.直角三角形 2.打印九九乘法表 三、打印等腰三角形 四、打印平行四边形 五、打印菱形 六、打印两个直角三角形 1.打印两个直角三角形 2.打印倒着的两个直角三角形 七、打印“蝴蝶” 一、打印矩形 //打印矩形for (int i 0; i < a;…

win10 python3.7 pycharm安装dlib库报错

使用pycharm安装dlib一直报错 心态有点崩 好在没有放弃 解决了 报错 note: This is an issue with the package mentioned above, not pip. hint: See above for output from the failure. 解决&#xff1a;直接好用的就是安装.whl文件 将下载好对应Python3.7版本的.whl…

C#中委托(delegate) 事件(event) Action<T>和Func<T,TResult>(泛型委托) 的区别

最近开发C#项目,发现有很多关键字不认识,特此笔记: 委托(delegate) 委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用,相当于C++中函数指针 的概念。 我们经常遇到的EventHandler 、其实就是一个委托、大家看看它的定义 使用如下: public delegate v…