【C++精华铺】10.STL string模拟实现

举报
子亦半截诗 发表于 2023/09/07 17:14:37 2023/09/07
【摘要】 STL(标准模板库)是一个C++标准库,其中包括一些通用的算法、容器和函数对象。STL的容器是C++ STL库的重要组成部分,它们提供了一种方便的方式来管理同类型的对象。其中,STLstring是一种常用的字符串类型。STLstring是一个类,它封装了字符串的操作,并提供了一组成员函数。STLstring的实现使用了动态的内存分配技术,这意味着字符串的大小可以随时改变。STLstring还提供了

1. 序言

        STL(标准模板库)是一个C++标准库,其中包括一些通用的算法、容器和函数对象。STL的容器是C++ STL库的重要组成部分,它们提供了一种方便的方式来管理同类型的对象。其中,STLstring是一种常用的字符串类型。

        STLstring是一个类,它封装了字符串的操作,并提供了一组成员函数。STLstring的实现使用了动态的内存分配技术,这意味着字符串的大小可以随时改变。STLstring还提供了一些高效的成员函数,例如substr、find、replace等,这些函数可以对字符串进行快速的操作。

        STLstring的实现主要基于字符数组。字符数组是一种固定大小的数组,其中每个元素包含一个字符。STLstring使用一个字符数组来存储字符串,并通过动态的内存分配技术来管理数组的大小。当向一个空的STLstring对象中添加字符时,STLstring会自动调整数组的大小。

        STLstring还实现了一些常见的字符串操作,例如连接字符串、查找字符串、替换字符串和分割字符串等。这些操作使用了C++ STL库中的algorithm算法,可以高效地处理字符串。同时,STLstring也提供了迭代器的支持,允许用户使用STL算法来处理字符串。

2. string类的接口实现

        在实现接口之前先要给出我们的初始类,包括三个私有成员(_size,_capacity,_str),接下来我们会对这个初始类一步一步的完善(为了文章易读后续接口函数不会展示类的全貌,只会展示实现的接口函数内容,在文章的末尾会给出完整的string类代码)如下:

namespace zybjs
{
	class string
	{
	public:
	
	private:
		size_t _size;
		size_t _capacity;
		char* _str;
	};
}

2.1 构造函数

(1)默认构造和C串构造

        在STL库中将C串构造和默认构造分开实现,但是我们在模拟实现string类的时候可以将这俩个构造函数合并,这样的代码会更简洁,也方便我们自己的使用。在此之前我们先按库里面的思路走一趟:在我们给初始容量的时候即便字符串长度是0我们也不能给0,避免后续无法倍数扩容,这里我们给的是4。并且给字符串开空间的时候要比容量多一个,最后一个留给'\0'。(VS下实现思路不同,VS下给了一个16字节的字符数组_buf,如果要存储的字符串小于16字节就存放在字符数组_buf中,否则就重新开一个空间)

default (1)            string();

from c - string(2)  string(const char* s);

string()
	:_size(0)
	,_capacity(4)  //不能给0,如果给0后续无法倍数扩容 下同
{
	_str = new char[_capacity + 1]; //开空间的时候要比容量多一个字节留给'\0',因为容量的大小不算'\0',下同
    _str[0] = '\0';
}
string(const char* str)  //const类型接收右值
	:_size(strlen(str))
{
	_capacity = _size == 0 ? 4 : _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}

         但是我们在自己实现的时候大可不必这样去写,我们之前学习了缺省参数,并且全缺省的构造函数也可以作为默认构造,所以我们可以对上述代码进行优化,给str一个空串作为缺省参数,这样当我们调用的时候不给实参就会默认使用空串进行构造来完成库中”string();“的功能。如下:

string(const char* str = "")   //const类型接收右值
	:_size(strlen(str))
{
	_capacity = _size == 0 ? 4 : _size;
	_str = new char[_capacity + 1];
	strcpy(_str,str);  
}

