C++STL详解(二)String类的模拟实现

string类各函数接口总览

namespace zpl
{
	//模拟实现string类
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;

		//默认成员函数
		string(const char* str = "");         //构造函数
		string(const string& s);              //拷贝构造函数
		string& operator=(const string& s);   //赋值运算符重载函数
		~string();                            //析构函数

		//迭代器相关函数
		iterator begin();
		iterator end();
		const_iterator begin()const;
		const_iterator end()const;

		//容量和大小相关函数
		size_t size();
		size_t capacity();
		void reserve(size_t n);
		void resize(size_t n, char ch = '\0');
		bool empty()const;

		//修改字符串相关函数
		void push_back(char ch);
		void append(const char* str);
		string& operator+=(char ch);
		string& operator+=(const char* str);
		string& insert(size_t pos, char ch);
		string& insert(size_t pos, const char* str);
		string& erase(size_t pos, size_t len);
		void clear();
		void swap(string& s);
		const char* c_str()const;

		//访问字符串相关函数
		char& operator[](size_t i);
		const char& operator[](size_t i)const;
		size_t find(char ch, size_t pos = 0)const;
		size_t find(const char* str, size_t pos = 0)const;
		size_t rfind(char ch, size_t pos = npos)const;
		size_t rfind(const char* str, size_t pos = 0)const;

		//关系运算符重载函数
		bool operator>(const string& s)const;
		bool operator>=(const string& s)const;
		bool operator<(const string& s)const;
		bool operator<=(const string& s)const;
		bool operator==(const string& s)const;
		bool operator!=(const string& s)const;

	private:
		char* _str;       //存储字符串
		size_t _size;     //记录字符串当前的有效长度
		size_t _capacity; //记录字符串当前的容量
		static const size_t npos; //静态成员变量(整型最大值)
	};
	const size_t string::npos = -1;

	//<<和>>运算符重载函数
	istream& operator>>(istream& in, string& s);
	ostream& operator<<(ostream& out, const string& s);
	istream& getline(istream& in, string& s);
}

默认成员函数

构造函数

构造函数设置缺省值,如果不传入参数,则默认构造空字符串_size和_capacity都设置为传入C字符串参数的长度,不包括’\0’

string(const char* str = "")  //设置缺省参数默认为空字符串
	:_size(strlen(str))   //初始时_size为传入字符串的长度
	,_capacity(_size)	//初始时_capacity为传入字符串的长度
{
	_str = new char[_capacity + 1];   //多开一个空间存'\0’
	strcpy(_str, str);			//将传入字符串拷贝至_str中
}

实际上string其实就是一个管理字符的顺序表,不同于C语言的是,定义成了类罢了

拷贝构造函数

在实现拷贝构造函数之前,我们得先了解一下什么是深浅拷贝。

浅拷贝(值拷贝):拷贝出来的目标对象的指针和源对象的指针指向的是同一块空间,其中一个对象的改动会影响另一个对象。
深拷贝:拷贝出来的目标对象的指针和源对象的指针指向的内存空间相互独立,一个改变不会影响另一个对象。

因此,为了拷贝出来的对象和源对象相互之间独立,我们就必须使用深拷贝。

写法一(传统写法):
先开辟一块和源对象同样大小的空间,以存储字符串,然后把其它成员也拷贝过去,因为指向的不是同一块空间符合深拷贝。

//传统写法
string(const string& s)
{
	_size = s._size;
	_capacity = s._capacity;
	_str = new char[_capacity + 1];
	strcpy(_str, s._str);
}

在这里插入图片描述
写法二:现代写法
先利用构造函数构造一个和源对象一模一样的临时对象tmp中,再调用swap函数交换目标对象和tmp临时对象即可,这样也完成了对目标对象的深拷贝。

//现代写法
string(const string& s)
{
	string tmp(s._str);
	swap(tmp);
}

在这里插入图片描述

赋值运算符重载函数

与拷贝构造函数类似,赋值运算符重载函数的模拟实现也涉及深浅拷贝问题,我们同样需要采用深拷贝。下面也提供深拷贝的两种写法:
写法一:传统写法
赋值运算符重载函数的传统写法与拷贝构造函数的传统写法几乎相同,只是左值的_str在开辟新空间之前需要先将原来的空间释放掉,并且在进行操作之前还需判断是否是自己给自己赋值,若是自己给自己赋值,则无需进行任何操作。

	//传统写法
	string& operator=(const string& s)
	{
		if (this != &s)   //防止自己给自己赋值
		{
			//释放就空间指向新空间
			delete[] _str;   //不用担心会释放空指针,因为delete里面异常处理了
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}
		return *this;
	}

