string 模拟实现

string是用来存储字符类型数据的容器,由于string比STL出现的早,所以在C++官方网string并不在container分类中,但是我们也可以称它为容器。

string有两个类模板,我们主要学习的是 basic_string这个类模板实例化出来的 string (basic_string<char>的重命名)类。我们在C语言中学的字符都是以ASCII码的形式来表示和存储的,但是ASCII码只能表示128个字符。对于不同的语言不同的字符,比如我们的汉字,是无法只用ASCII码就能完全表示的,所以在计算机科学中,还有很多种编码方式用来表示字符,比如utf-8、utf-16编码,不同的编码方式自然不能用同一个类来表示,以basic_string模板就能实例化多个不同的类,用来存储这些不同编码方式的字符。我们学习的string就是存储char类型的字符的类。

1.string的结构

string本质上是一个动态增长的数组,与顺序表类似,只是它是专门用来管理字符类的。

string实际上是对 basic_string<char>的重命名,char就是用来存utf-8编码,也可以存储表示ASCII码。

首先我们需要了解一个string对象的成员变量有哪些

在vs编译器中,一个string对象的内部有一个size和capacity变量,还有allocator空间配置器,我们目前可以将空间配置器直接理解为动态开辟数组,用来存储数据,我们直接以顺序表的方式来理解就行了。

2.接口及模拟实现

首先是string的构造函数,string的构造函数有很多个,但是我们实际上常用的也就三四个

使用起来也很简单,我们只要参数能够与构造函数匹配上就行了


	string s1;//默认构造
	string s2("abcdefg");//常量字符串构造
	string s3(s2);//拷贝构造
	string s4(s2, 3, 2);//用s2的字符串第三个位置开始的2个字符进行构造
	string s5("abcdefg", 5);//用常量字符串的前5 个字符进行构造

我们可以看到上面的额有一些构造函数中有一个缺省值 npos ,这是什么呢?

我们可以在文档中看到npos是一个string类的const修饰的静态成员,他的值是size_t的-1,也就是一个32位二进制全是1的正数,大约是四十二亿,她表示的是一个对象的最大数据个数,也就是_size的最大值。那么我们的string类就可以这样定义

private:
		char* _str;
		size_t _size;
		size_t _capacity;//表示可存储的数据个数,比实际容量要小 1 ,确保最后要存储一个 \0 
		const static size_t npos = -1;//C++的一个特例,就是const static的整型成员变量可以通过缺省值初始化。 

我们在这里直接用缺省值对npos进行初始化,这是C++的一个特殊情况,对于const static的整型类型的成员变量可以直接用缺省值进行初始化。

我们自己实现的string类,不一定按照vs的扩容规则来,我们可以按照自己的想法来进行扩容和初始化。

		string(const char* str)
		{
			int len = strlen(str);
			_capacity = len;
			_size = len;
			_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);

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

		}

同时我们可以将无参构造和常量字符串的构造写成一个构造函数,无参无非就是只有一个'\0'。

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

		}

三种遍历方式:

对于string对象的遍历我们有三种方式,第一种就是用方括号,以数组的形式进行遍历。我们可以在文档中看到对方括号的重载

我们可以看到,方括号的重载有两个函数重载,一个是针对普通的string对象的,它返回的是可读可写的引用,另一个是针对const对象的,它返回的是只读的引用。这种遍历方式我们需要知道string对象的size来控制循环条件

第二种方式就是范围for,范围for我们在之前就已经用过了,他的底层就是用迭代器来实现的。

第三种遍历方式就是直接使用迭代器来实现,而string的迭代器的接口如下

目前我们可以把迭代器当成指针来使用,它的用法和指针类似,但是他的实现并不一定是指针。

begin返回的是指向数据开头的迭代器,而end返回的是指向数据结尾的后一位的迭代器,也就是'\0'的位置。我们可以当成指针来理解,而string类的迭代器我们是可以直接用指针来实现的,因为对迭代器++就是指向下一个位置了,而对于其他的容器的迭代器则不一定使用指针来实现的。迭代器是容器的一种通用的遍历方式,后面我们见到的容器基本都是使用迭代器来遍历的。

同时我们还要实现size函数来支持 循环遍历

文档中说明 size接口返回的就是有效数据的个数,也就是字符串的长度。

