【C++】三百行代码实现一个简单的string类(下)

引言

在上篇中我们介绍了简单实现的string类中的私有类成员和部分类内方法,我们后面将继续实现剩余的类内方法和一些非成员方法。

part2 公有的类内方法(续)

string& insert(size_t pos, char ch)

insert方法这里我们实现两种,分别是插入一个字符或者一个字符串。insert和前面push_back的区别就是我们可以决定插入的位置。我们都学过顺序表,在顺序表中间插入数据的代价是比较大的,因此insert方法并不推荐经常使用。其实现方法也不难想到,主要就是将pos及其后面的内容在空间足够的前提下向后移动待插入内容的长度(空间不够要先扩容),然后将内容放入指定位置即可,实现代码如下:

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

思路很简单,空间不够现扩容,然后将pos位置及其以后的内容向后移动,然后插入数据,改变_size即可。

string& insert(size_t pos, const char* str)

实现思路相同,但是插入字符串的insert在边界上需要注意的点就比较多了:

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

	for (size_t i = pos, j = 0; i < pos + len; ++i)
	{
		_str[i] = str[j++];
	}
	_size += len;
	return *this;
}

首先前面的扩容和后面的拷贝都容易理解,主要就是中间的挪动环节细节比较多。首先我们用_size和len找到新的end处,然后当 end - pos 大于待插入内容长度时,将内容后移,但这样移动会漏掉一个字符,需要手动填上最后一个字符完成insert。

string& erase(size_t pos, size_t len = npos)

erase函数需要传入要删除的开始位置,第二个参数可选择缺省传入,如果不传则会删至字符串结尾,传入则删除掉指定长度,实现代码如下:

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

如果len为缺省或者删除内容大于pos及以后的全部内容,则直接在pos位置插入反斜杠零实现后面的erase,否则就将距离擦除长度为pos的内容逐个向前复制,然后将pos位置设置为新的字符串结束位置,最后修改对象大小即可。

size_t find(char ch, size_t pos = 0) const

查找函数也包含两种,分别是查找某个字符或者一个字符串,对于查找某个字符的方法很好实现,代码如下:

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

查找字符对string对象进行遍历即可。

size_t find(const char* str, size_t pos = 0) const

对于查找字符串我们选择调用C的接口strstr,代码如下:

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

我们从pos位置开始查找对应位置是否有目标串str,strstr的返回值如果不是空指针,则说明查找成功,我们返回查到的字串距离开头的位置,如果返回空指针说明没有查到子串,则返回npos。

void clear()

clear函数主要就是将字符串指针置空,然后把_size置为0:

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

拷贝构造函数

拷贝构造函数主要就是为了防止自定义类型的浅拷贝问题,这里我们提供两种拷贝构造的实现方法,分别是空间申请的常规写法和调用构造函数的简洁写法。

常规写法

string(const string& src)
{
	_str = new char[src._capacity + 1];
	_size = src._size;
	_capacity = src._capacity;
	strcpy(_str,src._str);
}

常规写法就是申请出新的空间并设置好其size和capacity,然后将src字符串的内容拷贝至该对象的内容中即可。

简洁写法

void swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

string(const string& src)
	:_str(nullptr)
	,_size(0)
	,_capacity(0)
{
	string s(src._str);
	swap(s);
}

这个的实现方法很巧妙,我们既然想要给调用拷贝构造的对象赋予目标对象的内容,我们就可以借助已经实现好的构造函数,用这个传入的参数作为构造函数的参数生成一个工具人对象,然后借助构造函数的初始化列表将正在调用拷贝构造的对象本身进行初始化(主要是将_str置空),因为如果不进行此操作,工具人对象接收到调用拷贝构造的这个对象的内容(创建后未初始化,是一个野指针)后,在析构过程中就会出现delete野指针的情况而报错,所以我们要提前先将调用拷贝构造的这个对象的内容进行初始化以防止这个问题,在分别通过初始化列表和构造函数设置好要进行交换的两个对象的数据之后,万事俱备只欠东风,我们手动实现一个类内的swap函数,将两个对象指向的字符串进行交换,并分别交换size和capacity,即可完成目标对象的拷贝构造,两个对象在析构时也就都不会出现问题了。

赋值重载函数

和拷贝构造一样,赋值重载函数我们也提供两种写法,而且思路类似,第一种是字形申请空间并进行分别赋值的传统写法,第二种则是借助拷贝构造并交换的简洁写法,我们一一体现:

常规写法

string& operator=(const string& str)
{
	if (this != &str)
	{
		char* tmp = new char[str._capacity + 1];
		strcpy(tmp, _str);

		delete[] _str;
		_str = tmp;

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

首先我们要先防止赋值重载发生自己给赋值这种老六情况,然后我们new出参数字符串的capacity+1的空间来并将其内容直接拷贝到这块新空间里,然后释放等号左侧对象的空间并将其指针指向刚new出来的新空间,最后对于size和capacity进行赋值即可完成对象的赋值。

简洁写法

string& operator=(string str)
{
	swap(str);
	return *this;
}

就问你够不够简洁?这里的赋值重载函数有一个细节就是参数不是const常量,而且是传值调用,因此在这个过程中系统会自动调用拷贝构造。既然调用了拷贝构造,我们就不必再担心浅拷贝的问题了,我们就可以用这个系统自己生成的工具人来帮我们完成赋值,也就是调用我们刚才写好的swap函数,把拷贝工造生成的工具人的内容交给等号左侧的对象,然后通过引用传回,即可完成赋值。

part3 非类成员函数

非类成员函数有一个共同的特点,就是他们会为类所用,但必须拿到类外来才能够定义,因为他们的定义中第一个参数不能是*this。至于需不需要添加友元属性,主要看是否需要直接访问到私有成员或者方法了。典型的非类成员函数包括加号重载,getline()函数和输入输出流函数,在这里我们主要实现输入输出流两个函数。

std::ostream& operator <<(std::ostream& out, const string& str)

首先是输出流的实现,在C++中之所以要出现输出流就是为了方便打印自定义类型的对象,因为printf可以覆盖到全部的内置类型,并不需要ostream这个类来输出。而且也正因为输出流函数在调用时要卸载最前面,所以我们把它拿到类外来进行定义,实现方法如下:

std::ostream& operator <<(std::ostream& out, const string& str)
{
	for (size_t i = 0; i < str.size(); ++i)
	{
		out << str[i];
	}
	return out;
}

输出流的实现方法很简单,主要就是将字符串中的每个字符载入到ostream类型的对象之中,最后返回这个输出流对象即可。

std::istream& operator >>(std::istream& in, string& str)

对于输入流的实现我们提供两种代码,一种是使用C++istream类中的get方法来一个字符一个字符读取到istream对象之中,最后返回这个对象,这个方法的实现如下:

	std::istream& operator >>(std::istream& in, string& str)
	{
		char ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			str += ch;
			ch = in.get();
		}
		return in;
	}

我们将从istream处或得到的内容一个字符一个字符的插入到str的后面,最后遇到空格或者回车停止,然后返回istream对象以实现连续的流提取。

但是上面这种方法我们要频繁的从缓冲区get数据,这样的代价是比较大的,所以我们考虑在流插入的重载函数中添加一个认为的缓冲区字符数组,当缓冲区满了再进行刷新,这样能够减少读取缓冲区的次数,减少代价:

std::istream& operator >>(std::istream& in, string& str)
{
	str.clear();
	char buffer[128] = { 0 };
	char ch = in.get();
	size_t i = 0;
	while (ch != ' ' && ch != '\n')
	{
		if (i == 127)
		{
			str += buffer;
			i = 0;
		}
		buffer[i++] = ch;
		ch = in.get();
	}
	if (i > 0)
	{
		buffer[i] = '\0';
		str += buffer;
	}

	return in;
}

为了能够保证输入能够将原字符串进行覆盖,我们先clear掉其中原有的内容,设置好缓冲区数组,然后向缓冲区数组进行插入。若缓冲区只剩一个位置,那么完成此次插入后要将i置为0,当输入中遇到空格或者回车时说明输入结束,此时判断缓冲区中是否有内容,这里我们用缓冲区的哨兵i来进行判断。如果有内容则将其添加到字符串后面完成流提取。我们为什么要将缓冲区的第i个位置置为反斜杠零呢?因为我们的缓冲区是反复使用的,若在本次使用中缓冲区的哨兵i被置0多次,说明最后结束时缓冲区后面仍然有内容,因此最后一次我们必须将缓冲区后面不属于插入范围的内容进行截断,才能够保证流提取的正确性。

结束语

至此关于简单实现string类的全部内容已呈现完毕,如本文有不足或遗漏之处还请大家指正,笔者感激不尽;同时也欢迎大家在评论区进行讨论,一起学习,共同进步!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值