【C++修行之路】STL——模拟实现string类

前言

这次我们分几个部分来实现string类。具体请看目录。
说明:模拟实现只实现了string中最常用的功能。

类框架

首先我们要与库里的string类区分,因此我们定义一个命名空间,名字可以随意起,这里教学因此命名为Teacher

string.h:
namespace Teacher
{
    
    
	class string
	{
    
    
	public:
		//函数实现
	private:
		char* _str;
		int _size;
		int _capacity;
	};
}

约定:在我们模拟实现过程中,_str存储字符串内容,_size表示现在字符串的大小(不包括\0)_capacity表示字符串一共有多大空间(不包括\0)
在这里插入图片描述

构造与析构

空字符串可以直接用缺省值处理,我们不必再写一个空参构造函数。

//函数实现
string(const char* str = "") : _size(strlen(str))
{
    
    
	_capacity = _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}

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

迭代器

用指针来模拟实现一下迭代器,唯一需要注意的就是一定要写成iterator和const_iterator,不然在使用范围for的时候会报错。

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

操作符重载

[]:

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

reverse与resize

reverse

当我们对string对象进行增加操作时(不管是追加一个字符还是追加一个串)我们都需要判断当前对象的容量还够不够,如果不够,我们就要按需要扩容。至于啥时候需要扩容,我们在需要的函数里再判断。reverse只管扩容。

void reserve(size_t newsize)
{
    
    
	char* tmp = new char[newsize + 1];//按照我们的约定容量不包括\0,因此我们在这里加上
	strcpy(tmp,_str);
	delete[] _str;
	_str = tmp;
	_capacity = newsize;
}

resize

没什么好说的,注意缺省值是如何使用的即可

void resize(size_t n, char ch = '\0')
{
    
    
	if (n < _size)
	{
    
    
		// 删除数据--保留前n个
		_size = n;
		_str[_size] = '\0';
	}
	else if (n > _size)
	{
    
    
		if (n > _capacity)
		{
    
    
			reserve(n);
		}

		size_t i = _size;
		while (i < n)
		{
    
    
			_str[i] = ch;
			++i;
		}

		_size = n;
		_str[_size] = '\0';
	}
}

push_back与append

实现了reverse函数之后,我们就可以实现push_back和append函数了
push_back:

void push_back(char ch)
{
    
    
	if (_size + 1 > _capacity)
	{
    
    
		reserve(_size * 2);
	}
	_str[_size] = ch;
	_size++;
	_str[_size] = '\0';
}

append:

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

如果你对其中判断是否需要扩容感到疑惑,建议你再想想我们之前对_size和_capacity的约定。

复用实现+=

因为我们已经写好了上面的接口,直接复用即可。

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

insert和erase

insert:可以在任意位置插入
在这里提供两个思路,但由于一些边界问题,第一个思路你要考虑判断坐标是否合法。
在这里插入图片描述

void insert(size_t pos, const char ch)
{
    
    
	//在pos处插入一个字节
	if (_size + 1 > _capacity)
	{
    
    
		reserve(_capacity * 2);
	}
	size_t end = _size;//无符号数会出越界的bug
	while (end >= pos && end != -1)
	{
    
    
		_str[end + 1] = _str[end];
		end--;
	}
	_str[pos] = ch;
	_size++;
}

void insert2(size_t pos, const char ch)
{
    
    
	//在pos处插入一个字节
	if (_size + 1 > _capacity)
	{
    
    
		reserve(_capacity * 2);
	}
	size_t end = _size+1;
	while (end > pos)
	{
    
    
		_str[end] = _str[end-1];
		end--;
	}
	_str[pos] = ch;
	_size++;
}

erase:删除一部分数据,注意这里给出了npos的缺省值,我们在类成员函数处加上即可。

static const size_t npos;

const size_t npos = -1;

上面这种给静态变量赋值的方式只能赋值成int,其他类型均不可以。

void erase(size_t pos, size_t len = npos)
{
    
    
	int begin = pos + len;
	while (begin <= _size)
	{
    
    
		_str[begin - len] = _str[begin];
		begin++;
	}
	_size -= len;
}

c_str与流插入、流提取

由于自定义的类型不能直接输出打印,因此我们要拿到对象内的字符数组,这样才能按照C语言的方式来打印字符串。
代码如下:

const char* c_str()
{
    
    
	return _str;
}

而流插入和流提取即重载两个操作符:
为什么需要重载流插入和流提取?只有c_str可以吗?
当然是不可以的,传统的C字符串以 \0 为结尾,即不允许存在这样的字符串:

           abcef\0jkl\0

为了能存储\0我们需要对 << 进行重载。
需要声明的是,不同的编译器对 \0 的处理方式不同,但可以肯定的是,按照<<打印的方式肯定要比c_str的方式更长。

流提取:

ostream& operator<<(ostream& out, const string& s)
{
    
    
	//需要写迭代器
	for (auto ch : s)
	{
    
    
		out << s;
	}
	return out;
}

流插入:

istream operator>>(istream& is,string& s)
{
    
    
	char ch = in.get()
	char buff[128];
	size_t i = 0;
	while(ch != ' ' && ch != '\n')
	{
    
    
		buff[i] = ch;
		i++;
		if(i == 127)
		{
    
    
			buff[127] = '\0';
			s += buff;
			i = 0;
		}
		ch = in.get();
	}
	if(i != 0)
	{
    
    
		buff[i] = '\0';
		s+=buff;
	}
}

erase

erase可以分两种情况考虑,即把pos后面全删了,还是删除pos后有限个元素,我们可以单独处理,具体详见代码:

void erase(size_t pos,size_t len = npos)
{
    
    
	if (pos + len >= _size || len == npos)
	{
    
    
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
    
    
		strcpy(_str+pos,_str+pos+len);
		_size -= len;
	}
}

因为设计到在同一个字符串里用strcpy,我们这里不用担心会覆盖的问题,因为左边是我们要删除的,右边的是我们的源头,所以覆盖的是无用的元素。可以直接使用。

swap(s1,s2)与s1.swap(s2)

最后来谈一下交换两个对象的函数。
更推荐使用第二种,

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

如果选择第一种会怎么样?其实会进行三次拷贝构造,这样是非常低效的,详细如图,采用第二种会好很多。1
在这里插入图片描述

结语

到这里,本篇文章就到此为止了,我们下次见~

猜你喜欢

转载自blog.csdn.net/m0_73209194/article/details/129213366