写法二:现代写法
赋值运算符重载函数的现代写法与拷贝构造函数的现代写法也是非常类似,但拷贝构造函数的现代写法是通过代码语句调用构造函数构造出一个对象,然后将该对象与拷贝对象交换;而赋值运算符重载函数的现代写法是通过采用“值传递”接收右值的方法,让编译器自动调用拷贝构造函数,然后我们再将拷贝出来的对象与左值进行交换即可。

//现代写法
string& operator=(string s)
{
	swap(s);
	return *this;
}

不用考虑自己给自己赋值,因为很少会有人自己给自己赋值。

析构函数

因为成员变量_str牵涉到资源的申请和释放所以以上成员函数乃至析构函数都要自己重新写一份,而不能使用编译器自己提供的了。

//析构函数
~string()
{
	delete[] _str;
	_str = nullptr;
	_size = 0;
	_capacity = 0;
}

迭代器相关函数

begin和end

string类当中的迭代器其实就是原生指针一个字符指针罢了,只是为了符合STL相统一的使用方式,命名为了iterator

typedef char* iterator;
typedef const char* const_iterator;

注意不是所有的迭代器都是原生指针,例如list的迭代器
string类中的begin和end函数的实现简单的可怕,begin函数的作用就是返回字符串中第一个字符的地址:
begin

iterator begin()
{
	return _str;   //返回字符串中第一个字符的地址
}
const_iterator begin() const
{
	return _str;    //返回字符串中第一个字符的const地址
}

end

iterator end()
{
	return _str + _size;	//返回字符串中最后一个字符的后一个字符的地址
}
const_iterator end() const
{
	return _str + _size;   //返回字符串中最后一个字符的后一个字符的const地址   即是'\0'的位置
}

由于迭代区间都是左闭右开,所有end指向’\0’的位置
迭代器测试遍历一下
支持迭代器就支持范围for,傻瓜式替换

void test1()
{
	string s1;
	string s2("hello c++");
	string s3;
	s3 = s2;
	string::iterator it = s3.begin();
	while (it != s3.end())
	{
		cout << *it;
		it++;
	}
	cout << endl;
	for (auto e : s3)
	{
		cout << e;
	}
	cout << endl;
}

容量和大小相关函数

size和capacity

因为string类的成员变量是私有的,我们并不能直接对其进行访问,所以string类设置了size和capacity这两个成员函数,用于获取string对象的大小和容量。

size函数用于获取字符串当前的有效长度(不包括’\0’)。

//大小
size_t size()const
{
	return _size; //返回字符串当前的有效长度
}
//容量
size_t capacity()const
{
	return _capacity; //返回字符串当前的容量
}

reserve和resize

reserve和resize这两个函数的执行规则一定要区分清楚。
reserve规则:
 1、当n大于对象当前的capacity时,将capacity扩大到n或大于n。
 2、当n小于对象当前的capacity时,什么也不做。

//扩容  一般不缩容,只会扩容
//只影响容量,不影响大小
void reserve(size_t n)
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1];  //永远多开一个存'\0'
		strncpy(tmp, _str, _size + 1);   //包括'\0'也要拷贝过去
		delete[] _str;
		_str = tmp;
		_capacity = n;
	}
}

注意:代码中使用strncpy进行拷贝对象C字符串而不是strcpy,是为了防止对象的C字符串中含有有效字符’\0’而无法拷贝(strcpy拷贝到第一个’\0’就结束拷贝了)。
在这里插入图片描述
resize规则:
 1、当n大于当前的size时,将size扩大到n,扩大的字符为ch,若ch未给出,则默认为’\0’。
 2、当n小于当前的size时,将size缩小到n。