我们可以重载方括号和迭代器来实现遍历

		//方括号重载
		//普通对象
		char& operator[](size_t pos)
		{
			return this->_str[pos];
		}
		//const对象
		const char& operator[](size_t pos)const
		{
			return this->_str[pos];
		}
		//迭代器接口实现
		typedef char* iterator;
		//普通对象
		iterator begin()
		{
			return &this->_str[0];
		}
		//const对象
		const iterator begin()const
		{
			return &this->_str[0];
		}

		//普通对象
		iterator end()
		{
			return &this->_str[_size];
		}
		//const对象
		const iterator end()const
		{
			return &this->_str[_size];
		}
		//size接口,返回_size
		size_t size()
		{
			return _size;
		}

string还有很多关于大小和容量的接口。

重点的其实就是上面用红色框起来的几个接口

size和length其实是完全一样的功能。capacity和empty以及clear接口我们也不难理解,resize和reserve接口的作用我们可以看文档来了解

resize 的功能就是调整数据的个数,如果 n 比原来的 size 小,那么就相当于丢掉后面的数据,只保留 n 个,而如果 n 大于原来的size,则多出来的空间用我们传的参数来补充 ,如果我们没有指定字符,那么就用空字符,一般是 '\0' 。

要注意的一点就是,resize影响的是数据的个数,如果是减少数据个数,则不会所容,如果增加数据个数同时容量不同了,就会扩容。

而string的扩容函数就是reserve函数,既能缩容,也能扩容。

对于reverse,如果n比原来的空间大的话,则扩容到可以存储 n 个数据,也就是实际空间 n+1,如果比 原来的空间小,则缩容到 n ,也就是实际空间 n+1 ,多出来的则截断。

		void resize(size_t n, char ch = '\0')
		{
			if (n >= _size)//扩容
			{
				reserve(n);
				for (size_t begin = _size; begin < n; begin++)
				{
					_str[begin] = ch;
				}
				_capacity = n;
			}
			//缩容不改_capacity
			_str[n] = '\0';
			_size = n;

		}

		void reserve(size_t n)
		{
			char* tmp = new char[n+1];
			//扩容
			if (n > _size)
			{
				strcpy(tmp, _str);
			}
			//缩容
			else
			{
				strncpy(tmp, _str, n);
				_size = n;
			}
			tmp[n] = '\0';
			delete[]_str;
			_str = tmp;
			_capacity = n;
		}
		void clear()
		{
			delete[]_str;
			_size = 0;
			_capacity = 0;
			_str = new char[1];
			_str[0] = '\0';
		}

		size_t capacity()
		{
			return _capacity;
		}

		bool empty()
		{
			return _size == 0;
		}

at 接口与方括号有一些类似,返回相应位置的字符,也要实现const对象和非const对象的重载。        

		char& at(size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		const char& at(size_t pos)const
		{
			assert(pos < _size);
			return _str[pos];
		}

修改的接口也有很多,但是我们常用的也只有几个

+=有三个重载函数,在实现+=的时候最重要的是控制扩容。

		//尾插对象
		string& operator+=(const string& s)
		{
			//扩容
			if(_size+s._size>_capacity)
			{	
				reserve(_size + s._size);
				_capacity = _size + s._size;
			}

			//尾插
			strcpy(_str + _size, s._str);
			_size += s._size;
			return *this;
		}
		//尾插字符串
		string& operator+=(const char* str)
		{
			size_t len = strlen(str);
			//扩容
			if (_size + len > _capacity)
			{
				reserve(_size + len);
				_capacity = _size + len;
			}
			//插入
			strcpy(_str + _size, str);
			_size += len;
			return *this;
		}

		//插入字符
		string& operator+=(char ch)
		{
			//扩容
			if (_size == _capacity)
			{
				_capacity *= 2;
				reserve(_capacity);
			}
			_str[_size] = ch;
			_size++;
			return *this;
		}

append函数的实现我们可以完全复用 += 来实现。

		string& append(const string&s)
		{
			*this += s;
			return *this;
		}

		string& append(const char* str)
		{
			*this += str;
			return*this;
		}
		string& append(const string& s, size_t begin, size_t num)
		{
			//扩容
			if (_size + num > _capacity)
			{
				reserve(_size + num);
				_capacity=_size + num;
			}

			size_t end = _size;
			_size += num;
			for (size_t i = 0; i < num; i++)
			{
				if (begin > s._size)
					break;

				(*this)[end++] = s[begin++];
			}
			(*this)[_size] = '\0';

			return *this;
		}
		string& append(size_t n, char ch)
		{
			while (n--)
			{
				*this += ch;
			}
			return *this;
		}

push_back我们直接复用 +=

		void push_back(char ch)
		{
			*this += ch;
		}

insert接口:

insert的实现最重要的地方就是挪数据的循环控制以及扩容

		string& insert(size_t pos,const string& s)
		{
			//当s为空对象,直接返回
			if (s._size == 0)
				return *this;
			assert(pos <= size());
			//扩容
			if (_size + s._size > _capacity)
			{
				reserve(_size + s._size);
				_capacity = _size + s._size;
			}
			//移动数据
			_size += s._size;
			size_t end = _size;
			while (end >= pos + s._size)
			{
				_str[end] = _str[end - s._size];
				--end;
			}
			//插入数据
			size_t begin = 0;
			while (begin < s._size)
			{
				_str[begin + pos] = s._str[begin];
				begin++;
			}
			return *this;
		}

		string& insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			size_t len = strlen(str);
			if (len == 0)
				return *this;
			//扩容
			if (_size + len > _capacity)
			{
				_capacity = _size + len;
				reserve(_capacity);
			}
			//挪数据
			size_t end = _size + len;
			_size += len;
			while (end >= pos+len)
			{
				_str[end] = _str[end - len];
				end--;
			}
			//插入
			size_t begin = 0;
			while (begin < len)
			{
				_str[begin + pos] = str[begin];
				begin++;
			}
			return *this;
		}

		string& insert(size_t pos, size_t n, char ch)
		{
			assert(pos <= _size);
			if (n==0)
				return *this;
			//扩容
			if (_size + n > _capacity)
			{
				_capacity = _size + n;
				reserve(_capacity);
			}
			//挪数据
			size_t end = _size + n;
			_size += n;
			while (end >= pos + n)
			{
				_str[end] = _str[end - n];
				--end;
			}
			//插入数据
			size_t begin = 0;
			while (begin < n)
			{
				_str[begin + pos] = ch;
				++begin;
			}
		}