(2)拷贝构造

        string类的拷贝构造很简单,要注意的点有俩个:其一,string类涉及到空间管理,所以在拷贝的时候要深拷贝,否则会导致同一空间多次析构导致报错;其二,传参不能传值传参,要使用传引用传参,否则会导致无穷递归,

string(const string& s)
	:_size(s._size)
	,_capacity(s._capacity)
{      
	//深拷贝
	_str = new char[_capacity + 1];  //重新开空间
	strcpy(_str,s._str);     //字符序列拷贝
}

2.2 析构函数

        string类因为涉及到内存的管理,所以析构函数不能使用默认生成的析构,需要我们自己去实现析构。在实现析构的时候将_size和_capacity全部置零,然后通过delete[]释放_str就可以了。如下:

~string()
{
	delete[] _str;   //释放_str
	_str = nullptr;
	_size = _capacity = 0;
}

2.3 size()、capacity()

        size()和capacity()很多人在实现的时候都觉得很简单反而会忽略一个点:就是const对象访问的时候是否会权限放大。因为这个俩个函数仅涉及到读取,没有修改的操作,所以我们只需要实现const版本来兼容const对象和非const对象。如下

size_t size() const  //兼容const对象和非const对象
{
	return _size;
}
size_t capacity() const//兼容const对象和非const对象
{
	return _capacity;
}

2.4 c_str()

        返回指向一个数组的指针,该数组包含以 null 结尾的字符序列(即 C 字符串),该序列表示字符串对象的当前值。只需完成const版本来兼容const对象和非const对象。指针的返回值也需要是const char *类型,防止通过指针对对象进行修改。

const char* c_str() const 
{
	return _str;
}

2.5 operator[]

        因为要实现类似于数组的访问方式,所以我们要实现[]的重载形式。[]的特性:能够随机访问元素,对于非const对象能够修改元素。所以operator[]要实现const版本和非const版本,const版本的返回值必须是const引用。

const char& operator[](size_t pos) const
{
	assert(pos < _size&& pos >= 0);
	return _str[pos];
}
char& operator[](size_t pos)
{
	assert(pos < _size && pos >= 0);
	return _str[pos];
}

2.6 operator=

        赋值运算符的重载是对字符串对象的深拷贝,和拷贝构造的过程相同,但是‘=’的特性支持连续的赋值,所以我们在实现赋值重载的时候需要返回一个string对象来支持赋值重载的连续赋值。因为我们不涉及对传入参数的修改,所以我们需要传入一个const string& 类型。注意在赋值之前需要释放原来的空间。如下:

string& operator=(const string& s)
{
	if (s._str != _str)
	{
		_size = s._size;
		_capacity = s._capacity;
		//深拷贝
		char* _tmp = new char[_capacity + 1]; 
		strcpy(_tmp,s._str);
		//先释放原来的空间
		delete[] _str;
		_str = _tmp;
	}
	return *this;
}

测试:

zybjs::string s1("cacaca");
zybjs::string s2("dadada");
zybjs::string s3("bababa");
s3 = s2 = s1;
std::cout << s1.c_str() << std::endl;
std::cout << s2.c_str() << std::endl;
std::cout << s3.c_str() << std::endl;

编辑

2.7 字符串比较 

        字符串比较我们通过strcmp来进行比较(strcmp(srt1,str2)),返回的值为0,字符串相同;返回的值大于0,str1>str2;返回的值小于0,str1<str2。基于此便可以实现字符串的比较函数。

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 *this > s || *this == s;
	return *this > s || s == *this;
}

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);
}

2.8 迭代器实现

        string的迭代器底层是一个指针,string类的迭代器是一种用于访问字符串中字符的对象,可以通过迭代器的运算符访问字符串中的字符。迭代器为C++容器提供了一种通用的访问手段。

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;
}

2.9 reserve

        reserve()是为了给对象预留空间,如果我们提前得知字符串需要的空间我们就可以提前开好,避免频繁扩容带来的性能消耗。当reserve的参数小于string底层空间大小的时候,reserve就不会对容量进行处理。