//改变大小
void resize(size_t n, char ch = '\0')
{
	if (n <= _size) //n小于当前size
	{
		_size = n; //将size调整为n
		_str[_size] = '\0'; //在size个字符后放上'\0'
	}
	else //n大于当前的size
	{
		if (n > _capacity) //判断是否需要扩容
		{
			reserve(n); //扩容
		}
		for (size_t i = _size; i < n; i++) //将size扩大到n,扩大的字符为ch
		{
			_str[i] = ch;
		}
		_size = n; //size更新
		_str[_size] = '\0'; //字符串后面放上'\0'
	}
}

empty

如果有效字符为0个,就是空字符串为空

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

修改字符串相关函数

push_back

尾插一个字符,尾插之前需要判断是否扩容,若需要扩容则调用reserve函数进行扩容,再尾插,尾插完之后的下一个位置需要置为’\0’否则打印字符串会出现非法访问。

void push_back(char ch)
{
	if (_size == _capacity)
	{
	//按两倍增容,避免频繁增容
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	_str[_size] = ch;
	_str[_size + 1] = '\0';
	_size++;
}

还可以复用insert函数

//尾插字符
void push_back(char ch)
{
	insert(_size, ch); //在字符串末尾插入字符ch
}

append

尾插一个字符串到对象末尾,添加字符串的有效字符长度,不包括’\0’,因为待尾插的对象末尾有字符’\0’,尾插前需要判断是否增容。

void append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size+len);
	}
	strncpy(_str + _size, str, len);
	_size += len;
}

operator+=

+=运算符的重载是为了实现字符串与字符、字符串与字符串之间能够直接使用+=运算符进行尾插。
+=运算符实现字符串与字符之间的尾插直接调用push_back函数即可。

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

+=运算符实现字符串与字符串之间的尾插直接调用append函数即可。

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

insert

nsert函数的作用是在字符串的任意位置插入字符或是字符串。
insert函数用于插入字符时,首先需要判断pos的合法性,若不合法则无法进行操作,紧接着还需判断当前对象能否容纳插入字符后的字符串,若不能则还需调用reserve函数进行扩容。插入字符的过程也是比较简单的,先将pos位置及其后面的字符统一向后挪动一位,给待插入的字符留出位置,然后将字符插入字符串即可。
在这里插入图片描述

string& insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	char* end = _str + _size;
	while (end >= _str + pos)
	{
		*(end + 1) = *end;
		end--;
	}
	_str[pos] = ch;
	_size++;
	return *this;   
}

insert函数用于插入字符串时,首先也是判断pos的合法性,若不合法则无法进行操作,再判断当前对象能否容纳插入该字符串后的字符串,若不能则还需调用reserve函数进行扩容。插入字符串时,先将pos位置及其后面的字符统一向后挪动len位(len为待插入字符串的长度),给待插入的字符串留出位置,然后将其插入字符串即可。

在这里插入图片描述

string& insert(size_t pos, const char* str)
{
	assert(pos <= _size);
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
	char* end = _str + _size;
	while (end >= _str + pos)
	{
		*(end + len) = *end;
		end--;
	}
	strncpy(_str + pos, str, len);
	_size += len;
	return *this;
}

erase

erase函数的作用是删除字符串任意位置开始的n个字符。删除字符前也需要判断pos的合法性,进行删除操作的时候分两种情况:
1、pos位置及其之后的有效字符都需要被删除。
这时我们只需在pos位置放上’\0’,然后将对象的size更新即可。
在这里插入图片描述

2、pos位置及其之后的有效字符只需删除一部分。
这时我们可以用后方需要保留的有效字符覆盖前方需要删除的有效字符,此时不用在字符串后方加’\0’,因为在此之前字符串末尾就有’\0’了。
在这里插入图片描述

string& erase(size_t pos, size_t len = npos)
{
	assert(pos < _size);  //检查删除位置合法性
	size_t n = _size - pos;   //计算pos位置之后有多少个有效字符
	if (len >= n)
	{
		_size = pos;
		_str[_size] = '\0';
	}
	else
	{
		strcpy(_str + pos, _str + pos + len);   //连'\0'也要拷贝过去
		_size -= len;
	}
	return *this;
}

clear

清空所有有效字符,变成一个空字符串

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

swap

要想调用到库里面的swap函数,来交换各个成员,需要指定域,所以需要::域作用限定符,优先到指定库去找,如果不指定,就近原则会到当前作用域,调用目前正在实现的swap,而发生错误。

//交换两个string对象
void swap(string& s)
{
	std::swap(s._str, _str);
	std::swap(s._capacity, _capacity);
	std::swap(s._size, _size);
}