erase函数的接口我们就实现一个就行了 ,如果第二个参数不传的话,默认把这个位置及以后的全部删除。

	string& erase(size_t pos, size_t len=npos)
		{
			assert(pos <= _size);
			if (len == npos||pos+len>_size)//npos要单独讨论,会溢出
			{
				_str[pos] = '\0';
				_size = pos;
				return *this;
			}
			else
			{
				//挪数据,要把'\0'也一起挪
				size_t begin = pos;
				while (begin <= _size-len)
				{
					_str[begin] = _str[begin + len];
					++begin;
				}
				_size  -= len;
				return *this;
			}
		}

find函数我们要注意的就是pos缺省值为0。找不到返回npos

	size_t find(const string& s, size_t pos = 0)const
		{
			assert(pos < _size);
			char* ret=strstr(_str + pos, s._str);
			if (ret == NULL)
				return npos;
			return ret - _str;
		}
		size_t find(const char*str, size_t pos = 0)const
		{
			assert(pos < _size);
			char* ret = strstr(_str + pos, str);
			if (ret == NULL)
				return npos;
			return ret - _str;
		}

		size_t find(char ch, size_t pos = 0)const
		{
			assert(pos < _size);
			size_t ret = 0;
			while (ret < _size)
			{
				if (_str[ret] == ch)
					return ret;
				ret++;
			}
			return npos;
		}

重载流插入和流提取

流插入重载我们要注意的就是不要直接打印 c_str ,因为我们存储的字符数据中间可能有 \0 , \0是可以作为数据存储的,所以我们要逐字符打印。

	ostream& operator<<(ostream& out, const string& s)
	{
		size_t begin = 0;
		size_t end = s.size();
		while (begin < end)
		{
			cout << s[begin++];
		}
		return out;
	}

而对于流提取的重载,首先要清除数据,然后我们要注意,如果是单纯用>>来逐个读取字符然后+=到后面的话

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

这样写的话,当读取到空格时就会停止读取,导致 ch 一直是空格,死循环了。而且这样写的话会频繁调用 += ,效率很低。

为了将空格也读取进来同时避免频繁调用 += ,我们可以用一个数组先存储读取到的内容,然后一次性 += 到对象中。但是这时候又有一个问题,就是我们无法预知到要输入的数据的个数,也就无法确定数组的大小。 一种思路就是设置一个固定大小的数组,当数组存满的时候就 += 到对象中,然后继续从头覆盖存储,用一个下标记录。

为了将空格也读取进来,我们可以用 istream 类的get函数

	istream& operator>>(istream& in,string& s)
	{
		s.clear();
		char buffer[128]={'\0'};
		size_t i = 0;
		char ch;
		in.get(ch);
		while (ch != '\n')
		{
			if (i == 127)//满了就先存进去
			{
				s += buffer;
				i = 0;
			}
			buffer[i++] = ch;
			in.get(ch);
		}
		//剩余的数据放进去
		buffer[i] = '\0';
		s += buffer;
		return in;
	}

赋值重载

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

  • 33
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值