void reserve(size_t n)
{
	if (n > _capacity)
	{
		char* _tmp = new char[n+1];
		strcpy(_tmp, _str);
		delete[] _str;
		_str = _tmp;
		_capacity = n;
	}
}

2.10 resize

          修改字符有效个数为n,多出的空间用字符ch填充。如果n小于有效字符数,本质就是删字符操作。如果容量不够会扩容。

void resize(size_t n, char ch = '\0')
{
	//当n<有效字符数的时候本质上就是删字符
	//但是我们一般不会进行缩容,在原来的空间上将n位置的字符设置为'\0'
	if (n < _size)
	{
		_size = n;
		_str[_size] = '\0';
	}
	else if (n > _size)
	{
		if (n > _capacity)  //n>_capacity就进行扩容
		{
			reserve(n);        
		}
		size_t i = _size;
		while (i < n)       //将非有效字符初始化为ch
		{
			_str[i] = ch;
			i++;
		}
		_size = n;
		_str[_size] = '\0'; //设置终止位
	}
}

2.11 push_back、append、operator+=

        push_back尾插,append是在字符串后面追加一个字符串,实现比较简单,但要注意检查容量。

void push_back(char ch)
{
	if (_size + 1 > _capacity)
	{
		reserve(2 * _capacity);
	}
	_str[_size] = ch;
	_size++;
	_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 += len;
}

        operator+=的功能可以由push_back和append的复用来实现:

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

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

2.12 insert

        insert是string类中支持pos位插入字符或者字符串的函数。其中,pos表示插入的位置,返回string的引用表示插入后的新字符串。

string& insert(size_t pos, char ch)
{
	if (_size + 1 > _capacity)
	{
		reserve(2 * _capacity);
	}
	size_t n = pos;
	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		end--;
	}
	_str[pos] = ch;
	_size += 1;
	_str[_size] = '\0';

	return *this;
}

string& insert(size_t pos, const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_capacity + len);
	}
	size_t n = pos;
	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end - 1 + len] = _str[end - 1];
		end--;
	}
	strncpy(_str + pos, str, len);
	_size += len;
	_str[_size] = '\0';

	return *this;
}

2.13 erase

        C++中的string类提供了一个名为erase()的成员函数,用于删除字符串中的一部分字符并修改该字符串。该函数可以接受1个或2个参数,具体取决于要删除的字符数。如果没有显式的指定删除字符数,会使用默认的npos也就是无符号整型-1来作为缺省参数(65535),就会默认删除pos位后面所有的字符。然后返回删除字符后生成的新字符。

        首先我们要定义一个和库里相同的npos:

编辑

string& erase(size_t pos,size_t len = npos)
{
	assert(pos < _size);
	if (len >= _size - pos - 1)
	{
		_size = pos;
		_str[_size] = '\0';
	}
	else
	{
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}
	return *this;
}

2.14 流插入和流提取

        

	std::ostream& operator<<(std::ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}
		return out;
	}

	std::istream& operator>>(std::istream& in, string& s)
	{
		s.clear();        //输入之前要清空字符串
		char ch = in.get();//获取字符包括'\n'
		char buff[32];  //设置缓冲区来防止频繁扩容
		size_t i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i] = ch;
			if (i == 30)
			{
				buff[31] = '\0';
				s += buff;
				i = 0;
			}
			i++;
			ch = in.get();
		}
		buff[i] = '\0';
		s += buff;
	}

