string类模拟实现(c++)(学习笔记)


框架:

namespace abc
{
    
    
	class string
	{
    
    
	private:
		char* _str;  
		size_t _size;  //有效字符大小
		size_t _capacity; //总容量
	};

}

1.构造函数

因为string类构造函数有多种形式,这里只实现两个最常用的。不带参数的,带参数的。

1.1 不带参构造

示例1·:cout对空指针解引用报错

		string()
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{
    
    }
		char* c_str()
		{
    
    
			return _str;
		}
	void Test1()
	{
    
    
		abc::string s1;
		cout << s1.c_str() << endl;//该行报错
	}

报错原因:首先s1的字符串指向的为空指针。c_str会返回一个c格式的字符串,但是cout<<s1.c_str()会自动识别类型,识别为字符串类型,打印就会解引用。造成空指针访问报错。
改正:如果换成标准库里的string就不会报错,因为它赋的不是空指针,是空字符串。

改1:
在这里插入图片描述
如果这样直接赋值的化,成员变量_str为非const变量,会出现权限放大的错误。这样做也不可行。

改2:
申请一个字符的空间,然后函数体里面初始化。

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

修改成这样就可以了。这样后续既可以修改字符串内容,也可以打印空字符串。

1.2 带参数的构造函数

示例2:

		string(const char* str)   //加const是因为常量字符串必须用const接收,不然会在传参时出错
			:_str(str)   //     *******该行会报错
			,_size(strlen(str))
			,_capacity(strlen(str)+1)
		{
    
    }

	void Test1()
	{
    
    
	
		abc::string s2("hello world");
		cout << s2.c_str() << endl;
	}

报错原因:
因为str为const类型,而成员变量_str为非const类型,赋值产生权限放大。给_str加const不可取,会导致后续没法修改字符串内容。

改1:还是开空间,能存上常量字符串(”hello“),并且还能保证能修改内容,初始化列表初始化不方便,选择在函数体初始化。

		string(const char* str)
			: _size(strlen(str))
		{
    
    
			_capacity = _size;//容量就是能装有效字符的个数
			_str = new char[_capacity + 1]; //开的空间要多包含一个\0
			strcpy(_str, str);//拷贝字符串内容
		}

1.3 合并两个构造函数。

看起来是两个无参有参的构造函数,其实可以合并成一个,因为第一个无参的就是一个空字符串。

示例:

		string(const char* str = "")  //使用缺省函数来合并
			: _size(strlen(str))
		{
    
    
			_capacity = _size;//容量就是能装有效字符的个数
			_str = new char[_capacity + 1]; //开的空间要多包含一个\0
			strcpy(_str, str);//拷贝字符串内容
		}

2. 析构函数

构造函数写完对应写析构函数,只需要保证new和delete符号匹配即可。
示例:

		//析构函数
		~string()
		{
    
    
			delete[] _str;  //都用带括号的
			_str = nullptr;
			_size = _capacity = 0;
		}

3.拷贝构造函数

拷贝构造函数逻辑上不难。
示例:

		//拷贝构造函数
		string(const string& str)
			:_size(str._size)
			, _capacity(str._capacity)
		{
    
    
			_str = new char[_capacity + 1];
			strcpy(_str, str._str);
		}
	void Test1()
	{
    
    
		abc::string s1;
		abc::string s2("hello world");

		abc::string s3(s1);
		abc::string s4(s2);

		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;
		cout << s3.c_str() << endl;
		cout << s4.c_str() << endl;
	}

程序运行正确,没有报错。

4. 赋值运算符重载