c_str

c_str函数用于获取对象C类型的字符串,实现时直接返回对象的成员变量_str即可。

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

访问字符串相关函数

operator[ ]

[ ]运算符的重载是为了让string对象能像C字符串一样,通过[ ] +下标的方式获取字符串对应位置的字符。
在C字符串中我们通过[ ] +下标的方式可以获取字符串对应位置的字符,并可以对其进行修改,实现[ ] 运算符的重载时只需返回对象C字符串对应位置字符的引用即可,这样便能实现对该位置的字符进行读取和修改操作了,但需要注意在此之前检测所给下标的合法性。

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

有时候只能读,不能改

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

find和rfind

find
1.正向查找第一个匹配的字符
首先判断所给pos的合法性,然后通过遍历的方式从pos位置开始向后寻找目标字符,若找到,则返回其下标;若没有找到,则返回npos。(npos是string类的一个静态成员变量,其值为整型最大值)

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

2、正向查找第一个匹配的字符串。
首先也是先判断所给pos的合法性,然后我们可以通过调用strstr函数进行查找。strstr函数若是找到了目标字符串会返回字符串的起始位置,若是没有找到会返回一个空指针。若是找到了目标字符串,我们可以通过计算目标字符串的起始位置和对象C字符串的起始位置的差值,进而得到目标字符串起始位置的下标。

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

rfind
1.反向查找第一个匹配字符

//反向查找第一个匹配的字符
size_t rfind(char ch, size_t pos = npos) const
{
	string tmp(*this); //拷贝构造对象tmp
	reverse(tmp.begin(), tmp.end()); //调用reverse逆置对象tmp的C字符串
	if (pos >= _size) //所给pos大于字符串有效长度
	{
		pos = _size - 1; //重新设置pos为字符串最后一个字符的下标
	}
	pos = _size - 1 - pos; //将pos改为镜像对称后的位置
	size_t ret = tmp.find(ch, pos); //复用find函数
	if (ret != npos)
		return _size - 1 - ret; //找到了,返回ret镜像对称后的位置
	else
		return npos; //没找到,返回npos
}

2、反向查找第一个匹配的字符串。
首先我们还是需要用对象拷贝构造一个临时对象tmp,然后将tmp对象的C字符串逆置,同时我们还需要拷贝一份待查找的字符串,也将其逆置。然后将所给pos镜像对称一下再调用find函数。注意:此时我们将从find函数接收到的值镜面对称后,得到的是待查找字符串的最后一个字符在对象C字符串中的位置,而我们需要返回的是待查找字符串在对象C字符串中的第一个字符的位置,所以还需做进一步调整后才能作为rfind函数的返回值返回。

size_t rfind(const char* str, size_t pos = npos) const
{
	string tmp(*this);
	reverse(tmp.begin(), tmp.end());
	if (pos >= _size)
	{
		pos = _size - 1;   //修改为默认成最后一个字符开始往前找
	}
	pos = _size - 1 - pos;  //翻转字符串后镜像对称的位置
	size_t ret = tmp.find(str, pos);
	if (ret!=npos)
	{
		return _size-1-ret;   //修改为原来的位置
	}
	else
	{
		return npos;
	}
}

关系运算符重载函数

>>和<<运算符的重载以及getline函数

>>运算符的重载

利用蓄水池原理buff数组提高效率

istream& operator>>(istream& in, string& s)
{
	s.clear();
	char ch = in.get();  //调用get能够将空格读入
	char buff[128];
	int i = 0;
	while (ch != ' ' || ch != '\n')
	{
		buff[i++] = ch;
		if (i == 127)
		{
			buff[i] = '\0';
			s += buff;
			i = 0;
		}
		ch = in.get();
	}
	if (i > 0)
	{
		buff[i] = '\0';
		s += buff;
	}
	return in;
}

<<运算符的重载

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

getline

getline函数用于读取一行含有空格的字符串。实现时于>>运算符的重载基本相同,只是当读取到’\n’的时候才停止读取字符。

istream& getline(istream& in, string& s)
{
	s.clear();
	char ch = in.get();
	while (ch != '\n')
	{
		s += ch;
		ch = in.get();
	}
	return in;
}
  • 17
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

维生素C++

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值