
🚀write in front🚀
📜所属专栏:初阶数据结构
🛰️博客主页:睿睿的博客主页
🛰️代码仓库:🎉VS2022_C语言仓库
🎡您的点赞、关注、收藏、评论,是对我最大的激励和支持!!!
关注我,关注我,关注我,你们将会看到更多的优质内容!!

 
文章目录
- 前言
- 一.string的成员变量:
- 二.string的成员函数:
- 1.构造函数:
- 2.析构函数:
- 3.赋值运算符重载:
- 4.对于深拷贝浅拷贝问题的说明与解决方案:
 
- 三.string的迭代器
- 四.string的容量操作
- 五.string的元素访问
- 六.string的修饰操作:
- push_back插入字符:
- append插入字符串:
- +=重载:
- insert插入数据:
- erase删除数据:
 
- 七.string的字符串操作
- find查找字符位置:
- substr返回所寻子串
- c_str返回字符串数组
 
- 八.string的非成员函数重载
- 比较运算符重载:
- 流输出运算符:
- 流输入运算符:
 
- 总结
前言
在C++中,字符串是一种非常常见的数据类型,用于存储和操作文本信息。标准库中的string类提供了强大的字符串操作功能,但是了解其背后的实现原理对于深入理解和灵活运用字符串类至关重要。在本篇博客中,我们将一起学习标准库中string类的实现原理,并模拟实现自己的string类。
一.string的成员变量:

 在标准库里面的string的成员变量看起来很复杂,但是其实本质就是一个顺序表,下面是对于该部分的模拟实现:
namespace zxr
{
	class bit
	{
	private:
		size_t _size;
		size_t _capacity;
		char* _str;
	};
};	
在这里我们要注意的是不同编译器下的成员变量会有微微的不同,比如vs下面,当成员长度小于16时,就会通过一个16个字节大小的数组来存储,当大于16时才是用堆来存储。
 
二.string的成员函数:

1.构造函数:
  构造函数有很多的重载:
 
 其中最常见的是如下:
void Teststring()
{
 string s1; // 构造空的string类对象s1
 string s2("hello bit"); // 用C格式字符串构造string类对象s2
 string s3(s2); // 拷贝构造s3
}
下面我们来模拟实现一个构造函数:
string(const char* str = "")
	{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
	}
这一个构造函数是一个默认的构造函数,将传进来的c字符串存入顺序表中,并且没有初始化的string类也能够进行初始化。
  有了构造函数还是远远不够的,对于复制构造函数,如果我们不通过自己写的深拷贝而调用编译器自己的浅拷贝,在析构的时候就会析构两次,产生错误:
 
所以我们要自己实现复制构造函数:
string(const string& s)
		{
			_str = new char[s._capacity + 1];
			//strcpy(_str, s._str);
			memcpy(_str, s._str, s._size + 1);
			_size = s._size;
			_capacity = s._capacity;
		}
2.析构函数:

~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}
3.赋值运算符重载:

对于赋值运算符重载有两种方法实现:
- 常规方法:
string& operator=(const string& s)
		{
			if (this != &s)
			{
				char* tmp = new char[s._capacity + 1];
				memcpy(tmp, s._str, s._size+1);
				delete[] _str;
				_str = tmp;
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}
- 简单方法:
void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
string& operator=(string tmp)
		{
			swap(tmp);
			return *this;
		}
在这里我们调用了库里面的swap函数,注意看,我们的tmp是一个形参,并没有用引用(很重要的一点),在出函数以后就会调用析构函数自动销毁,所以我们就可以放心的使用swap函数对其进行操作啦。
4.对于深拷贝浅拷贝问题的说明与解决方案:
我们之所以要深拷贝,原因就是对于在堆上面开辟的空间,如果我们单纯使用浅拷贝就会产生下面的问题:
- 析构两次
- 一个对象修改会影响另一个
在实际的编译器里会通过引用计数的方式来解决:

 大概过程如下:
- 构造函数中创建类的新对象时,初始化引用计数为1;
- 拷贝构造函数复制指针,并使相应的引用计数增加1;
- 赋值操作减少左操作数所值对象的引用计数,增加右操作数所指对象的引用计数;
- 析构函数使引用计数减少1,并且当引用计数为1时,释放指针说指向的对象;
简单的说,就是对象实例化之后,如果出现了复制构造,那么这个类的计数就会+1,如果这个对象要析构,先检查引用计数的次数是否等于1,不等于1就不释放这个空间,让引用计数-1,如果等于1在析构。
  对于要修改的对象,我们就要通过深拷贝构造出一个新对象,然后将引用计数-1就可以了!
 
 gcc测试如下:
 
三.string的迭代器
  对于string类型,其迭代器就是指针。
 
 所以我们可以通过重命名的方式来写出迭代器的类型:
typedef char* iterator;
		typedef const char* const_iterator;
下面我们模拟最简单begin和end
 
 对于begin(),模拟如下:
		iterator begin()
		{
			return _str;
		}
		const_iterator begin() const
		{
			return _str;
		}
对于end(),模拟如下:
 
		iterator end()
		{
			return _str + _size;
		}
		const_iterator end() const
		{
			return _str + _size;
		}
一个是只读一个是可读可写,这里就不必多说。
在就是迭代器的使用,其实当我们写好了迭代器,我们auto的语法糖也可以直接用了:
//zxr::string::const_iterator cit = s3.begin();
	auto cit = s3.begin();
	while (cit != s3.end())
	{
		//*cit += 1;
		cout << *cit << " ";
		++cit;
	}
	cout << endl;
	zxr::string::iterator it = s1.begin();
	while (it != s1.end())
	{
		*it += 1;
		cout << *it << " ";
		++it;
	}
	cout << endl;
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;
其实auto的底层也就是通过迭代器来遍历string.
四.string的容量操作

 这几个容量操作的函数非常好模拟实现,我们就说几个注意事项就可以了。
	返回数组大小size
		size_t size() const
		{
			return _size;
		}
		
	对string对象预留空间,注意:是空间,size大小没有改变
			void reserve(size_t n)
		{
			if (n > _capacity)
			{
				cout << "reserve()->" << n << endl;
				char* tmp = new char[n + 1];
				//strcpy(tmp, _str);
				memcpy(tmp, _str, _size+1);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}
		
	对string对象预留空间并将size也增加了,是上面的升级版,size和capacity大小都改变了
		void resize(size_t n, char ch = '\0')
		{
			if (n < _size)
			{
				_size = n;
				_str[_size] = '\0';
			}
			else
			{
				reserve(n);
				for (size_t i = _size; i < n; i++)
				{
					_str[i] = ch;
				}
				_size = n;
				_str[_size] = '\0';
			}
		}
		
	将string中有效字符清空,不改变底层空间大小。
		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}
注意事项:
- size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
- clear()只是将string中有效字符清空,不改变底层空间大小。
- resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
- reserve:为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
五.string的元素访问

 这里元素访问,我们最熟悉的就是[]
 
 模拟实现非常简单,不必多说:
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		const char& operator[](size_t pos) const 
		{
			assert(pos < _size);
			return _str[pos];
		}
六.string的修饰操作:

 这里的模拟实现就很需要大家的基本功了。
push_back插入字符:

void push_back(char ch)
		{
			if (_size == _capacity)
			{
				// 2倍扩容
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}
append插入字符串:

		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				// 至少扩容到_size + len
				reserve(_size+len);
			}
			//strcpy(_str + _size, str);
			memcpy(_str + _size, str, len+1);
			_size += len;
		}
+=重载:

		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}
insert插入数据:

		void insert(size_t pos, size_t n, char ch)
		{
			assert(pos <= _size);
			if (_size +n > _capacity)
			{
				// 至少扩容到_size + len
				reserve(_size + n);
			}
			// 添加注释最好
			// 防止越界
			size_t end = _size;
			while (end >= pos && end != npos)
			{
				_str[end + n] = _str[end];
				--end;
			}
			for (size_t i = 0; i < n; i++)
			{
				_str[pos + i] = ch;
			}
			_size += n;
		}
		void insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				// 至少扩容到_size + len
				reserve(_size + len);
			}
			// 添加注释最好
			//防止越界
			size_t end = _size;
			while (end >= pos && end != npos)
			{
				_str[end + len] = _str[end];
				--end;
			}
			for (size_t i = 0; i < len; i++)
			{
				_str[pos + i] = str[i];
			}
			_size += len;
		}