示例:(经典标0)

		//赋值运算符重载
		string& operator=(const string& s)
		{
    
    	
			_size = s._size;
			_capacity = s._capacity;
			_str = new char[_capacity + 1];
			strcpy(_str, s._str);
			return *this;
		}

	void Test2()
	{
    
    
		abc::string s1;
		abc::string s2("hello world");
		abc::string s3("i love you peter");

		s2 = s3;

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

问题:
1.首先s2的空间没有释放,导致内存泄露问题。
2. 没有考虑拷贝多少的问题,比如相等空间可以i直接赋值,大空间给小空间,小空间给大空间的问题。

优化1:首先要将s2的内存释放掉,然后申请一块新空间,赋给s2。(这样可以不用考虑原因2的三种情况,简化逻辑)

		string& operator=(const string& s)
		{
    
    
			delete[] _str;//释放空间
			_str = new char[s._capacity + 1]; //申请新空间
			strcpy(_str, s._str);   //拷贝
			_size = s._size;
			_capacity = s._capacity;
			return *this;
		}

问题:
3.如果内存申请失败,该版本会弄丢s2的原有值。

优化2:
使用临时变量开空间,开成功再赋回去。

		string& operator=(const string& s)
		{
    
    
			char* tem = new char[s._capacity + 1]; //先申请新空间
			strcpy(tem, s._str);   //拷贝
			//没有抛异常,往下执行
			delete[] _str;//释放空间
			_str = tem;   //赋回来

			_size = s._size; 
			_capacity = s._capacity;
			return *this;
		}

问题:如果自己给自己赋值,原地不动就好了。
优化3:

		string& operator=(const string& s)
		{
    
    
			if (this != &s)
			{
    
    
				char* tem = new char[s._capacity + 1]; //先申请新空间
				strcpy(tem, s._str);   //拷贝
				//没有抛异常,往下执行
				delete[] _str;//释放空间
				_str = tem;   //赋回来

				_size = s._size;
				_capacity = s._capacity;
			}

			return *this;
		}

5. size()/capacity()

示例:

		size_t size()
		{
    
    
			return _size;
		}
		size_t capacity()
		{
    
    
			return _capacity;
		}
			void Test3()
	{
    
    
		const abc::string s1;
		const abc::string s2("hello world");
		abc::string s3("i love you peter");

		cout << s1.size() << endl;   //报错
		cout << s2.capacity() << endl;  //报错
	}

问题:调用的两个函数,都出现了权限放大的错误。
改正:给this加上const即可。

		size_t size() const
		{
    
    
			return _size;
		}
		size_t capacity() const
		{
    
    
			return _capacity;
		}

6. 解引用[]

示例:

		char& operator[](size_t pos)
		{
    
    
			assert(pos < _size);  //pos位置得合法
			return _str[pos];
		}
		void Test3()
		{
    
    
			abc::string s2("hello world");
	
			for (size_t i = 0; i < s2.size(); i++)
			{
    
    
				cout << (s2[i]) << " ";
			}
		}

问题:如果const对象解引用,会产生权限放大的错误,得把this加const,解决问题。但const对象返回值也得是const(防止对象被修改)。这就与非const对象产生矛盾。

改正:再次重载一个适合const对象的引用函数,即可解决问题。(运算符重载yyds)

		char& operator[](size_t pos)
		{
    
    
			assert(pos < _size);  //pos位置得合法
			return _str[pos];
		}
		const char& operator[](size_t pos) const
		{
    
    
			assert(pos < _size);  //pos位置得合法
			return _str[pos];
		}

8.iterator迭代器

迭代器是一种比较方便的访问有序对象的一种通用方法。
示例:

	public:
		//迭代器
		typedef char* iterator;

	public:
		iterator begin()
		{
    
    
			return _str;
		}
		iterator end()
		{
    
    
			return _str + _size;
		}
//测试代码
	void Test4()
	{
    
    
		abc::string s2("hello world");

		abc::string::iterator it = s2.begin();
		while (it != s2.end())
		{
    
    
			cout << *it << " ";
			it++;
		}
		cout << endl;


		// 范围for
		for (auto ch : s2)
		{
    
    
			cout << ch << " ";
		}

	}

解释:因为范围for的底层就是迭代器,如果迭代器底层实现好了的话,范围for也可以用。

问题:这个代码同样仅仅考虑了非const的问题,因此再写一组重载。

改:加一组const迭代器就可以了。

	public:
		//迭代器
		typedef const char* const_iterator;  //const型

	public:
		const_iterator begin() const
		{
    
    
			return _str;
		}
		const_iterator end() const
		{
    
    
			return _str + _size;
		}
	void Test4()
	{
    
    
		const abc::string s2("hello world");

		abc::string::const_iterator it = s2.begin();
		while (it != s2.end())
		{
    
    
			cout << *it << " ";
			it++;
		}
		cout << endl;

		// 范围for
		for (auto ch : s2)
		{
    
    
			cout << ch << " ";
		}

	}

7.Print()

该函数实现不难,仅仅需要注意区分const和非const即可。
示例:

		void Print()const
		{
    
    
			// 范围for
			for (auto ch : *this)
			{
    
    
				cout << ch << " ";
			}
		}
		void Test5()
		{
    
    
			const abc::string s2("hello world");
		
			s2.Print();
		
		}

因为前面已经实现好const和非const迭代器,这样Print函数,const对象调用const迭代器,非const对象调用非const迭代器。能成功运行。

8.> ==

string类的比大小遵循c语言的方式,使用strcmp复现。
示例:注意const和非const对象。

		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 _str > s._str || _str == s._str;
		}
		bool operator<(const string& s)const
		{
    
    
			return !(_str>=s._str);
		}
		bool operator<=(const string& s)const
		{
    
    
			return _str < s._str || _str == s._str;
		}
		bool operator!=(const string& s)const
		{
    
    
			return !(_str==s._str);
		}