3. 完整代码(均调试通过)

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
#include<cassert>
namespace zybjs
{
	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()
		//	:_size(0)
		//	,_capacity(4)  //不能给0,如果给0后续无法倍数扩容 下同
		//{
		//	_str = new char[_capacity + 1]; //开空间的时候要比容量多一个字节留给'\0',因为容量的大小不算'\0',下同
		//	_str[0] = '\0';
		//}
		//string(const char* str)
		//	:_size(strlen(str))
		//{
		//	_capacity = _size == 0 ? 4 : _size;
		//	_str = new char[_capacity + 1];
		//	strcpy(_str, str);
		//}
		string(const char* str = "")   //const类型接收右值
			:_size(strlen(str))
		{
			_capacity = _size == 0 ? 4 : _size;
			_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()
		{
			delete[] _str;   //释放_str
			_str = nullptr;
			_size = _capacity = 0;
		}
		size_t size() const  //兼容const对象和非const对象
		{
			return _size;
		}
		size_t capacity() const//兼容const对象和非const对象
		{
			return _capacity;
		}

		const char* c_str() const 
		{
			return _str;
		}
		const char& operator[](size_t pos) const
		{
			assert(pos < _size&& pos >= 0);
			return _str[pos];
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size && pos >= 0);
			return _str[pos];
		}

		string& operator=(const string& s)
		{
			if (s._str != _str)
			{
				_size = s._size;
				_capacity = s._capacity;
				//深拷贝
				char* _tmp = new char[_capacity + 1]; 
				strcpy(_tmp,s._str);
				//先释放原来的空间
				delete[] _str;
				_str = _tmp;
			}
			return *this;
		}

		//字符串比较
		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 *this > s || *this == s;
			return *this > s || s == *this;
		}

		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);
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* _tmp = new char[n+1];
				strcpy(_tmp, _str);
				delete[] _str;
				_str = _tmp;
				_capacity = n;
			}
		}
		void resize(size_t n, char ch = '\0')
		{
			//当n<有效字符数的时候本质上就是删字符
			//但是我们一般不会进行缩容,在原来的空间上将n位置的字符设置为'\0'
			if (n < _size)
			{
				_size = n;
				_str[_size] = '\0';
			}
			else if (n > _size)
			{
				if (n > _capacity)  //n>_capacity就进行扩容
				{
					reserve(n);        
				}
				size_t i = _size;
				while (i < n)       //将非有效字符初始化为ch
				{
					_str[i] = ch;
					i++;
				}
				_size = n;
				_str[_size] = '\0'; //设置终止位
			}
		}
		void push_back(char ch)
		{
			if (_size + 1 > _capacity)
			{
				reserve(2 * _capacity);
			}
			_str[_size] = ch;
			_size++;
			_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 += len;
		}

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

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

		string& insert(size_t pos, char ch)
		{
			if (_size + 1 > _capacity)
			{
				reserve(2 * _capacity);
			}
			size_t n = pos;
			size_t end = _size + 1;
			while (end > pos)
			{
				_str[end] = _str[end - 1];
				end--;
			}
			_str[pos] = ch;
			_size += 1;
			_str[_size] = '\0';

			return *this;
		}

		string& insert(size_t pos, const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_capacity + len);
			}
			size_t n = pos;
			size_t end = _size + 1;
			while (end > pos)
			{
				_str[end - 1 + len] = _str[end - 1];
				end--;
			}
			strncpy(_str + pos, str, len);
			_size += len;
			_str[_size] = '\0';

			return *this;
		}

		string& erase(size_t pos,size_t len = npos)
		{
			assert(pos < _size);
			if (len >= _size - pos - 1)
			{
				_size = pos;
				_str[_size] = '\0';
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
			return *this;
		}

		void clear()
		{
			_size = 0;
			_str[_size] = '\0';
		}

	private:
		size_t _size;
		size_t _capacity;
		char* _str;

		static const size_t npos;
	};
	const size_t string::npos = -1;


	std::ostream& operator<<(std::ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}
		return out;
	}

	std::istream& operator>>(std::istream& in, string& s)
	{
		s.clear();        //输入之前要清空字符串
		char ch = in.get();//获取字符包括'\n'
		char buff[32];  //设置缓冲区来防止频繁扩容
		size_t i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i] = ch;
			if (i == 30)
			{
				buff[31] = '\0';
				s += buff;
				i = 0;
			}
			i++;
			ch = in.get();
		}
		buff[i] = '\0';
		s += buff;
	}
}

 

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。