erase删除数据:

		void erase(size_t pos, size_t len = npos)
		{
			assert(pos <= _size);
			if (len == npos || pos + len >= _size)
			{
				//_str[pos] = '\0';
				_size = pos;
				_str[_size] = '\0';
			}
			else
			{
				size_t end = pos + len;
				while (end <= _size)
				{
					_str[pos++] = _str[end++];
				}
				_size -= len;
			}
		}
七.string的字符串操作

find查找字符位置:
返回该字符的下标
		size_t find(char ch, size_t pos = 0)
		{
			assert(pos < _size);
			for (size_t i = pos; i < _size; i++)
			{
				if (_str[i] == ch)
				{
					return i;
				}
			}
			return npos;
		}
		size_t find(const char* str , size_t pos = 0)
		{
			assert(pos < _size);
			const char* ptr = strstr(_str + pos, str);
			if (ptr)
			{
				return ptr - _str;
			}
			else
			{
				return npos;
			}
		}
substr返回所寻子串
		string substr(size_t pos = 0, size_t len = npos)
		{
			assert(pos < _size);
			size_t n = len;
			if (len == npos || pos + len > _size)
			{
				n = _size - pos;
			}
			string tmp;
			tmp.reserve(n);
			for (size_t i = pos; i < pos + n; i++)
			{
				tmp += _str[i];
			}
			return tmp;
		}
c_str返回字符串数组
返回字符串数组的时候使用,适用于printf打印。
		const char* c_str() const
		{
			return _str;
		}
八.string的非成员函数重载

比较运算符重载:
bool operator<(const string& s) const
		{
			int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
			return ret == 0 ? _size < s._size : ret < 0;
		}
		bool operator==(const string& s) const
		{
			return _size == s._size 
				&& memcmp(_str, s._str, _size) == 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);
		}
流输出运算符:

ostream反拷贝,所以用引用。
ostream& operator<<(ostream& out, const string& s)
	{
		/*for (size_t i = 0; i < s.size(); i++)
		{
			out << s[i];
		}*/
		for (auto ch : s)
		{
			out << ch;
		}
		return out;
	}
在这里我们可以明显看出在打印string对象时和字符串数组的区别,字符串数组打印时以’\0’为结束,而string对象以迭代器的end结束,所以string里面如果存在’\0’也没关系。这就是为什么上面的拷贝要用memcpy而不是strcpy!
流输入运算符:

istream& operator>>(istream& in, string& s)
	{
		s.clear();
		//处理掉之前对象里的内容
		char ch = in.get();
		// 处理前缓冲区前面的空格或者换行
		while (ch == ' ' || ch == '\n')
		{
			ch = in.get();
		}
		//in >> ch;
		char buff[128];
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			//in >> ch;
			ch = in.get();
		}
		if (i != 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}
};
这里由于我们不知道我们要输入多少字符,不能预先开好空间,所以只能一个字符一个字符的输入。但是我们也不能通过cin>>ch来输入字符,因为cin只有读到空格或换行符的时候才能读取成功一个,对于一段完整的字符串,我们要使用该类里面的另一个函数get()来读取才行。该函数输入一个字符就读取,符合该功能。
 
 对于中间开的buff数组,只是为了增加效率,不然每输入一个字符就扩容一次,效率低下。
总结
这就是string类的深入学习与模拟实现啦!如果在使用方面还有上面疑惑的话可以去官网查看或者私信我c++使用手册,想看模拟实现相关代码的可以去我的giteestring模拟实现上看!如果本篇文章有上面错误的话请私信我哟!
更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!
参考文献:https://legacy.cplusplus.com/
专栏订阅:
每日一题
C语言学习
算法
智力题
初阶数据结构
Linux学习
C++学习
更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!




