注意:仅仅写出来>+==就可以把其他都复用出来了。

8. push_back()&append()

该算法实现一个字符串增和一个字符增。
示例:


		void reserve(size_t n)
		{
    
    
			char* tem = new char[n + 1];//多开一个空间存\0
			strcpy(tem, _str);
			delete[] _str;
			_str = tem;

			_capacity = n;
		}
		void append(const char* str)
		{
    
    
			size_t len = strlen(str);
			//需要扩容
			if(_size + len > _capacity)
			{
    
    
				//防止new_capacity比len小
				size_t new_capacity = (2 * _capacity) > (_size + len) ? (2 * _capacity) : (_size + len);
				reserve(new_capacity);
			}
			strcpy(_str + _size, str);
			_size += len;
		}


		void push_back(char ch)
		{
    
    
			//容量不够需要扩容
			if (_size + 1 > _capacity)
			{
    
    
				reserve(2 * _capacity);
			}
			_str[_size++] = ch;  
			_str[_size] = '\0';  // 别忘了\0
		}
	void Test7()
	{
    
    
		abc::string s2("hello world");
		const char ch = 'a';
		
		s2.push_back(ch);
		s2.append("xxxxaaa");

	}

问题:但是当字符串为空的时候,如果添加字符的话,push_back会报错,因为capacity为0,导致扩容还是0。

改:为了简单起见,我们只需要保证capacity不为0即可,可以在构造函数中修改。如下:

		string(const char* str = "")
			: _size(strlen(str))
		{
    
    
			_capacity = (_size == 0 ? 3 : _size);//容量就是能装有效字符的个数
			_str = new char[_capacity + 1]; //开的空间要多包含一个\0
			strcpy(_str, str);//拷贝字符串内容
		}

这样即可解决问题。

8.1 reserve()

该函数的实现发方法仍然有一些问题,就是当要保留的空间小于原有的空间。函数不做改动。
改:加一个判断语句即可。

		void reserve(size_t n)
		{
    
    
			if (n > _capacity)
			{
    
    
				char* tem = new char[n + 1];//多开一个空间存\0
				strcpy(tem, _str);
				delete[] _str;
				_str = tem;
				_capacity = n;
			}
		}

9. +=

使用上面的append和push,实现此函数轻而易举。
示例:

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

	void Test7()
	{
    
    
		abc::string s2("hello world");
		const char ch = 'a';
		
		//s2.push_back(ch);
		//s2.append("xxxxaaa");
		s2 += "hello world";
		s2 += 'a';
	}

10.insert()

10.1 任意位置插入一个字符

示例:
在pos位置插入一个字符ch。

		//在第pos位置插入
		void insert(size_t pos, char ch)
		{
    
    
			//位置合法
			assert(pos <= _size);

			if (_size + 1 > _capacity)
			{
    
    
				reserve(2 * _capacity);
			}
			size_t  end = _size - 1;
			while (end >= pos)
			{
    
    
				_str[end + 1] = _str[end];
				end--;
			}

			_str[pos] = ch;
			_size++;
		}

问题:
1.插入一个字符串后,结尾没加\0。
2.最好不要用end>=pos,因为都是无符号数,当pos和end同时等于0,end–,end变成了-1(但是是无符号数),因此会导致越界访问。

改正:
1.不能让end等于0,让end到1就结束,从前往后赋值。
2.最后加上\0。

		void insert(size_t pos, char ch)
		{
    
    
			//位置合法
			assert(pos <= _size);

			if (_size + 1 > _capacity)
			{
    
    
				reserve(2 * _capacity);
			}
			size_t  end = _size;
			while (end > pos)  
			{
    
    
				_str[end] = _str[end - 1];
				end--;
			}

			_str[pos] = ch;
			_size++;
			_str[_size] = '\0';  //没有写\0
		}

10.2 在任意位置插入字符串

示例:

		//插入字符串
		void insert(size_t pos, const char* str)
		{
    
    
			assert(str);
			assert(pos <= _size);
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
    
    
				size_t new_capacity = (2 * _capacity) > (_size + len) ? (2 * _capacity) : (_size + len);
				reserve(new_capacity);   
			}
			//从后往前一个字符一个字符移动,记得最后不\0,到插入位置结束

			size_t end = _size - 1;
			while (end >= pos&&end!=-1)
			{
    
    
				_str[end + len] = _str[end];
				end--;
			}
			int i = 0;  //计数
			size_t count = len;
			while (count--)
			{
    
    
				_str[pos++] = str[i++];
			}

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

问题:
1.同样是end和pos的关系,应该从前往后赋值,然后让后面的作为结束条件,这样就防止end越界了。

改正:
1.使用后面的下标作为结束条件,值得学习。
2.挪动完数据后,需要拷贝,可以使用strncpy进行拷贝。

				//插入字符串
		void insert(size_t pos, const char* str)
		{
    
    
			assert(str);
			assert(pos <= _size);
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
    
    
				size_t new_capacity = (2 * _capacity) > (_size + len) ? (2 * _capacity) : (_size + len);
				reserve(new_capacity);
			}
			//从后往前一个字符一个字符移动,记得最后不\0,到插入位置结束
			size_t end = _size + len;
			while (end > pos+len-1)
			{
    
    
				_str[end] = _str[end - len];
				end--;
			}
			//按字节拷贝
			strncpy(_str + pos, str, len);
			_size += len;
		}

11. resize()

当容量小于size时,直接赋\0;当容量>size&&<capacity时,直接填充指定字符;当容量>capacity时,先扩容再填充指定字符。
示例:

	//比_size小,就直接阶段、比_capacity大就用\0填充
		void resize(size_t n, char ch = '\0')
		{
    
    
			if (n <= _size)
			{
    
    
				_str[n] = '\0';
			}
			//需不需要异地扩容
			else
			{
    
    
				if (n <= _capacity)
				{
    
    
					memset(_str + _size, ch, n - _size);
					//最后再加上\0;
					_str[n] = '\0';
				}
				else
				{
    
    
					
					//char* tem = new char[n+1];  //要多申请一个空间
					//strcpy(tem, _str);
					//delete[] _str;
					//_str = tem;
					reserve(n);
						
					memset(_str + _size, ch, n - _size);
					_str[n] = '\0';
					_capacity = n;
				}
			}
			_size = n;
			
		}

优化:
此代码逻辑有些冗余,可以修改成如下代码,更加简洁。

				//比_size小,就直接阶段、比_capacity大就用\0填充
		void resize(size_t n, char ch = '\0')
		{
    
    
			if (n <= _size)
			{
    
    
				_str[n] = '\0';
			}
			//需不需要异地扩容
			else
			{
    
    
				if (n > _capacity)
				{
    
    
					reserve(n);
					_capacity = n;
				}
				memset(_str + _size, ch, n - _size);
				_str[n] = '\0';
			}
			_size = n;

		}

12 erase()

擦除给定位置的n个字符。

		// pos为位置
		void erase(size_t pos, size_t len = npos)
		{
    
    
			assert(pos < _size);
			if (len == npos||len>=_size-pos)
			{
    
    
				_str[pos] = '\0';
				_size = pos + 1;
			}
			else
			{
    
    
				size_t end = pos;
				while (end + len <= _size)
				{
    
    
					_str[end] = _str[end + len];
					end++;
				}
				_size -= len;
			}

		}

问题:else的代码有一些冗余,可以使用strcpy来简化

优化:

				// pos为位置
		void erase(size_t pos, size_t len = npos)
		{
    
    
			assert(pos < _size);
			if (len == npos || len >= _size - pos)
			{
    
    
				_str[pos] = '\0';
				_size = pos + 1;
			}
			else
			{
    
    
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}

		}

13. swap()

交换两个string对象的内容。

		//1.   swap(s1,s2)
		//2.   s1.swap(s2)
		void swap(string& s)
		{
    
    
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

对于两种swap的方式,很明显第二种更高效,第一种需要拷贝构造,第二种自己实现的方式则不需要。

14.find()

14.1 查找字符

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

14.2 查找字符串

使用strstr()库函数,复现。

		size_t find(const char* str, size_t pos = 0)
		{
    
    
			assert(str);
			char* p = strstr(_str + pos, str);   //查找字符串的库函数
			if (p == nullptr)
			{
    
    
				return npos;
			}
			else
			{
    
    
				return p - _str;
			}
		}

15 <<和>>

15.1 流提取

因为this指针的原因,不能将其定义为成员函数。

	ostream& operator<< (ostream& out, const string& s)
	{
    
    
		for (auto ch : s)
		{
    
    
			out << ch;
		}
		return out;
	}
	
	void Test8()
	{
    
    
		abc::string s1("hello world");
		s1 += '\0';
		s1 += "aaaaaaaaaa";

		cout << s1 << endl;  //hello worldaaaaaaaaaa
		cout << s1.c_str() << endl; //hello world
	}

注意:这里要简单提一下:为什么两个输出的函数不一样。
第一个s1是我们自己实现的函数,它是根据字符串的个数打印的。
而第二个s1.c_str返回的是指针,编译器会根据指针来打印,遇到\0就停止!

15.2 流插入

示例:

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

报错:输入“hello world”
该代码不能完成功能,因为in>>ch,读不进去空格和回车。所以根本跳不出循环。

改正:换一种方式获取缓冲区数据。使用get()函数。

	istream& operator>> (istream& in, string& s)
	{
    
    
		char ch = in.get();   //可以读到空格
		while (ch != ' ' && ch != '\n')
		{
    
    
			s += ch;
			ch = in.get();
		}
		return in;
	}

问题:1.频繁+=会导致频繁扩容。
2.连续两次读取数据不会清除第一次的数据。

优化:1.使用缓冲区的概念,一下+=一个缓冲区。
2.每次提取之前,要清空字符。


	void clear()
	{
    
    
		_str[0] = '\0';
		_size = 0;
	}
	istream& operator>> (istream& in, string& s)
	{
    
    

		s.clear();  //每次读取前,清空s
		char buf[128];  //申请一个缓冲区
		char ch = in.get();
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
    
    
			buf[i++] = ch;
			if (i == 127)  //剩一个位置给\0
			{
    
    
				buf[i] = '\0';
				s += buf;
				i = 0;
			}
			ch = in.get();
		}
		if (i != 0)   //缓冲区有内容,再加上
		{
    
    
			buf[i] = '\0';
			s += buf;
		}
		return in;
	}

以上就是string类部分库函数实现。

猜你喜欢

转载自blog.csdn.net/weixin_45153969/article/details/133806259